tribunal-kit 5.7.0 → 5.8.0

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 (56) hide show
  1. package/.agent/ARCHITECTURE.md +6 -7
  2. package/.agent/agents/frontend-reviewer.md +13 -0
  3. package/.agent/agents/frontend-specialist.md +14 -0
  4. package/.agent/agents/logic-reviewer.md +11 -0
  5. package/.agent/agents/orchestrator.md +15 -0
  6. package/.agent/agents/security-auditor.md +13 -0
  7. package/.agent/agents/ui-ux-auditor.md +7 -31
  8. package/.agent/history/memory/.memory.idx +766 -0
  9. package/.agent/history/memory/MEMORY.md +62 -0
  10. package/.agent/routing_index.json +694 -714
  11. package/.agent/rules/GEMINI.md +58 -8
  12. package/.agent/scripts/_colors.js +131 -89
  13. package/.agent/scripts/_utils.js +163 -128
  14. package/.agent/scripts/auto_preview.js +207 -197
  15. package/.agent/scripts/bundle_analyzer.js +227 -192
  16. package/.agent/scripts/case_law_manager.js +991 -689
  17. package/.agent/scripts/checklist.js +233 -190
  18. package/.agent/scripts/context_broker.js +930 -605
  19. package/.agent/scripts/dependency_analyzer.js +275 -184
  20. package/.agent/scripts/graph_builder.js +412 -341
  21. package/.agent/scripts/graph_visualizer.js +392 -390
  22. package/.agent/scripts/graph_zoom.js +198 -156
  23. package/.agent/scripts/inner_loop_validator.js +523 -445
  24. package/.agent/scripts/lint_runner.js +199 -157
  25. package/.agent/scripts/marathon_harness.js +819 -661
  26. package/.agent/scripts/minify_context.js +115 -100
  27. package/.agent/scripts/mutation_runner.js +321 -280
  28. package/.agent/scripts/prompt_compiler.js +62 -42
  29. package/.agent/scripts/schema_validator.js +373 -280
  30. package/.agent/scripts/security_scan.js +333 -190
  31. package/.agent/scripts/session_manager.js +306 -270
  32. package/.agent/scripts/skill_evolution.js +810 -637
  33. package/.agent/scripts/skill_integrator.js +327 -307
  34. package/.agent/scripts/strengthen_skills.js +203 -193
  35. package/.agent/scripts/swarm_dispatcher.js +558 -457
  36. package/.agent/scripts/test_runner.js +178 -152
  37. package/.agent/scripts/verify_all.js +200 -168
  38. package/.agent/skills/fabel-protocol/SKILL.md +235 -0
  39. package/.agent/skills/thinking-protocol/SKILL.md +27 -0
  40. package/.agent/workflows/generate.md +1 -1
  41. package/.agent/workflows/tribunal-speed.md +1 -1
  42. package/README.md +53 -53
  43. package/bin/mcp-server.js +460 -175
  44. package/bin/tribunal-kit.js +1245 -987
  45. package/bin/wrapper.js +104 -74
  46. package/dist/cli.js +31 -0
  47. package/dist/commands/case.js +23 -0
  48. package/dist/commands/compile.js +84 -0
  49. package/dist/commands/init.js +42 -0
  50. package/dist/commands/learn.js +57 -0
  51. package/dist/commands/memory.js +456 -0
  52. package/package.json +2 -2
  53. package/scripts/benchmark.js +162 -125
  54. package/scripts/changelog.js +196 -168
  55. package/scripts/sync-version.js +94 -81
  56. package/scripts/validate-payload.js +85 -78
@@ -19,27 +19,45 @@
19
19
  * node .agent/scripts/marathon_harness.js add-feature "category" "description" "step1" "step2" ...
20
20
  */
21
21
 
22
- 'use strict';
22
+ "use strict";
23
23
 
24
- const fs = require('fs');
25
- const path = require('path');
26
- const { execSync } = require('child_process');
24
+ const fs = require("fs");
25
+ const path = require("path");
26
+ const { execSync } = require("child_process");
27
27
 
28
28
  const {
29
- GREEN, YELLOW, CYAN, RED, BLUE, MAGENTA, GRAY,
30
- BOLD, DIM, RESET,
31
- BOX, banner, sectionHeader, formatMs, ok, fail, warn, info, summaryTable, timer
32
- } = require('./_colors');
29
+ GREEN,
30
+ YELLOW,
31
+ CYAN,
32
+ RED,
33
+ MAGENTA,
34
+ BOLD,
35
+ DIM,
36
+ RESET,
37
+ BOX,
38
+ banner,
39
+ ok,
40
+ warn,
41
+ info,
42
+ } = require("./_colors");
33
43
 
34
44
  // ── Paths ────────────────────────────────────────────────────────────────────
35
- const MARATHON_DIR = path.resolve('.agent', 'history', 'marathon');
36
- const FEATURE_LIST_FILE = path.join(MARATHON_DIR, 'feature_list.json');
37
- const PROGRESS_FILE = path.join(MARATHON_DIR, 'progress.json');
38
- const ARCHIVE_DIR = path.join(MARATHON_DIR, 'archive');
45
+ const MARATHON_DIR = path.resolve(".agent", "history", "marathon");
46
+ const FEATURE_LIST_FILE = path.join(MARATHON_DIR, "feature_list.json");
47
+ const PROGRESS_FILE = path.join(MARATHON_DIR, "progress.json");
48
+ const ARCHIVE_DIR = path.join(MARATHON_DIR, "archive");
39
49
 
40
50
  const VALID_COMMANDS = new Set([
41
- 'init', 'status', 'next', 'mark', 'log',
42
- 'session-start', 'session-end', 'reset', 'add-feature', 'distill'
51
+ "init",
52
+ "status",
53
+ "next",
54
+ "mark",
55
+ "log",
56
+ "session-start",
57
+ "session-end",
58
+ "reset",
59
+ "add-feature",
60
+ "distill",
43
61
  ]);
44
62
 
45
63
  // ── Schema Defaults ──────────────────────────────────────────────────────────
@@ -50,12 +68,12 @@ const VALID_COMMANDS = new Set([
50
68
  * @returns {object}
51
69
  */
52
70
  function createFeatureList(spec) {
53
- return {
54
- spec,
55
- createdAt: new Date().toISOString(),
56
- totalFeatures: 0,
57
- features: []
58
- };
71
+ return {
72
+ spec,
73
+ createdAt: new Date().toISOString(),
74
+ totalFeatures: 0,
75
+ features: [],
76
+ };
59
77
  }
60
78
 
61
79
  /**
@@ -64,13 +82,13 @@ function createFeatureList(spec) {
64
82
  * @returns {object}
65
83
  */
66
84
  function createProgress(spec) {
67
- return {
68
- spec,
69
- startedAt: new Date().toISOString(),
70
- totalSessions: 0,
71
- sessions: [],
72
- log: []
73
- };
85
+ return {
86
+ spec,
87
+ startedAt: new Date().toISOString(),
88
+ totalSessions: 0,
89
+ sessions: [],
90
+ log: [],
91
+ };
74
92
  }
75
93
 
76
94
  // ── File I/O ─────────────────────────────────────────────────────────────────
@@ -81,14 +99,16 @@ function createProgress(spec) {
81
99
  * @returns {object|null}
82
100
  */
83
101
  function readJSON(filePath) {
84
- if (!fs.existsSync(filePath)) return null;
85
- try {
86
- const content = fs.readFileSync(filePath, 'utf8');
87
- return JSON.parse(content);
88
- } catch (e) {
89
- console.error(`${RED}Error reading ${path.basename(filePath)}: ${e.message}${RESET}`);
90
- return null;
91
- }
102
+ if (!fs.existsSync(filePath)) return null;
103
+ try {
104
+ const content = fs.readFileSync(filePath, "utf8");
105
+ return JSON.parse(content);
106
+ } catch (e) {
107
+ console.error(
108
+ `${RED}Error reading ${path.basename(filePath)}: ${e.message}${RESET}`,
109
+ );
110
+ return null;
111
+ }
92
112
  }
93
113
 
94
114
  /**
@@ -97,15 +117,15 @@ function readJSON(filePath) {
97
117
  * @param {object} data
98
118
  */
99
119
  function writeJSON(filePath, data) {
100
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
101
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
120
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
121
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
102
122
  }
103
123
 
104
124
  /**
105
125
  * Ensure the marathon directory exists.
106
126
  */
107
127
  function ensureDir() {
108
- fs.mkdirSync(MARATHON_DIR, { recursive: true });
128
+ fs.mkdirSync(MARATHON_DIR, { recursive: true });
109
129
  }
110
130
 
111
131
  /**
@@ -113,7 +133,7 @@ function ensureDir() {
113
133
  * @returns {boolean}
114
134
  */
115
135
  function isActive() {
116
- return fs.existsSync(FEATURE_LIST_FILE) && fs.existsSync(PROGRESS_FILE);
136
+ return fs.existsSync(FEATURE_LIST_FILE) && fs.existsSync(PROGRESS_FILE);
117
137
  }
118
138
 
119
139
  // ── Git Helpers ──────────────────────────────────────────────────────────────
@@ -124,15 +144,15 @@ function isActive() {
124
144
  * @returns {string[]}
125
145
  */
126
146
  function getGitLog(count = 20) {
127
- try {
128
- const output = execSync(`git log --oneline -${count}`, {
129
- encoding: 'utf8',
130
- stdio: ['pipe', 'pipe', 'pipe']
131
- });
132
- return output.trim().split('\n').filter(Boolean);
133
- } catch {
134
- return [];
135
- }
147
+ try {
148
+ const output = execSync(`git log --oneline -${count}`, {
149
+ encoding: "utf8",
150
+ stdio: ["pipe", "pipe", "pipe"],
151
+ });
152
+ return output.trim().split("\n").filter(Boolean);
153
+ } catch {
154
+ return [];
155
+ }
136
156
  }
137
157
 
138
158
  /**
@@ -140,14 +160,14 @@ function getGitLog(count = 20) {
140
160
  * @returns {string}
141
161
  */
142
162
  function getGitBranch() {
143
- try {
144
- return execSync('git branch --show-current', {
145
- encoding: 'utf8',
146
- stdio: ['pipe', 'pipe', 'pipe']
147
- }).trim();
148
- } catch {
149
- return 'unknown';
150
- }
163
+ try {
164
+ return execSync("git branch --show-current", {
165
+ encoding: "utf8",
166
+ stdio: ["pipe", "pipe", "pipe"],
167
+ }).trim();
168
+ } catch {
169
+ return "unknown";
170
+ }
151
171
  }
152
172
 
153
173
  // ── Progress Helpers ─────────────────────────────────────────────────────────
@@ -158,22 +178,22 @@ function getGitBranch() {
158
178
  * @returns {{ total: number, passing: number, failing: number, blocked: number }}
159
179
  */
160
180
  function countFeatures(featureList) {
161
- const features = featureList.features || [];
162
- const total = features.length;
163
- const passing = features.filter(f => f.passes === true).length;
164
- let blocked = 0;
165
-
166
- features.forEach(f => {
167
- if (!f.passes && f.dependencies && f.dependencies.length > 0) {
168
- const allPassed = f.dependencies.every(depId => {
169
- const dep = features.find(d => d.id === depId);
170
- return dep && dep.passes === true;
171
- });
172
- if (!allPassed) blocked++;
173
- }
174
- });
175
-
176
- return { total, passing, failing: total - passing, blocked };
181
+ const features = featureList.features || [];
182
+ const total = features.length;
183
+ const passing = features.filter((f) => f.passes === true).length;
184
+ let blocked = 0;
185
+
186
+ features.forEach((f) => {
187
+ if (!f.passes && f.dependencies && f.dependencies.length > 0) {
188
+ const allPassed = f.dependencies.every((depId) => {
189
+ const dep = features.find((d) => d.id === depId);
190
+ return dep && dep.passes === true;
191
+ });
192
+ if (!allPassed) blocked++;
193
+ }
194
+ });
195
+
196
+ return { total, passing, failing: total - passing, blocked };
177
197
  }
178
198
 
179
199
  /**
@@ -182,21 +202,23 @@ function countFeatures(featureList) {
182
202
  * @returns {object|null}
183
203
  */
184
204
  function getNextFeature(featureList) {
185
- const features = featureList.features || [];
186
- return features.find(f => {
187
- if (f.passes === true) return false;
188
-
189
- // Check dependencies (DAG)
190
- if (f.dependencies && f.dependencies.length > 0) {
191
- const allPassed = f.dependencies.every(depId => {
192
- const dep = features.find(d => d.id === depId);
193
- return dep && dep.passes === true;
194
- });
195
- if (!allPassed) return false; // Feature is blocked
196
- }
197
-
198
- return true;
199
- }) || null;
205
+ const features = featureList.features || [];
206
+ return (
207
+ features.find((f) => {
208
+ if (f.passes === true) return false;
209
+
210
+ // Check dependencies (DAG)
211
+ if (f.dependencies && f.dependencies.length > 0) {
212
+ const allPassed = f.dependencies.every((depId) => {
213
+ const dep = features.find((d) => d.id === depId);
214
+ return dep && dep.passes === true;
215
+ });
216
+ if (!allPassed) return false; // Feature is blocked
217
+ }
218
+
219
+ return true;
220
+ }) || null
221
+ );
200
222
  }
201
223
 
202
224
  /**
@@ -207,17 +229,17 @@ function getNextFeature(featureList) {
207
229
  * @returns {string}
208
230
  */
209
231
  function progressBar(current, total, width = 30) {
210
- if (total === 0) return `${DIM}[${''.repeat(width)}]${RESET} 0%`;
211
- const pct = Math.round((current / total) * 100);
212
- const filled = Math.round((current / total) * width);
213
- const empty = width - filled;
232
+ if (total === 0) return `${DIM}[${"".repeat(width)}]${RESET} 0%`;
233
+ const pct = Math.round((current / total) * 100);
234
+ const filled = Math.round((current / total) * width);
235
+ const empty = width - filled;
214
236
 
215
- let color = RED;
216
- if (pct >= 75) color = GREEN;
217
- else if (pct >= 40) color = YELLOW;
218
- else if (pct >= 15) color = CYAN;
237
+ let color = RED;
238
+ if (pct >= 75) color = GREEN;
239
+ else if (pct >= 40) color = YELLOW;
240
+ else if (pct >= 15) color = CYAN;
219
241
 
220
- return `${color}[${''.repeat(filled)}${''.repeat(empty)}]${RESET} ${BOLD}${pct}%${RESET}`;
242
+ return `${color}[${"".repeat(filled)}${"".repeat(empty)}]${RESET} ${BOLD}${pct}%${RESET}`;
221
243
  }
222
244
 
223
245
  // ── Commands ─────────────────────────────────────────────────────────────────
@@ -227,37 +249,49 @@ function progressBar(current, total, width = 30) {
227
249
  * @param {string} spec
228
250
  */
229
251
  function cmdInit(spec) {
230
- if (isActive()) {
231
- console.error(`${RED}❌ A marathon is already active.${RESET}`);
232
- console.error(` Use ${CYAN}reset${RESET} to archive it first, or ${CYAN}status${RESET} to view progress.`);
233
- process.exit(1);
234
- }
235
-
236
- if (!spec) {
237
- console.error(`${RED}❌ Spec required. Usage: marathon_harness.js init "Build a todo app"${RESET}`);
238
- process.exit(1);
239
- }
240
-
241
- ensureDir();
242
-
243
- const featureList = createFeatureList(spec);
244
- const progress = createProgress(spec);
245
-
246
- writeJSON(FEATURE_LIST_FILE, featureList);
247
- writeJSON(PROGRESS_FILE, progress);
248
-
249
- console.log(banner('marathon_harness.js', { Mode: 'INIT' }));
250
- console.log();
251
- ok(`Marathon initialized for: ${BOLD}${spec}${RESET}`);
252
- console.log();
253
- info('Next steps for the agent:');
254
- console.log(` ${DIM}1.${RESET} Decompose the spec into 30-200 atomic features`);
255
- console.log(` ${DIM}2.${RESET} Add each feature with: ${CYAN}add-feature "category" "description" "step1" "step2" ...${RESET}`);
256
- console.log(` ${DIM}3.${RESET} Make an initial git commit: ${CYAN}git commit -m "marathon: initial scaffold"${RESET}`);
257
- console.log(` ${DIM}4.${RESET} Start the first session: ${CYAN}session-start${RESET}`);
258
- console.log();
259
- console.log(` ${DIM}State directory: ${MARATHON_DIR}${RESET}`);
260
- console.log();
252
+ if (isActive()) {
253
+ console.error(`${RED}❌ A marathon is already active.${RESET}`);
254
+ console.error(
255
+ ` Use ${CYAN}reset${RESET} to archive it first, or ${CYAN}status${RESET} to view progress.`,
256
+ );
257
+ process.exit(1);
258
+ }
259
+
260
+ if (!spec) {
261
+ console.error(
262
+ `${RED}❌ Spec required. Usage: marathon_harness.js init "Build a todo app"${RESET}`,
263
+ );
264
+ process.exit(1);
265
+ }
266
+
267
+ ensureDir();
268
+
269
+ const featureList = createFeatureList(spec);
270
+ const progress = createProgress(spec);
271
+
272
+ writeJSON(FEATURE_LIST_FILE, featureList);
273
+ writeJSON(PROGRESS_FILE, progress);
274
+
275
+ console.log(banner("marathon_harness.js", { Mode: "INIT" }));
276
+ console.log();
277
+ ok(`Marathon initialized for: ${BOLD}${spec}${RESET}`);
278
+ console.log();
279
+ info("Next steps for the agent:");
280
+ console.log(
281
+ ` ${DIM}1.${RESET} Decompose the spec into 30-200 atomic features`,
282
+ );
283
+ console.log(
284
+ ` ${DIM}2.${RESET} Add each feature with: ${CYAN}add-feature "category" "description" "step1" "step2" ...${RESET}`,
285
+ );
286
+ console.log(
287
+ ` ${DIM}3.${RESET} Make an initial git commit: ${CYAN}git commit -m "marathon: initial scaffold"${RESET}`,
288
+ );
289
+ console.log(
290
+ ` ${DIM}4.${RESET} Start the first session: ${CYAN}session-start${RESET}`,
291
+ );
292
+ console.log();
293
+ console.log(` ${DIM}State directory: ${MARATHON_DIR}${RESET}`);
294
+ console.log();
261
295
  }
262
296
 
263
297
  /**
@@ -268,183 +302,224 @@ function cmdInit(spec) {
268
302
  * @param {number[]} deps
269
303
  */
270
304
  function cmdAddFeature(category, description, steps, deps = []) {
271
- if (!isActive()) {
272
- console.error(`${RED}❌ No active marathon. Run ${CYAN}init${RED} first.${RESET}`);
273
- process.exit(1);
274
- }
275
-
276
- if (!category || !description) {
277
- console.error(`${RED}❌ Usage: add-feature "category" "description" "step1" "step2" ...${RESET}`);
278
- process.exit(1);
279
- }
280
-
281
- const featureList = readJSON(FEATURE_LIST_FILE);
282
- if (!featureList) process.exit(1);
283
-
284
- const newId = (featureList.features.length > 0)
285
- ? Math.max(...featureList.features.map(f => f.id)) + 1
286
- : 1;
287
-
288
- const feature = {
289
- id: newId,
290
- category: category.toLowerCase(),
291
- description,
292
- steps: steps.length > 0 ? steps : ['Implement and verify'],
293
- dependencies: deps,
294
- attempts: 0,
295
- failureReasons: [],
296
- passes: false,
297
- sessionCompleted: null
298
- };
299
-
300
- featureList.features.push(feature);
301
- featureList.totalFeatures = featureList.features.length;
302
-
303
- writeJSON(FEATURE_LIST_FILE, featureList);
304
-
305
- console.log(` ${GREEN}+${RESET} Feature ${BOLD}#${newId}${RESET} [${MAGENTA}${category}${RESET}]: ${description}`);
305
+ if (!isActive()) {
306
+ console.error(
307
+ `${RED}❌ No active marathon. Run ${CYAN}init${RED} first.${RESET}`,
308
+ );
309
+ process.exit(1);
310
+ }
311
+
312
+ if (!category || !description) {
313
+ console.error(
314
+ `${RED}❌ Usage: add-feature "category" "description" "step1" "step2" ...${RESET}`,
315
+ );
316
+ process.exit(1);
317
+ }
318
+
319
+ const featureList = readJSON(FEATURE_LIST_FILE);
320
+ if (!featureList) process.exit(1);
321
+
322
+ const newId =
323
+ featureList.features.length > 0
324
+ ? Math.max(...featureList.features.map((f) => f.id)) + 1
325
+ : 1;
326
+
327
+ const feature = {
328
+ id: newId,
329
+ category: category.toLowerCase(),
330
+ description,
331
+ steps: steps.length > 0 ? steps : ["Implement and verify"],
332
+ dependencies: deps,
333
+ attempts: 0,
334
+ failureReasons: [],
335
+ passes: false,
336
+ sessionCompleted: null,
337
+ };
338
+
339
+ featureList.features.push(feature);
340
+ featureList.totalFeatures = featureList.features.length;
341
+
342
+ writeJSON(FEATURE_LIST_FILE, featureList);
343
+
344
+ console.log(
345
+ ` ${GREEN}+${RESET} Feature ${BOLD}#${newId}${RESET} [${MAGENTA}${category}${RESET}]: ${description}`,
346
+ );
306
347
  }
307
348
 
308
349
  /**
309
350
  * Show the marathon status dashboard.
310
351
  */
311
352
  function cmdStatus() {
312
- if (!isActive()) {
313
- console.log(`${YELLOW}No active marathon.${RESET} Start one with: ${CYAN}marathon_harness.js init "spec"${RESET}`);
314
- return;
353
+ if (!isActive()) {
354
+ console.log(
355
+ `${YELLOW}No active marathon.${RESET} Start one with: ${CYAN}marathon_harness.js init "spec"${RESET}`,
356
+ );
357
+ return;
358
+ }
359
+
360
+ const featureList = readJSON(FEATURE_LIST_FILE);
361
+ const progress = readJSON(PROGRESS_FILE);
362
+ if (!featureList || !progress) return;
363
+
364
+ const { total, passing, blocked } = countFeatures(featureList);
365
+ const nextFeature = getNextFeature(featureList);
366
+ const sessions = progress.sessions || [];
367
+ const lastSession = sessions[sessions.length - 1] || null;
368
+
369
+ console.log(banner("marathon_harness.js", { Mode: "STATUS" }));
370
+ console.log();
371
+
372
+ // ── Spec ──
373
+ console.log(` ${BOLD}Spec:${RESET} ${featureList.spec}`);
374
+ console.log(` ${DIM}Started: ${featureList.createdAt.slice(0, 16)}${RESET}`);
375
+ console.log();
376
+
377
+ // ── Progress Bar ──
378
+ const blockedInfo =
379
+ blocked > 0 ? ` (${YELLOW}${blocked} blocked${RESET})` : "";
380
+ console.log(
381
+ ` ${BOLD}Progress:${RESET} ${progressBar(passing, total)} ${GREEN}${passing}${RESET}/${total} features${blockedInfo}`,
382
+ );
383
+ console.log();
384
+
385
+ // ── Category Breakdown ──
386
+ const categories = {};
387
+ for (const f of featureList.features) {
388
+ const cat = f.category || "uncategorized";
389
+ if (!categories[cat]) categories[cat] = { total: 0, passing: 0 };
390
+ categories[cat].total++;
391
+ if (f.passes) categories[cat].passing++;
392
+ }
393
+
394
+ if (Object.keys(categories).length > 0) {
395
+ console.log(` ${BOLD}By Category:${RESET}`);
396
+ for (const [cat, counts] of Object.entries(categories)) {
397
+ const catPct =
398
+ counts.total > 0
399
+ ? Math.round((counts.passing / counts.total) * 100)
400
+ : 0;
401
+ const catColor = catPct === 100 ? GREEN : catPct >= 50 ? YELLOW : RED;
402
+ console.log(
403
+ ` ${MAGENTA}${cat.padEnd(18)}${RESET} ${catColor}${counts.passing}/${counts.total}${RESET} (${catPct}%)`,
404
+ );
315
405
  }
316
-
317
- const featureList = readJSON(FEATURE_LIST_FILE);
318
- const progress = readJSON(PROGRESS_FILE);
319
- if (!featureList || !progress) return;
320
-
321
- const { total, passing, failing, blocked } = countFeatures(featureList);
322
- const nextFeature = getNextFeature(featureList);
323
- const sessions = progress.sessions || [];
324
- const lastSession = sessions[sessions.length - 1] || null;
325
-
326
- console.log(banner('marathon_harness.js', { Mode: 'STATUS' }));
327
406
  console.log();
328
-
329
- // ── Spec ──
330
- console.log(` ${BOLD}Spec:${RESET} ${featureList.spec}`);
331
- console.log(` ${DIM}Started: ${featureList.createdAt.slice(0, 16)}${RESET}`);
332
- console.log();
333
-
334
- // ── Progress Bar ──
335
- const blockedInfo = blocked > 0 ? ` (${YELLOW}${blocked} blocked${RESET})` : '';
336
- console.log(` ${BOLD}Progress:${RESET} ${progressBar(passing, total)} ${GREEN}${passing}${RESET}/${total} features${blockedInfo}`);
337
- console.log();
338
-
339
- // ── Category Breakdown ──
340
- const categories = {};
341
- for (const f of featureList.features) {
342
- const cat = f.category || 'uncategorized';
343
- if (!categories[cat]) categories[cat] = { total: 0, passing: 0 };
344
- categories[cat].total++;
345
- if (f.passes) categories[cat].passing++;
407
+ }
408
+
409
+ // ── Sessions ──
410
+ console.log(` ${BOLD}Sessions:${RESET} ${sessions.length} completed`);
411
+ if (lastSession) {
412
+ console.log(
413
+ ` ${DIM}Last session:${RESET} #${lastSession.session} ${lastSession.endedAt?.slice(0, 16) || "in progress"}`,
414
+ );
415
+ if (lastSession.notes) {
416
+ console.log(` ${DIM}Notes:${RESET} ${lastSession.notes.slice(0, 80)}`);
346
417
  }
347
-
348
- if (Object.keys(categories).length > 0) {
349
- console.log(` ${BOLD}By Category:${RESET}`);
350
- for (const [cat, counts] of Object.entries(categories)) {
351
- const catPct = counts.total > 0 ? Math.round((counts.passing / counts.total) * 100) : 0;
352
- const catColor = catPct === 100 ? GREEN : catPct >= 50 ? YELLOW : RED;
353
- console.log(` ${MAGENTA}${cat.padEnd(18)}${RESET} ${catColor}${counts.passing}/${counts.total}${RESET} (${catPct}%)`);
354
- }
355
- console.log();
418
+ if (lastSession.featuresAtEnd) {
419
+ const delta =
420
+ lastSession.featuresAtEnd.passing -
421
+ (lastSession.featuresAtStart?.passing || 0);
422
+ console.log(
423
+ ` ${DIM}Features completed:${RESET} ${GREEN}+${delta}${RESET}`,
424
+ );
356
425
  }
357
-
358
- // ── Sessions ──
359
- console.log(` ${BOLD}Sessions:${RESET} ${sessions.length} completed`);
360
- if (lastSession) {
361
- console.log(` ${DIM}Last session:${RESET} #${lastSession.session} — ${lastSession.endedAt?.slice(0, 16) || 'in progress'}`);
362
- if (lastSession.notes) {
363
- console.log(` ${DIM}Notes:${RESET} ${lastSession.notes.slice(0, 80)}`);
364
- }
365
- if (lastSession.featuresAtEnd) {
366
- const delta = lastSession.featuresAtEnd.passing - (lastSession.featuresAtStart?.passing || 0);
367
- console.log(` ${DIM}Features completed:${RESET} ${GREEN}+${delta}${RESET}`);
368
- }
426
+ }
427
+ console.log();
428
+
429
+ // ── Next Feature ──
430
+ if (nextFeature) {
431
+ console.log(
432
+ ` ${BOLD}Next Feature:${RESET} ${CYAN}#${nextFeature.id}${RESET} [${MAGENTA}${nextFeature.category}${RESET}]`,
433
+ );
434
+ console.log(` ${nextFeature.description}`);
435
+ if (nextFeature.steps && nextFeature.steps.length > 0) {
436
+ console.log(` ${DIM}Steps:${RESET}`);
437
+ for (const step of nextFeature.steps) {
438
+ console.log(` ${DIM}${BOX.bulletEmpty}${RESET} ${step}`);
439
+ }
369
440
  }
370
- console.log();
371
-
372
- // ── Next Feature ──
373
- if (nextFeature) {
374
- console.log(` ${BOLD}Next Feature:${RESET} ${CYAN}#${nextFeature.id}${RESET} [${MAGENTA}${nextFeature.category}${RESET}]`);
375
- console.log(` ${nextFeature.description}`);
376
- if (nextFeature.steps && nextFeature.steps.length > 0) {
377
- console.log(` ${DIM}Steps:${RESET}`);
378
- for (const step of nextFeature.steps) {
379
- console.log(` ${DIM}${BOX.bulletEmpty}${RESET} ${step}`);
380
- }
381
- }
382
- } else if (total > 0) {
383
- console.log(` ${GREEN}${BOLD}🎉 All ${total} features are passing!${RESET}`);
441
+ } else if (total > 0) {
442
+ console.log(
443
+ ` ${GREEN}${BOLD}🎉 All ${total} features are passing!${RESET}`,
444
+ );
445
+ }
446
+ console.log();
447
+
448
+ // ── Git ──
449
+ const branch = getGitBranch();
450
+ const recentCommits = getGitLog(5);
451
+ if (recentCommits.length > 0) {
452
+ console.log(` ${BOLD}Git:${RESET} ${DIM}branch: ${branch}${RESET}`);
453
+ for (const commit of recentCommits.slice(0, 3)) {
454
+ console.log(` ${DIM}${commit}${RESET}`);
384
455
  }
385
- console.log();
386
-
387
- // ── Git ──
388
- const branch = getGitBranch();
389
- const recentCommits = getGitLog(5);
390
- if (recentCommits.length > 0) {
391
- console.log(` ${BOLD}Git:${RESET} ${DIM}branch: ${branch}${RESET}`);
392
- for (const commit of recentCommits.slice(0, 3)) {
393
- console.log(` ${DIM}${commit}${RESET}`);
394
- }
395
- }
396
- console.log();
456
+ }
457
+ console.log();
397
458
  }
398
459
 
399
460
  /**
400
461
  * Show the next unfinished feature.
401
462
  */
402
463
  function cmdNext() {
403
- if (!isActive()) {
404
- console.error(`${RED}❌ No active marathon.${RESET}`);
405
- process.exit(1);
464
+ if (!isActive()) {
465
+ console.error(`${RED}❌ No active marathon.${RESET}`);
466
+ process.exit(1);
467
+ }
468
+
469
+ const featureList = readJSON(FEATURE_LIST_FILE);
470
+ if (!featureList) process.exit(1);
471
+
472
+ const { total, passing } = countFeatures(featureList);
473
+ const nextFeature = getNextFeature(featureList);
474
+
475
+ if (!nextFeature) {
476
+ if (passing === total) {
477
+ console.log(
478
+ `${GREEN}${BOLD}🎉 All ${total} features are passing! Marathon complete.${RESET}`,
479
+ );
480
+ } else {
481
+ console.log(
482
+ `${RED}${BOLD}⚠️ Deadlock detected: ${total - passing} features remain, but all are blocked by failing dependencies.${RESET}`,
483
+ );
484
+ console.log(
485
+ ` ${DIM}Check 'status' and use 'mark <id> pass' to resolve dependencies.${RESET}`,
486
+ );
406
487
  }
407
-
408
- const featureList = readJSON(FEATURE_LIST_FILE);
409
- if (!featureList) process.exit(1);
410
-
411
- const { total, passing } = countFeatures(featureList);
412
- const nextFeature = getNextFeature(featureList);
413
-
414
- if (!nextFeature) {
415
- if (passing === total) {
416
- console.log(`${GREEN}${BOLD}🎉 All ${total} features are passing! Marathon complete.${RESET}`);
417
- } else {
418
- console.log(`${RED}${BOLD}⚠️ Deadlock detected: ${total - passing} features remain, but all are blocked by failing dependencies.${RESET}`);
419
- console.log(` ${DIM}Check 'status' and use 'mark <id> pass' to resolve dependencies.${RESET}`);
420
- }
421
- return;
488
+ return;
489
+ }
490
+
491
+ console.log(
492
+ `\n ${BOLD}Progress:${RESET} ${progressBar(passing, total)} ${GREEN}${passing}${RESET}/${total}`,
493
+ );
494
+ console.log();
495
+ console.log(
496
+ ` ${BOLD}Next Feature:${RESET} ${CYAN}#${nextFeature.id}${RESET} [${MAGENTA}${nextFeature.category}${RESET}]`,
497
+ );
498
+ console.log(` ${nextFeature.description}`);
499
+ console.log();
500
+
501
+ if (nextFeature.steps && nextFeature.steps.length > 0) {
502
+ console.log(` ${BOLD}Steps:${RESET}`);
503
+ for (const step of nextFeature.steps) {
504
+ console.log(` ${BOX.bulletEmpty} ${step}`);
422
505
  }
423
-
424
- console.log(`\n ${BOLD}Progress:${RESET} ${progressBar(passing, total)} ${GREEN}${passing}${RESET}/${total}`);
425
506
  console.log();
426
- console.log(` ${BOLD}Next Feature:${RESET} ${CYAN}#${nextFeature.id}${RESET} [${MAGENTA}${nextFeature.category}${RESET}]`);
427
- console.log(` ${nextFeature.description}`);
428
- console.log();
429
-
430
- if (nextFeature.steps && nextFeature.steps.length > 0) {
431
- console.log(` ${BOLD}Steps:${RESET}`);
432
- for (const step of nextFeature.steps) {
433
- console.log(` ${BOX.bulletEmpty} ${step}`);
434
- }
435
- console.log();
436
- }
437
-
438
- if (nextFeature.failureReasons && nextFeature.failureReasons.length > 0) {
439
- console.log(` ${RED}${BOLD}Previous Failures (${nextFeature.attempts} attempts):${RESET}`);
440
- for (const reason of nextFeature.failureReasons) {
441
- console.log(` ${DIM}* ${reason}${RESET}`);
442
- }
443
- console.log();
507
+ }
508
+
509
+ if (nextFeature.failureReasons && nextFeature.failureReasons.length > 0) {
510
+ console.log(
511
+ ` ${RED}${BOLD}Previous Failures (${nextFeature.attempts} attempts):${RESET}`,
512
+ );
513
+ for (const reason of nextFeature.failureReasons) {
514
+ console.log(` ${DIM}* ${reason}${RESET}`);
444
515
  }
445
-
446
- console.log(` ${DIM}When done: marathon_harness.js mark ${nextFeature.id} pass${RESET}`);
447
516
  console.log();
517
+ }
518
+
519
+ console.log(
520
+ ` ${DIM}When done: marathon_harness.js mark ${nextFeature.id} pass${RESET}`,
521
+ );
522
+ console.log();
448
523
  }
449
524
 
450
525
  /**
@@ -454,56 +529,64 @@ function cmdNext() {
454
529
  * @param {string} [reason] - Reason for failure
455
530
  */
456
531
  function cmdMark(id, verdict, reason) {
457
- if (!isActive()) {
458
- console.error(`${RED}❌ No active marathon.${RESET}`);
459
- process.exit(1);
460
- }
461
-
462
- const validVerdicts = ['pass', 'fail'];
463
- if (!validVerdicts.includes(verdict)) {
464
- console.error(`${RED}❌ Invalid verdict "${verdict}". Use: pass | fail${RESET}`);
465
- process.exit(1);
532
+ if (!isActive()) {
533
+ console.error(`${RED}❌ No active marathon.${RESET}`);
534
+ process.exit(1);
535
+ }
536
+
537
+ const validVerdicts = ["pass", "fail"];
538
+ if (!validVerdicts.includes(verdict)) {
539
+ console.error(
540
+ `${RED}❌ Invalid verdict "${verdict}". Use: pass | fail${RESET}`,
541
+ );
542
+ process.exit(1);
543
+ }
544
+
545
+ const featureList = readJSON(FEATURE_LIST_FILE);
546
+ if (!featureList) process.exit(1);
547
+
548
+ const feature = featureList.features.find((f) => f.id === id);
549
+ if (!feature) {
550
+ console.error(
551
+ `${RED}❌ Feature #${id} not found. Valid IDs: 1-${featureList.features.length}${RESET}`,
552
+ );
553
+ process.exit(1);
554
+ }
555
+
556
+ const newPasses = verdict === "pass";
557
+ const oldPasses = feature.passes;
558
+
559
+ // Guard: don't allow editing description or steps
560
+ feature.passes = newPasses;
561
+ feature.sessionCompleted = newPasses ? new Date().toISOString() : null;
562
+
563
+ if (!newPasses) {
564
+ feature.attempts = (feature.attempts || 0) + 1;
565
+ if (reason) {
566
+ if (!feature.failureReasons) feature.failureReasons = [];
567
+ feature.failureReasons.push(`Attempt ${feature.attempts}: ${reason}`);
466
568
  }
467
-
468
- const featureList = readJSON(FEATURE_LIST_FILE);
469
- if (!featureList) process.exit(1);
470
-
471
- const feature = featureList.features.find(f => f.id === id);
472
- if (!feature) {
473
- console.error(`${RED}❌ Feature #${id} not found. Valid IDs: 1-${featureList.features.length}${RESET}`);
474
- process.exit(1);
475
- }
476
-
477
- const newPasses = verdict === 'pass';
478
- const oldPasses = feature.passes;
479
-
480
- // Guard: don't allow editing description or steps
481
- feature.passes = newPasses;
482
- feature.sessionCompleted = newPasses ? new Date().toISOString() : null;
483
-
484
- if (!newPasses) {
485
- feature.attempts = (feature.attempts || 0) + 1;
486
- if (reason) {
487
- if (!feature.failureReasons) feature.failureReasons = [];
488
- feature.failureReasons.push(`Attempt ${feature.attempts}: ${reason}`);
489
- }
490
- }
491
-
492
- writeJSON(FEATURE_LIST_FILE, featureList);
493
-
494
- const { total, passing } = countFeatures(featureList);
495
-
496
- if (newPasses && !oldPasses) {
497
- ok(`Feature #${id} marked as ${GREEN}PASSING${RESET}`);
498
- } else if (!newPasses && oldPasses) {
499
- warn(`Feature #${id} marked as ${RED}FAILING${RESET}`);
500
- } else {
501
- info(`Feature #${id} unchanged (already ${newPasses ? 'passing' : 'failing'})`);
502
- }
503
-
504
- console.log(` ${DIM}${feature.description}${RESET}`);
505
- console.log(` ${progressBar(passing, total)} ${GREEN}${passing}${RESET}/${total}`);
506
- console.log();
569
+ }
570
+
571
+ writeJSON(FEATURE_LIST_FILE, featureList);
572
+
573
+ const { total, passing } = countFeatures(featureList);
574
+
575
+ if (newPasses && !oldPasses) {
576
+ ok(`Feature #${id} marked as ${GREEN}PASSING${RESET}`);
577
+ } else if (!newPasses && oldPasses) {
578
+ warn(`Feature #${id} marked as ${RED}FAILING${RESET}`);
579
+ } else {
580
+ info(
581
+ `Feature #${id} unchanged (already ${newPasses ? "passing" : "failing"})`,
582
+ );
583
+ }
584
+
585
+ console.log(` ${DIM}${feature.description}${RESET}`);
586
+ console.log(
587
+ ` ${progressBar(passing, total)} ${GREEN}${passing}${RESET}/${total}`,
588
+ );
589
+ console.log();
507
590
  }
508
591
 
509
592
  /**
@@ -511,27 +594,29 @@ function cmdMark(id, verdict, reason) {
511
594
  * @param {string} message
512
595
  */
513
596
  function cmdLog(message) {
514
- if (!isActive()) {
515
- console.error(`${RED}❌ No active marathon.${RESET}`);
516
- process.exit(1);
517
- }
518
-
519
- if (!message) {
520
- console.error(`${RED}❌ Message required. Usage: log "Your progress note"${RESET}`);
521
- process.exit(1);
522
- }
523
-
524
- const progress = readJSON(PROGRESS_FILE);
525
- if (!progress) process.exit(1);
526
-
527
- if (!progress.log) progress.log = [];
528
- progress.log.push({
529
- timestamp: new Date().toISOString(),
530
- message
531
- });
532
-
533
- writeJSON(PROGRESS_FILE, progress);
534
- ok(`Logged: ${message}`);
597
+ if (!isActive()) {
598
+ console.error(`${RED}❌ No active marathon.${RESET}`);
599
+ process.exit(1);
600
+ }
601
+
602
+ if (!message) {
603
+ console.error(
604
+ `${RED}❌ Message required. Usage: log "Your progress note"${RESET}`,
605
+ );
606
+ process.exit(1);
607
+ }
608
+
609
+ const progress = readJSON(PROGRESS_FILE);
610
+ if (!progress) process.exit(1);
611
+
612
+ if (!progress.log) progress.log = [];
613
+ progress.log.push({
614
+ timestamp: new Date().toISOString(),
615
+ message,
616
+ });
617
+
618
+ writeJSON(PROGRESS_FILE, progress);
619
+ ok(`Logged: ${message}`);
535
620
  }
536
621
 
537
622
  /**
@@ -539,125 +624,145 @@ function cmdLog(message) {
539
624
  * @param {string} lesson
540
625
  */
541
626
  function cmdDistill(lesson) {
542
- if (!isActive()) {
543
- console.error(`${RED}❌ No active marathon.${RESET}`);
544
- process.exit(1);
545
- }
546
-
547
- if (!lesson) {
548
- console.error(`${RED}❌ Lesson required. Usage: distill "Your architectural lesson"${RESET}`);
549
- process.exit(1);
550
- }
551
-
552
- ensureDir();
553
- const DISTILL_FILE = path.join(MARATHON_DIR, 'distilled_context.md');
554
- const timestamp = new Date().toISOString().slice(0, 16);
555
- const entry = `- [${timestamp}] ${lesson}\n`;
556
-
557
- fs.appendFileSync(DISTILL_FILE, entry, 'utf8');
558
- ok(`Distilled memory saved: ${lesson}`);
627
+ if (!isActive()) {
628
+ console.error(`${RED}❌ No active marathon.${RESET}`);
629
+ process.exit(1);
630
+ }
631
+
632
+ if (!lesson) {
633
+ console.error(
634
+ `${RED}❌ Lesson required. Usage: distill "Your architectural lesson"${RESET}`,
635
+ );
636
+ process.exit(1);
637
+ }
638
+
639
+ ensureDir();
640
+ const DISTILL_FILE = path.join(MARATHON_DIR, "distilled_context.md");
641
+ const timestamp = new Date().toISOString().slice(0, 16);
642
+ const entry = `- [${timestamp}] ${lesson}\n`;
643
+
644
+ fs.appendFileSync(DISTILL_FILE, entry, "utf8");
645
+ ok(`Distilled memory saved: ${lesson}`);
559
646
  }
560
647
 
561
648
  /**
562
649
  * Start a new session — reads state, shows bearings.
563
650
  */
564
651
  function cmdSessionStart() {
565
- if (!isActive()) {
566
- console.error(`${RED}❌ No active marathon.${RESET}`);
567
- process.exit(1);
652
+ if (!isActive()) {
653
+ console.error(`${RED}❌ No active marathon.${RESET}`);
654
+ process.exit(1);
655
+ }
656
+
657
+ const featureList = readJSON(FEATURE_LIST_FILE);
658
+ const progress = readJSON(PROGRESS_FILE);
659
+ if (!featureList || !progress) process.exit(1);
660
+
661
+ const sessionNum = progress.sessions.length + 1;
662
+ const { total, passing } = countFeatures(featureList);
663
+ const nextFeature = getNextFeature(featureList);
664
+
665
+ // Record session start
666
+ const session = {
667
+ session: sessionNum,
668
+ startedAt: new Date().toISOString(),
669
+ endedAt: null,
670
+ featuresAtStart: { total, passing },
671
+ featuresAtEnd: null,
672
+ featuresCompleted: [],
673
+ notes: null,
674
+ gitCommits: [],
675
+ };
676
+
677
+ progress.sessions.push(session);
678
+ progress.totalSessions = progress.sessions.length;
679
+ writeJSON(PROGRESS_FILE, progress);
680
+
681
+ // Display bearings
682
+ console.log(
683
+ banner("marathon_harness.js", {
684
+ Mode: "SESSION START",
685
+ Session: `#${sessionNum}`,
686
+ }),
687
+ );
688
+ console.log();
689
+
690
+ // ── Spec ──
691
+ console.log(` ${BOLD}Spec:${RESET} ${featureList.spec}`);
692
+ console.log(
693
+ ` ${BOLD}Progress:${RESET} ${progressBar(passing, total)} ${GREEN}${passing}${RESET}/${total}`,
694
+ );
695
+ console.log();
696
+
697
+ // ── Recent git commits ──
698
+ const commits = getGitLog(10);
699
+ if (commits.length > 0) {
700
+ console.log(` ${BOLD}Recent Commits:${RESET}`);
701
+ for (const commit of commits.slice(0, 5)) {
702
+ console.log(` ${DIM}${commit}${RESET}`);
568
703
  }
569
-
570
- const featureList = readJSON(FEATURE_LIST_FILE);
571
- const progress = readJSON(PROGRESS_FILE);
572
- if (!featureList || !progress) process.exit(1);
573
-
574
- const sessionNum = (progress.sessions.length) + 1;
575
- const { total, passing } = countFeatures(featureList);
576
- const nextFeature = getNextFeature(featureList);
577
-
578
- // Record session start
579
- const session = {
580
- session: sessionNum,
581
- startedAt: new Date().toISOString(),
582
- endedAt: null,
583
- featuresAtStart: { total, passing },
584
- featuresAtEnd: null,
585
- featuresCompleted: [],
586
- notes: null,
587
- gitCommits: []
588
- };
589
-
590
- progress.sessions.push(session);
591
- progress.totalSessions = progress.sessions.length;
592
- writeJSON(PROGRESS_FILE, progress);
593
-
594
- // Display bearings
595
- console.log(banner('marathon_harness.js', {
596
- Mode: 'SESSION START',
597
- Session: `#${sessionNum}`
598
- }));
599
704
  console.log();
600
-
601
- // ── Spec ──
602
- console.log(` ${BOLD}Spec:${RESET} ${featureList.spec}`);
603
- console.log(` ${BOLD}Progress:${RESET} ${progressBar(passing, total)} ${GREEN}${passing}${RESET}/${total}`);
604
- console.log();
605
-
606
- // ── Recent git commits ──
607
- const commits = getGitLog(10);
608
- if (commits.length > 0) {
609
- console.log(` ${BOLD}Recent Commits:${RESET}`);
610
- for (const commit of commits.slice(0, 5)) {
611
- console.log(` ${DIM}${commit}${RESET}`);
612
- }
613
- console.log();
705
+ }
706
+
707
+ // ── Last session notes ──
708
+ if (progress.sessions.length > 1) {
709
+ const prev = progress.sessions[progress.sessions.length - 2];
710
+ if (prev && prev.notes) {
711
+ console.log(` ${BOLD}Last Session Notes:${RESET}`);
712
+ console.log(` ${DIM}${prev.notes}${RESET}`);
713
+ console.log();
614
714
  }
615
-
616
- // ── Last session notes ──
617
- if (progress.sessions.length > 1) {
618
- const prev = progress.sessions[progress.sessions.length - 2];
619
- if (prev && prev.notes) {
620
- console.log(` ${BOLD}Last Session Notes:${RESET}`);
621
- console.log(` ${DIM}${prev.notes}${RESET}`);
622
- console.log();
623
- }
715
+ }
716
+
717
+ // ── Recent log entries ──
718
+ const recentLogs = (progress.log || []).slice(-3);
719
+ if (recentLogs.length > 0) {
720
+ console.log(` ${BOLD}Recent Log:${RESET}`);
721
+ for (const entry of recentLogs) {
722
+ console.log(
723
+ ` ${DIM}${entry.timestamp.slice(0, 16)}${RESET} ${entry.message}`,
724
+ );
624
725
  }
625
-
626
- // ── Recent log entries ──
627
- const recentLogs = (progress.log || []).slice(-3);
628
- if (recentLogs.length > 0) {
629
- console.log(` ${BOLD}Recent Log:${RESET}`);
630
- for (const entry of recentLogs) {
631
- console.log(` ${DIM}${entry.timestamp.slice(0, 16)}${RESET} ${entry.message}`);
632
- }
633
- console.log();
726
+ console.log();
727
+ }
728
+
729
+ // ── Next feature ──
730
+ if (nextFeature) {
731
+ console.log(
732
+ ` ${BOLD}${CYAN}▸ Next Feature:${RESET} ${CYAN}#${nextFeature.id}${RESET} [${MAGENTA}${nextFeature.category}${RESET}]`,
733
+ );
734
+ console.log(` ${nextFeature.description}`);
735
+ if (nextFeature.steps && nextFeature.steps.length > 0) {
736
+ for (const step of nextFeature.steps) {
737
+ console.log(` ${DIM}${BOX.bulletEmpty}${RESET} ${step}`);
738
+ }
634
739
  }
635
-
636
- // ── Next feature ──
637
- if (nextFeature) {
638
- console.log(` ${BOLD}${CYAN} Next Feature:${RESET} ${CYAN}#${nextFeature.id}${RESET} [${MAGENTA}${nextFeature.category}${RESET}]`);
639
- console.log(` ${nextFeature.description}`);
640
- if (nextFeature.steps && nextFeature.steps.length > 0) {
641
- for (const step of nextFeature.steps) {
642
- console.log(` ${DIM}${BOX.bulletEmpty}${RESET} ${step}`);
643
- }
644
- }
740
+ } else {
741
+ if (passing === total) {
742
+ console.log(
743
+ ` ${GREEN}${BOLD}🎉 All features passing! Nothing to implement.${RESET}`,
744
+ );
645
745
  } else {
646
- if (passing === total) {
647
- console.log(` ${GREEN}${BOLD}🎉 All features passing! Nothing to implement.${RESET}`);
648
- } else {
649
- console.log(` ${RED}${BOLD}⚠️ Deadlock: ${total - passing} features are blocked by failing dependencies.${RESET}`);
650
- }
746
+ console.log(
747
+ ` ${RED}${BOLD}⚠️ Deadlock: ${total - passing} features are blocked by failing dependencies.${RESET}`,
748
+ );
651
749
  }
652
- console.log();
653
-
654
- // ── Recommended actions ──
655
- console.log(` ${BOLD}Recommended Actions:${RESET}`);
656
- console.log(` ${DIM}1.${RESET} Start dev server (if applicable): ${CYAN}node .agent/scripts/auto_preview.js start${RESET}`);
657
- console.log(` ${DIM}2.${RESET} Smoke test the app to verify it's not broken`);
658
- console.log(` ${DIM}3.${RESET} Implement the next feature shown above`);
659
- console.log(` ${DIM}4.${RESET} Test, mark as passing, commit, then pick next feature`);
660
- console.log();
750
+ }
751
+ console.log();
752
+
753
+ // ── Recommended actions ──
754
+ console.log(` ${BOLD}Recommended Actions:${RESET}`);
755
+ console.log(
756
+ ` ${DIM}1.${RESET} Start dev server (if applicable): ${CYAN}node .agent/scripts/auto_preview.js start${RESET}`,
757
+ );
758
+ console.log(
759
+ ` ${DIM}2.${RESET} Smoke test the app to verify it's not broken`,
760
+ );
761
+ console.log(` ${DIM}3.${RESET} Implement the next feature shown above`);
762
+ console.log(
763
+ ` ${DIM}4.${RESET} Test, mark as passing, commit, then pick next feature`,
764
+ );
765
+ console.log();
661
766
  }
662
767
 
663
768
  /**
@@ -665,232 +770,285 @@ function cmdSessionStart() {
665
770
  * @param {string} summary
666
771
  */
667
772
  function cmdSessionEnd(summary) {
668
- if (!isActive()) {
669
- console.error(`${RED}❌ No active marathon.${RESET}`);
670
- process.exit(1);
671
- }
672
-
673
- const featureList = readJSON(FEATURE_LIST_FILE);
674
- const progress = readJSON(PROGRESS_FILE);
675
- if (!featureList || !progress) process.exit(1);
676
-
677
- const sessions = progress.sessions || [];
678
- if (sessions.length === 0) {
679
- console.error(`${RED}❌ No active session. Run ${CYAN}session-start${RED} first.${RESET}`);
680
- process.exit(1);
681
- }
682
-
683
- const currentSession = sessions[sessions.length - 1];
684
- const { total, passing } = countFeatures(featureList);
685
-
686
- // Calculate features completed during this session
687
- const startPassing = currentSession.featuresAtStart?.passing || 0;
688
- const completedThisSession = passing - startPassing;
689
-
690
- // Find which features were completed (have sessionCompleted in this session range)
691
- const sessionStartTime = currentSession.startedAt;
692
- const completedIds = featureList.features
693
- .filter(f => f.passes && f.sessionCompleted && f.sessionCompleted >= sessionStartTime)
694
- .map(f => f.id);
695
-
696
- // Get git commits since session start
697
- let sessionCommits = [];
698
- try {
699
- const since = currentSession.startedAt;
700
- const output = execSync(`git log --oneline --since="${since}"`, {
701
- encoding: 'utf8',
702
- stdio: ['pipe', 'pipe', 'pipe']
703
- });
704
- sessionCommits = output.trim().split('\n').filter(Boolean).map(l => l.split(' ')[0]);
705
- } catch {
706
- // Git not available or no commits
707
- }
708
-
709
- // Update session record
710
- currentSession.endedAt = new Date().toISOString();
711
- currentSession.featuresAtEnd = { total, passing };
712
- currentSession.featuresCompleted = completedIds;
713
- currentSession.notes = summary || `Session ${currentSession.session}: ${completedThisSession} features completed`;
714
- currentSession.gitCommits = sessionCommits;
715
-
716
- writeJSON(PROGRESS_FILE, progress);
717
-
718
- // Display summary
719
- console.log(banner('marathon_harness.js', {
720
- Mode: 'SESSION END',
721
- Session: `#${currentSession.session}`
722
- }));
723
- console.log();
724
-
725
- console.log(` ${BOLD}Session #${currentSession.session} Summary:${RESET}`);
726
- console.log(` Started: ${currentSession.startedAt.slice(0, 16)}`);
727
- console.log(` Ended: ${currentSession.endedAt.slice(0, 16)}`);
728
- console.log(` Features: ${GREEN}+${completedThisSession}${RESET} completed (${completedIds.map(id => `#${id}`).join(', ') || 'none'})`);
729
- console.log(` Commits: ${sessionCommits.length}`);
730
- if (summary) {
731
- console.log(` Notes: ${summary}`);
732
- }
733
- console.log();
734
-
735
- console.log(` ${BOLD}Overall Progress:${RESET} ${progressBar(passing, total)} ${GREEN}${passing}${RESET}/${total}`);
736
- console.log();
737
-
738
- const remaining = total - passing;
739
- if (remaining > 0) {
740
- const avgPerSession = sessions.length > 0
741
- ? Math.max(1, Math.round(passing / sessions.length))
742
- : 1;
743
- const estRemaining = Math.ceil(remaining / avgPerSession);
744
- console.log(` ${DIM}Estimated sessions remaining: ~${estRemaining} (avg ${avgPerSession} features/session)${RESET}`);
745
- } else {
746
- console.log(` ${GREEN}${BOLD}🎉 Marathon complete! All features passing.${RESET}`);
747
- }
748
- console.log();
773
+ if (!isActive()) {
774
+ console.error(`${RED}❌ No active marathon.${RESET}`);
775
+ process.exit(1);
776
+ }
777
+
778
+ const featureList = readJSON(FEATURE_LIST_FILE);
779
+ const progress = readJSON(PROGRESS_FILE);
780
+ if (!featureList || !progress) process.exit(1);
781
+
782
+ const sessions = progress.sessions || [];
783
+ if (sessions.length === 0) {
784
+ console.error(
785
+ `${RED}❌ No active session. Run ${CYAN}session-start${RED} first.${RESET}`,
786
+ );
787
+ process.exit(1);
788
+ }
789
+
790
+ const currentSession = sessions[sessions.length - 1];
791
+ const { total, passing } = countFeatures(featureList);
792
+
793
+ // Calculate features completed during this session
794
+ const startPassing = currentSession.featuresAtStart?.passing || 0;
795
+ const completedThisSession = passing - startPassing;
796
+
797
+ // Find which features were completed (have sessionCompleted in this session range)
798
+ const sessionStartTime = currentSession.startedAt;
799
+ const completedIds = featureList.features
800
+ .filter(
801
+ (f) =>
802
+ f.passes &&
803
+ f.sessionCompleted &&
804
+ f.sessionCompleted >= sessionStartTime,
805
+ )
806
+ .map((f) => f.id);
807
+
808
+ // Get git commits since session start
809
+ let sessionCommits = [];
810
+ try {
811
+ const since = currentSession.startedAt;
812
+ const output = execSync(`git log --oneline --since="${since}"`, {
813
+ encoding: "utf8",
814
+ stdio: ["pipe", "pipe", "pipe"],
815
+ });
816
+ sessionCommits = output
817
+ .trim()
818
+ .split("\n")
819
+ .filter(Boolean)
820
+ .map((l) => l.split(" ")[0]);
821
+ } catch {
822
+ // Git not available or no commits
823
+ }
824
+
825
+ // Update session record
826
+ currentSession.endedAt = new Date().toISOString();
827
+ currentSession.featuresAtEnd = { total, passing };
828
+ currentSession.featuresCompleted = completedIds;
829
+ currentSession.notes =
830
+ summary ||
831
+ `Session ${currentSession.session}: ${completedThisSession} features completed`;
832
+ currentSession.gitCommits = sessionCommits;
833
+
834
+ writeJSON(PROGRESS_FILE, progress);
835
+
836
+ // Display summary
837
+ console.log(
838
+ banner("marathon_harness.js", {
839
+ Mode: "SESSION END",
840
+ Session: `#${currentSession.session}`,
841
+ }),
842
+ );
843
+ console.log();
844
+
845
+ console.log(` ${BOLD}Session #${currentSession.session} Summary:${RESET}`);
846
+ console.log(` Started: ${currentSession.startedAt.slice(0, 16)}`);
847
+ console.log(` Ended: ${currentSession.endedAt.slice(0, 16)}`);
848
+ console.log(
849
+ ` Features: ${GREEN}+${completedThisSession}${RESET} completed (${completedIds.map((id) => `#${id}`).join(", ") || "none"})`,
850
+ );
851
+ console.log(` Commits: ${sessionCommits.length}`);
852
+ if (summary) {
853
+ console.log(` Notes: ${summary}`);
854
+ }
855
+ console.log();
856
+
857
+ console.log(
858
+ ` ${BOLD}Overall Progress:${RESET} ${progressBar(passing, total)} ${GREEN}${passing}${RESET}/${total}`,
859
+ );
860
+ console.log();
861
+
862
+ const remaining = total - passing;
863
+ if (remaining > 0) {
864
+ const avgPerSession =
865
+ sessions.length > 0
866
+ ? Math.max(1, Math.round(passing / sessions.length))
867
+ : 1;
868
+ const estRemaining = Math.ceil(remaining / avgPerSession);
869
+ console.log(
870
+ ` ${DIM}Estimated sessions remaining: ~${estRemaining} (avg ${avgPerSession} features/session)${RESET}`,
871
+ );
872
+ } else {
873
+ console.log(
874
+ ` ${GREEN}${BOLD}🎉 Marathon complete! All features passing.${RESET}`,
875
+ );
876
+ }
877
+ console.log();
749
878
  }
750
879
 
751
880
  /**
752
881
  * Archive the current marathon and reset.
753
882
  */
754
883
  function cmdReset() {
755
- if (!isActive()) {
756
- console.log(`${YELLOW}No active marathon to reset.${RESET}`);
757
- return;
758
- }
759
-
760
- const featureList = readJSON(FEATURE_LIST_FILE);
761
- const { total, passing } = featureList ? countFeatures(featureList) : { total: 0, passing: 0 };
762
-
763
- // Archive current state
764
- const archiveTimestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
765
- const archivePath = path.join(ARCHIVE_DIR, archiveTimestamp);
766
- fs.mkdirSync(archivePath, { recursive: true });
767
-
768
- if (fs.existsSync(FEATURE_LIST_FILE)) {
769
- fs.cpSync(FEATURE_LIST_FILE, path.join(archivePath, 'feature_list.json'));
770
- }
771
- if (fs.existsSync(PROGRESS_FILE)) {
772
- fs.cpSync(PROGRESS_FILE, path.join(archivePath, 'progress.json'));
773
- }
774
-
775
- // Remove current state files
776
- if (fs.existsSync(FEATURE_LIST_FILE)) fs.unlinkSync(FEATURE_LIST_FILE);
777
- if (fs.existsSync(PROGRESS_FILE)) fs.unlinkSync(PROGRESS_FILE);
778
-
779
- ok(`Marathon archived to: ${archivePath}`);
780
- console.log(` ${DIM}Progress at archive: ${passing}/${total} features passing${RESET}`);
781
- console.log(` ${DIM}Start a new marathon with: marathon_harness.js init "new spec"${RESET}`);
782
- console.log();
884
+ if (!isActive()) {
885
+ console.log(`${YELLOW}No active marathon to reset.${RESET}`);
886
+ return;
887
+ }
888
+
889
+ const featureList = readJSON(FEATURE_LIST_FILE);
890
+ const { total, passing } = featureList
891
+ ? countFeatures(featureList)
892
+ : { total: 0, passing: 0 };
893
+
894
+ // Archive current state
895
+ const archiveTimestamp = new Date()
896
+ .toISOString()
897
+ .replace(/[:.]/g, "-")
898
+ .slice(0, 19);
899
+ const archivePath = path.join(ARCHIVE_DIR, archiveTimestamp);
900
+ fs.mkdirSync(archivePath, { recursive: true });
901
+
902
+ if (fs.existsSync(FEATURE_LIST_FILE)) {
903
+ fs.cpSync(FEATURE_LIST_FILE, path.join(archivePath, "feature_list.json"));
904
+ }
905
+ if (fs.existsSync(PROGRESS_FILE)) {
906
+ fs.cpSync(PROGRESS_FILE, path.join(archivePath, "progress.json"));
907
+ }
908
+
909
+ // Remove current state files
910
+ if (fs.existsSync(FEATURE_LIST_FILE)) fs.unlinkSync(FEATURE_LIST_FILE);
911
+ if (fs.existsSync(PROGRESS_FILE)) fs.unlinkSync(PROGRESS_FILE);
912
+
913
+ ok(`Marathon archived to: ${archivePath}`);
914
+ console.log(
915
+ ` ${DIM}Progress at archive: ${passing}/${total} features passing${RESET}`,
916
+ );
917
+ console.log(
918
+ ` ${DIM}Start a new marathon with: marathon_harness.js init "new spec"${RESET}`,
919
+ );
920
+ console.log();
783
921
  }
784
922
 
785
923
  // ── Help ─────────────────────────────────────────────────────────────────────
786
924
 
787
925
  function showHelp() {
788
- console.log(banner('marathon_harness.js', { Mode: 'HELP' }));
789
- console.log();
790
- console.log(` ${BOLD}Long-Running Agent Harness${RESET}`);
791
- console.log(` ${DIM}Tracks features, progress, and sessions for multi-session agent workflows.${RESET}`);
792
- console.log();
793
-
794
- const cmd = (name, desc) => console.log(` ${CYAN}${name.padEnd(16)}${RESET} ${desc}`);
795
-
796
- cmd('init "spec"', 'Start a new marathon with the given specification');
797
- cmd('status', 'Show progress dashboard');
798
- cmd('next', 'Show the next unfinished feature');
799
- cmd('mark <id> pass', 'Mark a feature as passing');
800
- cmd('mark <id> fail', 'Mark a feature as failing (optional: "reason")');
801
- cmd('log "note"', 'Add a timestamped progress note');
802
- cmd('distill "rule"', 'Save an architectural rule or lesson to memory');
803
- cmd('session-start', 'Begin a new work session (reads state, shows bearings)');
804
- cmd('session-end', 'End session with optional summary');
805
- cmd('add-feature', 'Add a feature (supports --deps=1,2,3 for DAG dependencies)');
806
- cmd('reset', 'Archive current marathon and start fresh');
807
- console.log();
926
+ console.log(banner("marathon_harness.js", { Mode: "HELP" }));
927
+ console.log();
928
+ console.log(` ${BOLD}Long-Running Agent Harness${RESET}`);
929
+ console.log(
930
+ ` ${DIM}Tracks features, progress, and sessions for multi-session agent workflows.${RESET}`,
931
+ );
932
+ console.log();
933
+
934
+ const cmd = (name, desc) =>
935
+ console.log(` ${CYAN}${name.padEnd(16)}${RESET} ${desc}`);
936
+
937
+ cmd('init "spec"', "Start a new marathon with the given specification");
938
+ cmd("status", "Show progress dashboard");
939
+ cmd("next", "Show the next unfinished feature");
940
+ cmd("mark <id> pass", "Mark a feature as passing");
941
+ cmd("mark <id> fail", 'Mark a feature as failing (optional: "reason")');
942
+ cmd('log "note"', "Add a timestamped progress note");
943
+ cmd('distill "rule"', "Save an architectural rule or lesson to memory");
944
+ cmd(
945
+ "session-start",
946
+ "Begin a new work session (reads state, shows bearings)",
947
+ );
948
+ cmd("session-end", "End session with optional summary");
949
+ cmd(
950
+ "add-feature",
951
+ "Add a feature (supports --deps=1,2,3 for DAG dependencies)",
952
+ );
953
+ cmd("reset", "Archive current marathon and start fresh");
954
+ console.log();
808
955
  }
809
956
 
810
957
  // ── Main ─────────────────────────────────────────────────────────────────────
811
958
 
812
959
  function main() {
813
- const args = process.argv.slice(2);
814
-
815
- if (args.length === 0 || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
816
- showHelp();
817
- return;
960
+ const args = process.argv.slice(2);
961
+
962
+ if (
963
+ args.length === 0 ||
964
+ args[0] === "help" ||
965
+ args[0] === "--help" ||
966
+ args[0] === "-h"
967
+ ) {
968
+ showHelp();
969
+ return;
970
+ }
971
+
972
+ const cmd = args[0].toLowerCase();
973
+
974
+ if (!VALID_COMMANDS.has(cmd)) {
975
+ console.error(`${RED}Unknown command: "${cmd}"${RESET}`);
976
+ console.error(`Valid commands: ${[...VALID_COMMANDS].sort().join(", ")}`);
977
+ process.exit(1);
978
+ }
979
+
980
+ switch (cmd) {
981
+ case "init": {
982
+ const spec = args.slice(1).join(" ").trim();
983
+ cmdInit(spec);
984
+ break;
818
985
  }
819
-
820
- const cmd = args[0].toLowerCase();
821
-
822
- if (!VALID_COMMANDS.has(cmd)) {
823
- console.error(`${RED}Unknown command: "${cmd}"${RESET}`);
824
- console.error(`Valid commands: ${[...VALID_COMMANDS].sort().join(', ')}`);
986
+ case "status":
987
+ cmdStatus();
988
+ break;
989
+ case "next":
990
+ cmdNext();
991
+ break;
992
+ case "mark": {
993
+ const id = parseInt(args[1], 10);
994
+ const verdict = (args[2] || "").toLowerCase();
995
+ const reason = args.slice(3).join(" ").trim();
996
+ if (isNaN(id)) {
997
+ console.error(
998
+ `${RED}❌ Feature ID required. Usage: mark <id> pass|fail "reason"${RESET}`,
999
+ );
825
1000
  process.exit(1);
1001
+ }
1002
+ cmdMark(id, verdict, reason);
1003
+ break;
826
1004
  }
827
-
828
- switch (cmd) {
829
- case 'init': {
830
- const spec = args.slice(1).join(' ').trim();
831
- cmdInit(spec);
832
- break;
833
- }
834
- case 'status':
835
- cmdStatus();
836
- break;
837
- case 'next':
838
- cmdNext();
839
- break;
840
- case 'mark': {
841
- const id = parseInt(args[1], 10);
842
- const verdict = (args[2] || '').toLowerCase();
843
- const reason = args.slice(3).join(' ').trim();
844
- if (isNaN(id)) {
845
- console.error(`${RED}❌ Feature ID required. Usage: mark <id> pass|fail "reason"${RESET}`);
846
- process.exit(1);
847
- }
848
- cmdMark(id, verdict, reason);
849
- break;
850
- }
851
- case 'log': {
852
- const message = args.slice(1).join(' ').trim();
853
- cmdLog(message);
854
- break;
855
- }
856
- case 'session-start':
857
- cmdSessionStart();
858
- break;
859
- case 'session-end': {
860
- const summary = args.slice(1).join(' ').trim() || null;
861
- cmdSessionEnd(summary);
862
- break;
863
- }
864
- case 'add-feature': {
865
- const category = args[1] || '';
866
- const description = args[2] || '';
867
- let steps = args.slice(3);
868
- let deps = [];
869
-
870
- steps = steps.filter(step => {
871
- if (step.startsWith('--deps=')) {
872
- deps = step.replace('--deps=', '').split(',').map(Number).filter(n => !isNaN(n));
873
- return false;
874
- }
875
- return true;
876
- });
877
-
878
- cmdAddFeature(category, description, steps, deps);
879
- break;
880
- }
881
- case 'distill': {
882
- const lesson = args.slice(1).join(' ').trim();
883
- cmdDistill(lesson);
884
- break;
1005
+ case "log": {
1006
+ const message = args.slice(1).join(" ").trim();
1007
+ cmdLog(message);
1008
+ break;
1009
+ }
1010
+ case "session-start":
1011
+ cmdSessionStart();
1012
+ break;
1013
+ case "session-end": {
1014
+ const summary = args.slice(1).join(" ").trim() || null;
1015
+ cmdSessionEnd(summary);
1016
+ break;
1017
+ }
1018
+ case "add-feature": {
1019
+ const category = args[1] || "";
1020
+ const description = args[2] || "";
1021
+ let steps = args.slice(3);
1022
+ let deps = [];
1023
+
1024
+ steps = steps.filter((step) => {
1025
+ if (step.startsWith("--deps=")) {
1026
+ deps = step
1027
+ .replace("--deps=", "")
1028
+ .split(",")
1029
+ .map(Number)
1030
+ .filter((n) => !isNaN(n));
1031
+ return false;
885
1032
  }
886
- case 'reset':
887
- cmdReset();
888
- break;
889
- default:
890
- showHelp();
1033
+ return true;
1034
+ });
1035
+
1036
+ cmdAddFeature(category, description, steps, deps);
1037
+ break;
1038
+ }
1039
+ case "distill": {
1040
+ const lesson = args.slice(1).join(" ").trim();
1041
+ cmdDistill(lesson);
1042
+ break;
891
1043
  }
1044
+ case "reset":
1045
+ cmdReset();
1046
+ break;
1047
+ default:
1048
+ showHelp();
1049
+ }
892
1050
  }
893
1051
 
894
1052
  if (require.main === module) {
895
- main();
1053
+ main();
896
1054
  }