Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,22 @@ jobs:
run: |
cd /opt/services
docker compose up -d --force-recreate cascade-router

- name: Verify cascade-router is healthy
run: |
echo "Waiting for cascade-router to start..."
for i in $(seq 1 30); do
if docker inspect cascade-router --format '{{.State.Health.Status}}' 2>/dev/null | grep -q healthy; then
echo "cascade-router is healthy"
exit 0
fi
if docker inspect cascade-router --format '{{.State.Status}}' 2>/dev/null | grep -q restarting; then
echo "ERROR: cascade-router is crashlooping!"
docker logs cascade-router --tail 20
exit 1
fi
sleep 5
done
echo "ERROR: cascade-router did not become healthy within 150s"
docker logs cascade-router --tail 20
exit 1
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ src/cli/dashboard/
└── webhooks/ # 3 commands
```

The `cascade` binary is separate from `cascade-tools` (which is for agents). The `cascade-tools` binary uses a custom oclif config in `bin/cascade-tools.js` to discover only agent tool commands (`dist/cli/trello/`, `dist/cli/github/`, `dist/cli/session/`), while `cascade` discovers only dashboard commands (`dist/cli/dashboard/`).
The `cascade` binary is separate from `cascade-tools` (which is for agents). The `cascade-tools` binary uses a custom oclif config in `bin/cascade-tools.js` to discover only agent tool commands (`dist/cli/pm/`, `dist/cli/github/`, `dist/cli/session/`), while `cascade` discovers only dashboard commands (`dist/cli/dashboard/`).

## Adding New Triggers

Expand Down
48 changes: 45 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"drizzle-orm": "^0.45.1",
"eta": "^4.5.0",
"hono": "^4.6.14",
"jira.js": "^5.3.0",
"llmist": "^15.18.0",
"pg": "^8.18.0",
"trello.js": "^1.2.8",
Expand Down
92 changes: 52 additions & 40 deletions src/agents/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,20 @@ import { RipGrep } from '../gadgets/RipGrep.js';
import { Sleep } from '../gadgets/Sleep.js';
import { VerifyChanges } from '../gadgets/VerifyChanges.js';
import { CreatePR } from '../gadgets/github/index.js';
import {
AddChecklist,
CreateWorkItem,
ListWorkItems,
PMUpdateChecklistItem,
PostComment,
ReadWorkItem,
UpdateWorkItem,
formatWorkItemData,
} from '../gadgets/pm/index.js';
import { Tmux } from '../gadgets/tmux.js';
import { TodoDelete, TodoUpdateStatus, TodoUpsert } from '../gadgets/todo/index.js';
import { type Todo, formatTodoList, initTodoSession, saveTodos } from '../gadgets/todo/storage.js';
import {
AddChecklistToCard,
CreateTrelloCard,
// GetMyRecentActivity, // Temporarily disabled
ListTrelloCards,
PostTrelloComment,
ReadTrelloCard,
UpdateChecklistItem,
UpdateTrelloCard,
formatCardData,
} from '../gadgets/trello/index.js';
import { trelloClient } from '../trello/client.js';
import { getPMProvider } from '../pm/index.js';
import type { AgentInput, AgentResult, CascadeConfig, ProjectConfig } from '../types/index.js';
import { logger } from '../utils/logging.js';
import type { PromptContext } from './prompts/index.js';
Expand Down Expand Up @@ -86,10 +85,11 @@ interface AgentContextData {

export async function fetchImplementationSteps(cardId: string): Promise<string[] | undefined> {
try {
const checklists = await trelloClient.getCardChecklists(cardId);
const provider = getPMProvider();
const checklists = await provider.getChecklists(cardId);
const implChecklist = checklists.find((cl) => cl.name.includes('Implementation Steps'));
if (!implChecklist || implChecklist.checkItems.length === 0) return undefined;
const incompleteItems = implChecklist.checkItems.filter((item) => item.state !== 'complete');
if (!implChecklist || implChecklist.items.length === 0) return undefined;
const incompleteItems = implChecklist.items.filter((item) => !item.complete);
return incompleteItems.length > 0 ? incompleteItems.map((item) => item.name) : undefined;
} catch {
return undefined;
Expand All @@ -116,13 +116,21 @@ async function buildAgentContext(
commentContext?: { text: string; author: string },
): Promise<AgentContextData> {
// Build prompt context for template rendering
const pmProvider = getPMProvider();
const isJira = pmProvider.type === 'jira';
const promptContext: PromptContext = {
cardId,
cardUrl: cardId ? `https://trello.com/c/${cardId}` : undefined,
cardUrl: cardId ? pmProvider.getWorkItemUrl(cardId) : undefined,
projectId: project.id,
baseBranch: project.baseBranch,
storiesListId: project.trello?.lists?.stories,
processedLabelId: project.trello?.labels?.processed,
pmType: pmProvider.type,
workItemNoun: isJira ? 'issue' : 'card',
workItemNounPlural: isJira ? 'issues' : 'cards',
workItemNounCap: isJira ? 'Issue' : 'Card',
workItemNounPluralCap: isJira ? 'Issues' : 'Cards',
pmName: isJira ? 'JIRA' : 'Trello',
...(prContext && {
prNumber: prContext.prNumber,
prBranch: prContext.prBranch,
Expand Down Expand Up @@ -155,11 +163,11 @@ async function buildAgentContext(
configKey: configKeyOverrides[agentType],
});

// Pre-fetch card data for synthetic gadget call (only if cardId exists and not debug flow)
// Pre-fetch work item data for synthetic gadget call (only if cardId exists and not debug flow)
let cardData = '';
if (cardId && !debugContext) {
log.info('Fetching card data for context', { cardId });
cardData = await formatCardData(cardId, true);
log.info('Fetching work item data for context', { cardId });
cardData = await formatWorkItemData(cardId, true);
}

// Pre-fetch implementation steps for synthetic todo injection
Expand Down Expand Up @@ -196,19 +204,19 @@ function buildCommentResponsePrompt(
commentText: string,
commentAuthor: string,
): string {
return `A user (@${commentAuthor}) mentioned you in a comment on Trello card ${cardId}.
return `A user (@${commentAuthor}) mentioned you in a comment on work item ${cardId}.

Their comment:
---
${commentText}
---

The card data (title, description, checklists, attachments, comments) has been pre-loaded above.
The work item data (title, description, checklists, attachments, comments) has been pre-loaded above.
Read the user's comment carefully and respond accordingly. Default to surgical, targeted updates unless they clearly ask for a full rewrite.`;
}

function buildPrompt(cardId: string): string {
return `Analyze and process the Trello card with ID: ${cardId}. The card data (title, description, checklists, attachments, comments) has been pre-loaded above. Review it and proceed with your task.`;
return `Analyze and process the work item with ID: ${cardId}. The work item data (title, description, checklists, attachments, comments) has been pre-loaded above. Review it and proceed with your task.`;
}

function buildCheckFailurePrompt(prContext: {
Expand Down Expand Up @@ -297,17 +305,16 @@ function getBaseAgentGadgets(agentType: string) {
new TodoDelete(),
// GitHub gadgets (no PR creation for planning)
...(isReadOnlyAgent ? [] : [new CreatePR()]),
// Trello gadgets
new ReadTrelloCard(),
new PostTrelloComment(),
new UpdateTrelloCard(),
new CreateTrelloCard(),
new ListTrelloCards(),
// new GetMyRecentActivity(), // Temporarily disabled
new AddChecklistToCard(),
// PM gadgets (work items, comments, checklists — PM-agnostic)
new ReadWorkItem(),
new PostComment(),
new UpdateWorkItem(),
new CreateWorkItem(),
new ListWorkItems(),
new AddChecklist(),
// UpdateChecklistItem not available for planning - prevents marking items complete prematurely
// But respond-to-planning-comment CAN update checklist items (user may ask to check/uncheck steps)
...(agentType === 'planning' ? [] : [new UpdateChecklistItem()]),
...(agentType === 'planning' ? [] : [new PMUpdateChecklistItem()]),
// Session control
new Finish(),
];
Expand Down Expand Up @@ -369,13 +376,13 @@ async function injectSyntheticCalls(
// before encountering specific file paths from the card
builder = injectSquintContext(builder, trackingContext, repoDir);

// Inject card data as synthetic ReadTrelloCard call (only if cardId exists)
// Inject work item data as synthetic ReadWorkItem call (only if cardId exists)
if (cardId && cardData) {
builder = injectSyntheticCall(
builder,
trackingContext,
'ReadTrelloCard',
{ cardId, includeComments: true },
'ReadWorkItem',
{ workItemId: cardId, includeComments: true },
cardData,
'gc_card',
);
Expand Down Expand Up @@ -493,11 +500,16 @@ export async function executeAgent(

onWatchdogTimeout: async (_fileLogger: FileLogger, runId?: string) => {
if (cardId) {
await trelloClient.addComment(
cardId,
`⏱️ Agent timed out (watchdog).${runId ? ` Run ID: ${runId}` : ''}`,
);
logger.info('Posted timeout comment to card', { cardId, runId });
try {
const provider = getPMProvider();
await provider.addComment(
cardId,
`⏱️ Agent timed out (watchdog).${runId ? ` Run ID: ${runId}` : ''}`,
);
logger.info('Posted timeout comment to work item', { cardId, runId });
} catch {
logger.warn('Failed to post timeout comment', { cardId, runId });
}
}
},

Expand Down Expand Up @@ -563,7 +575,7 @@ export async function executeAgent(
createProgressMonitor({
logWriter: fileLogger.write.bind(fileLogger),
agentType,
taskDescription: cardId ? `Trello card ${cardId}` : 'Unknown task',
taskDescription: cardId ? `Work item ${cardId}` : 'Unknown task',
progressModel: config.defaults.progressModel,
intervalMinutes: config.defaults.progressIntervalMinutes,
customModels: CUSTOM_MODELS as ModelSpec[],
Expand Down
8 changes: 8 additions & 0 deletions src/agents/prompts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ export interface PromptContext {
projectId?: string;
baseBranch?: string;

// PM vocabulary (computed from pmType)
pmType?: 'trello' | 'jira';
workItemNoun?: string; // "card" or "issue"
workItemNounPlural?: string; // "cards" or "issues"
workItemNounCap?: string; // "Card" or "Issue"
workItemNounPluralCap?: string; // "Cards" or "Issues"
pmName?: string; // "Trello" or "JIRA"

// Briefing-specific
storiesListId?: string;
processedLabelId?: string;
Expand Down
Loading