tribunal-kit 4.4.2 → 4.4.3

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 (98) hide show
  1. package/.agent/scripts/marathon_harness.js +799 -0
  2. package/.agent/scripts/prompt_compiler.js +56 -0
  3. package/.agent/skills/agent-organizer/SKILL.md +42 -0
  4. package/.agent/skills/agentic-patterns/SKILL.md +42 -0
  5. package/.agent/skills/ai-prompt-injection-defense/SKILL.md +42 -0
  6. package/.agent/skills/api-patterns/SKILL.md +42 -0
  7. package/.agent/skills/api-security-auditor/SKILL.md +42 -0
  8. package/.agent/skills/app-builder/SKILL.md +42 -0
  9. package/.agent/skills/appflow-wireframe/SKILL.md +42 -0
  10. package/.agent/skills/architecture/SKILL.md +42 -0
  11. package/.agent/skills/authentication-best-practices/SKILL.md +42 -0
  12. package/.agent/skills/backend-security-expert/SKILL.md +122 -0
  13. package/.agent/skills/bash-linux/SKILL.md +42 -0
  14. package/.agent/skills/behavioral-modes/SKILL.md +42 -0
  15. package/.agent/skills/brainstorming/SKILL.md +42 -0
  16. package/.agent/skills/building-native-ui/SKILL.md +42 -0
  17. package/.agent/skills/clean-code/SKILL.md +42 -0
  18. package/.agent/skills/code-review-checklist/SKILL.md +42 -0
  19. package/.agent/skills/config-validator/SKILL.md +42 -0
  20. package/.agent/skills/csharp-developer/SKILL.md +42 -0
  21. package/.agent/skills/data-validation-schemas/SKILL.md +42 -0
  22. package/.agent/skills/database-design/SKILL.md +42 -0
  23. package/.agent/skills/deployment-procedures/SKILL.md +42 -0
  24. package/.agent/skills/devops-engineer/SKILL.md +42 -0
  25. package/.agent/skills/devops-incident-responder/SKILL.md +42 -0
  26. package/.agent/skills/documentation-templates/SKILL.md +42 -0
  27. package/.agent/skills/edge-computing/SKILL.md +42 -0
  28. package/.agent/skills/error-resilience/SKILL.md +42 -0
  29. package/.agent/skills/extract-design-system/SKILL.md +42 -0
  30. package/.agent/skills/framer-motion-expert/SKILL.md +42 -0
  31. package/.agent/skills/frontend-design/SKILL.md +42 -0
  32. package/.agent/skills/frontend-security-expert/SKILL.md +123 -0
  33. package/.agent/skills/game-design-expert/SKILL.md +42 -0
  34. package/.agent/skills/game-engineering-expert/SKILL.md +42 -0
  35. package/.agent/skills/geo-fundamentals/SKILL.md +42 -0
  36. package/.agent/skills/github-operations/SKILL.md +42 -0
  37. package/.agent/skills/gsap-core/SKILL.md +42 -0
  38. package/.agent/skills/gsap-frameworks/SKILL.md +42 -0
  39. package/.agent/skills/gsap-performance/SKILL.md +42 -0
  40. package/.agent/skills/gsap-plugins/SKILL.md +42 -0
  41. package/.agent/skills/gsap-react/SKILL.md +42 -0
  42. package/.agent/skills/gsap-scrolltrigger/SKILL.md +42 -0
  43. package/.agent/skills/gsap-timeline/SKILL.md +42 -0
  44. package/.agent/skills/gsap-utils/SKILL.md +42 -0
  45. package/.agent/skills/i18n-localization/SKILL.md +42 -0
  46. package/.agent/skills/intelligent-routing/SKILL.md +42 -0
  47. package/.agent/skills/knowledge-graph/SKILL.md +42 -0
  48. package/.agent/skills/lint-and-validate/SKILL.md +42 -0
  49. package/.agent/skills/llm-engineering/SKILL.md +42 -0
  50. package/.agent/skills/local-first/SKILL.md +42 -0
  51. package/.agent/skills/mcp-builder/SKILL.md +42 -0
  52. package/.agent/skills/mobile-design/SKILL.md +42 -0
  53. package/.agent/skills/monorepo-management/SKILL.md +42 -0
  54. package/.agent/skills/motion-engineering/SKILL.md +42 -0
  55. package/.agent/skills/nextjs-react-expert/SKILL.md +42 -0
  56. package/.agent/skills/nodejs-best-practices/SKILL.md +42 -0
  57. package/.agent/skills/observability/SKILL.md +42 -0
  58. package/.agent/skills/parallel-agents/SKILL.md +42 -0
  59. package/.agent/skills/performance-profiling/SKILL.md +42 -0
  60. package/.agent/skills/plan-writing/SKILL.md +42 -0
  61. package/.agent/skills/platform-engineer/SKILL.md +42 -0
  62. package/.agent/skills/playwright-best-practices/SKILL.md +42 -0
  63. package/.agent/skills/powershell-windows/SKILL.md +42 -0
  64. package/.agent/skills/project-idioms/SKILL.md +42 -0
  65. package/.agent/skills/python-patterns/SKILL.md +42 -0
  66. package/.agent/skills/python-pro/SKILL.md +42 -0
  67. package/.agent/skills/react-specialist/SKILL.md +42 -0
  68. package/.agent/skills/readme-builder/SKILL.md +42 -0
  69. package/.agent/skills/realtime-patterns/SKILL.md +42 -0
  70. package/.agent/skills/red-team-tactics/SKILL.md +42 -0
  71. package/.agent/skills/rust-pro/SKILL.md +42 -0
  72. package/.agent/skills/seo-fundamentals/SKILL.md +42 -0
  73. package/.agent/skills/server-management/SKILL.md +42 -0
  74. package/.agent/skills/shadcn-ui-expert/SKILL.md +42 -0
  75. package/.agent/skills/skill-creator/SKILL.md +42 -0
  76. package/.agent/skills/sql-pro/SKILL.md +42 -0
  77. package/.agent/skills/supabase-postgres-best-practices/SKILL.md +42 -0
  78. package/.agent/skills/swiftui-expert/SKILL.md +42 -0
  79. package/.agent/skills/systematic-debugging/SKILL.md +42 -0
  80. package/.agent/skills/tailwind-patterns/SKILL.md +42 -0
  81. package/.agent/skills/tdd-workflow/SKILL.md +42 -0
  82. package/.agent/skills/test-result-analyzer/SKILL.md +42 -0
  83. package/.agent/skills/testing-patterns/SKILL.md +42 -0
  84. package/.agent/skills/trend-researcher/SKILL.md +42 -0
  85. package/.agent/skills/typescript-advanced/SKILL.md +42 -0
  86. package/.agent/skills/ui-ux-pro-max/SKILL.md +42 -0
  87. package/.agent/skills/ui-ux-researcher/SKILL.md +42 -0
  88. package/.agent/skills/vue-expert/SKILL.md +42 -0
  89. package/.agent/skills/vulnerability-scanner/SKILL.md +42 -0
  90. package/.agent/skills/web-accessibility-auditor/SKILL.md +42 -0
  91. package/.agent/skills/web-design-guidelines/SKILL.md +42 -0
  92. package/.agent/skills/webapp-testing/SKILL.md +42 -0
  93. package/.agent/skills/whimsy-injector/SKILL.md +42 -0
  94. package/.agent/skills/workflow-optimizer/SKILL.md +42 -0
  95. package/.agent/workflows/marathon.md +247 -0
  96. package/.agent/workflows/super-prompt.md +27 -0
  97. package/bin/tribunal-kit.js +47 -1
  98. package/package.json +3 -2
@@ -0,0 +1,799 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * marathon_harness.js — Long-Running Agent Harness for Tribunal Kit
4
+ * ═══════════════════════════════════════════════════════════════════
5
+ * Manages feature decomposition, progress tracking, and session handoffs
6
+ * for multi-session agent workflows.
7
+ *
8
+ * Inspired by: https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents
9
+ *
10
+ * Usage:
11
+ * node .agent/scripts/marathon_harness.js init "Build a clone of claude.ai"
12
+ * node .agent/scripts/marathon_harness.js status
13
+ * node .agent/scripts/marathon_harness.js next
14
+ * node .agent/scripts/marathon_harness.js mark <id> pass|fail
15
+ * node .agent/scripts/marathon_harness.js log "Completed auth flow"
16
+ * node .agent/scripts/marathon_harness.js session-start
17
+ * node .agent/scripts/marathon_harness.js session-end "Summary of work done"
18
+ * node .agent/scripts/marathon_harness.js reset
19
+ * node .agent/scripts/marathon_harness.js add-feature "category" "description" "step1" "step2" ...
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+ const { execSync } = require('child_process');
27
+
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');
33
+
34
+ // ── 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');
39
+
40
+ const VALID_COMMANDS = new Set([
41
+ 'init', 'status', 'next', 'mark', 'log',
42
+ 'session-start', 'session-end', 'reset', 'add-feature'
43
+ ]);
44
+
45
+ // ── Schema Defaults ──────────────────────────────────────────────────────────
46
+
47
+ /**
48
+ * Create an empty feature list structure.
49
+ * @param {string} spec - The original user specification
50
+ * @returns {object}
51
+ */
52
+ function createFeatureList(spec) {
53
+ return {
54
+ spec,
55
+ createdAt: new Date().toISOString(),
56
+ totalFeatures: 0,
57
+ features: []
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Create an empty progress structure.
63
+ * @param {string} spec - The original user specification
64
+ * @returns {object}
65
+ */
66
+ function createProgress(spec) {
67
+ return {
68
+ spec,
69
+ startedAt: new Date().toISOString(),
70
+ totalSessions: 0,
71
+ sessions: [],
72
+ log: []
73
+ };
74
+ }
75
+
76
+ // ── File I/O ─────────────────────────────────────────────────────────────────
77
+
78
+ /**
79
+ * Read and parse a JSON file with schema validation.
80
+ * @param {string} filePath
81
+ * @returns {object|null}
82
+ */
83
+ 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
+ }
92
+ }
93
+
94
+ /**
95
+ * Write an object to a JSON file with pretty formatting.
96
+ * @param {string} filePath
97
+ * @param {object} data
98
+ */
99
+ function writeJSON(filePath, data) {
100
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
101
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
102
+ }
103
+
104
+ /**
105
+ * Ensure the marathon directory exists.
106
+ */
107
+ function ensureDir() {
108
+ fs.mkdirSync(MARATHON_DIR, { recursive: true });
109
+ }
110
+
111
+ /**
112
+ * Check if a marathon is currently active.
113
+ * @returns {boolean}
114
+ */
115
+ function isActive() {
116
+ return fs.existsSync(FEATURE_LIST_FILE) && fs.existsSync(PROGRESS_FILE);
117
+ }
118
+
119
+ // ── Git Helpers ──────────────────────────────────────────────────────────────
120
+
121
+ /**
122
+ * Get recent git log entries.
123
+ * @param {number} count
124
+ * @returns {string[]}
125
+ */
126
+ 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
+ }
136
+ }
137
+
138
+ /**
139
+ * Get current git branch name.
140
+ * @returns {string}
141
+ */
142
+ 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
+ }
151
+ }
152
+
153
+ // ── Progress Helpers ─────────────────────────────────────────────────────────
154
+
155
+ /**
156
+ * Count passing features.
157
+ * @param {object} featureList
158
+ * @returns {{ total: number, passing: number, failing: number }}
159
+ */
160
+ function countFeatures(featureList) {
161
+ const features = featureList.features || [];
162
+ const total = features.length;
163
+ const passing = features.filter(f => f.passes === true).length;
164
+ return { total, passing, failing: total - passing };
165
+ }
166
+
167
+ /**
168
+ * Get the next unfinished feature.
169
+ * @param {object} featureList
170
+ * @returns {object|null}
171
+ */
172
+ function getNextFeature(featureList) {
173
+ const features = featureList.features || [];
174
+ return features.find(f => f.passes !== true) || null;
175
+ }
176
+
177
+ /**
178
+ * Build a progress bar string.
179
+ * @param {number} current
180
+ * @param {number} total
181
+ * @param {number} width
182
+ * @returns {string}
183
+ */
184
+ function progressBar(current, total, width = 30) {
185
+ if (total === 0) return `${DIM}[${'░'.repeat(width)}]${RESET} 0%`;
186
+ const pct = Math.round((current / total) * 100);
187
+ const filled = Math.round((current / total) * width);
188
+ const empty = width - filled;
189
+
190
+ let color = RED;
191
+ if (pct >= 75) color = GREEN;
192
+ else if (pct >= 40) color = YELLOW;
193
+ else if (pct >= 15) color = CYAN;
194
+
195
+ return `${color}[${'█'.repeat(filled)}${'░'.repeat(empty)}]${RESET} ${BOLD}${pct}%${RESET}`;
196
+ }
197
+
198
+ // ── Commands ─────────────────────────────────────────────────────────────────
199
+
200
+ /**
201
+ * Initialize a new marathon.
202
+ * @param {string} spec
203
+ */
204
+ function cmdInit(spec) {
205
+ if (isActive()) {
206
+ console.error(`${RED}❌ A marathon is already active.${RESET}`);
207
+ console.error(` Use ${CYAN}reset${RESET} to archive it first, or ${CYAN}status${RESET} to view progress.`);
208
+ process.exit(1);
209
+ }
210
+
211
+ if (!spec) {
212
+ console.error(`${RED}❌ Spec required. Usage: marathon_harness.js init "Build a todo app"${RESET}`);
213
+ process.exit(1);
214
+ }
215
+
216
+ ensureDir();
217
+
218
+ const featureList = createFeatureList(spec);
219
+ const progress = createProgress(spec);
220
+
221
+ writeJSON(FEATURE_LIST_FILE, featureList);
222
+ writeJSON(PROGRESS_FILE, progress);
223
+
224
+ console.log(banner('marathon_harness.js', { Mode: 'INIT' }));
225
+ console.log();
226
+ ok(`Marathon initialized for: ${BOLD}${spec}${RESET}`);
227
+ console.log();
228
+ info('Next steps for the agent:');
229
+ console.log(` ${DIM}1.${RESET} Decompose the spec into 30-200 atomic features`);
230
+ console.log(` ${DIM}2.${RESET} Add each feature with: ${CYAN}add-feature "category" "description" "step1" "step2" ...${RESET}`);
231
+ console.log(` ${DIM}3.${RESET} Make an initial git commit: ${CYAN}git commit -m "marathon: initial scaffold"${RESET}`);
232
+ console.log(` ${DIM}4.${RESET} Start the first session: ${CYAN}session-start${RESET}`);
233
+ console.log();
234
+ console.log(` ${DIM}State directory: ${MARATHON_DIR}${RESET}`);
235
+ console.log();
236
+ }
237
+
238
+ /**
239
+ * Add a feature to the feature list.
240
+ * @param {string} category
241
+ * @param {string} description
242
+ * @param {string[]} steps
243
+ */
244
+ function cmdAddFeature(category, description, steps) {
245
+ if (!isActive()) {
246
+ console.error(`${RED}❌ No active marathon. Run ${CYAN}init${RED} first.${RESET}`);
247
+ process.exit(1);
248
+ }
249
+
250
+ if (!category || !description) {
251
+ console.error(`${RED}❌ Usage: add-feature "category" "description" "step1" "step2" ...${RESET}`);
252
+ process.exit(1);
253
+ }
254
+
255
+ const featureList = readJSON(FEATURE_LIST_FILE);
256
+ if (!featureList) process.exit(1);
257
+
258
+ const newId = (featureList.features.length > 0)
259
+ ? Math.max(...featureList.features.map(f => f.id)) + 1
260
+ : 1;
261
+
262
+ const feature = {
263
+ id: newId,
264
+ category: category.toLowerCase(),
265
+ description,
266
+ steps: steps.length > 0 ? steps : ['Implement and verify'],
267
+ passes: false,
268
+ sessionCompleted: null
269
+ };
270
+
271
+ featureList.features.push(feature);
272
+ featureList.totalFeatures = featureList.features.length;
273
+
274
+ writeJSON(FEATURE_LIST_FILE, featureList);
275
+
276
+ console.log(` ${GREEN}+${RESET} Feature ${BOLD}#${newId}${RESET} [${MAGENTA}${category}${RESET}]: ${description}`);
277
+ }
278
+
279
+ /**
280
+ * Show the marathon status dashboard.
281
+ */
282
+ function cmdStatus() {
283
+ if (!isActive()) {
284
+ console.log(`${YELLOW}No active marathon.${RESET} Start one with: ${CYAN}marathon_harness.js init "spec"${RESET}`);
285
+ return;
286
+ }
287
+
288
+ const featureList = readJSON(FEATURE_LIST_FILE);
289
+ const progress = readJSON(PROGRESS_FILE);
290
+ if (!featureList || !progress) return;
291
+
292
+ const { total, passing, failing } = countFeatures(featureList);
293
+ const nextFeature = getNextFeature(featureList);
294
+ const sessions = progress.sessions || [];
295
+ const lastSession = sessions[sessions.length - 1] || null;
296
+
297
+ console.log(banner('marathon_harness.js', { Mode: 'STATUS' }));
298
+ console.log();
299
+
300
+ // ── Spec ──
301
+ console.log(` ${BOLD}Spec:${RESET} ${featureList.spec}`);
302
+ console.log(` ${DIM}Started: ${featureList.createdAt.slice(0, 16)}${RESET}`);
303
+ console.log();
304
+
305
+ // ── Progress Bar ──
306
+ console.log(` ${BOLD}Progress:${RESET} ${progressBar(passing, total)} ${GREEN}${passing}${RESET}/${total} features`);
307
+ console.log();
308
+
309
+ // ── Category Breakdown ──
310
+ const categories = {};
311
+ for (const f of featureList.features) {
312
+ const cat = f.category || 'uncategorized';
313
+ if (!categories[cat]) categories[cat] = { total: 0, passing: 0 };
314
+ categories[cat].total++;
315
+ if (f.passes) categories[cat].passing++;
316
+ }
317
+
318
+ if (Object.keys(categories).length > 0) {
319
+ console.log(` ${BOLD}By Category:${RESET}`);
320
+ for (const [cat, counts] of Object.entries(categories)) {
321
+ const catPct = counts.total > 0 ? Math.round((counts.passing / counts.total) * 100) : 0;
322
+ const catColor = catPct === 100 ? GREEN : catPct >= 50 ? YELLOW : RED;
323
+ console.log(` ${MAGENTA}${cat.padEnd(18)}${RESET} ${catColor}${counts.passing}/${counts.total}${RESET} (${catPct}%)`);
324
+ }
325
+ console.log();
326
+ }
327
+
328
+ // ── Sessions ──
329
+ console.log(` ${BOLD}Sessions:${RESET} ${sessions.length} completed`);
330
+ if (lastSession) {
331
+ console.log(` ${DIM}Last session:${RESET} #${lastSession.session} — ${lastSession.endedAt?.slice(0, 16) || 'in progress'}`);
332
+ if (lastSession.notes) {
333
+ console.log(` ${DIM}Notes:${RESET} ${lastSession.notes.slice(0, 80)}`);
334
+ }
335
+ if (lastSession.featuresAtEnd) {
336
+ const delta = lastSession.featuresAtEnd.passing - (lastSession.featuresAtStart?.passing || 0);
337
+ console.log(` ${DIM}Features completed:${RESET} ${GREEN}+${delta}${RESET}`);
338
+ }
339
+ }
340
+ console.log();
341
+
342
+ // ── Next Feature ──
343
+ if (nextFeature) {
344
+ console.log(` ${BOLD}Next Feature:${RESET} ${CYAN}#${nextFeature.id}${RESET} [${MAGENTA}${nextFeature.category}${RESET}]`);
345
+ console.log(` ${nextFeature.description}`);
346
+ if (nextFeature.steps && nextFeature.steps.length > 0) {
347
+ console.log(` ${DIM}Steps:${RESET}`);
348
+ for (const step of nextFeature.steps) {
349
+ console.log(` ${DIM}${BOX.bulletEmpty}${RESET} ${step}`);
350
+ }
351
+ }
352
+ } else if (total > 0) {
353
+ console.log(` ${GREEN}${BOLD}🎉 All ${total} features are passing!${RESET}`);
354
+ }
355
+ console.log();
356
+
357
+ // ── Git ──
358
+ const branch = getGitBranch();
359
+ const recentCommits = getGitLog(5);
360
+ if (recentCommits.length > 0) {
361
+ console.log(` ${BOLD}Git:${RESET} ${DIM}branch: ${branch}${RESET}`);
362
+ for (const commit of recentCommits.slice(0, 3)) {
363
+ console.log(` ${DIM}${commit}${RESET}`);
364
+ }
365
+ }
366
+ console.log();
367
+ }
368
+
369
+ /**
370
+ * Show the next unfinished feature.
371
+ */
372
+ function cmdNext() {
373
+ if (!isActive()) {
374
+ console.error(`${RED}❌ No active marathon.${RESET}`);
375
+ process.exit(1);
376
+ }
377
+
378
+ const featureList = readJSON(FEATURE_LIST_FILE);
379
+ if (!featureList) process.exit(1);
380
+
381
+ const { total, passing } = countFeatures(featureList);
382
+ const nextFeature = getNextFeature(featureList);
383
+
384
+ if (!nextFeature) {
385
+ console.log(`${GREEN}${BOLD}🎉 All ${total} features are passing! Marathon complete.${RESET}`);
386
+ return;
387
+ }
388
+
389
+ console.log(`\n ${BOLD}Progress:${RESET} ${progressBar(passing, total)} ${GREEN}${passing}${RESET}/${total}`);
390
+ console.log();
391
+ console.log(` ${BOLD}Next Feature:${RESET} ${CYAN}#${nextFeature.id}${RESET} [${MAGENTA}${nextFeature.category}${RESET}]`);
392
+ console.log(` ${nextFeature.description}`);
393
+ console.log();
394
+
395
+ if (nextFeature.steps && nextFeature.steps.length > 0) {
396
+ console.log(` ${BOLD}Steps:${RESET}`);
397
+ for (const step of nextFeature.steps) {
398
+ console.log(` ${BOX.bulletEmpty} ${step}`);
399
+ }
400
+ console.log();
401
+ }
402
+
403
+ console.log(` ${DIM}When done: marathon_harness.js mark ${nextFeature.id} pass${RESET}`);
404
+ console.log();
405
+ }
406
+
407
+ /**
408
+ * Mark a feature as passing or failing.
409
+ * @param {number} id
410
+ * @param {string} verdict - 'pass' or 'fail'
411
+ */
412
+ function cmdMark(id, verdict) {
413
+ if (!isActive()) {
414
+ console.error(`${RED}❌ No active marathon.${RESET}`);
415
+ process.exit(1);
416
+ }
417
+
418
+ const validVerdicts = ['pass', 'fail'];
419
+ if (!validVerdicts.includes(verdict)) {
420
+ console.error(`${RED}❌ Invalid verdict "${verdict}". Use: pass | fail${RESET}`);
421
+ process.exit(1);
422
+ }
423
+
424
+ const featureList = readJSON(FEATURE_LIST_FILE);
425
+ if (!featureList) process.exit(1);
426
+
427
+ const feature = featureList.features.find(f => f.id === id);
428
+ if (!feature) {
429
+ console.error(`${RED}❌ Feature #${id} not found. Valid IDs: 1-${featureList.features.length}${RESET}`);
430
+ process.exit(1);
431
+ }
432
+
433
+ const newPasses = verdict === 'pass';
434
+ const oldPasses = feature.passes;
435
+
436
+ // Guard: don't allow editing description or steps
437
+ feature.passes = newPasses;
438
+ feature.sessionCompleted = newPasses ? new Date().toISOString() : null;
439
+
440
+ writeJSON(FEATURE_LIST_FILE, featureList);
441
+
442
+ const { total, passing } = countFeatures(featureList);
443
+
444
+ if (newPasses && !oldPasses) {
445
+ ok(`Feature #${id} marked as ${GREEN}PASSING${RESET}`);
446
+ } else if (!newPasses && oldPasses) {
447
+ warn(`Feature #${id} marked as ${RED}FAILING${RESET}`);
448
+ } else {
449
+ info(`Feature #${id} unchanged (already ${newPasses ? 'passing' : 'failing'})`);
450
+ }
451
+
452
+ console.log(` ${DIM}${feature.description}${RESET}`);
453
+ console.log(` ${progressBar(passing, total)} ${GREEN}${passing}${RESET}/${total}`);
454
+ console.log();
455
+ }
456
+
457
+ /**
458
+ * Add a timestamped log entry.
459
+ * @param {string} message
460
+ */
461
+ function cmdLog(message) {
462
+ if (!isActive()) {
463
+ console.error(`${RED}❌ No active marathon.${RESET}`);
464
+ process.exit(1);
465
+ }
466
+
467
+ if (!message) {
468
+ console.error(`${RED}❌ Message required. Usage: log "Your progress note"${RESET}`);
469
+ process.exit(1);
470
+ }
471
+
472
+ const progress = readJSON(PROGRESS_FILE);
473
+ if (!progress) process.exit(1);
474
+
475
+ if (!progress.log) progress.log = [];
476
+ progress.log.push({
477
+ timestamp: new Date().toISOString(),
478
+ message
479
+ });
480
+
481
+ writeJSON(PROGRESS_FILE, progress);
482
+ ok(`Logged: ${message}`);
483
+ }
484
+
485
+ /**
486
+ * Start a new session — reads state, shows bearings.
487
+ */
488
+ function cmdSessionStart() {
489
+ if (!isActive()) {
490
+ console.error(`${RED}❌ No active marathon.${RESET}`);
491
+ process.exit(1);
492
+ }
493
+
494
+ const featureList = readJSON(FEATURE_LIST_FILE);
495
+ const progress = readJSON(PROGRESS_FILE);
496
+ if (!featureList || !progress) process.exit(1);
497
+
498
+ const sessionNum = (progress.sessions.length) + 1;
499
+ const { total, passing } = countFeatures(featureList);
500
+ const nextFeature = getNextFeature(featureList);
501
+
502
+ // Record session start
503
+ const session = {
504
+ session: sessionNum,
505
+ startedAt: new Date().toISOString(),
506
+ endedAt: null,
507
+ featuresAtStart: { total, passing },
508
+ featuresAtEnd: null,
509
+ featuresCompleted: [],
510
+ notes: null,
511
+ gitCommits: []
512
+ };
513
+
514
+ progress.sessions.push(session);
515
+ progress.totalSessions = progress.sessions.length;
516
+ writeJSON(PROGRESS_FILE, progress);
517
+
518
+ // Display bearings
519
+ console.log(banner('marathon_harness.js', {
520
+ Mode: 'SESSION START',
521
+ Session: `#${sessionNum}`
522
+ }));
523
+ console.log();
524
+
525
+ // ── Spec ──
526
+ console.log(` ${BOLD}Spec:${RESET} ${featureList.spec}`);
527
+ console.log(` ${BOLD}Progress:${RESET} ${progressBar(passing, total)} ${GREEN}${passing}${RESET}/${total}`);
528
+ console.log();
529
+
530
+ // ── Recent git commits ──
531
+ const commits = getGitLog(10);
532
+ if (commits.length > 0) {
533
+ console.log(` ${BOLD}Recent Commits:${RESET}`);
534
+ for (const commit of commits.slice(0, 5)) {
535
+ console.log(` ${DIM}${commit}${RESET}`);
536
+ }
537
+ console.log();
538
+ }
539
+
540
+ // ── Last session notes ──
541
+ if (progress.sessions.length > 1) {
542
+ const prev = progress.sessions[progress.sessions.length - 2];
543
+ if (prev && prev.notes) {
544
+ console.log(` ${BOLD}Last Session Notes:${RESET}`);
545
+ console.log(` ${DIM}${prev.notes}${RESET}`);
546
+ console.log();
547
+ }
548
+ }
549
+
550
+ // ── Recent log entries ──
551
+ const recentLogs = (progress.log || []).slice(-3);
552
+ if (recentLogs.length > 0) {
553
+ console.log(` ${BOLD}Recent Log:${RESET}`);
554
+ for (const entry of recentLogs) {
555
+ console.log(` ${DIM}${entry.timestamp.slice(0, 16)}${RESET} ${entry.message}`);
556
+ }
557
+ console.log();
558
+ }
559
+
560
+ // ── Next feature ──
561
+ if (nextFeature) {
562
+ console.log(` ${BOLD}${CYAN}▸ Next Feature:${RESET} ${CYAN}#${nextFeature.id}${RESET} [${MAGENTA}${nextFeature.category}${RESET}]`);
563
+ console.log(` ${nextFeature.description}`);
564
+ if (nextFeature.steps && nextFeature.steps.length > 0) {
565
+ for (const step of nextFeature.steps) {
566
+ console.log(` ${DIM}${BOX.bulletEmpty}${RESET} ${step}`);
567
+ }
568
+ }
569
+ } else {
570
+ console.log(` ${GREEN}${BOLD}🎉 All features passing! Nothing to implement.${RESET}`);
571
+ }
572
+ console.log();
573
+
574
+ // ── Recommended actions ──
575
+ console.log(` ${BOLD}Recommended Actions:${RESET}`);
576
+ console.log(` ${DIM}1.${RESET} Start dev server (if applicable): ${CYAN}node .agent/scripts/auto_preview.js start${RESET}`);
577
+ console.log(` ${DIM}2.${RESET} Smoke test the app to verify it's not broken`);
578
+ console.log(` ${DIM}3.${RESET} Implement the next feature shown above`);
579
+ console.log(` ${DIM}4.${RESET} Test, mark as passing, commit, then pick next feature`);
580
+ console.log();
581
+ }
582
+
583
+ /**
584
+ * End the current session — records summary.
585
+ * @param {string} summary
586
+ */
587
+ function cmdSessionEnd(summary) {
588
+ if (!isActive()) {
589
+ console.error(`${RED}❌ No active marathon.${RESET}`);
590
+ process.exit(1);
591
+ }
592
+
593
+ const featureList = readJSON(FEATURE_LIST_FILE);
594
+ const progress = readJSON(PROGRESS_FILE);
595
+ if (!featureList || !progress) process.exit(1);
596
+
597
+ const sessions = progress.sessions || [];
598
+ if (sessions.length === 0) {
599
+ console.error(`${RED}❌ No active session. Run ${CYAN}session-start${RED} first.${RESET}`);
600
+ process.exit(1);
601
+ }
602
+
603
+ const currentSession = sessions[sessions.length - 1];
604
+ const { total, passing } = countFeatures(featureList);
605
+
606
+ // Calculate features completed during this session
607
+ const startPassing = currentSession.featuresAtStart?.passing || 0;
608
+ const completedThisSession = passing - startPassing;
609
+
610
+ // Find which features were completed (have sessionCompleted in this session range)
611
+ const sessionStartTime = currentSession.startedAt;
612
+ const completedIds = featureList.features
613
+ .filter(f => f.passes && f.sessionCompleted && f.sessionCompleted >= sessionStartTime)
614
+ .map(f => f.id);
615
+
616
+ // Get git commits since session start
617
+ let sessionCommits = [];
618
+ try {
619
+ const since = currentSession.startedAt;
620
+ const output = execSync(`git log --oneline --since="${since}"`, {
621
+ encoding: 'utf8',
622
+ stdio: ['pipe', 'pipe', 'pipe']
623
+ });
624
+ sessionCommits = output.trim().split('\n').filter(Boolean).map(l => l.split(' ')[0]);
625
+ } catch {
626
+ // Git not available or no commits
627
+ }
628
+
629
+ // Update session record
630
+ currentSession.endedAt = new Date().toISOString();
631
+ currentSession.featuresAtEnd = { total, passing };
632
+ currentSession.featuresCompleted = completedIds;
633
+ currentSession.notes = summary || `Session ${currentSession.session}: ${completedThisSession} features completed`;
634
+ currentSession.gitCommits = sessionCommits;
635
+
636
+ writeJSON(PROGRESS_FILE, progress);
637
+
638
+ // Display summary
639
+ console.log(banner('marathon_harness.js', {
640
+ Mode: 'SESSION END',
641
+ Session: `#${currentSession.session}`
642
+ }));
643
+ console.log();
644
+
645
+ console.log(` ${BOLD}Session #${currentSession.session} Summary:${RESET}`);
646
+ console.log(` Started: ${currentSession.startedAt.slice(0, 16)}`);
647
+ console.log(` Ended: ${currentSession.endedAt.slice(0, 16)}`);
648
+ console.log(` Features: ${GREEN}+${completedThisSession}${RESET} completed (${completedIds.map(id => `#${id}`).join(', ') || 'none'})`);
649
+ console.log(` Commits: ${sessionCommits.length}`);
650
+ if (summary) {
651
+ console.log(` Notes: ${summary}`);
652
+ }
653
+ console.log();
654
+
655
+ console.log(` ${BOLD}Overall Progress:${RESET} ${progressBar(passing, total)} ${GREEN}${passing}${RESET}/${total}`);
656
+ console.log();
657
+
658
+ const remaining = total - passing;
659
+ if (remaining > 0) {
660
+ const avgPerSession = sessions.length > 0
661
+ ? Math.max(1, Math.round(passing / sessions.length))
662
+ : 1;
663
+ const estRemaining = Math.ceil(remaining / avgPerSession);
664
+ console.log(` ${DIM}Estimated sessions remaining: ~${estRemaining} (avg ${avgPerSession} features/session)${RESET}`);
665
+ } else {
666
+ console.log(` ${GREEN}${BOLD}🎉 Marathon complete! All features passing.${RESET}`);
667
+ }
668
+ console.log();
669
+ }
670
+
671
+ /**
672
+ * Archive the current marathon and reset.
673
+ */
674
+ function cmdReset() {
675
+ if (!isActive()) {
676
+ console.log(`${YELLOW}No active marathon to reset.${RESET}`);
677
+ return;
678
+ }
679
+
680
+ const featureList = readJSON(FEATURE_LIST_FILE);
681
+ const { total, passing } = featureList ? countFeatures(featureList) : { total: 0, passing: 0 };
682
+
683
+ // Archive current state
684
+ const archiveTimestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
685
+ const archivePath = path.join(ARCHIVE_DIR, archiveTimestamp);
686
+ fs.mkdirSync(archivePath, { recursive: true });
687
+
688
+ if (fs.existsSync(FEATURE_LIST_FILE)) {
689
+ fs.cpSync(FEATURE_LIST_FILE, path.join(archivePath, 'feature_list.json'));
690
+ }
691
+ if (fs.existsSync(PROGRESS_FILE)) {
692
+ fs.cpSync(PROGRESS_FILE, path.join(archivePath, 'progress.json'));
693
+ }
694
+
695
+ // Remove current state files
696
+ if (fs.existsSync(FEATURE_LIST_FILE)) fs.unlinkSync(FEATURE_LIST_FILE);
697
+ if (fs.existsSync(PROGRESS_FILE)) fs.unlinkSync(PROGRESS_FILE);
698
+
699
+ ok(`Marathon archived to: ${archivePath}`);
700
+ console.log(` ${DIM}Progress at archive: ${passing}/${total} features passing${RESET}`);
701
+ console.log(` ${DIM}Start a new marathon with: marathon_harness.js init "new spec"${RESET}`);
702
+ console.log();
703
+ }
704
+
705
+ // ── Help ─────────────────────────────────────────────────────────────────────
706
+
707
+ function showHelp() {
708
+ console.log(banner('marathon_harness.js', { Mode: 'HELP' }));
709
+ console.log();
710
+ console.log(` ${BOLD}Long-Running Agent Harness${RESET}`);
711
+ console.log(` ${DIM}Tracks features, progress, and sessions for multi-session agent workflows.${RESET}`);
712
+ console.log();
713
+
714
+ const cmd = (name, desc) => console.log(` ${CYAN}${name.padEnd(16)}${RESET} ${desc}`);
715
+
716
+ cmd('init "spec"', 'Start a new marathon with the given specification');
717
+ cmd('status', 'Show progress dashboard');
718
+ cmd('next', 'Show the next unfinished feature');
719
+ cmd('mark <id> pass', 'Mark a feature as passing');
720
+ cmd('mark <id> fail', 'Mark a feature as failing');
721
+ cmd('log "note"', 'Add a timestamped progress note');
722
+ cmd('session-start', 'Begin a new work session (reads state, shows bearings)');
723
+ cmd('session-end', 'End session with optional summary');
724
+ cmd('add-feature', 'Add a feature: add-feature "category" "description" "step1" ...');
725
+ cmd('reset', 'Archive current marathon and start fresh');
726
+ console.log();
727
+ }
728
+
729
+ // ── Main ─────────────────────────────────────────────────────────────────────
730
+
731
+ function main() {
732
+ const args = process.argv.slice(2);
733
+
734
+ if (args.length === 0 || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
735
+ showHelp();
736
+ return;
737
+ }
738
+
739
+ const cmd = args[0].toLowerCase();
740
+
741
+ if (!VALID_COMMANDS.has(cmd)) {
742
+ console.error(`${RED}Unknown command: "${cmd}"${RESET}`);
743
+ console.error(`Valid commands: ${[...VALID_COMMANDS].sort().join(', ')}`);
744
+ process.exit(1);
745
+ }
746
+
747
+ switch (cmd) {
748
+ case 'init': {
749
+ const spec = args.slice(1).join(' ').trim();
750
+ cmdInit(spec);
751
+ break;
752
+ }
753
+ case 'status':
754
+ cmdStatus();
755
+ break;
756
+ case 'next':
757
+ cmdNext();
758
+ break;
759
+ case 'mark': {
760
+ const id = parseInt(args[1], 10);
761
+ const verdict = (args[2] || '').toLowerCase();
762
+ if (isNaN(id)) {
763
+ console.error(`${RED}❌ Feature ID required. Usage: mark <id> pass|fail${RESET}`);
764
+ process.exit(1);
765
+ }
766
+ cmdMark(id, verdict);
767
+ break;
768
+ }
769
+ case 'log': {
770
+ const message = args.slice(1).join(' ').trim();
771
+ cmdLog(message);
772
+ break;
773
+ }
774
+ case 'session-start':
775
+ cmdSessionStart();
776
+ break;
777
+ case 'session-end': {
778
+ const summary = args.slice(1).join(' ').trim() || null;
779
+ cmdSessionEnd(summary);
780
+ break;
781
+ }
782
+ case 'add-feature': {
783
+ const category = args[1] || '';
784
+ const description = args[2] || '';
785
+ const steps = args.slice(3);
786
+ cmdAddFeature(category, description, steps);
787
+ break;
788
+ }
789
+ case 'reset':
790
+ cmdReset();
791
+ break;
792
+ default:
793
+ showHelp();
794
+ }
795
+ }
796
+
797
+ if (require.main === module) {
798
+ main();
799
+ }