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
21 changes: 16 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,24 @@ CASCADE stores all project configuration in PostgreSQL (Supabase). The `config/p
### Database Scripts

```bash
npm run db:generate # Generate migration SQL from schema changes
npm run db:migrate # Apply migrations
npm run db:push # Push schema directly (dev)
npm run db:studio # Open Drizzle Studio
npm run db:seed # Seed DB from config/projects.json
npm run db:generate # Generate migration SQL from schema changes
npm run db:migrate # Apply pending migrations
npm run db:push # Push schema directly (dev only)
npm run db:studio # Open Drizzle Studio
npm run db:seed # Seed DB from config/projects.json
npm run db:bootstrap-journal # Bootstrap migration journal (one-time setup for existing DBs)
```

### Migration Workflow

Migrations are hand-written SQL files in `src/db/migrations/` tracked by drizzle-kit's journal (`meta/_journal.json`). When adding a new migration:

1. Create `src/db/migrations/NNNN_description.sql`
2. Add a corresponding entry to `src/db/migrations/meta/_journal.json` with a unique `when` timestamp (ms since epoch) and `tag` matching the filename without `.sql`
3. Run `npm run db:migrate` to apply

For databases initially set up with `drizzle-kit push` (no migration journal), run `npm run db:bootstrap-journal` once to register existing migrations in the `drizzle.__drizzle_migrations` tracking table.

### Per-Project Secrets

Credentials are stored in the `credentials` table (org-scoped) with optional per-project overrides via `project_credential_overrides`.
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
"tool:run-local": "node --env-file=.env --import tsx tools/run-local.ts",
"tool:debug-run": "node --env-file=.env --import tsx tools/debug-run.ts",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"db:seed": "node --env-file=.env --import tsx tools/seed-config-from-json.ts"
"db:migrate": "node --env-file=.env ./node_modules/.bin/drizzle-kit migrate",
"db:push": "node --env-file=.env ./node_modules/.bin/drizzle-kit push",
"db:studio": "node --env-file=.env ./node_modules/.bin/drizzle-kit studio",
"db:seed": "node --env-file=.env --import tsx tools/seed-config-from-json.ts",
"db:bootstrap-journal": "node --env-file=.env --import tsx tools/db-bootstrap-journal.ts"
},
"keywords": [
"trello",
Expand Down
279 changes: 47 additions & 232 deletions src/agents/respond-to-pr-comment.ts
Original file line number Diff line number Diff line change
@@ -1,132 +1,19 @@
import { githubClient } from '../github/client.js';
import type { AgentResult, CascadeConfig, ProjectConfig } from '../types/index.js';
import type { AgentResult } from '../types/index.js';
import { createPRAgentGadgets } from './shared/gadgets.js';
import { type GitHubAgentDefinition, executeGitHubAgent } from './shared/githubAgent.js';
import {
type GitHubAgentContext,
type GitHubAgentDefinition,
type GitHubAgentInput,
createInitialPRComment,
executeGitHubAgent,
} from './shared/githubAgent.js';
import { resolveModelConfig } from './shared/modelResolution.js';
import {
formatPRComments,
formatPRDetails,
formatPRDiff,
formatPRIssueComments,
formatPRReviews,
} from './shared/prFormatting.js';
import {
injectContextFiles,
injectDirectoryListing,
injectSquintContext,
injectSyntheticCall,
} from './shared/syntheticCalls.js';

interface RespondToPRCommentAgentInput extends GitHubAgentInput {
triggerCommentId: number;
triggerCommentBody: string;
triggerCommentPath: string;
triggerCommentUrl: string;
acknowledgmentCommentId?: number;
}

// ============================================================================
// Context Building
// ============================================================================

interface PRCommentContextData extends GitHubAgentContext {
contextFiles: Awaited<ReturnType<typeof resolveModelConfig>>['contextFiles'];
prDetailsFormatted: string;
commentsFormatted: string;
reviewsFormatted: string;
issueCommentsFormatted: string;
diffFormatted: string;
}

async function buildPRCommentContext(
owner: string,
repo: string,
prNumber: number,
prBranch: string,
repoDir: string,
project: ProjectConfig,
config: CascadeConfig,
log: { info: (msg: string, ctx?: Record<string, unknown>) => void },
modelOverride?: string,
): Promise<PRCommentContextData> {
// respond-to-pr-comment shares model/iteration config with 'review' agent
const { systemPrompt, model, maxIterations, contextFiles } = await resolveModelConfig({
agentType: 'respond-to-pr-comment',
project,
config,
repoDir,
modelOverride,
configKey: 'review',
});

// Fetch PR details, comments, reviews, issue comments, and diff
log.info('Fetching PR details, comments, reviews, issue comments, and diff', {
owner,
repo,
prNumber,
});
const prDetails = await githubClient.getPR(owner, repo, prNumber);
const prComments = await githubClient.getPRReviewComments(owner, repo, prNumber);
const prReviews = await githubClient.getPRReviews(owner, repo, prNumber);
const prIssueComments = await githubClient.getPRIssueComments(owner, repo, prNumber);
const prDiff = await githubClient.getPRDiff(owner, repo, prNumber);

// Format PR data
const prDetailsFormatted = formatPRDetails(prDetails);
const commentsFormatted = formatPRComments(prComments);
const reviewsFormatted = formatPRReviews(prReviews);
const issueCommentsFormatted = formatPRIssueComments(prIssueComments);
const diffFormatted = formatPRDiff(prDiff);

// Build prompt
const prompt = buildPRCommentPrompt(prBranch, prNumber, owner, repo);

return {
systemPrompt,
model,
maxIterations,
contextFiles,
prDetailsFormatted,
commentsFormatted,
reviewsFormatted,
issueCommentsFormatted,
diffFormatted,
prompt,
};
}

function buildPRCommentPrompt(
prBranch: string,
prNumber: number,
owner: string,
repo: string,
): string {
return `You are on the branch \`${prBranch}\` for PR #${prNumber}.

A user @mentioned you in a PR comment. Read their request and execute it.

## GitHub Context

Owner: ${owner}
Repo: ${repo}
PR Number: ${prNumber}

Use these values when calling GitHub gadgets (GetPRComments, ReplyToReviewComment, PostPRComment, UpdatePRComment).`;
}

// ============================================================================
// Agent Definition
// ============================================================================
type PRResponseAgentInput,
type PRResponseContextData,
buildPRResponseContext,
buildPRResponsePrompt,
injectPRResponseSyntheticCalls,
postInitialPRResponseComment,
} from './shared/prResponseAgent.js';
import { injectSyntheticCall } from './shared/syntheticCalls.js';

const respondToPRCommentDefinition: GitHubAgentDefinition<
RespondToPRCommentAgentInput,
PRCommentContextData
PRResponseAgentInput,
PRResponseContextData
> = {
agentType: 'respond-to-pr-comment',
headerMessage: '🤖 Working on your request...',
Expand All @@ -136,21 +23,10 @@ const respondToPRCommentDefinition: GitHubAgentDefinition<

getGadgets: () => createPRAgentGadgets({ includeReviewComments: true }),

async postInitialComment(input, id, headerMessage) {
if (input.acknowledgmentCommentId) {
const comment = await githubClient.updatePRComment(
id.owner,
id.repo,
input.acknowledgmentCommentId,
headerMessage,
);
return { id: comment.id, htmlUrl: comment.htmlUrl, gadgetName: 'UpdatePRComment' };
}
return createInitialPRComment(input.prNumber, id, headerMessage);
},
postInitialComment: postInitialPRResponseComment,

buildContext: ({ owner, repo }, input, repoDir, log) =>
buildPRCommentContext(
buildPRResponseContext(
owner,
repo,
input.prNumber,
Expand All @@ -159,108 +35,47 @@ const respondToPRCommentDefinition: GitHubAgentDefinition<
input.project,
input.config,
log,
'respond-to-pr-comment',
(prBranch, prNumber, o, r) =>
buildPRResponsePrompt(
prBranch,
prNumber,
o,
r,
'A user @mentioned you in a PR comment. Read their request and execute it.',
'GetPRComments, ReplyToReviewComment, PostPRComment, UpdatePRComment',
),
input.modelOverride,
),

async injectSyntheticCalls({
builder,
ctx,
trackingContext,
repoDir,
id: { owner, repo },
input,
}) {
let b = injectDirectoryListing(builder, trackingContext);

// Inject the triggering comment prominently
b = injectSyntheticCall(
b,
trackingContext,
'TriggeringComment',
{
comment:
'The @mention comment that triggered this agent — this is your primary instruction',
commentId: input.triggerCommentId,
url: input.triggerCommentUrl,
path: input.triggerCommentPath || '(general PR comment)',
},
input.triggerCommentBody,
'gc_triggering_comment',
);

b = injectSyntheticCall(
b,
trackingContext,
'GetPRDetails',
{ comment: 'Pre-fetching PR details for context', owner, repo, prNumber: input.prNumber },
ctx.prDetailsFormatted,
'gc_pr_details',
);

b = injectSyntheticCall(
b,
trackingContext,
'GetPRComments',
{
comment: 'Pre-fetching line-specific review comments for context',
owner,
repo,
prNumber: input.prNumber,
async injectSyntheticCalls(params) {
return injectPRResponseSyntheticCalls(params, {
preSyntheticCalls: (builder, trackingContext, input) =>
injectSyntheticCall(
builder,
trackingContext,
'TriggeringComment',
{
comment:
'The @mention comment that triggered this agent — this is your primary instruction',
commentId: input.triggerCommentId,
url: input.triggerCommentUrl,
path: input.triggerCommentPath || '(general PR comment)',
},
input.triggerCommentBody,
'gc_triggering_comment',
),
commentDescriptions: {
prComments: 'Pre-fetching line-specific review comments for context',
prReviews: 'Pre-fetching review submissions for context',
prIssueComments: 'Pre-fetching general PR comments for context',
},
ctx.commentsFormatted,
'gc_pr_comments',
);

b = injectSyntheticCall(
b,
trackingContext,
'GetPRReviews',
{
comment: 'Pre-fetching review submissions for context',
owner,
repo,
prNumber: input.prNumber,
},
ctx.reviewsFormatted,
'gc_pr_reviews',
);

b = injectSyntheticCall(
b,
trackingContext,
'GetPRIssueComments',
{
comment: 'Pre-fetching general PR comments for context',
owner,
repo,
prNumber: input.prNumber,
},
ctx.issueCommentsFormatted,
'gc_pr_issue_comments',
);

b = injectSyntheticCall(
b,
trackingContext,
'GetPRDiff',
{ comment: 'Pre-fetching PR diff for context', owner, repo, prNumber: input.prNumber },
ctx.diffFormatted,
'gc_pr_diff',
);

b = injectContextFiles(b, trackingContext, ctx.contextFiles);
b = injectSquintContext(b, trackingContext, repoDir);

return b;
});
},
};

// ============================================================================
// PR Comment Agent Execution
// ============================================================================

export async function executeRespondToPRCommentAgent(
input: RespondToPRCommentAgentInput,
input: PRResponseAgentInput,
): Promise<AgentResult> {
return executeGitHubAgent(respondToPRCommentDefinition, input);
}
Loading