@shipfast-ai/shipfast 1.1.0 → 1.3.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.
Files changed (44) hide show
  1. package/README.md +166 -201
  2. package/agents/architect.md +7 -7
  3. package/agents/builder.md +9 -10
  4. package/agents/critic.md +3 -3
  5. package/agents/scout.md +1 -1
  6. package/agents/scribe.md +9 -13
  7. package/bin/install.js +250 -9
  8. package/brain/index.cjs +38 -80
  9. package/brain/indexer.cjs +6 -9
  10. package/brain/schema.sql +4 -2
  11. package/commands/sf/brain.md +4 -0
  12. package/commands/sf/check-plan.md +3 -4
  13. package/commands/sf/config.md +1 -0
  14. package/commands/sf/cost.md +83 -0
  15. package/commands/sf/diff.md +53 -0
  16. package/commands/sf/discuss.md +115 -68
  17. package/commands/sf/do.md +140 -72
  18. package/commands/sf/help.md +10 -5
  19. package/commands/sf/map.md +16 -24
  20. package/commands/sf/plan.md +6 -9
  21. package/commands/sf/project.md +4 -4
  22. package/commands/sf/rollback.md +70 -0
  23. package/commands/sf/ship.md +13 -0
  24. package/commands/sf/status.md +1 -3
  25. package/commands/sf/verify.md +4 -9
  26. package/commands/sf/worktree.md +286 -0
  27. package/core/ambiguity.cjs +229 -125
  28. package/core/architecture.cjs +5 -8
  29. package/core/autopilot.cjs +1 -0
  30. package/core/budget.cjs +5 -11
  31. package/core/constants.cjs +63 -0
  32. package/core/context-builder.cjs +1 -58
  33. package/core/executor.cjs +18 -4
  34. package/core/guardrails.cjs +6 -5
  35. package/core/model-selector.cjs +5 -48
  36. package/core/retry.cjs +5 -1
  37. package/core/session.cjs +2 -2
  38. package/core/skip-logic.cjs +5 -1
  39. package/core/verify.cjs +11 -14
  40. package/hooks/sf-first-run.js +2 -2
  41. package/mcp/server.cjs +135 -4
  42. package/package.json +18 -4
  43. package/scripts/postinstall.js +1 -1
  44. package/commands/sf/workstream.md +0 -51
package/brain/index.cjs CHANGED
@@ -8,12 +8,9 @@
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
  const { execFileSync } = require('child_process');
11
+ const { DB_NAME } = require('../core/constants.cjs');
11
12
 
12
- const DB_NAME = '.shipfast/brain.db';
13
-
14
- // ============================================================
15
- // Database initialization
16
- // ============================================================
13
+ // --- Database initialization ---
17
14
 
18
15
  function getBrainPath(cwd) {
19
16
  return path.join(cwd || process.cwd(), DB_NAME);
@@ -41,9 +38,7 @@ function brainExists(cwd) {
41
38
  return fs.existsSync(getBrainPath(cwd));
42
39
  }
43
40
 
44
- // ============================================================
45
- // Query helpers (all zero-LLM-cost)
46
- // ============================================================
41
+ // --- Query helpers ---
47
42
 
48
43
  function query(cwd, sql) {
49
44
  const dbPath = getBrainPath(cwd);
@@ -55,7 +50,10 @@ function query(cwd, sql) {
55
50
  stdio: ['pipe', 'pipe', 'pipe']
56
51
  });
57
52
  return result.trim() ? JSON.parse(result) : [];
58
- } catch {
53
+ } catch (err) {
54
+ if (process.env.SF_DEBUG) {
55
+ console.error('[brain] query error:', err.message, '\n SQL:', sql.slice(0, 200));
56
+ }
59
57
  return [];
60
58
  }
61
59
  }
@@ -65,9 +63,7 @@ function run(cwd, sql) {
65
63
  execFileSync('sqlite3', [dbPath, sql], { stdio: ['pipe', 'pipe', 'pipe'] });
66
64
  }
67
65
 
68
- // ============================================================
69
- // Codebase graph operations
70
- // ============================================================
66
+ // --- Codebase graph ---
71
67
 
72
68
  function upsertNode(cwd, node) {
73
69
  const { id, kind, name, file_path, line_start, line_end, signature, hash, metadata } = node;
@@ -83,7 +79,7 @@ function addEdge(cwd, source, target, kind, weight = 1.0) {
83
79
 
84
80
  function getBlastRadius(cwd, filePaths, maxDepth = 3) {
85
81
  const fileList = filePaths.map(f => `'file:${esc(f)}'`).join(',');
86
- return query(cwd, `
82
+ const sql = `
87
83
  WITH RECURSIVE affected(id, depth) AS (
88
84
  SELECT id, 0 FROM nodes WHERE id IN (${fileList})
89
85
  UNION
@@ -96,7 +92,35 @@ function getBlastRadius(cwd, filePaths, maxDepth = 3) {
96
92
  WHERE n.signature IS NOT NULL AND n.signature != ''
97
93
  ORDER BY a.depth ASC
98
94
  LIMIT 30
99
- `);
95
+ `;
96
+
97
+ const local = query(cwd, sql);
98
+
99
+ // Cross-repo blast radius: query linked repos too
100
+ const linkedConfig = getConfig(cwd, 'linked_repos');
101
+ if (linkedConfig) {
102
+ try {
103
+ const linkedPaths = JSON.parse(linkedConfig);
104
+ for (const repoPath of linkedPaths) {
105
+ const linkedDb = path.join(repoPath, '.shipfast', 'brain.db');
106
+ if (fs.existsSync(linkedDb)) {
107
+ try {
108
+ const result = execFileSync('sqlite3', ['-json', linkedDb, sql], {
109
+ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
110
+ }).trim();
111
+ if (result) {
112
+ const parsed = JSON.parse(result);
113
+ const repoName = path.basename(repoPath);
114
+ parsed.forEach(row => { row._repo = repoName; });
115
+ local.push(...parsed);
116
+ }
117
+ } catch { /* linked repo query failed */ }
118
+ }
119
+ }
120
+ } catch { /* linked_repos not valid JSON */ }
121
+ }
122
+
123
+ return local.slice(0, 30);
100
124
  }
101
125
 
102
126
  function getSignaturesForFile(cwd, filePath) {
@@ -112,9 +136,7 @@ function getStaleNodes(cwd) {
112
136
  return query(cwd, `SELECT id, file_path, hash FROM nodes WHERE kind = 'file'`);
113
137
  }
114
138
 
115
- // ============================================================
116
139
  // Context operations (replaces STATE.md / REQUIREMENTS.md)
117
- // ============================================================
118
140
 
119
141
  function setContext(cwd, scope, key, value) {
120
142
  const val = typeof value === 'string' ? value : JSON.stringify(value);
@@ -134,9 +156,7 @@ function getAllContext(cwd, scope) {
134
156
  return query(cwd, `SELECT key, value FROM context WHERE scope = '${esc(scope)}'`);
135
157
  }
136
158
 
137
- // ============================================================
138
159
  // Decision operations
139
- // ============================================================
140
160
 
141
161
  function addDecision(cwd, { question, decision, reasoning, phase, tags }) {
142
162
  run(cwd, `INSERT INTO decisions (question, decision, reasoning, phase, tags)
@@ -148,9 +168,7 @@ function getDecisions(cwd, phase) {
148
168
  return query(cwd, `SELECT question, decision, reasoning, phase FROM decisions ${where} ORDER BY created_at DESC LIMIT 20`);
149
169
  }
150
170
 
151
- // ============================================================
152
171
  // Task operations
153
- // ============================================================
154
172
 
155
173
  function addTask(cwd, task) {
156
174
  const { id, phase, description, plan_text } = task;
@@ -170,9 +188,7 @@ function getTasks(cwd, phase) {
170
188
  return query(cwd, `SELECT * FROM tasks ${where} ORDER BY created_at`);
171
189
  }
172
190
 
173
- // ============================================================
174
191
  // Learnings (self-improving memory)
175
- // ============================================================
176
192
 
177
193
  function addLearning(cwd, { pattern, problem, solution, domain, source }) {
178
194
  run(cwd, `INSERT INTO learnings (pattern, problem, solution, domain, source)
@@ -193,9 +209,7 @@ function pruneOldLearnings(cwd, daysOld = 30) {
193
209
  run(cwd, `DELETE FROM learnings WHERE confidence < 0.3 AND times_used = 0 AND created_at < strftime('%s', 'now') - ${daysOld * 86400}`);
194
210
  }
195
211
 
196
- // ============================================================
197
212
  // Checkpoints
198
- // ============================================================
199
213
 
200
214
  function createCheckpoint(cwd, id, description) {
201
215
  let gitRef = '';
@@ -218,9 +232,7 @@ function rollbackCheckpoint(cwd, id) {
218
232
  run(cwd, `DELETE FROM checkpoints WHERE id = '${esc(id)}'`);
219
233
  }
220
234
 
221
- // ============================================================
222
235
  // Token budget
223
- // ============================================================
224
236
 
225
237
  function getTokenBudget(cwd) {
226
238
  const rows = query(cwd, `SELECT value FROM config WHERE key = 'token_budget'`);
@@ -241,9 +253,7 @@ function getTokensByAgent(cwd, sessionId) {
241
253
  return query(cwd, `SELECT agent, SUM(input_tokens + output_tokens) as total FROM token_usage WHERE session_id = '${esc(sessionId)}' GROUP BY agent`);
242
254
  }
243
255
 
244
- // ============================================================
245
256
  // Hot files (git-derived)
246
- // ============================================================
247
257
 
248
258
  function updateHotFiles(cwd, limit = 100) {
249
259
  try {
@@ -264,9 +274,7 @@ function getHotFiles(cwd, limit = 20) {
264
274
  return query(cwd, `SELECT file_path, change_count FROM hot_files ORDER BY change_count DESC LIMIT ${limit}`);
265
275
  }
266
276
 
267
- // ============================================================
268
277
  // Config
269
- // ============================================================
270
278
 
271
279
  function getConfig(cwd, key) {
272
280
  const rows = query(cwd, `SELECT value FROM config WHERE key = '${esc(key)}'`);
@@ -277,9 +285,7 @@ function setConfig(cwd, key, value) {
277
285
  run(cwd, `INSERT OR REPLACE INTO config (key, value) VALUES ('${esc(key)}', '${esc(String(value))}')`);
278
286
  }
279
287
 
280
- // ============================================================
281
288
  // Context builder for agents (the key token-saving function)
282
- // ============================================================
283
289
 
284
290
  function buildAgentContext(cwd, { agent, taskDescription, affectedFiles, phase, domain }) {
285
291
  const parts = [];
@@ -322,18 +328,14 @@ function buildAgentContext(cwd, { agent, taskDescription, affectedFiles, phase,
322
328
  return parts.join('\n\n');
323
329
  }
324
330
 
325
- // ============================================================
326
331
  // Model Performance (feedback loop)
327
- // ============================================================
328
332
 
329
333
  function recordModelOutcome(cwd, { agent, model, domain, taskId, outcome }) {
330
334
  run(cwd, `INSERT INTO model_performance (agent, model, domain, task_id, outcome)
331
335
  VALUES ('${esc(agent)}', '${esc(model)}', '${esc(domain || '')}', '${esc(taskId || '')}', '${esc(outcome)}')`);
332
336
  }
333
337
 
334
- // ============================================================
335
338
  // Seeds (forward ideas captured during work)
336
- // ============================================================
337
339
 
338
340
  function addSeed(cwd, { idea, sourceTask, domain, priority }) {
339
341
  run(cwd, `INSERT INTO seeds (idea, source_task, domain, priority)
@@ -357,9 +359,7 @@ function dismissSeed(cwd, seedId) {
357
359
  run(cwd, `UPDATE seeds SET status = 'dismissed' WHERE id = ${parseInt(seedId)}`);
358
360
  }
359
361
 
360
- // ============================================================
361
362
  // Utils
362
- // ============================================================
363
363
 
364
364
  function esc(s) {
365
365
  if (s == null) return '';
@@ -408,46 +408,4 @@ module.exports = {
408
408
  getSeeds,
409
409
  promoteSeed,
410
410
  dismissSeed,
411
- // Requirements
412
- addRequirement,
413
- getRequirements,
414
- updateRequirement,
415
- getRequirementCoverage
416
411
  };
417
-
418
- // ============================================================
419
- // Requirements (REQ-ID tracing)
420
- // ============================================================
421
-
422
- function addRequirement(cwd, { id, category, description, priority, phase }) {
423
- run(cwd, `INSERT OR REPLACE INTO requirements (id, category, description, priority, phase)
424
- VALUES ('${esc(id)}', '${esc(category)}', '${esc(description)}', '${esc(priority || 'v1')}', '${esc(phase || '')}')`);
425
- }
426
-
427
- function getRequirements(cwd, opts = {}) {
428
- const conditions = [];
429
- if (opts.phase) conditions.push(`phase = '${esc(opts.phase)}'`);
430
- if (opts.category) conditions.push(`category = '${esc(opts.category)}'`);
431
- if (opts.status) conditions.push(`status = '${esc(opts.status)}'`);
432
- if (opts.priority) conditions.push(`priority = '${esc(opts.priority)}'`);
433
- const where = conditions.length ? 'WHERE ' + conditions.join(' AND ') : '';
434
- return query(cwd, `SELECT * FROM requirements ${where} ORDER BY category, id`);
435
- }
436
-
437
- function updateRequirement(cwd, id, updates) {
438
- const sets = Object.entries(updates).map(([k, v]) => `${k} = '${esc(String(v))}'`).join(', ');
439
- run(cwd, `UPDATE requirements SET ${sets} WHERE id = '${esc(id)}'`);
440
- }
441
-
442
- function getRequirementCoverage(cwd) {
443
- const total = query(cwd, "SELECT COUNT(*) as c FROM requirements WHERE priority = 'v1'");
444
- const mapped = query(cwd, "SELECT COUNT(*) as c FROM requirements WHERE priority = 'v1' AND phase IS NOT NULL AND phase != ''");
445
- const done = query(cwd, "SELECT COUNT(*) as c FROM requirements WHERE priority = 'v1' AND status = 'done'");
446
- const verified = query(cwd, "SELECT COUNT(*) as c FROM requirements WHERE priority = 'v1' AND verified = 1");
447
- return {
448
- total: total[0] ? total[0].c : 0,
449
- mapped: mapped[0] ? mapped[0].c : 0,
450
- done: done[0] ? done[0].c : 0,
451
- verified: verified[0] ? verified[0].c : 0
452
- };
453
- }
package/brain/indexer.cjs CHANGED
@@ -152,13 +152,10 @@ function hashContent(content) {
152
152
  }
153
153
 
154
154
  // ============================================================
155
- // SQL escaping (for batch mode)
155
+ // SQL escaping — use brain.esc() as single source of truth
156
156
  // ============================================================
157
157
 
158
- function esc(s) {
159
- if (s == null) return '';
160
- return String(s).replace(/'/g, "''");
161
- }
158
+ const esc = brain.esc;
162
159
 
163
160
  // ============================================================
164
161
  // Batch SQL collector
@@ -461,7 +458,7 @@ function indexCodebase(cwd, opts = {}) {
461
458
  execFileSync('sqlite3', [dbPath], { input: sql, stdio: ['pipe', 'pipe', 'pipe'] });
462
459
  }
463
460
 
464
- // FIX #1: Clean orphan nodes (deleted/renamed/moved files)
461
+ // Clean orphan nodes: remove entries for files that no longer exist on disk
465
462
  let cleaned = 0;
466
463
  if (!changedOnly) {
467
464
  const discoveredPaths = new Set(files.map(f => path.relative(cwd, f).replace(/\\/g, '/')));
@@ -483,10 +480,10 @@ function indexCodebase(cwd, opts = {}) {
483
480
  }
484
481
  }
485
482
 
486
- // FIX #6: Update hot files on every index
483
+ // Update hot files from git history on every index
487
484
  brain.updateHotFiles(cwd);
488
485
 
489
- // FIX #11: Run co-change analysis
486
+ // Run co-change analysis from git history to detect files that change together
490
487
  try {
491
488
  const gitIntelPath = path.join(__dirname, '..', 'core', 'git-intel.cjs');
492
489
  if (fs.existsSync(gitIntelPath)) {
@@ -517,7 +514,7 @@ if (require.main === module) {
517
514
  const fresh = args.includes('--fresh');
518
515
  const cwd = args.find(a => !a.startsWith('-')) || process.cwd();
519
516
 
520
- // FIX #8: --fresh flag deletes brain.db first
517
+ // --fresh flag: delete existing brain.db for full reindex
521
518
  if (fresh) {
522
519
  const dbPath = path.join(cwd, '.shipfast', 'brain.db');
523
520
  if (fs.existsSync(dbPath)) {
package/brain/schema.sql CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  CREATE TABLE IF NOT EXISTS nodes (
9
9
  id TEXT PRIMARY KEY, -- 'file:src/auth.ts', 'fn:validateToken', 'type:User'
10
- kind TEXT NOT NULL, -- file | function | type | component | route | class | variable | export
10
+ kind TEXT NOT NULL CHECK (kind IN ('file','function','type','component','route','class','variable','export')),
11
11
  name TEXT NOT NULL, -- human-readable name
12
12
  file_path TEXT, -- relative file path
13
13
  line_start INTEGER,
@@ -25,7 +25,7 @@ CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);
25
25
  CREATE TABLE IF NOT EXISTS edges (
26
26
  source TEXT NOT NULL,
27
27
  target TEXT NOT NULL,
28
- kind TEXT NOT NULL, -- imports | calls | implements | depends | mutates | exports | extends
28
+ kind TEXT NOT NULL CHECK (kind IN ('imports','calls','implements','depends','mutates','exports','extends','co_changes')),
29
29
  weight REAL DEFAULT 1.0,
30
30
  PRIMARY KEY (source, target, kind),
31
31
  FOREIGN KEY (source) REFERENCES nodes(id) ON DELETE CASCADE,
@@ -52,6 +52,7 @@ CREATE TABLE IF NOT EXISTS context (
52
52
  );
53
53
 
54
54
  CREATE INDEX IF NOT EXISTS idx_context_scope ON context(scope);
55
+ CREATE INDEX IF NOT EXISTS idx_context_scope_key ON context(scope, key);
55
56
 
56
57
  -- ============================================================
57
58
  -- DECISIONS (replaces decision sections in STATE.md)
@@ -270,3 +271,4 @@ CREATE TABLE IF NOT EXISTS _migrations (
270
271
  INSERT OR IGNORE INTO _migrations (version, name) VALUES (1, 'initial_schema');
271
272
  INSERT OR IGNORE INTO _migrations (version, name) VALUES (2, 'add_seeds_table');
272
273
  INSERT OR IGNORE INTO _migrations (version, name) VALUES (3, 'add_model_performance_table');
274
+ INSERT OR IGNORE INTO _migrations (version, name) VALUES (4, 'add_context_index_and_cleanup');
@@ -10,6 +10,10 @@ allowed-tools:
10
10
  <objective>
11
11
  Direct interface to brain.db for querying the codebase knowledge graph,
12
12
  decisions, learnings, and task history.
13
+
14
+ This is the CANONICAL reference for brain.db queries. Other commands that need
15
+ brain data should use MCP tools (brain_decisions, brain_learnings, brain_search, etc.)
16
+ or reference the SQL patterns below — do not invent new queries.
13
17
  </objective>
14
18
 
15
19
  <process>
@@ -17,10 +17,9 @@ Catches scope creep, missing consumers, broken dependencies, and uncovered must-
17
17
 
18
18
  ## Step 1: Load tasks and must-haves
19
19
 
20
- ```bash
21
- sqlite3 -json .shipfast/brain.db "SELECT id, description, plan_text FROM tasks WHERE status = 'pending' ORDER BY created_at;" 2>/dev/null
22
- sqlite3 -json .shipfast/brain.db "SELECT value FROM context WHERE key LIKE 'must_haves:%' LIMIT 1;" 2>/dev/null
23
- ```
20
+ Use the `brain_tasks` MCP tool with: `{ "action": "list", "status": "pending" }` — returns pending tasks ordered by created_at.
21
+
22
+ Use the `brain_context` MCP tool with: `{ "action": "get", "key_like": "must_haves:%", "limit": 1 }` — returns the most recent must-haves entry.
24
23
 
25
24
  ## Step 2: Check each task
26
25
 
@@ -42,6 +42,7 @@ Parse `key value` from arguments. Valid keys:
42
42
  - `model-scribe` -> `model_tier_scribe`
43
43
  - `auto-checkpoint` -> `auto_checkpoint` (true/false)
44
44
  - `auto-learn` -> `auto_learn` (true/false)
45
+ - `post-ship-hook` -> `post_ship_hook` (shell command to run after /sf-ship)
45
46
 
46
47
  Update brain.db config table. Confirm the change:
47
48
 
@@ -0,0 +1,83 @@
1
+ ---
2
+ name: sf:cost
3
+ description: "Show token usage breakdown by agent, domain, and model."
4
+ allowed-tools:
5
+ - Bash
6
+ ---
7
+
8
+ <objective>
9
+ Analyze token spending to identify expensive patterns and optimize model selection.
10
+ </objective>
11
+
12
+ <process>
13
+
14
+ ## Query token_usage and model_performance tables
15
+
16
+ ### By agent
17
+ ```sql
18
+ SELECT agent, SUM(input_tokens + output_tokens) as total_tokens, COUNT(*) as calls
19
+ FROM token_usage GROUP BY agent ORDER BY total_tokens DESC;
20
+ ```
21
+
22
+ ### By model
23
+ ```sql
24
+ SELECT model, SUM(input_tokens + output_tokens) as total_tokens, COUNT(*) as calls
25
+ FROM token_usage WHERE model != '' GROUP BY model ORDER BY total_tokens DESC;
26
+ ```
27
+
28
+ ### By domain (from model_performance)
29
+ ```sql
30
+ SELECT domain, COUNT(*) as tasks,
31
+ SUM(CASE WHEN outcome='success' THEN 1 ELSE 0 END) as successes,
32
+ SUM(CASE WHEN outcome='failure' THEN 1 ELSE 0 END) as failures
33
+ FROM model_performance WHERE domain != '' GROUP BY domain ORDER BY tasks DESC;
34
+ ```
35
+
36
+ ### Model success rates
37
+ ```sql
38
+ SELECT agent, model, outcome, COUNT(*) as count
39
+ FROM model_performance GROUP BY agent, model, outcome ORDER BY agent, model;
40
+ ```
41
+
42
+ ### Budget status
43
+ ```sql
44
+ SELECT value FROM config WHERE key = 'token_budget';
45
+ ```
46
+ ```sql
47
+ SELECT COALESCE(SUM(input_tokens + output_tokens), 0) as used FROM token_usage;
48
+ ```
49
+
50
+ ## Format report
51
+
52
+ ```
53
+ Token Cost Analysis
54
+ ===================
55
+
56
+ Budget: [used]/[total] ([pct]%)
57
+
58
+ By Agent:
59
+ builder [tokens] tokens ([calls] calls)
60
+ scout [tokens] tokens ([calls] calls)
61
+ architect [tokens] tokens ([calls] calls)
62
+ critic [tokens] tokens ([calls] calls)
63
+ scribe [tokens] tokens ([calls] calls)
64
+
65
+ By Model:
66
+ sonnet [tokens] tokens ([calls] calls)
67
+ haiku [tokens] tokens ([calls] calls)
68
+ opus [tokens] tokens ([calls] calls)
69
+
70
+ By Domain:
71
+ auth [tasks] tasks [successes]✓ [failures]✗
72
+ database [tasks] tasks [successes]✓ [failures]✗
73
+
74
+ Model Success Rates:
75
+ builder/haiku: [success]/[total] ([pct]%)
76
+ builder/sonnet: [success]/[total] ([pct]%)
77
+ ```
78
+
79
+ </process>
80
+
81
+ <context>
82
+ $ARGUMENTS
83
+ </context>
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: sf:diff
3
+ description: "Smart diff viewer — changes grouped by task with file stats."
4
+ allowed-tools:
5
+ - Bash
6
+ ---
7
+
8
+ <objective>
9
+ Show recent changes organized by task, not by commit. Maps each file change
10
+ to the task that caused it, making it easy to review what was done.
11
+ </objective>
12
+
13
+ <process>
14
+
15
+ ## Step 1: Get recent passed tasks
16
+
17
+ Use the `brain_tasks` MCP tool with: `{ "action": "list", "status": "passed", "has_commit_sha": true, "limit": 10 }` — returns passed tasks with commits ordered by finished_at descending.
18
+
19
+ ## Step 2: For each task, get the diff stats
20
+
21
+ ```bash
22
+ git show --stat --format='' [commit_sha]
23
+ ```
24
+
25
+ ## Step 3: Format as grouped report
26
+
27
+ ```
28
+ Recent Changes by Task
29
+ ======================
30
+
31
+ Task: [description]
32
+ Commit: [sha] ([date])
33
+ Files:
34
+ M src/auth/login.ts (+15 -3)
35
+ A src/auth/validate.ts (+42)
36
+ M src/types/user.ts (+5 -1)
37
+
38
+ Task: [description]
39
+ Commit: [sha] ([date])
40
+ Files:
41
+ M src/api/billing.ts (+28 -12)
42
+ A src/hooks/usePayment.ts (+35)
43
+
44
+ Summary: [N] tasks | [M] files changed | +[additions] -[deletions]
45
+ ```
46
+
47
+ If $ARGUMENTS contains a task ID or description, filter to show only that task's diff in full detail (`git show [sha]`).
48
+
49
+ </process>
50
+
51
+ <context>
52
+ $ARGUMENTS
53
+ </context>