ticket-to-pr 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +114 -9
- package/package.json +1 -1
- package/prompts/retro.md +27 -0
package/dist/index.js
CHANGED
|
@@ -52,6 +52,7 @@ function log(color, label, msg) {
|
|
|
52
52
|
const reviewPrompt = readFileSync(join(PACKAGE_ROOT, 'prompts', 'review.md'), 'utf-8');
|
|
53
53
|
const executePrompt = readFileSync(join(PACKAGE_ROOT, 'prompts', 'execute.md'), 'utf-8');
|
|
54
54
|
const diffReviewPrompt = readFileSync(join(PACKAGE_ROOT, 'prompts', 'diff-review.md'), 'utf-8');
|
|
55
|
+
const retroPrompt = readFileSync(join(PACKAGE_ROOT, 'prompts', 'retro.md'), 'utf-8');
|
|
55
56
|
// -- Agent Runner --
|
|
56
57
|
async function runReviewAgent(ticket) {
|
|
57
58
|
const projectDir = getProjectDir(ticket.project);
|
|
@@ -275,6 +276,108 @@ async function runDiffReviewAgent(worktreeDir, baseBranch, spec, description, af
|
|
|
275
276
|
cost,
|
|
276
277
|
};
|
|
277
278
|
}
|
|
279
|
+
async function runRetroAgent(projectDir, worktreeDir, baseBranch, ticket, outcome) {
|
|
280
|
+
try {
|
|
281
|
+
// Get the diff (may be empty if agent failed before committing)
|
|
282
|
+
let diff = '';
|
|
283
|
+
try {
|
|
284
|
+
diff = execSync(`git diff origin/${shellEscape(baseBranch)}...HEAD`, {
|
|
285
|
+
cwd: worktreeDir, stdio: 'pipe', timeout: 15_000,
|
|
286
|
+
}).toString();
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// No diff available
|
|
290
|
+
}
|
|
291
|
+
// Read existing learnings to avoid repeats
|
|
292
|
+
const existingLearnings = readLearnings(projectDir);
|
|
293
|
+
// Build the retro prompt
|
|
294
|
+
const parts = [
|
|
295
|
+
retroPrompt,
|
|
296
|
+
'',
|
|
297
|
+
`## Ticket: ${ticket.title}`,
|
|
298
|
+
'',
|
|
299
|
+
'**Description**:',
|
|
300
|
+
ticket.description,
|
|
301
|
+
'',
|
|
302
|
+
'**Spec**:',
|
|
303
|
+
ticket.spec ?? '(none)',
|
|
304
|
+
'',
|
|
305
|
+
`## Outcome: ${outcome.success ? 'SUCCESS' : 'FAILED'}`,
|
|
306
|
+
];
|
|
307
|
+
if (!outcome.success && outcome.error) {
|
|
308
|
+
parts.push('', '**Error**:', outcome.error.slice(0, 1000));
|
|
309
|
+
}
|
|
310
|
+
if (outcome.diffReviewIssues && outcome.diffReviewIssues.length > 0) {
|
|
311
|
+
parts.push('', '**Diff Review Issues**:', ...outcome.diffReviewIssues.map(i => `- ${i}`));
|
|
312
|
+
}
|
|
313
|
+
if (outcome.buildFailed) {
|
|
314
|
+
parts.push('', '**Build**: FAILED');
|
|
315
|
+
}
|
|
316
|
+
if (diff) {
|
|
317
|
+
const truncatedDiff = diff.length > 50_000 ? diff.slice(0, 50_000) + '\n...(truncated)' : diff;
|
|
318
|
+
parts.push('', '## Diff', '```diff', truncatedDiff, '```');
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
parts.push('', '## Diff', 'No diff available (agent may have failed before committing).');
|
|
322
|
+
}
|
|
323
|
+
if (existingLearnings) {
|
|
324
|
+
parts.push('', '## Existing Learnings (do not repeat these)', existingLearnings);
|
|
325
|
+
}
|
|
326
|
+
const prompt = parts.join('\n');
|
|
327
|
+
const messages = query({
|
|
328
|
+
prompt,
|
|
329
|
+
options: {
|
|
330
|
+
model: CONFIG.DIFF_REVIEW_MODEL, // Reuse Haiku config
|
|
331
|
+
cwd: projectDir,
|
|
332
|
+
tools: ['Read', 'Glob', 'Grep'],
|
|
333
|
+
allowedTools: ['Read', 'Glob', 'Grep'],
|
|
334
|
+
maxTurns: 5,
|
|
335
|
+
maxBudgetUsd: 0.25,
|
|
336
|
+
permissionMode: 'bypassPermissions',
|
|
337
|
+
allowDangerouslySkipPermissions: true,
|
|
338
|
+
settingSources: ['project'],
|
|
339
|
+
systemPrompt: { type: 'preset', preset: 'claude_code' },
|
|
340
|
+
stderr: () => { },
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
let output = '';
|
|
344
|
+
for await (const message of messages) {
|
|
345
|
+
if (message.type === 'assistant') {
|
|
346
|
+
const content = message.message?.content;
|
|
347
|
+
if (Array.isArray(content)) {
|
|
348
|
+
for (const block of content) {
|
|
349
|
+
if (block.type === 'text')
|
|
350
|
+
output = block.text;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else if (typeof content === 'string') {
|
|
354
|
+
output = content;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (message.type === 'result') {
|
|
358
|
+
if ('result' in message && message.result) {
|
|
359
|
+
output = message.result;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Only save if the retro produced something meaningful
|
|
364
|
+
const trimmed = output.trim();
|
|
365
|
+
if (trimmed && !trimmed.toLowerCase().includes('no new learnings')) {
|
|
366
|
+
const header = outcome.success
|
|
367
|
+
? `**Retro (success): ${ticket.title}**`
|
|
368
|
+
: `**Retro (failed): ${ticket.title}**`;
|
|
369
|
+
appendLearning(projectDir, `${header}\n${trimmed}`);
|
|
370
|
+
log(DIM, 'RETRO', `Saved ${trimmed.split('\n').filter(l => l.startsWith('-') || l.startsWith('*')).length} learnings`);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
log(DIM, 'RETRO', 'No new learnings');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch (e) {
|
|
377
|
+
// Retro is best-effort — never block the pipeline
|
|
378
|
+
log(DIM, 'RETRO', `Skipped: ${e instanceof Error ? e.message : e}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
278
381
|
async function runExecuteAgent(ticket) {
|
|
279
382
|
const projectDir = getProjectDir(ticket.project);
|
|
280
383
|
if (!projectDir) {
|
|
@@ -303,6 +406,7 @@ async function runExecuteAgent(ticket) {
|
|
|
303
406
|
createWorktree(projectDir, branchName, worktreeDir, baseBranch);
|
|
304
407
|
let cost = 0;
|
|
305
408
|
let commitCount = 0;
|
|
409
|
+
let retroOutcome = { success: false };
|
|
306
410
|
try {
|
|
307
411
|
const promptParts = [
|
|
308
412
|
executePrompt,
|
|
@@ -402,6 +506,7 @@ async function runExecuteAgent(ticket) {
|
|
|
402
506
|
const diffReview = await runDiffReviewAgent(worktreeDir, baseBranch, ticket.spec ?? '', ticket.description, []);
|
|
403
507
|
cost += diffReview.cost;
|
|
404
508
|
if (!diffReview.result.approved) {
|
|
509
|
+
retroOutcome = { success: false, error: 'Diff review rejected the changes', diffReviewIssues: diffReview.result.issues };
|
|
405
510
|
throw new Error(`Diff review failed:\n${diffReview.result.issues.map(i => ` - ${i}`).join('\n')}`);
|
|
406
511
|
}
|
|
407
512
|
log(GREEN, 'REVIEW', `Diff review passed: ${diffReview.result.summary}`);
|
|
@@ -484,11 +589,7 @@ async function runExecuteAgent(ticket) {
|
|
|
484
589
|
`Cost: $${cost.toFixed(2)} | Duration: ${duration}s`,
|
|
485
590
|
].join('\n');
|
|
486
591
|
await addComment(ticket.id, comment);
|
|
487
|
-
|
|
488
|
-
`**Execute: ${ticket.title}**`,
|
|
489
|
-
`Branch: ${branchName}, Commits: ${commitCount}`,
|
|
490
|
-
`Cost: $${cost.toFixed(2)}`,
|
|
491
|
-
].join('\n'));
|
|
592
|
+
retroOutcome = { success: true };
|
|
492
593
|
log(GREEN, 'EXECUTE', `Done: branch=${branchName} cost=$${cost.toFixed(2)}${prUrl ? ` pr=${prUrl}` : ''}`);
|
|
493
594
|
}
|
|
494
595
|
catch (error) {
|
|
@@ -502,13 +603,17 @@ async function runExecuteAgent(ticket) {
|
|
|
502
603
|
`Cost: $${cost.toFixed(2)} | Duration: ${duration}s`,
|
|
503
604
|
].join('\n');
|
|
504
605
|
await addComment(ticket.id, comment);
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
606
|
+
retroOutcome = {
|
|
607
|
+
success: false,
|
|
608
|
+
error: errMsg,
|
|
609
|
+
buildFailed: errMsg.includes('Build validation failed'),
|
|
610
|
+
};
|
|
509
611
|
throw error;
|
|
510
612
|
}
|
|
511
613
|
finally {
|
|
614
|
+
// Run retro before cleaning up worktree (so it can read the diff)
|
|
615
|
+
log(DIM, 'RETRO', 'Running post-execution retrospective...');
|
|
616
|
+
await runRetroAgent(projectDir, worktreeDir, baseBranch, ticket, retroOutcome);
|
|
512
617
|
// Always clean up the worktree
|
|
513
618
|
removeWorktree(projectDir, worktreeDir);
|
|
514
619
|
}
|
package/package.json
CHANGED
package/prompts/retro.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
You are conducting a retrospective on an AI agent's work on a codebase.
|
|
2
|
+
|
|
3
|
+
## Your Task
|
|
4
|
+
Analyze what happened during this ticket execution and extract lessons that will help future agent runs on this same project. You are writing notes for a future AI agent, not a human.
|
|
5
|
+
|
|
6
|
+
## What to Look For
|
|
7
|
+
|
|
8
|
+
### On Success
|
|
9
|
+
- **Conventions discovered**: file naming, import patterns, export style, component structure, API response shapes, error handling patterns
|
|
10
|
+
- **What worked**: approaches or patterns that led to clean implementation
|
|
11
|
+
- **Codebase quirks**: path aliases, custom configs, non-obvious setup requirements, framework-specific patterns
|
|
12
|
+
|
|
13
|
+
### On Failure
|
|
14
|
+
- **Root cause**: what specifically went wrong and why (not just the error message)
|
|
15
|
+
- **What to do differently**: concrete, actionable advice for next time
|
|
16
|
+
- **Codebase constraints**: things the agent didn't know about that caused the failure
|
|
17
|
+
|
|
18
|
+
### Always
|
|
19
|
+
- **Capability assessment**: what types of changes are easy/hard in this project
|
|
20
|
+
- **Suggestions**: improvements to the project's CLAUDE.md or configuration that would help future runs
|
|
21
|
+
|
|
22
|
+
## Output Rules
|
|
23
|
+
- Write 2-5 bullet points. Each must be a specific, actionable lesson.
|
|
24
|
+
- Start each bullet with a category tag: `[convention]`, `[mistake]`, `[capability]`, or `[suggestion]`
|
|
25
|
+
- Be specific to THIS project. "Use TypeScript" is useless. "This project uses strict TypeScript with no implicit any — always add explicit return types on exported functions" is useful.
|
|
26
|
+
- Don't repeat lessons that already exist in the project learnings.
|
|
27
|
+
- If nothing useful was learned (e.g., trivial change, obvious outcome), just write: `No new learnings.`
|