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
7 changes: 7 additions & 0 deletions src/agents/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ function createAgentBuilderWithGadgets(
llmCallAccumulator?: AccumulatedLlmCall[],
runId?: string,
baseBranch?: string,
projectId?: string,
cardId?: string,
): BuilderType {
return createConfiguredBuilder({
client,
Expand All @@ -203,6 +205,8 @@ function createAgentBuilderWithGadgets(
llmCallAccumulator,
runId,
baseBranch,
projectId,
cardId,
// Implementation agent uses sequential execution to ensure file operations
// are properly ordered (e.g., FileSearchAndReplace then ReadFile on same file)
postConfigure:
Expand Down Expand Up @@ -411,6 +415,8 @@ export async function executeAgent(
llmCallAccumulator,
runId,
project.baseBranch,
project.id,
cardId,
),

injectSyntheticCalls: ({ builder, ctx, trackingContext, repoDir }) =>
Expand All @@ -434,6 +440,7 @@ export async function executeAgent(
customModels: CUSTOM_MODELS as ModelSpec[],
repoDir,
trello: cardId ? { cardId } : undefined,
preSeededCommentId: input.ackCommentId as string | undefined,
}),

interactive,
Expand Down
5 changes: 3 additions & 2 deletions src/agents/prompts/templates/planning.eta
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ Update the <%= it.workItemNoun || 'card' %> description with **emoji section hea

**IMPORTANT:**
- After updating the <%= it.workItemNoun || 'card' %>, ALWAYS call `AddChecklist` to create an interactive "📋 Implementation Steps" checklist with each step as an item.
- When referencing other <%= it.workItemNounPlural || 'cards' %> (related stories, dependencies), ALWAYS use markdown links: `[<%= it.workItemNounCap || 'Card' %> Title](URL)`
<% if (it.pmType === 'jira') { %>- When calling `AddChecklist`, pass items as objects with `name` and `description`. The description should include the files to modify, specific changes, and testing notes from the corresponding Implementation Step section. This becomes the JIRA subtask description.
<% } %>- When referencing other <%= it.workItemNounPlural || 'cards' %> (related stories, dependencies), ALWAYS use markdown links: `[<%= it.workItemNounCap || 'Card' %> Title](URL)`

## Comment Format

Expand All @@ -124,7 +125,7 @@ Review the updated description and move to TODO when ready to implement!
**<%= it.pmName || 'Trello' %> (for outputting your plan):**
- `ReadWorkItem` - Read <%= it.workItemNoun || 'card' %> details (title, description, comments, labels)
- `UpdateWorkItem` - Update <%= it.workItemNoun || 'card' %> title/description
- `AddChecklist` - Add an interactive checklist to a <%= it.workItemNoun || 'card' %> (use for implementation steps)
- `AddChecklist` - Add an interactive checklist to a <%= it.workItemNoun || 'card' %> (use for implementation steps)<% if (it.pmType === 'jira') { %>. Each item can include a `description` with files-to-modify, specific changes, and testing notes — these become subtask descriptions.<% } %>
- `PostComment` - Post a comment on a <%= it.workItemNoun || 'card' %>

**Codebase exploration (READ-ONLY):**
Expand Down
12 changes: 12 additions & 0 deletions src/agents/prompts/templates/respond-to-planning-comment.eta
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ You are running in a cloned copy of the project repository. Before updating the
4. **Make surgical updates** to the <%= it.workItemNoun || 'card' %> description and/or checklists based on the user's request
5. **Post a reply comment** via PostComment explaining what you changed and why

## Updating the Plan and Checklists

When modifying the plan, **update the existing checklists in place** — do NOT create duplicate checklists.

- **Adding steps**: Use `AddChecklist` only when there is no existing checklist to add items to. Otherwise use `UpdateChecklistItem` to rename existing items or add to an existing checklist.
- **Renaming/rewriting steps**: Use `UpdateChecklistItem` to change the text of existing checklist items.
- **Removing steps**: Use `DeleteChecklistItem` to permanently remove checklist items / subtasks that are no longer needed. Do NOT mark removed items as "complete" — they were never done, so deleting is the correct action.
- **Reordering**: Delete and re-add items as needed to achieve the desired order.

When the user asks to narrow scope, focus on a subset, or drop items from the plan, **always delete** the out-of-scope items rather than leaving them in the checklist.

## Response Format

When updating the <%= it.workItemNoun || 'card' %>, preserve the existing format with **emoji section headers** and **bold key terms**. Only modify the sections that need to change.
Expand All @@ -78,6 +89,7 @@ Based on your comment, I've made the following changes:
- `UpdateWorkItem` - Update <%= it.workItemNoun || 'card' %> title/description
- `AddChecklist` - Add an interactive checklist to a <%= it.workItemNoun || 'card' %>
- `UpdateChecklistItem` - Update checklist item state (complete/incomplete) or name
- `DeleteChecklistItem` - Delete a checklist item / subtask (use to remove descoped steps — do NOT mark removed items as complete)
- `PostComment` - Post a comment on a <%= it.workItemNoun || 'card' %>

**Codebase exploration (READ-ONLY):**
Expand Down
6 changes: 5 additions & 1 deletion src/agents/shared/builderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export interface CreateBuilderOptions {
runId?: string;
/** Base branch for PR creation (e.g. 'main', 'dev'). Passed to session state. */
baseBranch?: string;
/** Project ID for PR ↔ work item linking. Passed to session state. */
projectId?: string;
/** Work item (card) ID for PR ↔ work item linking. Passed to session state. */
cardId?: string;
}

const MAX_GADGETS_PER_RESPONSE = 25;
Expand Down Expand Up @@ -72,7 +76,7 @@ export function createConfiguredBuilder(options: CreateBuilderOptions): BuilderT

// Initialize session state for gadgets (e.g., Finish checks PR requirement for implementation)
if (!skipSessionState) {
initSessionState(agentType, options.baseBranch);
initSessionState(agentType, options.baseBranch, options.projectId, options.cardId);
}

let builder = new AgentBuilder(client)
Expand Down
3 changes: 2 additions & 1 deletion src/agents/shared/gadgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
AddChecklist,
CreateWorkItem,
ListWorkItems,
PMDeleteChecklistItem,
PMUpdateChecklistItem,
PostComment,
ReadWorkItem,
Expand Down Expand Up @@ -73,7 +74,7 @@ export function buildWorkItemGadgets(caps: AgentCapabilities): CreateBuilderOpti
new AddChecklist(),
// UpdateChecklistItem gated by capability — prevents planning from marking items complete
// prematurely, while respond-to-planning-comment CAN update them
...(caps.canUpdateChecklists ? [new PMUpdateChecklistItem()] : []),
...(caps.canUpdateChecklists ? [new PMUpdateChecklistItem(), new PMDeleteChecklistItem()] : []),
// Session control
new Finish(),
];
Expand Down
2 changes: 2 additions & 0 deletions src/agents/shared/githubAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ export async function executeGitHubAgent<
llmCallAccumulator,
runId,
baseBranch: project.baseBranch,
projectId: project.id,
cardId: input.cardId,
...definition.builderOptions,
}),

Expand Down
7 changes: 4 additions & 3 deletions src/agents/shared/promptContext.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getTrelloConfig } from '../../pm/config.js';
import { getPMProvider } from '../../pm/index.js';
import type { ProjectConfig } from '../../types/index.js';
import type { PromptContext } from '../prompts/index.js';
Expand Down Expand Up @@ -29,8 +30,8 @@ export function buildPromptContext(
cardUrl: cardId ? pmProvider.getWorkItemUrl(cardId) : undefined,
projectId: project.id,
baseBranch: project.baseBranch,
storiesListId: project.trello?.lists?.stories,
processedLabelId: project.trello?.labels?.processed,
storiesListId: getTrelloConfig(project)?.lists?.stories,
processedLabelId: getTrelloConfig(project)?.labels?.processed,
pmType: pmProvider.type,
workItemNoun: isJira ? 'issue' : 'card',
workItemNounPlural: isJira ? 'issues' : 'cards',
Expand All @@ -50,7 +51,7 @@ export function buildPromptContext(
originalCardName: debugContext.originalCardName,
originalCardUrl: debugContext.originalCardUrl,
detectedAgentType: debugContext.detectedAgentType,
debugListId: project.trello?.lists?.debug,
debugListId: getTrelloConfig(project)?.lists?.debug,
}),
};
}
19 changes: 11 additions & 8 deletions src/api/routers/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getAllProjectCredentials } from '../../config/provider.js';
import { getDb } from '../../db/client.js';
import { findProjectByIdFromDb } from '../../db/repositories/configRepository.js';
import { projects } from '../../db/schema/index.js';
import { getJiraConfig, getTrelloConfig } from '../../pm/config.js';
import { parseRepoFullName } from '../../utils/repo.js';
import { protectedProcedure, router } from '../trpc.js';

Expand Down Expand Up @@ -78,12 +79,14 @@ async function resolveProjectContext(
const creds = await getAllProjectCredentials(projectId);

// Resolve JIRA label names from config (with defaults)
const jiraLabels = project.jira
const jiraConfig = getJiraConfig(project);
const trelloConfig = getTrelloConfig(project);
const jiraLabels = jiraConfig
? [
project.jira.labels?.processing ?? 'cascade-processing',
project.jira.labels?.processed ?? 'cascade-processed',
project.jira.labels?.error ?? 'cascade-error',
project.jira.labels?.readyToProcess ?? 'cascade-ready',
jiraConfig.labels?.processing ?? 'cascade-processing',
jiraConfig.labels?.processed ?? 'cascade-processed',
jiraConfig.labels?.error ?? 'cascade-error',
jiraConfig.labels?.readyToProcess ?? 'cascade-ready',
]
: undefined;

Expand All @@ -92,9 +95,9 @@ async function resolveProjectContext(
orgId: project.orgId,
repo: project.repo,
pmType: project.pm?.type ?? 'trello',
boardId: project.trello?.boardId,
jiraBaseUrl: project.jira?.baseUrl,
jiraProjectKey: project.jira?.projectKey,
boardId: trelloConfig?.boardId,
jiraBaseUrl: jiraConfig?.baseUrl,
jiraProjectKey: jiraConfig?.projectKey,
jiraLabels,
trelloApiKey: creds.TRELLO_API_KEY ?? '',
trelloToken: creds.TRELLO_TOKEN ?? '',
Expand Down
12 changes: 7 additions & 5 deletions src/backends/secretBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getAllProjectCredentials } from '../config/provider.js';
import { getPersonaToken } from '../github/personas.js';
import { getJiraConfig } from '../pm/config.js';
import type { AgentInput, ProjectConfig } from '../types/index.js';
import { parseRepoFullName } from '../utils/repo.js';
import type { AgentProfile } from './agent-profiles.js';
Expand Down Expand Up @@ -41,11 +42,12 @@ export async function augmentProjectSecrets(
}

// Inject JIRA integration config so cascade-tools can construct JiraPMProvider
if (project.jira) {
projectSecrets.CASCADE_JIRA_PROJECT_KEY = project.jira.projectKey;
projectSecrets.CASCADE_JIRA_BASE_URL = project.jira.baseUrl;
if (project.jira.statuses) {
projectSecrets.CASCADE_JIRA_STATUSES = JSON.stringify(project.jira.statuses);
const jiraConfig = getJiraConfig(project);
if (jiraConfig) {
projectSecrets.CASCADE_JIRA_PROJECT_KEY = jiraConfig.projectKey;
projectSecrets.CASCADE_JIRA_BASE_URL = jiraConfig.baseUrl;
if (jiraConfig.statuses) {
projectSecrets.CASCADE_JIRA_STATUSES = JSON.stringify(jiraConfig.statuses);
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/db/migrations/0014_pr_work_items.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE pr_work_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
repo_full_name TEXT NOT NULL,
pr_number INTEGER NOT NULL,
work_item_id TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT uq_pr_work_items_project_pr UNIQUE (project_id, pr_number)
);
CREATE INDEX idx_pr_work_items_work_item ON pr_work_items (work_item_id);
7 changes: 7 additions & 0 deletions src/db/migrations/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@
"when": 1748000000000,
"tag": "0013_integration_model_refactor",
"breakpoints": false
},
{
"idx": 13,
"version": "7",
"when": 1749000000000,
"tag": "0014_pr_work_items",
"breakpoints": false
}
]
}
40 changes: 40 additions & 0 deletions src/db/repositories/prWorkItemsRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { and, eq } from 'drizzle-orm';
import { getDb } from '../client.js';
import { prWorkItems } from '../schema/index.js';

/**
* Upsert a PR ↔ work item link. If a row already exists for the
* (projectId, prNumber) pair, update the work item ID.
*/
export async function linkPRToWorkItem(
projectId: string,
repoFullName: string,
prNumber: number,
workItemId: string,
): Promise<void> {
const db = getDb();
await db
.insert(prWorkItems)
.values({ projectId, repoFullName, prNumber, workItemId })
.onConflictDoUpdate({
target: [prWorkItems.projectId, prWorkItems.prNumber],
set: { workItemId, repoFullName },
});
}

/**
* Look up the work item ID linked to a PR.
* Returns null if no link exists.
*/
export async function lookupWorkItemForPR(
projectId: string,
prNumber: number,
): Promise<string | null> {
const db = getDb();
const rows = await db
.select({ workItemId: prWorkItems.workItemId })
.from(prWorkItems)
.where(and(eq(prWorkItems.projectId, projectId), eq(prWorkItems.prNumber, prNumber)))
.limit(1);
return rows.length > 0 ? rows[0].workItemId : null;
}
1 change: 1 addition & 0 deletions src/db/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export { projects } from './projects.js';
export { agentRunLlmCalls, agentRunLogs, agentRuns, debugAnalyses } from './runs.js';
export { promptPartials } from './promptPartials.js';
export { sessions, users } from './users.js';
export { prWorkItems } from './prWorkItems.js';
export { webhookLogs } from './webhookLogs.js';
20 changes: 20 additions & 0 deletions src/db/schema/prWorkItems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { index, integer, pgTable, text, timestamp, unique, uuid } from 'drizzle-orm/pg-core';
import { projects } from './projects.js';

export const prWorkItems = pgTable(
'pr_work_items',
{
id: uuid('id').primaryKey().defaultRandom(),
projectId: text('project_id')
.notNull()
.references(() => projects.id, { onDelete: 'cascade' }),
repoFullName: text('repo_full_name').notNull(),
prNumber: integer('pr_number').notNull(),
workItemId: text('work_item_id').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(table) => [
unique('uq_pr_work_items_project_pr').on(table.projectId, table.prNumber),
index('idx_pr_work_items_work_item').on(table.workItemId),
],
);
20 changes: 19 additions & 1 deletion src/gadgets/github/CreatePR.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Gadget, z } from 'llmist';
import { getBaseBranch, recordPRCreation } from '../sessionState.js';
import { linkPRToWorkItem } from '../../db/repositories/prWorkItemsRepository.js';
import { logger } from '../../utils/logging.js';
import { getBaseBranch, getCardId, getProjectId, recordPRCreation } from '../sessionState.js';
import { createPR } from './core/createPR.js';

export class CreatePR extends Gadget({
Expand Down Expand Up @@ -92,6 +94,22 @@ If hooks fail or timeout, the full output will be shown.`,

recordPRCreation(result.prUrl);

// Persist PR ↔ work item link (best-effort, don't fail PR creation)
const projectId = getProjectId();
const cardId = getCardId();
if (projectId && cardId) {
try {
await linkPRToWorkItem(projectId, result.repoFullName, result.prNumber, cardId);
} catch (err) {
logger.warn('Failed to persist PR-work-item link', {
projectId,
prNumber: result.prNumber,
cardId,
error: String(err),
});
}
}

if (result.alreadyExisted) {
return `PR already exists for this branch: #${result.prNumber} — ${result.prUrl}`;
}
Expand Down
9 changes: 8 additions & 1 deletion src/gadgets/github/core/createPR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface CreatePRParams {
export interface CreatePRResult {
prNumber: number;
prUrl: string;
repoFullName: string;
alreadyExisted: boolean;
}

Expand Down Expand Up @@ -108,7 +109,12 @@ export async function createPR(params: CreatePRParams): Promise<CreatePRResult>
draft: params.draft,
});

return { prNumber: pr.number, prUrl: pr.htmlUrl, alreadyExisted: false };
return {
prNumber: pr.number,
prUrl: pr.htmlUrl,
repoFullName: `${owner}/${repo}`,
alreadyExisted: false,
};
} catch (error) {
if (
error instanceof Error &&
Expand All @@ -121,6 +127,7 @@ export async function createPR(params: CreatePRParams): Promise<CreatePRResult>
return {
prNumber: existingPR.number,
prUrl: existingPR.htmlUrl,
repoFullName: `${owner}/${repo}`,
alreadyExisted: true,
};
}
Expand Down
12 changes: 0 additions & 12 deletions src/gadgets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,5 @@ export { VerifyChanges } from './VerifyChanges.js';
// Search gadgets
export { RipGrep } from './RipGrep.js';
export { AstGrep } from './AstGrep.js';

// Trello gadgets
export {
ReadTrelloCard,
PostTrelloComment,
UpdateTrelloCard,
CreateTrelloCard,
ListTrelloCards,
GetMyRecentActivity,
AddChecklistToCard,
} from './trello/index.js';

// GitHub gadgets
export { GetPRDetails, GetPRComments, ReplyToReviewComment } from './github/index.js';
Loading