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.
- package/.agent/scripts/marathon_harness.js +799 -0
- package/.agent/scripts/prompt_compiler.js +56 -0
- package/.agent/skills/agent-organizer/SKILL.md +42 -0
- package/.agent/skills/agentic-patterns/SKILL.md +42 -0
- package/.agent/skills/ai-prompt-injection-defense/SKILL.md +42 -0
- package/.agent/skills/api-patterns/SKILL.md +42 -0
- package/.agent/skills/api-security-auditor/SKILL.md +42 -0
- package/.agent/skills/app-builder/SKILL.md +42 -0
- package/.agent/skills/appflow-wireframe/SKILL.md +42 -0
- package/.agent/skills/architecture/SKILL.md +42 -0
- package/.agent/skills/authentication-best-practices/SKILL.md +42 -0
- package/.agent/skills/backend-security-expert/SKILL.md +122 -0
- package/.agent/skills/bash-linux/SKILL.md +42 -0
- package/.agent/skills/behavioral-modes/SKILL.md +42 -0
- package/.agent/skills/brainstorming/SKILL.md +42 -0
- package/.agent/skills/building-native-ui/SKILL.md +42 -0
- package/.agent/skills/clean-code/SKILL.md +42 -0
- package/.agent/skills/code-review-checklist/SKILL.md +42 -0
- package/.agent/skills/config-validator/SKILL.md +42 -0
- package/.agent/skills/csharp-developer/SKILL.md +42 -0
- package/.agent/skills/data-validation-schemas/SKILL.md +42 -0
- package/.agent/skills/database-design/SKILL.md +42 -0
- package/.agent/skills/deployment-procedures/SKILL.md +42 -0
- package/.agent/skills/devops-engineer/SKILL.md +42 -0
- package/.agent/skills/devops-incident-responder/SKILL.md +42 -0
- package/.agent/skills/documentation-templates/SKILL.md +42 -0
- package/.agent/skills/edge-computing/SKILL.md +42 -0
- package/.agent/skills/error-resilience/SKILL.md +42 -0
- package/.agent/skills/extract-design-system/SKILL.md +42 -0
- package/.agent/skills/framer-motion-expert/SKILL.md +42 -0
- package/.agent/skills/frontend-design/SKILL.md +42 -0
- package/.agent/skills/frontend-security-expert/SKILL.md +123 -0
- package/.agent/skills/game-design-expert/SKILL.md +42 -0
- package/.agent/skills/game-engineering-expert/SKILL.md +42 -0
- package/.agent/skills/geo-fundamentals/SKILL.md +42 -0
- package/.agent/skills/github-operations/SKILL.md +42 -0
- package/.agent/skills/gsap-core/SKILL.md +42 -0
- package/.agent/skills/gsap-frameworks/SKILL.md +42 -0
- package/.agent/skills/gsap-performance/SKILL.md +42 -0
- package/.agent/skills/gsap-plugins/SKILL.md +42 -0
- package/.agent/skills/gsap-react/SKILL.md +42 -0
- package/.agent/skills/gsap-scrolltrigger/SKILL.md +42 -0
- package/.agent/skills/gsap-timeline/SKILL.md +42 -0
- package/.agent/skills/gsap-utils/SKILL.md +42 -0
- package/.agent/skills/i18n-localization/SKILL.md +42 -0
- package/.agent/skills/intelligent-routing/SKILL.md +42 -0
- package/.agent/skills/knowledge-graph/SKILL.md +42 -0
- package/.agent/skills/lint-and-validate/SKILL.md +42 -0
- package/.agent/skills/llm-engineering/SKILL.md +42 -0
- package/.agent/skills/local-first/SKILL.md +42 -0
- package/.agent/skills/mcp-builder/SKILL.md +42 -0
- package/.agent/skills/mobile-design/SKILL.md +42 -0
- package/.agent/skills/monorepo-management/SKILL.md +42 -0
- package/.agent/skills/motion-engineering/SKILL.md +42 -0
- package/.agent/skills/nextjs-react-expert/SKILL.md +42 -0
- package/.agent/skills/nodejs-best-practices/SKILL.md +42 -0
- package/.agent/skills/observability/SKILL.md +42 -0
- package/.agent/skills/parallel-agents/SKILL.md +42 -0
- package/.agent/skills/performance-profiling/SKILL.md +42 -0
- package/.agent/skills/plan-writing/SKILL.md +42 -0
- package/.agent/skills/platform-engineer/SKILL.md +42 -0
- package/.agent/skills/playwright-best-practices/SKILL.md +42 -0
- package/.agent/skills/powershell-windows/SKILL.md +42 -0
- package/.agent/skills/project-idioms/SKILL.md +42 -0
- package/.agent/skills/python-patterns/SKILL.md +42 -0
- package/.agent/skills/python-pro/SKILL.md +42 -0
- package/.agent/skills/react-specialist/SKILL.md +42 -0
- package/.agent/skills/readme-builder/SKILL.md +42 -0
- package/.agent/skills/realtime-patterns/SKILL.md +42 -0
- package/.agent/skills/red-team-tactics/SKILL.md +42 -0
- package/.agent/skills/rust-pro/SKILL.md +42 -0
- package/.agent/skills/seo-fundamentals/SKILL.md +42 -0
- package/.agent/skills/server-management/SKILL.md +42 -0
- package/.agent/skills/shadcn-ui-expert/SKILL.md +42 -0
- package/.agent/skills/skill-creator/SKILL.md +42 -0
- package/.agent/skills/sql-pro/SKILL.md +42 -0
- package/.agent/skills/supabase-postgres-best-practices/SKILL.md +42 -0
- package/.agent/skills/swiftui-expert/SKILL.md +42 -0
- package/.agent/skills/systematic-debugging/SKILL.md +42 -0
- package/.agent/skills/tailwind-patterns/SKILL.md +42 -0
- package/.agent/skills/tdd-workflow/SKILL.md +42 -0
- package/.agent/skills/test-result-analyzer/SKILL.md +42 -0
- package/.agent/skills/testing-patterns/SKILL.md +42 -0
- package/.agent/skills/trend-researcher/SKILL.md +42 -0
- package/.agent/skills/typescript-advanced/SKILL.md +42 -0
- package/.agent/skills/ui-ux-pro-max/SKILL.md +42 -0
- package/.agent/skills/ui-ux-researcher/SKILL.md +42 -0
- package/.agent/skills/vue-expert/SKILL.md +42 -0
- package/.agent/skills/vulnerability-scanner/SKILL.md +42 -0
- package/.agent/skills/web-accessibility-auditor/SKILL.md +42 -0
- package/.agent/skills/web-design-guidelines/SKILL.md +42 -0
- package/.agent/skills/webapp-testing/SKILL.md +42 -0
- package/.agent/skills/whimsy-injector/SKILL.md +42 -0
- package/.agent/skills/workflow-optimizer/SKILL.md +42 -0
- package/.agent/workflows/marathon.md +247 -0
- package/.agent/workflows/super-prompt.md +27 -0
- package/bin/tribunal-kit.js +47 -1
- 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
|
+
}
|