ticket-to-pr 1.4.0 → 1.4.2
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/README.md +44 -9
- package/dist/config.d.ts +4 -1
- package/dist/config.js +2 -1
- package/dist/index.js +329 -16
- package/dist/lib/notion.d.ts +15 -0
- package/dist/lib/notion.js +127 -10
- package/package.json +1 -1
- package/prompts/feedback.md +34 -0
- package/prompts/retro.md +27 -0
package/README.md
CHANGED
|
@@ -74,14 +74,17 @@ Execute Claude creates branch, implements code, commits changes.
|
|
|
74
74
|
In Progress Set automatically when the execute agent starts working.
|
|
75
75
|
|
|
|
76
76
|
v
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
Testing Branch pushed. PR created. QA checklist posted as comment.
|
|
78
|
+
| Dev reviews PR, merges, deploys. Tester verifies in prod.
|
|
79
|
+
v
|
|
80
|
+
Done Tester drags here after verifying. Comments with feedback.
|
|
81
|
+
TicketToPR reads comments, extracts learnings, saves to project memory.
|
|
79
82
|
|
|
80
83
|
Failed Agent errored. Error details on ticket.
|
|
81
84
|
Drag back to Review or Execute to retry.
|
|
82
85
|
```
|
|
83
86
|
|
|
84
|
-
The rhythm is **you, AI, you, AI, AI, you** —
|
|
87
|
+
The rhythm is **you, AI, you, AI, AI, you, AI** — human touchpoints at Review, Execute, and Done. You're always the decision-maker. The AI is always the worker.
|
|
85
88
|
|
|
86
89
|
### What It's Great At
|
|
87
90
|
|
|
@@ -165,11 +168,15 @@ Create a new **Board view** database in Notion with these properties:
|
|
|
165
168
|
| `Branch` | Text | Git branch name (written by AI) |
|
|
166
169
|
| `Cost` | Text | USD spent on the Claude run |
|
|
167
170
|
| `PR URL` | URL | GitHub pull request link (written by AI) |
|
|
171
|
+
| `Reviewed At` | Date | When review completed (sort Scored by this) |
|
|
172
|
+
| `Executed At` | Date | When execution completed (sort Testing by this) |
|
|
173
|
+
| `Failed At` | Date | When ticket failed (sort Failed by this) |
|
|
174
|
+
| `Done At` | Date | When feedback processed (sort Done by this) |
|
|
168
175
|
|
|
169
|
-
Add these **
|
|
176
|
+
Add these **8 status columns**:
|
|
170
177
|
|
|
171
178
|
```
|
|
172
|
-
Backlog | Review | Scored | Execute | In Progress |
|
|
179
|
+
Backlog | Review | Scored | Execute | In Progress | Testing | Done | Failed
|
|
173
180
|
```
|
|
174
181
|
|
|
175
182
|
Connect the integration: **"..." menu** on the database page -> **Connections** -> search **TicketToPR** -> add it.
|
|
@@ -381,7 +388,7 @@ Learnings are stored in each project directory at `.ticket-to-pr/learnings.md` (
|
|
|
381
388
|
6. Run `ticket-to-pr --once` and watch it score the ticket
|
|
382
389
|
7. Check Notion — ticket should be in **Scored** with Ease, Confidence, Spec, Impact filled in
|
|
383
390
|
8. Drag to **Execute**, run `ticket-to-pr --once` again
|
|
384
|
-
9. Check Notion — ticket should be in **
|
|
391
|
+
9. Check Notion — ticket should be in **Testing** with Branch, Cost, and PR link
|
|
385
392
|
|
|
386
393
|
Typical cost for this test: **~$0.49** ($0.22 review + $0.27 execute).
|
|
387
394
|
|
|
@@ -486,9 +493,37 @@ The execute agent implements the code based on the spec. When the review agent g
|
|
|
486
493
|
7. All checks pass: pushes branch to origin
|
|
487
494
|
8. Creates a GitHub PR via `gh pr create` targeting the base branch (unless `skipPR` is enabled)
|
|
488
495
|
9. PR URL written back to the Notion ticket
|
|
489
|
-
10. Ticket moves to **
|
|
496
|
+
10. Ticket moves to **Testing**
|
|
490
497
|
11. Any check fails (diff review, build, blocked files): no code is pushed, ticket moves to **Failed**
|
|
491
498
|
|
|
499
|
+
## Human Feedback Loop
|
|
500
|
+
|
|
501
|
+
Non-technical team members can test results and give feedback directly in Notion. TicketToPR reads their comments and saves learnings to improve future runs.
|
|
502
|
+
|
|
503
|
+
### Workflow for testers (PMs, founders, QA)
|
|
504
|
+
|
|
505
|
+
1. A ticket lands in **Testing** with a QA checklist comment — the dev reviews and merges the PR, deploys
|
|
506
|
+
4. Test the change and **comment on the ticket** with what you found
|
|
507
|
+
5. **Drag the ticket**:
|
|
508
|
+
- To **Done** if it works
|
|
509
|
+
- To **Failed** if something's wrong (explain what happened in a comment)
|
|
510
|
+
|
|
511
|
+
That's it. TicketToPR reads your comments, extracts learnings, and saves them so the AI makes fewer mistakes over time.
|
|
512
|
+
|
|
513
|
+
### Feedback on Failed tickets
|
|
514
|
+
|
|
515
|
+
When you drag a ticket to Failed and comment why, the system saves your context alongside the technical error. This is especially valuable because humans often know *why* something failed better than the error log — "this broke because we changed the API last week" or "wrong approach, we use Redis for this."
|
|
516
|
+
|
|
517
|
+
### What gets saved
|
|
518
|
+
|
|
519
|
+
Comments are processed by a lightweight AI agent that extracts tagged learnings:
|
|
520
|
+
- `[feedback]` — general observations about the result
|
|
521
|
+
- `[preference]` — style or approach preferences ("put buttons in the header, not sidebar")
|
|
522
|
+
- `[bug]` — things that broke ("this change broke the checkout flow")
|
|
523
|
+
- `[quality]` — performance, UX, or code quality notes
|
|
524
|
+
|
|
525
|
+
These learnings are injected into future agent prompts for the same project.
|
|
526
|
+
|
|
492
527
|
## Costs
|
|
493
528
|
|
|
494
529
|
TicketToPR itself is free. You pay Anthropic for Claude API usage. Based on real usage:
|
|
@@ -614,7 +649,7 @@ Add to `projects.json` (or re-run `ticket-to-pr init`):
|
|
|
614
649
|
| Build validation fails | Ticket -> Failed with command, directory, and build output (up to 500 chars) |
|
|
615
650
|
| Blocked file violation | Ticket -> Failed with list of matched files and patterns. No code is pushed. |
|
|
616
651
|
| Push fails | Ticket -> Failed, branch remains local |
|
|
617
|
-
| PR creation fails | Ticket still moves to
|
|
652
|
+
| PR creation fails | Ticket still moves to Testing (best-effort) |
|
|
618
653
|
| Duplicate poll trigger | Skipped via in-memory lock per ticket ID |
|
|
619
654
|
| Agent hangs > 30 min | Lock force-released, ticket -> Failed |
|
|
620
655
|
|
|
@@ -673,7 +708,7 @@ Add to `projects.json` (or re-run `ticket-to-pr init`):
|
|
|
673
708
|
- Authenticate: `gh auth login`
|
|
674
709
|
- Verify: `gh auth status`
|
|
675
710
|
- The project must have a GitHub `origin` remote
|
|
676
|
-
- PR creation is best-effort — the ticket still moves to
|
|
711
|
+
- PR creation is best-effort — the ticket still moves to Testing without it
|
|
677
712
|
|
|
678
713
|
</details>
|
|
679
714
|
|
package/dist/config.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ export declare const CONFIG: {
|
|
|
6
6
|
readonly SCORED: "Scored";
|
|
7
7
|
readonly EXECUTE: "Execute";
|
|
8
8
|
readonly IN_PROGRESS: "In Progress";
|
|
9
|
-
readonly
|
|
9
|
+
readonly TESTING: "Testing";
|
|
10
|
+
readonly DONE: "Done";
|
|
10
11
|
readonly FAILED: "Failed";
|
|
11
12
|
};
|
|
12
13
|
readonly REVIEW_BUDGET_USD: 2;
|
|
@@ -88,6 +89,8 @@ export interface TicketDetails extends NotionTicket {
|
|
|
88
89
|
bodyBlocks: string;
|
|
89
90
|
spec?: string;
|
|
90
91
|
impact?: string;
|
|
92
|
+
ease?: number;
|
|
93
|
+
confidence?: number;
|
|
91
94
|
}
|
|
92
95
|
export interface ReviewOutput {
|
|
93
96
|
easeScore: number;
|
package/dist/config.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -3,9 +3,9 @@ import { execSync } from 'node:child_process';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
5
5
|
import { CONFIG, REVIEW_OUTPUT_SCHEMA, DIFF_REVIEW_SCHEMA, isPro } from './config.js';
|
|
6
|
-
import { sleep, clamp, extractJsonFromOutput, shellEscape,
|
|
6
|
+
import { sleep, clamp, extractJsonFromOutput, shellEscape, loadEnv, parseEnvFile, createWorktree, removeWorktree, getDefaultBranch, validateNoBlockedFiles, readLearnings, appendLearning } from './lib/utils.js';
|
|
7
7
|
import { getProjectDir, getProjectNames, getBuildCommand, getBaseBranch, getBlockedFiles, getSkipPR, getDevAccess, getEnvFile } from './lib/projects.js';
|
|
8
|
-
import { fetchTicketsByStatus, fetchTicketDetails, writeReviewResults, writeExecutionResults, moveTicketStatus, writeFailure, addComment, } from './lib/notion.js';
|
|
8
|
+
import { fetchTicketsByStatus, fetchTicketDetails, writeReviewResults, writeExecutionResults, moveTicketStatus, writeFailure, addComment, fetchComments, hasFeedbackMarker, hasTestingMarker, trySetDate, } from './lib/notion.js';
|
|
9
9
|
import { PACKAGE_ROOT, CONFIG_DIR } from './lib/paths.js';
|
|
10
10
|
// Load .env.local from the user's working directory
|
|
11
11
|
loadEnv(join(CONFIG_DIR, '.env.local'));
|
|
@@ -32,6 +32,8 @@ const DRY_RUN = args.includes('--dry-run');
|
|
|
32
32
|
const ONCE = args.includes('--once');
|
|
33
33
|
// -- State --
|
|
34
34
|
const activeLocks = new Map();
|
|
35
|
+
const feedbackProcessed = new Set();
|
|
36
|
+
const testingNotified = new Set();
|
|
35
37
|
let shuttingDown = false;
|
|
36
38
|
let activeAgentCount = 0;
|
|
37
39
|
// -- Logging --
|
|
@@ -52,6 +54,8 @@ function log(color, label, msg) {
|
|
|
52
54
|
const reviewPrompt = readFileSync(join(PACKAGE_ROOT, 'prompts', 'review.md'), 'utf-8');
|
|
53
55
|
const executePrompt = readFileSync(join(PACKAGE_ROOT, 'prompts', 'execute.md'), 'utf-8');
|
|
54
56
|
const diffReviewPrompt = readFileSync(join(PACKAGE_ROOT, 'prompts', 'diff-review.md'), 'utf-8');
|
|
57
|
+
const retroPrompt = readFileSync(join(PACKAGE_ROOT, 'prompts', 'retro.md'), 'utf-8');
|
|
58
|
+
const feedbackPrompt = readFileSync(join(PACKAGE_ROOT, 'prompts', 'feedback.md'), 'utf-8');
|
|
55
59
|
// -- Agent Runner --
|
|
56
60
|
async function runReviewAgent(ticket) {
|
|
57
61
|
const projectDir = getProjectDir(ticket.project);
|
|
@@ -275,6 +279,278 @@ async function runDiffReviewAgent(worktreeDir, baseBranch, spec, description, af
|
|
|
275
279
|
cost,
|
|
276
280
|
};
|
|
277
281
|
}
|
|
282
|
+
async function runRetroAgent(projectDir, worktreeDir, baseBranch, ticket, outcome) {
|
|
283
|
+
try {
|
|
284
|
+
// Get the diff (may be empty if agent failed before committing)
|
|
285
|
+
let diff = '';
|
|
286
|
+
try {
|
|
287
|
+
diff = execSync(`git diff origin/${shellEscape(baseBranch)}...HEAD`, {
|
|
288
|
+
cwd: worktreeDir, stdio: 'pipe', timeout: 15_000,
|
|
289
|
+
}).toString();
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// No diff available
|
|
293
|
+
}
|
|
294
|
+
// Read existing learnings to avoid repeats
|
|
295
|
+
const existingLearnings = readLearnings(projectDir);
|
|
296
|
+
// Build the retro prompt
|
|
297
|
+
const parts = [
|
|
298
|
+
retroPrompt,
|
|
299
|
+
'',
|
|
300
|
+
`## Ticket: ${ticket.title}`,
|
|
301
|
+
'',
|
|
302
|
+
'**Description**:',
|
|
303
|
+
ticket.description,
|
|
304
|
+
'',
|
|
305
|
+
'**Spec**:',
|
|
306
|
+
ticket.spec ?? '(none)',
|
|
307
|
+
'',
|
|
308
|
+
`## Outcome: ${outcome.success ? 'SUCCESS' : 'FAILED'}`,
|
|
309
|
+
];
|
|
310
|
+
if (!outcome.success && outcome.error) {
|
|
311
|
+
parts.push('', '**Error**:', outcome.error.slice(0, 1000));
|
|
312
|
+
}
|
|
313
|
+
if (outcome.diffReviewIssues && outcome.diffReviewIssues.length > 0) {
|
|
314
|
+
parts.push('', '**Diff Review Issues**:', ...outcome.diffReviewIssues.map(i => `- ${i}`));
|
|
315
|
+
}
|
|
316
|
+
if (outcome.buildFailed) {
|
|
317
|
+
parts.push('', '**Build**: FAILED');
|
|
318
|
+
}
|
|
319
|
+
if (diff) {
|
|
320
|
+
const truncatedDiff = diff.length > 50_000 ? diff.slice(0, 50_000) + '\n...(truncated)' : diff;
|
|
321
|
+
parts.push('', '## Diff', '```diff', truncatedDiff, '```');
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
parts.push('', '## Diff', 'No diff available (agent may have failed before committing).');
|
|
325
|
+
}
|
|
326
|
+
if (existingLearnings) {
|
|
327
|
+
parts.push('', '## Existing Learnings (do not repeat these)', existingLearnings);
|
|
328
|
+
}
|
|
329
|
+
const prompt = parts.join('\n');
|
|
330
|
+
const messages = query({
|
|
331
|
+
prompt,
|
|
332
|
+
options: {
|
|
333
|
+
model: CONFIG.DIFF_REVIEW_MODEL, // Reuse Haiku config
|
|
334
|
+
cwd: projectDir,
|
|
335
|
+
tools: ['Read', 'Glob', 'Grep'],
|
|
336
|
+
allowedTools: ['Read', 'Glob', 'Grep'],
|
|
337
|
+
maxTurns: 5,
|
|
338
|
+
maxBudgetUsd: 0.25,
|
|
339
|
+
permissionMode: 'bypassPermissions',
|
|
340
|
+
allowDangerouslySkipPermissions: true,
|
|
341
|
+
settingSources: ['project'],
|
|
342
|
+
systemPrompt: { type: 'preset', preset: 'claude_code' },
|
|
343
|
+
stderr: () => { },
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
let output = '';
|
|
347
|
+
for await (const message of messages) {
|
|
348
|
+
if (message.type === 'assistant') {
|
|
349
|
+
const content = message.message?.content;
|
|
350
|
+
if (Array.isArray(content)) {
|
|
351
|
+
for (const block of content) {
|
|
352
|
+
if (block.type === 'text')
|
|
353
|
+
output = block.text;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
else if (typeof content === 'string') {
|
|
357
|
+
output = content;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (message.type === 'result') {
|
|
361
|
+
if ('result' in message && message.result) {
|
|
362
|
+
output = message.result;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Only save if the retro produced something meaningful
|
|
367
|
+
const trimmed = output.trim();
|
|
368
|
+
if (trimmed && !trimmed.toLowerCase().includes('no new learnings')) {
|
|
369
|
+
const header = outcome.success
|
|
370
|
+
? `**Retro (success): ${ticket.title}**`
|
|
371
|
+
: `**Retro (failed): ${ticket.title}**`;
|
|
372
|
+
appendLearning(projectDir, `${header}\n${trimmed}`);
|
|
373
|
+
log(DIM, 'RETRO', `Saved ${trimmed.split('\n').filter(l => l.startsWith('-') || l.startsWith('*')).length} learnings`);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
log(DIM, 'RETRO', 'No new learnings');
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
catch (e) {
|
|
380
|
+
// Retro is best-effort — never block the pipeline
|
|
381
|
+
log(DIM, 'RETRO', `Skipped: ${e instanceof Error ? e.message : e}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async function postTestChecklist(ticket) {
|
|
385
|
+
log(CYAN, 'TESTING', `Posting test checklist for "${ticket.title}"`);
|
|
386
|
+
try {
|
|
387
|
+
// Cross-session dedup: check if we already posted a testing checklist
|
|
388
|
+
const alreadyPosted = await hasTestingMarker(ticket.id);
|
|
389
|
+
if (alreadyPosted) {
|
|
390
|
+
log(DIM, 'TESTING', `Checklist already posted for "${ticket.title}", skipping`);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
// Extract acceptance tests from the spec
|
|
394
|
+
const spec = ticket.spec ?? '';
|
|
395
|
+
const description = ticket.description ?? '';
|
|
396
|
+
let checklist = [];
|
|
397
|
+
// Parse acceptance tests from spec (format: "## Acceptance Tests\n- GIVEN/WHEN/THEN...")
|
|
398
|
+
const acceptanceMatch = spec.match(/## Acceptance Tests\n([\s\S]*?)(?:\n##|$)/);
|
|
399
|
+
if (acceptanceMatch) {
|
|
400
|
+
checklist = acceptanceMatch[1]
|
|
401
|
+
.split('\n')
|
|
402
|
+
.filter(line => line.trim().startsWith('-'))
|
|
403
|
+
.map(line => {
|
|
404
|
+
// Convert GIVEN/WHEN/THEN to plain language
|
|
405
|
+
let text = line.replace(/^-\s*/, '').trim();
|
|
406
|
+
text = text
|
|
407
|
+
.replace(/^GIVEN\s+/i, 'Start with: ')
|
|
408
|
+
.replace(/\s*WHEN\s+/i, ' → Do: ')
|
|
409
|
+
.replace(/\s*THEN\s+/i, ' → Expect: ');
|
|
410
|
+
return text;
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
// If no acceptance tests, generate a basic checklist from description
|
|
414
|
+
if (checklist.length === 0) {
|
|
415
|
+
checklist = [
|
|
416
|
+
`Verify the change described in the ticket works: "${description.slice(0, 200)}"`,
|
|
417
|
+
'Check that nothing else looks broken',
|
|
418
|
+
'Comment on this ticket with what you found',
|
|
419
|
+
];
|
|
420
|
+
}
|
|
421
|
+
// Build the comment
|
|
422
|
+
const checklistText = checklist.map((item, i) => `${i + 1}. ${item}`).join('\n');
|
|
423
|
+
const comment = [
|
|
424
|
+
`🧪 Ready for Testing`,
|
|
425
|
+
'',
|
|
426
|
+
`This ticket has been deployed. Please test the following:`,
|
|
427
|
+
'',
|
|
428
|
+
checklistText,
|
|
429
|
+
'',
|
|
430
|
+
`When done, comment with what you found and drag to:`,
|
|
431
|
+
`→ "Done" if it works`,
|
|
432
|
+
`→ "Failed" if something's wrong (please describe what happened)`,
|
|
433
|
+
].join('\n');
|
|
434
|
+
await addComment(ticket.id, comment);
|
|
435
|
+
// Executed At is already set when the ticket lands here — no separate timestamp needed
|
|
436
|
+
log(GREEN, 'TESTING', `Posted checklist for "${ticket.title}" (${checklist.length} items)`);
|
|
437
|
+
}
|
|
438
|
+
catch (e) {
|
|
439
|
+
log(YELLOW, 'TESTING', `Failed to post checklist: ${e instanceof Error ? e.message : e}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function runFeedbackRetro(ticket, status) {
|
|
443
|
+
const projectDir = getProjectDir(ticket.project);
|
|
444
|
+
if (!projectDir)
|
|
445
|
+
return;
|
|
446
|
+
const statusLabel = status === CONFIG.COLUMNS.FAILED ? 'failed' : 'done';
|
|
447
|
+
log(CYAN, 'FEEDBACK', `Processing feedback for "${ticket.title}" (${statusLabel})`);
|
|
448
|
+
try {
|
|
449
|
+
// Cross-session dedup: check if already processed
|
|
450
|
+
const alreadyProcessed = await hasFeedbackMarker(ticket.id);
|
|
451
|
+
if (alreadyProcessed) {
|
|
452
|
+
log(DIM, 'FEEDBACK', `Already processed, skipping "${ticket.title}"`);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
// Read human comments from the ticket
|
|
456
|
+
const comments = await fetchComments(ticket.id);
|
|
457
|
+
if (comments.length === 0) {
|
|
458
|
+
log(DIM, 'FEEDBACK', 'No human comments found, skipping');
|
|
459
|
+
// Only mark Done tickets as processed (Failed might get retried with comments later)
|
|
460
|
+
if (status === CONFIG.COLUMNS.DONE) {
|
|
461
|
+
await addComment(ticket.id, '🔄 Feedback processed (no human comments found)');
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const existingLearnings = readLearnings(projectDir);
|
|
466
|
+
// Build feedback prompt with status context
|
|
467
|
+
const parts = [
|
|
468
|
+
feedbackPrompt,
|
|
469
|
+
'',
|
|
470
|
+
`## Ticket: ${ticket.title}`,
|
|
471
|
+
`## Status: ${status}`,
|
|
472
|
+
'',
|
|
473
|
+
'**Description**:',
|
|
474
|
+
ticket.description,
|
|
475
|
+
'',
|
|
476
|
+
'**Spec**:',
|
|
477
|
+
ticket.spec ?? '(none)',
|
|
478
|
+
'',
|
|
479
|
+
'**Impact/Error info**:',
|
|
480
|
+
ticket.impact ?? '(none)',
|
|
481
|
+
];
|
|
482
|
+
if (status === CONFIG.COLUMNS.FAILED) {
|
|
483
|
+
parts.push('', '## Context', 'This ticket FAILED. The human comments below describe what went wrong from the user\'s perspective (not just the agent error). Extract learnings about what the AI should do differently.');
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
parts.push('', '## Context', 'This ticket was completed and the human tested the result. Their comments describe what they found.');
|
|
487
|
+
}
|
|
488
|
+
parts.push('', '## Human Feedback', ...comments.map(c => `**${c.author}** (${c.createdTime}):\n> ${c.text}`));
|
|
489
|
+
if (existingLearnings) {
|
|
490
|
+
parts.push('', '## Existing Learnings (do not repeat these)', existingLearnings);
|
|
491
|
+
}
|
|
492
|
+
const prompt = parts.join('\n');
|
|
493
|
+
const messages = query({
|
|
494
|
+
prompt,
|
|
495
|
+
options: {
|
|
496
|
+
model: CONFIG.DIFF_REVIEW_MODEL, // Haiku — cheap and fast
|
|
497
|
+
cwd: projectDir,
|
|
498
|
+
allowedTools: [],
|
|
499
|
+
maxTurns: 3,
|
|
500
|
+
maxBudgetUsd: 0.10,
|
|
501
|
+
permissionMode: 'bypassPermissions',
|
|
502
|
+
allowDangerouslySkipPermissions: true,
|
|
503
|
+
systemPrompt: { type: 'preset', preset: 'claude_code' },
|
|
504
|
+
stderr: () => { },
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
let output = '';
|
|
508
|
+
for await (const message of messages) {
|
|
509
|
+
if (message.type === 'assistant') {
|
|
510
|
+
const content = message.message?.content;
|
|
511
|
+
if (Array.isArray(content)) {
|
|
512
|
+
for (const block of content) {
|
|
513
|
+
if (block.type === 'text')
|
|
514
|
+
output = block.text;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
else if (typeof content === 'string') {
|
|
518
|
+
output = content;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (message.type === 'result') {
|
|
522
|
+
if ('result' in message && message.result) {
|
|
523
|
+
output = message.result;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// Save learnings if meaningful
|
|
528
|
+
const trimmed = output.trim();
|
|
529
|
+
if (trimmed && !trimmed.toLowerCase().includes('no new learnings')) {
|
|
530
|
+
const tag = status === CONFIG.COLUMNS.FAILED ? 'Feedback (failed)' : 'Feedback';
|
|
531
|
+
appendLearning(projectDir, `**${tag}: ${ticket.title}**\n${trimmed}`);
|
|
532
|
+
const count = trimmed.split('\n').filter(l => l.startsWith('-') || l.startsWith('*')).length;
|
|
533
|
+
log(GREEN, 'FEEDBACK', `Saved ${count} learnings from human feedback`);
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
log(DIM, 'FEEDBACK', 'No new learnings from feedback');
|
|
537
|
+
}
|
|
538
|
+
// Mark as processed with a summary comment
|
|
539
|
+
const feedbackSummary = comments.map(c => `- ${c.author}: "${c.text.slice(0, 100)}${c.text.length > 100 ? '...' : ''}"`).join('\n');
|
|
540
|
+
const learningNote = trimmed && !trimmed.toLowerCase().includes('no new learnings')
|
|
541
|
+
? 'Learnings saved to project memory.'
|
|
542
|
+
: 'No new learnings extracted.';
|
|
543
|
+
await addComment(ticket.id, `🔄 Feedback processed\n\nComments analyzed:\n${feedbackSummary}\n\n${learningNote}`);
|
|
544
|
+
if (status === CONFIG.COLUMNS.DONE) {
|
|
545
|
+
await trySetDate(ticket.id, 'Done At');
|
|
546
|
+
}
|
|
547
|
+
log(GREEN, 'FEEDBACK', `Done processing feedback for "${ticket.title}"`);
|
|
548
|
+
}
|
|
549
|
+
catch (e) {
|
|
550
|
+
// Feedback is best-effort
|
|
551
|
+
log(YELLOW, 'FEEDBACK', `Failed: ${e instanceof Error ? e.message : e}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
278
554
|
async function runExecuteAgent(ticket) {
|
|
279
555
|
const projectDir = getProjectDir(ticket.project);
|
|
280
556
|
if (!projectDir) {
|
|
@@ -303,6 +579,7 @@ async function runExecuteAgent(ticket) {
|
|
|
303
579
|
createWorktree(projectDir, branchName, worktreeDir, baseBranch);
|
|
304
580
|
let cost = 0;
|
|
305
581
|
let commitCount = 0;
|
|
582
|
+
let retroOutcome = { success: false };
|
|
306
583
|
try {
|
|
307
584
|
const promptParts = [
|
|
308
585
|
executePrompt,
|
|
@@ -402,6 +679,7 @@ async function runExecuteAgent(ticket) {
|
|
|
402
679
|
const diffReview = await runDiffReviewAgent(worktreeDir, baseBranch, ticket.spec ?? '', ticket.description, []);
|
|
403
680
|
cost += diffReview.cost;
|
|
404
681
|
if (!diffReview.result.approved) {
|
|
682
|
+
retroOutcome = { success: false, error: 'Diff review rejected the changes', diffReviewIssues: diffReview.result.issues };
|
|
405
683
|
throw new Error(`Diff review failed:\n${diffReview.result.issues.map(i => ` - ${i}`).join('\n')}`);
|
|
406
684
|
}
|
|
407
685
|
log(GREEN, 'REVIEW', `Diff review passed: ${diffReview.result.summary}`);
|
|
@@ -459,7 +737,7 @@ async function runExecuteAgent(ticket) {
|
|
|
459
737
|
`[View in Notion](https://www.notion.so/${ticket.id.replace(/-/g, '')})`,
|
|
460
738
|
'',
|
|
461
739
|
'---',
|
|
462
|
-
`Cost: $${cost.toFixed(2)} | Review: Ease ${
|
|
740
|
+
`Cost: $${cost.toFixed(2)} | Review: Ease ${ticket.ease ?? '?'}/10, Confidence ${ticket.confidence ?? '?'}/10`,
|
|
463
741
|
].join('\n');
|
|
464
742
|
const prResult = execSync(`gh pr create --title ${shellEscape(ticket.title)} --body ${shellEscape(prBody)} --base ${shellEscape(baseBranch)} --head ${branchName}`, { cwd: worktreeDir, stdio: 'pipe', timeout: 30_000 });
|
|
465
743
|
prUrl = prResult.toString().trim();
|
|
@@ -472,7 +750,7 @@ async function runExecuteAgent(ticket) {
|
|
|
472
750
|
}
|
|
473
751
|
// Update Notion
|
|
474
752
|
await writeExecutionResults(ticket.id, { branch: branchName, cost, prUrl });
|
|
475
|
-
await moveTicketStatus(ticket.id, CONFIG.COLUMNS.
|
|
753
|
+
await moveTicketStatus(ticket.id, CONFIG.COLUMNS.TESTING);
|
|
476
754
|
const duration = Math.round((Date.now() - startTime) / 1000);
|
|
477
755
|
// Add success audit trail comment
|
|
478
756
|
const comment = [
|
|
@@ -484,11 +762,7 @@ async function runExecuteAgent(ticket) {
|
|
|
484
762
|
`Cost: $${cost.toFixed(2)} | Duration: ${duration}s`,
|
|
485
763
|
].join('\n');
|
|
486
764
|
await addComment(ticket.id, comment);
|
|
487
|
-
|
|
488
|
-
`**Execute: ${ticket.title}**`,
|
|
489
|
-
`Branch: ${branchName}, Commits: ${commitCount}`,
|
|
490
|
-
`Cost: $${cost.toFixed(2)}`,
|
|
491
|
-
].join('\n'));
|
|
765
|
+
retroOutcome = { success: true };
|
|
492
766
|
log(GREEN, 'EXECUTE', `Done: branch=${branchName} cost=$${cost.toFixed(2)}${prUrl ? ` pr=${prUrl}` : ''}`);
|
|
493
767
|
}
|
|
494
768
|
catch (error) {
|
|
@@ -502,13 +776,17 @@ async function runExecuteAgent(ticket) {
|
|
|
502
776
|
`Cost: $${cost.toFixed(2)} | Duration: ${duration}s`,
|
|
503
777
|
].join('\n');
|
|
504
778
|
await addComment(ticket.id, comment);
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
779
|
+
retroOutcome = {
|
|
780
|
+
success: false,
|
|
781
|
+
error: errMsg,
|
|
782
|
+
buildFailed: errMsg.includes('Build validation failed'),
|
|
783
|
+
};
|
|
509
784
|
throw error;
|
|
510
785
|
}
|
|
511
786
|
finally {
|
|
787
|
+
// Run retro before cleaning up worktree (so it can read the diff)
|
|
788
|
+
log(DIM, 'RETRO', 'Running post-execution retrospective...');
|
|
789
|
+
await runRetroAgent(projectDir, worktreeDir, baseBranch, ticket, retroOutcome);
|
|
512
790
|
// Always clean up the worktree
|
|
513
791
|
removeWorktree(projectDir, worktreeDir);
|
|
514
792
|
}
|
|
@@ -589,20 +867,35 @@ async function poll() {
|
|
|
589
867
|
try {
|
|
590
868
|
// Clear stale locks
|
|
591
869
|
clearStaleLocks();
|
|
592
|
-
// Fetch tickets in Review and
|
|
593
|
-
const [reviewTickets, executeTickets] = await Promise.all([
|
|
870
|
+
// Fetch tickets in Review, Execute, Testing, Done, and Failed columns
|
|
871
|
+
const [reviewTickets, executeTickets, testingTickets, doneTickets, failedTickets] = await Promise.all([
|
|
594
872
|
fetchTicketsByStatus(CONFIG.COLUMNS.REVIEW),
|
|
595
873
|
fetchTicketsByStatus(CONFIG.COLUMNS.EXECUTE),
|
|
874
|
+
fetchTicketsByStatus(CONFIG.COLUMNS.TESTING),
|
|
875
|
+
fetchTicketsByStatus(CONFIG.COLUMNS.DONE),
|
|
876
|
+
fetchTicketsByStatus(CONFIG.COLUMNS.FAILED),
|
|
596
877
|
]);
|
|
597
878
|
const pendingReview = reviewTickets.filter((t) => !activeLocks.has(t.id));
|
|
598
879
|
const pendingExecute = executeTickets.filter((t) => !activeLocks.has(t.id));
|
|
880
|
+
const pendingTesting = testingTickets.filter((t) => !testingNotified.has(t.id));
|
|
881
|
+
// Feedback candidates: Done tickets + Failed tickets (both can have human comments)
|
|
882
|
+
const pendingFeedback = [
|
|
883
|
+
...doneTickets.map(t => ({ ...t, feedbackStatus: CONFIG.COLUMNS.DONE })),
|
|
884
|
+
...failedTickets.map(t => ({ ...t, feedbackStatus: CONFIG.COLUMNS.FAILED })),
|
|
885
|
+
].filter((t) => !feedbackProcessed.has(t.id));
|
|
599
886
|
if (pendingReview.length > 0) {
|
|
600
887
|
log(CYAN, 'POLL', `Found ${pendingReview.length} ticket(s) to review`);
|
|
601
888
|
}
|
|
602
889
|
if (pendingExecute.length > 0) {
|
|
603
890
|
log(MAGENTA, 'POLL', `Found ${pendingExecute.length} ticket(s) to execute`);
|
|
604
891
|
}
|
|
605
|
-
if (
|
|
892
|
+
if (pendingTesting.length > 0) {
|
|
893
|
+
log(YELLOW, 'POLL', `Found ${pendingTesting.length} ticket(s) needing test checklists`);
|
|
894
|
+
}
|
|
895
|
+
if (pendingFeedback.length > 0) {
|
|
896
|
+
log(GREEN, 'POLL', `Found ${pendingFeedback.length} ticket(s) to check for feedback`);
|
|
897
|
+
}
|
|
898
|
+
if (pendingReview.length === 0 && pendingExecute.length === 0 && pendingTesting.length === 0 && pendingFeedback.length === 0) {
|
|
606
899
|
log(DIM, 'POLL', 'No tickets to process');
|
|
607
900
|
}
|
|
608
901
|
if (DRY_RUN)
|
|
@@ -631,6 +924,26 @@ async function poll() {
|
|
|
631
924
|
log(RED, 'UNHANDLED', `Unexpected error in ${mode} for "${details.title}": ${err instanceof Error ? err.message : err}`);
|
|
632
925
|
});
|
|
633
926
|
}
|
|
927
|
+
// Post test checklists for Testing tickets (lightweight, no AI agent)
|
|
928
|
+
for (const ticket of pendingTesting) {
|
|
929
|
+
if (shuttingDown)
|
|
930
|
+
break;
|
|
931
|
+
testingNotified.add(ticket.id);
|
|
932
|
+
const details = await fetchTicketDetails(ticket.id);
|
|
933
|
+
postTestChecklist(details).catch((err) => {
|
|
934
|
+
log(YELLOW, 'TESTING', `Error posting checklist for "${details.title}": ${err instanceof Error ? err.message : err}`);
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
// Process feedback for Done and Failed tickets (lightweight, doesn't count toward concurrency)
|
|
938
|
+
for (const ticket of pendingFeedback) {
|
|
939
|
+
if (shuttingDown)
|
|
940
|
+
break;
|
|
941
|
+
feedbackProcessed.add(ticket.id);
|
|
942
|
+
const details = await fetchTicketDetails(ticket.id);
|
|
943
|
+
runFeedbackRetro(details, ticket.feedbackStatus).catch((err) => {
|
|
944
|
+
log(YELLOW, 'FEEDBACK', `Error processing feedback for "${details.title}": ${err instanceof Error ? err.message : err}`);
|
|
945
|
+
});
|
|
946
|
+
}
|
|
634
947
|
}
|
|
635
948
|
catch (error) {
|
|
636
949
|
const errMsg = error instanceof Error ? error.message : String(error);
|
package/dist/lib/notion.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ export declare function extractPlainText(richText: RichTextItemResponse[]): stri
|
|
|
4
4
|
export declare function extractProjectName(page: PageObjectResponse): string;
|
|
5
5
|
export declare function pageToTicket(page: PageObjectResponse): NotionTicket;
|
|
6
6
|
export declare function blockToMarkdown(block: BlockObjectResponse): string;
|
|
7
|
+
/**
|
|
8
|
+
* Set a date property on a page (best-effort — skips silently if property doesn't exist).
|
|
9
|
+
*/
|
|
10
|
+
export declare function trySetDate(pageId: string, propertyName: string): Promise<void>;
|
|
7
11
|
/**
|
|
8
12
|
* Fetch all tickets with a given status from the Notion database.
|
|
9
13
|
*/
|
|
@@ -37,6 +41,17 @@ export declare function writeFailure(pageId: string, error: string): Promise<voi
|
|
|
37
41
|
* Used for agent audit trail - does not throw if it fails.
|
|
38
42
|
*/
|
|
39
43
|
export declare function addComment(pageId: string, text: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Fetch all comments on a Notion page.
|
|
46
|
+
* Returns human comments (filters out bot-authored comments from this integration).
|
|
47
|
+
*/
|
|
48
|
+
export declare function fetchComments(pageId: string): Promise<Array<{
|
|
49
|
+
author: string;
|
|
50
|
+
text: string;
|
|
51
|
+
createdTime: string;
|
|
52
|
+
}>>;
|
|
53
|
+
export declare function hasFeedbackMarker(pageId: string): Promise<boolean>;
|
|
54
|
+
export declare function hasTestingMarker(pageId: string): Promise<boolean>;
|
|
40
55
|
export declare function truncate(str: string, maxLen: number): string;
|
|
41
56
|
/** Chunk text into Notion rich_text segments (each max 2000 chars). */
|
|
42
57
|
export declare function chunkRichText(str: string): Array<{
|
package/dist/lib/notion.js
CHANGED
|
@@ -26,6 +26,10 @@ function extractRichText(page, name) {
|
|
|
26
26
|
const prop = getProperty(page, name);
|
|
27
27
|
return prop?.rich_text ? extractPlainText(prop.rich_text) : '';
|
|
28
28
|
}
|
|
29
|
+
function extractNumber(page, name) {
|
|
30
|
+
const prop = getProperty(page, name);
|
|
31
|
+
return prop?.number ?? undefined;
|
|
32
|
+
}
|
|
29
33
|
function extractSelect(page, name) {
|
|
30
34
|
const prop = getProperty(page, name);
|
|
31
35
|
return prop?.select?.name ?? '';
|
|
@@ -90,6 +94,26 @@ export function blockToMarkdown(block) {
|
|
|
90
94
|
return text;
|
|
91
95
|
}
|
|
92
96
|
}
|
|
97
|
+
function nowISO() {
|
|
98
|
+
return new Date().toISOString();
|
|
99
|
+
}
|
|
100
|
+
function dateProperty(iso) {
|
|
101
|
+
return { date: { start: iso } };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Set a date property on a page (best-effort — skips silently if property doesn't exist).
|
|
105
|
+
*/
|
|
106
|
+
export async function trySetDate(pageId, propertyName) {
|
|
107
|
+
try {
|
|
108
|
+
await notion().pages.update({
|
|
109
|
+
page_id: pageId,
|
|
110
|
+
properties: { [propertyName]: dateProperty(nowISO()) },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Property might not exist yet — skip silently
|
|
115
|
+
}
|
|
116
|
+
}
|
|
93
117
|
// -- Exported Functions --
|
|
94
118
|
/**
|
|
95
119
|
* Fetch all tickets with a given status from the Notion database.
|
|
@@ -123,6 +147,8 @@ export async function fetchTicketDetails(pageId) {
|
|
|
123
147
|
bodyBlocks,
|
|
124
148
|
spec: extractRichText(page, 'Spec') || undefined,
|
|
125
149
|
impact: extractRichText(page, 'Impact') || undefined,
|
|
150
|
+
ease: extractNumber(page, 'Ease'),
|
|
151
|
+
confidence: extractNumber(page, 'Confidence'),
|
|
126
152
|
};
|
|
127
153
|
}
|
|
128
154
|
/**
|
|
@@ -144,15 +170,21 @@ export async function writeReviewResults(pageId, results) {
|
|
|
144
170
|
Impact: {
|
|
145
171
|
rich_text: chunkRichText(`${results.impactReport}\n\nFiles: ${results.affectedFiles.join(', ')}${results.risks ? `\n\nRisks: ${results.risks}` : ''}`),
|
|
146
172
|
},
|
|
173
|
+
'Reviewed At': dateProperty(nowISO()),
|
|
147
174
|
};
|
|
148
175
|
try {
|
|
149
176
|
await notion().pages.update({ page_id: pageId, properties });
|
|
150
177
|
}
|
|
151
178
|
catch (e) {
|
|
152
|
-
// If Confidence property doesn't exist yet, retry without it
|
|
153
179
|
const errMsg = String(e);
|
|
180
|
+
// If a property doesn't exist yet, retry without it
|
|
154
181
|
if (errMsg.includes('Confidence')) {
|
|
155
182
|
delete properties.Confidence;
|
|
183
|
+
}
|
|
184
|
+
if (errMsg.includes('Reviewed At')) {
|
|
185
|
+
delete properties['Reviewed At'];
|
|
186
|
+
}
|
|
187
|
+
if (errMsg.includes('Confidence') || errMsg.includes('Reviewed At')) {
|
|
156
188
|
await notion().pages.update({ page_id: pageId, properties });
|
|
157
189
|
}
|
|
158
190
|
else {
|
|
@@ -172,13 +204,26 @@ export async function writeExecutionResults(pageId, results) {
|
|
|
172
204
|
Cost: {
|
|
173
205
|
rich_text: [{ text: { content: `$${(Math.round(results.cost * 100) / 100).toFixed(2)}` } }],
|
|
174
206
|
},
|
|
207
|
+
'Executed At': dateProperty(nowISO()),
|
|
175
208
|
};
|
|
176
209
|
if (results.prUrl) {
|
|
177
210
|
properties['PR URL'] = {
|
|
178
211
|
url: results.prUrl,
|
|
179
212
|
};
|
|
180
213
|
}
|
|
181
|
-
|
|
214
|
+
try {
|
|
215
|
+
await notion().pages.update({ page_id: pageId, properties });
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
const errMsg = String(e);
|
|
219
|
+
if (errMsg.includes('Executed At')) {
|
|
220
|
+
delete properties['Executed At'];
|
|
221
|
+
await notion().pages.update({ page_id: pageId, properties });
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
throw e;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
182
227
|
}
|
|
183
228
|
/**
|
|
184
229
|
* Move a ticket to a new status column.
|
|
@@ -195,15 +240,27 @@ export async function moveTicketStatus(pageId, newStatus) {
|
|
|
195
240
|
* Write error details and move ticket to Failed.
|
|
196
241
|
*/
|
|
197
242
|
export async function writeFailure(pageId, error) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
rich_text: chunkRichText(`ERROR: ${error}`),
|
|
204
|
-
},
|
|
243
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
244
|
+
const properties = {
|
|
245
|
+
Status: { status: { name: CONFIG.COLUMNS.FAILED } },
|
|
246
|
+
Impact: {
|
|
247
|
+
rich_text: chunkRichText(`ERROR: ${error}`),
|
|
205
248
|
},
|
|
206
|
-
|
|
249
|
+
'Failed At': dateProperty(nowISO()),
|
|
250
|
+
};
|
|
251
|
+
try {
|
|
252
|
+
await notion().pages.update({ page_id: pageId, properties });
|
|
253
|
+
}
|
|
254
|
+
catch (e) {
|
|
255
|
+
const errMsg = String(e);
|
|
256
|
+
if (errMsg.includes('Failed At')) {
|
|
257
|
+
delete properties['Failed At'];
|
|
258
|
+
await notion().pages.update({ page_id: pageId, properties });
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
throw e;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
207
264
|
}
|
|
208
265
|
/**
|
|
209
266
|
* Add a comment to a Notion page (best-effort).
|
|
@@ -221,6 +278,66 @@ export async function addComment(pageId, text) {
|
|
|
221
278
|
console.warn(`[NOTION] Failed to add comment to ${pageId}:`, e);
|
|
222
279
|
}
|
|
223
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Fetch all comments on a Notion page.
|
|
283
|
+
* Returns human comments (filters out bot-authored comments from this integration).
|
|
284
|
+
*/
|
|
285
|
+
export async function fetchComments(pageId) {
|
|
286
|
+
try {
|
|
287
|
+
const response = await notion().comments.list({ block_id: pageId });
|
|
288
|
+
const comments = [];
|
|
289
|
+
for (const comment of response.results) {
|
|
290
|
+
// Skip bot-authored comments (our own audit trail)
|
|
291
|
+
const createdBy = comment.created_by;
|
|
292
|
+
if (createdBy?.type === 'bot')
|
|
293
|
+
continue;
|
|
294
|
+
const text = 'rich_text' in comment
|
|
295
|
+
? extractPlainText(comment.rich_text)
|
|
296
|
+
: '';
|
|
297
|
+
if (!text.trim())
|
|
298
|
+
continue;
|
|
299
|
+
comments.push({
|
|
300
|
+
author: createdBy?.name || 'Unknown',
|
|
301
|
+
text: text.trim(),
|
|
302
|
+
createdTime: comment.created_time,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return comments;
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
console.warn(`[NOTION] Failed to fetch comments for ${pageId}:`, e);
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Check if a ticket already has a specific bot marker comment.
|
|
314
|
+
* Used for cross-session deduplication.
|
|
315
|
+
*/
|
|
316
|
+
async function hasBotMarker(pageId, marker) {
|
|
317
|
+
try {
|
|
318
|
+
const response = await notion().comments.list({ block_id: pageId });
|
|
319
|
+
for (const comment of response.results) {
|
|
320
|
+
const createdBy = comment.created_by;
|
|
321
|
+
if (createdBy?.type !== 'bot')
|
|
322
|
+
continue;
|
|
323
|
+
const text = 'rich_text' in comment
|
|
324
|
+
? extractPlainText(comment.rich_text)
|
|
325
|
+
: '';
|
|
326
|
+
if (text.includes(marker))
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
export function hasFeedbackMarker(pageId) {
|
|
336
|
+
return hasBotMarker(pageId, 'Feedback processed');
|
|
337
|
+
}
|
|
338
|
+
export function hasTestingMarker(pageId) {
|
|
339
|
+
return hasBotMarker(pageId, 'Ready for Testing');
|
|
340
|
+
}
|
|
224
341
|
export function truncate(str, maxLen) {
|
|
225
342
|
if (str.length <= maxLen)
|
|
226
343
|
return str;
|
package/package.json
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
You are analyzing human feedback on AI-generated code changes.
|
|
2
|
+
|
|
3
|
+
## Your Task
|
|
4
|
+
A human (PM, designer, developer, QA tester, or founder) reviewed the result of an AI agent's work and left comments on the Notion ticket. Extract actionable learnings from their feedback 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 (ticket status: Done)
|
|
9
|
+
- **What the human liked**: patterns, code style, thoroughness that should be repeated
|
|
10
|
+
- **What worked well in production**: successful deployment, no regressions
|
|
11
|
+
- **What could be better**: minor issues that didn't block approval but should be improved next time
|
|
12
|
+
- **Preferences revealed**: the human's style preferences, naming conventions, UX expectations
|
|
13
|
+
|
|
14
|
+
### On Failure (ticket status: Failed)
|
|
15
|
+
- **What broke from the user's perspective**: not the agent error message, but what the human actually experienced
|
|
16
|
+
- **Root cause insight**: the human often knows WHY something failed better than the error log
|
|
17
|
+
- **What the human expected vs what happened**: gap between intent and implementation
|
|
18
|
+
- **Scope issues**: was the change too big, too small, missing context, or in the wrong place?
|
|
19
|
+
|
|
20
|
+
### Implicit Signals
|
|
21
|
+
- If the human just says "looks good" or "works" — not much to learn
|
|
22
|
+
- If the human gives detailed feedback — they care about quality, extract everything
|
|
23
|
+
- If multiple people comment — note which role cares about what (PM vs dev vs designer)
|
|
24
|
+
|
|
25
|
+
## Output Rules
|
|
26
|
+
- Write 1-4 bullet points. Each must be a specific, actionable lesson from the human's perspective.
|
|
27
|
+
- Start each bullet with a category tag: `[feedback]`, `[preference]`, `[bug]`, or `[quality]`
|
|
28
|
+
- Translate vague human feedback into specific technical guidance for the AI agent:
|
|
29
|
+
- "This doesn't look right" on a UI change → `[preference] Stakeholders in this project prefer X style over Y`
|
|
30
|
+
- "It broke the login" → `[bug] Changes to auth-related files can break the login flow — always test auth after touching these files`
|
|
31
|
+
- "Wrong approach" → `[feedback] For this type of change, the preferred pattern is X (not Y)`
|
|
32
|
+
- If the feedback is just "looks good", "approved", "works", or similar with no specific lessons, write: `No new learnings.`
|
|
33
|
+
- Don't repeat lessons that already exist in the project learnings.
|
|
34
|
+
- Be specific to THIS project. Generic advice is useless.
|
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.`
|