tribunal-kit 4.4.2 → 4.4.4

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 (108) hide show
  1. package/.agent/scripts/marathon_harness.js +896 -0
  2. package/.agent/scripts/prompt_compiler.js +87 -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/README.md +113 -242
  98. package/bin/tribunal-kit.js +49 -3
  99. package/package.json +3 -2
  100. package/.agent/scripts/append_flow.js +0 -72
  101. package/.agent/scripts/colors.js +0 -11
  102. package/.agent/scripts/compress_skills.js +0 -141
  103. package/.agent/scripts/consolidate_skills.js +0 -149
  104. package/.agent/scripts/deep_compress.js +0 -150
  105. package/.agent/scripts/patch_skills_meta.js +0 -156
  106. package/.agent/scripts/patch_skills_output.js +0 -244
  107. package/.agent/scripts/strip_tribunal.js +0 -47
  108. package/.agent/scripts/utils.js +0 -17
@@ -0,0 +1,896 @@
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', 'distill'
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 and blocked features.
157
+ * @param {object} featureList
158
+ * @returns {{ total: number, passing: number, failing: number, blocked: 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
+ 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 };
177
+ }
178
+
179
+ /**
180
+ * Get the next unfinished, unblocked feature.
181
+ * @param {object} featureList
182
+ * @returns {object|null}
183
+ */
184
+ 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;
200
+ }
201
+
202
+ /**
203
+ * Build a progress bar string.
204
+ * @param {number} current
205
+ * @param {number} total
206
+ * @param {number} width
207
+ * @returns {string}
208
+ */
209
+ 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;
214
+
215
+ let color = RED;
216
+ if (pct >= 75) color = GREEN;
217
+ else if (pct >= 40) color = YELLOW;
218
+ else if (pct >= 15) color = CYAN;
219
+
220
+ return `${color}[${'█'.repeat(filled)}${'░'.repeat(empty)}]${RESET} ${BOLD}${pct}%${RESET}`;
221
+ }
222
+
223
+ // ── Commands ─────────────────────────────────────────────────────────────────
224
+
225
+ /**
226
+ * Initialize a new marathon.
227
+ * @param {string} spec
228
+ */
229
+ 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();
261
+ }
262
+
263
+ /**
264
+ * Add a feature to the feature list.
265
+ * @param {string} category
266
+ * @param {string} description
267
+ * @param {string[]} steps
268
+ * @param {number[]} deps
269
+ */
270
+ 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}`);
306
+ }
307
+
308
+ /**
309
+ * Show the marathon status dashboard.
310
+ */
311
+ 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;
315
+ }
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
+ 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++;
346
+ }
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();
356
+ }
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
+ }
369
+ }
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}`);
384
+ }
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();
397
+ }
398
+
399
+ /**
400
+ * Show the next unfinished feature.
401
+ */
402
+ function cmdNext() {
403
+ if (!isActive()) {
404
+ console.error(`${RED}❌ No active marathon.${RESET}`);
405
+ process.exit(1);
406
+ }
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;
422
+ }
423
+
424
+ console.log(`\n ${BOLD}Progress:${RESET} ${progressBar(passing, total)} ${GREEN}${passing}${RESET}/${total}`);
425
+ 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();
444
+ }
445
+
446
+ console.log(` ${DIM}When done: marathon_harness.js mark ${nextFeature.id} pass${RESET}`);
447
+ console.log();
448
+ }
449
+
450
+ /**
451
+ * Mark a feature as passing or failing.
452
+ * @param {number} id
453
+ * @param {string} verdict - 'pass' or 'fail'
454
+ * @param {string} [reason] - Reason for failure
455
+ */
456
+ 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);
466
+ }
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();
507
+ }
508
+
509
+ /**
510
+ * Add a timestamped log entry.
511
+ * @param {string} message
512
+ */
513
+ 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}`);
535
+ }
536
+
537
+ /**
538
+ * Distill a lesson learned into memory context.
539
+ * @param {string} lesson
540
+ */
541
+ 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}`);
559
+ }
560
+
561
+ /**
562
+ * Start a new session — reads state, shows bearings.
563
+ */
564
+ function cmdSessionStart() {
565
+ if (!isActive()) {
566
+ console.error(`${RED}❌ No active marathon.${RESET}`);
567
+ process.exit(1);
568
+ }
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
+ 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();
614
+ }
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
+ }
624
+ }
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();
634
+ }
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
+ }
645
+ } 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
+ }
651
+ }
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();
661
+ }
662
+
663
+ /**
664
+ * End the current session — records summary.
665
+ * @param {string} summary
666
+ */
667
+ 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();
749
+ }
750
+
751
+ /**
752
+ * Archive the current marathon and reset.
753
+ */
754
+ 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();
783
+ }
784
+
785
+ // ── Help ─────────────────────────────────────────────────────────────────────
786
+
787
+ 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();
808
+ }
809
+
810
+ // ── Main ─────────────────────────────────────────────────────────────────────
811
+
812
+ 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;
818
+ }
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(', ')}`);
825
+ process.exit(1);
826
+ }
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;
885
+ }
886
+ case 'reset':
887
+ cmdReset();
888
+ break;
889
+ default:
890
+ showHelp();
891
+ }
892
+ }
893
+
894
+ if (require.main === module) {
895
+ main();
896
+ }