dev-harness-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +299 -0
  3. package/adapters/amazon-q/README.md +23 -0
  4. package/adapters/antigravity/README.md +22 -0
  5. package/adapters/claude-code/README.md +30 -0
  6. package/adapters/cline/README.md +23 -0
  7. package/adapters/codex/README.md +31 -0
  8. package/adapters/copilot/README.md +23 -0
  9. package/adapters/cursor/README.md +29 -0
  10. package/adapters/gemini/README.md +23 -0
  11. package/adapters/generic/README.md +40 -0
  12. package/adapters/hermes/README.md +31 -0
  13. package/adapters/hermes/SKILL.md +89 -0
  14. package/adapters/hermes/scripts/init.mjs +27 -0
  15. package/adapters/hermes/scripts/phase.mjs +27 -0
  16. package/adapters/hermes/scripts/validate.mjs +27 -0
  17. package/adapters/kilo-code/README.md +23 -0
  18. package/adapters/openclaw/README.md +22 -0
  19. package/adapters/pi/README.md +22 -0
  20. package/adapters/roo/README.md +23 -0
  21. package/adapters/windsurf/README.md +23 -0
  22. package/cli/commands/checkpoint.mjs +94 -0
  23. package/cli/commands/config.mjs +268 -0
  24. package/cli/commands/contract.mjs +155 -0
  25. package/cli/commands/detect-tool.mjs +112 -0
  26. package/cli/commands/init.mjs +351 -0
  27. package/cli/commands/learn.mjs +47 -0
  28. package/cli/commands/pause.mjs +34 -0
  29. package/cli/commands/phase.mjs +182 -0
  30. package/cli/commands/resume.mjs +33 -0
  31. package/cli/commands/rollback.mjs +261 -0
  32. package/cli/commands/set-mode.mjs +75 -0
  33. package/cli/commands/status.mjs +168 -0
  34. package/cli/commands/validate.mjs +118 -0
  35. package/cli/commands/worktree.mjs +298 -0
  36. package/cli/harness-dev.mjs +88 -0
  37. package/cli/lib/args.mjs +111 -0
  38. package/cli/lib/command-helpers.mjs +50 -0
  39. package/cli/lib/config-registry.mjs +329 -0
  40. package/cli/lib/constants.mjs +30 -0
  41. package/cli/lib/contract.mjs +306 -0
  42. package/cli/lib/detect-stack.mjs +235 -0
  43. package/cli/lib/errors.mjs +71 -0
  44. package/cli/lib/file-io.mjs +90 -0
  45. package/cli/lib/gates.mjs +492 -0
  46. package/cli/lib/git.mjs +144 -0
  47. package/cli/lib/help.mjs +246 -0
  48. package/cli/lib/modes.mjs +92 -0
  49. package/cli/lib/output.mjs +49 -0
  50. package/cli/lib/paths.mjs +75 -0
  51. package/cli/lib/phases.mjs +58 -0
  52. package/cli/lib/platform.mjs +78 -0
  53. package/cli/lib/progress.mjs +357 -0
  54. package/cli/lib/ralph-inner.mjs +314 -0
  55. package/cli/lib/ralph-outer.mjs +249 -0
  56. package/cli/lib/ralph-output.mjs +178 -0
  57. package/cli/lib/scaffold.mjs +431 -0
  58. package/cli/lib/schemas/stacks.json +477 -0
  59. package/cli/lib/state.mjs +333 -0
  60. package/cli/lib/templates.mjs +264 -0
  61. package/cli/lib/tool-registry.mjs +218 -0
  62. package/cli/lib/validate-schema.mjs +131 -0
  63. package/cli/lib/vars.mjs +114 -0
  64. package/package.json +50 -0
  65. package/schema/harness-config.schema.json +127 -0
  66. package/templates/AGENTS.md +63 -0
  67. package/templates/ci/github-actions.yml +78 -0
  68. package/templates/ci/gitlab-ci.yml +59 -0
  69. package/templates/docs/agents/evaluator.md +14 -0
  70. package/templates/docs/agents/generator.md +13 -0
  71. package/templates/docs/agents/planner.md +13 -0
  72. package/templates/docs/agents/simplifier.md +13 -0
  73. package/templates/docs/phases/build.md +41 -0
  74. package/templates/docs/phases/define.md +51 -0
  75. package/templates/docs/phases/plan.md +36 -0
  76. package/templates/docs/phases/review.md +42 -0
  77. package/templates/docs/phases/ship.md +43 -0
  78. package/templates/docs/phases/simplify.md +40 -0
  79. package/templates/docs/phases/verify.md +38 -0
  80. package/templates/evaluator-rubric.md +28 -0
  81. package/templates/init.ps1 +97 -0
  82. package/templates/init.sh +102 -0
  83. package/templates/sprint-contract.md +31 -0
@@ -0,0 +1,168 @@
1
+ /**
2
+ * status — Show current phase + gate state + detected stack.
3
+ *
4
+ * Reads harness-config.json via state.mjs for live project state,
5
+ * plus runs stack detection and gate checks for current status.
6
+ *
7
+ * Usage: harness-dev status [--json] [--target <dir>]
8
+ */
9
+ import { resolve, basename } from 'node:path';
10
+ import { detectStack } from '../lib/detect-stack.mjs';
11
+ import { loadConfig } from '../lib/state.mjs';
12
+ import { readLessons } from '../lib/progress.mjs';
13
+ import { loadFeatureList, getNextFeature } from '../lib/ralph-inner.mjs';
14
+ import { runChecks, areGatesEnabled } from '../lib/gates.mjs';
15
+
16
+ export default async function statusCommand(args) {
17
+ const rawTarget = args.flags?.target;
18
+ const targetDir = (typeof rawTarget === 'string') ? resolve(rawTarget) : process.cwd();
19
+ const json = !!(args.json || args.flags?.json);
20
+
21
+ // Stack detection
22
+ const stack = detectStack(targetDir);
23
+
24
+ // Config state (graceful if missing)
25
+ const { config, ok: configOk, schemaErrors = [] } = loadConfig(targetDir);
26
+ const phase = configOk ? config.currentPhase : null;
27
+ const mode = configOk ? config.mode : 'copilot';
28
+
29
+ // Current feature from feature_list.json
30
+ let currentFeature = null;
31
+ if (configOk && phase) {
32
+ try {
33
+ const fl = loadFeatureList(targetDir);
34
+ const next = getNextFeature(fl.features);
35
+ if (next) {
36
+ currentFeature = { id: next.id, name: next.name };
37
+ }
38
+ } catch {
39
+ // feature_list.json missing or invalid
40
+ }
41
+ }
42
+
43
+ // Gate status — run checks for current phase
44
+ let gateStatus = 'disabled';
45
+ let checksPassing = 0;
46
+ let checksTotal = 0;
47
+ if (phase && areGatesEnabled(targetDir)) {
48
+ const gateResult = runChecks(targetDir, phase);
49
+ checksTotal = gateResult.checks.length;
50
+ checksPassing = gateResult.checks.filter(c => c.pass).length;
51
+ gateStatus = gateResult.overall ? 'pass' : 'fail';
52
+ }
53
+
54
+ // Lessons — last 3
55
+ const allLessons = readLessons(targetDir);
56
+ const recentLessons = allLessons.slice(-3);
57
+
58
+ if (json) {
59
+ process.stdout.write(JSON.stringify({
60
+ command: 'status',
61
+ status: 'ok',
62
+ message: configOk
63
+ ? `Phase: ${phase || 'not started'}, Stack: ${stack.label}`
64
+ : 'No harness-config.json found — run harness-dev init',
65
+ project: basename(targetDir),
66
+ stack: stack.name,
67
+ stackLabel: stack.label,
68
+ mode,
69
+ currentPhase: phase,
70
+ currentFeature: currentFeature?.name || null,
71
+ gateStatus,
72
+ checksPassing,
73
+ checksTotal,
74
+ paused: configOk ? config.paused : false,
75
+ features: configOk ? config.features : { remaining: 0, passing: 0, total: 0 },
76
+ git: configOk ? config.git : { clean: true },
77
+ maxRetries: configOk ? config.maxRetries : 3,
78
+ recentLessons: recentLessons.map(l => ({ date: l.date, author: l.author, text: l.text })),
79
+ schemaErrors,
80
+ nextAction: determineNextAction(targetDir, configOk, config, phase, gateStatus),
81
+ }) + '\n');
82
+ return;
83
+ }
84
+
85
+ // ── Human-readable output ─────────────────────────────────────────────
86
+ let out = '';
87
+ out += '═══ dev-harness Status ═══\n';
88
+ out += line('Project:', basename(targetDir)) + '\n';
89
+ out += line('Stack:', `${stack.label}${stack.name !== 'generic' ? '' : ' (not detected)'}`) + '\n';
90
+ out += line('Mode:', modeLabel(mode)) + '\n';
91
+ out += '\n';
92
+
93
+ if (configOk && phase) {
94
+ out += line('Current Phase:', phase.toUpperCase()) + '\n';
95
+ if (currentFeature) {
96
+ out += line('Current Feature:', `${currentFeature.name} (${currentFeature.id})`) + '\n';
97
+ }
98
+ out += line('Gate Status:', gateStatusLabel(gateStatus, checksPassing, checksTotal)) + '\n';
99
+ if (config.git?.branch) {
100
+ out += line('Branch:', config.git.branch) + '\n';
101
+ }
102
+ out += '\n';
103
+ } else if (configOk) {
104
+ out += ' Phase: not started.\n';
105
+ out += '\n';
106
+ } else {
107
+ out += ' No harness-config.json found.\n';
108
+ out += '\n';
109
+ }
110
+
111
+ // Lessons
112
+ if (recentLessons.length > 0) {
113
+ out += `Last ${recentLessons.length} lesson(s):\n`;
114
+ for (const l of recentLessons) {
115
+ out += ` ${l.date} | ${l.text}\n`;
116
+ }
117
+ out += '\n';
118
+ }
119
+
120
+ // Schema violations (if any) — surface so users know config is malformed
121
+ if (configOk && schemaErrors.length > 0) {
122
+ out += `Schema warnings (${schemaErrors.length}):\n`;
123
+ for (const e of schemaErrors) {
124
+ out += ` ⚠ ${e}\n`;
125
+ }
126
+ out += '\n';
127
+ }
128
+
129
+ // Next action
130
+ out += ' ' + determineNextAction(targetDir, configOk, config, phase, gateStatus) + '\n';
131
+
132
+ process.stdout.write(out);
133
+ }
134
+
135
+ // ── Helpers ──────────────────────────────────────────────────────────────────
136
+
137
+ function line(k, v) {
138
+ return `${k.padEnd(18)}${v}`;
139
+ }
140
+
141
+ function modeLabel(mode) {
142
+ return mode === 'autopilot' ? 'Autopilot' : 'Copilot';
143
+ }
144
+
145
+ function gateStatusLabel(status, passing, total) {
146
+ if (status === 'disabled') {return 'disabled';}
147
+ if (total === 0) {return status;}
148
+ return `${status === 'pass' ? 'passing' : 'failing'} — ${passing}/${total} checks passing`;
149
+ }
150
+
151
+ function determineNextAction(targetDir, configOk, config, phase, gateStatus) {
152
+ if (!configOk) {
153
+ return 'Run: harness-dev init';
154
+ }
155
+ if (!phase) {
156
+ return 'Run: harness-dev phase define to start';
157
+ }
158
+ if (gateStatus === 'fail') {
159
+ return 'Run: harness-dev validate to re-check';
160
+ }
161
+ // Determine next phase
162
+ const order = ['define', 'plan', 'build', 'verify', 'review', 'ship'];
163
+ const idx = order.indexOf(phase);
164
+ if (idx >= 0 && idx < order.length - 1) {
165
+ return `Run: harness-dev phase ${order[idx + 1]}`;
166
+ }
167
+ return `Run: harness-dev validate`;
168
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * validate — Run gate checks for current phase.
3
+ *
4
+ * If gates.enabled is false, prints "Gates disabled" and exits 0.
5
+ * Otherwise runs phase-specific checks and reports results.
6
+ *
7
+ * Usage:
8
+ * harness-dev validate — check current phase
9
+ * harness-dev validate --json — machine-readable output
10
+ * harness-dev validate --phase X — check specific phase
11
+ *
12
+ * Examples:
13
+ * harness-dev validate
14
+ * # → BUILD Gate: PASS — 3/3 checks pass
15
+ *
16
+ * harness-dev validate --json
17
+ * # → {"phase":"build","checks":[...],"overall":false,"failures":["lint"]}
18
+ */
19
+ import { resolve } from 'node:path';
20
+ import { die, CliError, EXIT } from '../lib/errors.mjs';
21
+ import { runChecks, getPhase, areGatesEnabled } from '../lib/gates.mjs';
22
+ import { phaseLabel } from '../lib/command-helpers.mjs';
23
+
24
+ export default async function validateCommand(args) {
25
+ const json = !!(args.json || args.flags?.json);
26
+ const rawTarget = args.flags?.target;
27
+ const targetDir = (typeof rawTarget === 'string') ? resolve(rawTarget) : process.cwd();
28
+
29
+ // Allow explicit --phase override
30
+ const explicitPhase = args.flags?.phase;
31
+ const phase = explicitPhase || getPhase(targetDir);
32
+
33
+ // Feature/task scoping for inner-loop per-task validation
34
+ // NOTE: gates.mjs currently runs full phase-level checks.
35
+ // Per-feature/task filtering should be implemented when the
36
+ // gate engine grows feature-aware check functions (T8 follow-up).
37
+ const feature = typeof args.flags?.feature === 'string' ? args.flags.feature : null;
38
+ const task = typeof args.flags?.task === 'string' ? args.flags.task : null;
39
+
40
+ // Gates disabled check
41
+ if (!areGatesEnabled(targetDir)) {
42
+ if (json) {
43
+ const out = {
44
+ command: 'validate',
45
+ phase,
46
+ status: 'ok',
47
+ message: 'Gates disabled — enable with: config set gates.enabled true',
48
+ checks: [],
49
+ overall: true,
50
+ failures: [],
51
+ };
52
+ if (feature) { out.feature = feature; }
53
+ if (task) { out.task = task; }
54
+ process.stdout.write(JSON.stringify(out) + '\n');
55
+ } else {
56
+ process.stdout.write('Gates disabled. Enable with: harness-dev config set gates.enabled true\n');
57
+ }
58
+ return;
59
+ }
60
+
61
+ // No phase determined
62
+ if (!phase) {
63
+ die(
64
+ new CliError(
65
+ 'No phase found in config. Run: harness-dev init or harness-dev phase <name>',
66
+ EXIT.VALIDATION_FAILURE,
67
+ ),
68
+ json,
69
+ );
70
+ return;
71
+ }
72
+
73
+ // Run checks
74
+ const result = runChecks(targetDir, phase, { feature, task });
75
+
76
+ if (json) {
77
+ const out = {
78
+ command: 'validate',
79
+ phase: result.phase,
80
+ status: result.overall ? 'ok' : 'error',
81
+ message: result.overall
82
+ ? `${phaseLabel(result.phase)} Gate: PASS — ${result.checks.length}/${result.checks.length} checks pass`
83
+ : `${phaseLabel(result.phase)} Gate: FAIL — ${result.checks.length - result.failures.length}/${result.checks.length} checks pass`,
84
+ checks: result.checks,
85
+ overall: result.overall,
86
+ failures: result.failures,
87
+ };
88
+ if (feature) { out.feature = feature; }
89
+ if (task) { out.task = task; }
90
+ process.stdout.write(JSON.stringify(out) + '\n');
91
+ if (!result.overall) {
92
+ process.exit(EXIT.VALIDATION_FAILURE);
93
+ }
94
+ return;
95
+ }
96
+
97
+ // Human output
98
+ const label = phaseLabel(result.phase);
99
+ if (result.overall) {
100
+ process.stdout.write(`${label} Gate: PASS — ${result.checks.length}/${result.checks.length} checks pass\n`);
101
+ } else {
102
+ process.stdout.write(`${label} Gate: FAIL — ${result.checks.length - result.failures.length}/${result.checks.length} checks pass\n`);
103
+ }
104
+
105
+ for (const check of result.checks) {
106
+ const icon = check.pass ? ' ✅' : ' ❌';
107
+ process.stdout.write(`${icon} ${check.name}: ${check.detail}\n`);
108
+ }
109
+
110
+ if (!result.overall) {
111
+ process.stdout.write(`\nFailed: ${result.failures.join(', ')}\n`);
112
+ }
113
+
114
+ // Exit with failure code when checks fail
115
+ if (!result.overall) {
116
+ process.exit(EXIT.VALIDATION_FAILURE);
117
+ }
118
+ }
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * worktree — Git worktree management (create/list/prune/remove).
4
+ *
5
+ * T17 implementation:
6
+ * create <name> — git worktree add ../feat-<name> feat/<name> + scaffold
7
+ * list — list active worktrees with branch, path
8
+ * prune — git worktree prune (remove orphaned metadata)
9
+ * remove <name> — git worktree remove + optionally delete branch
10
+ *
11
+ * Usage: harness-dev worktree <subcommand> [name] [options]
12
+ */
13
+ import { existsSync } from 'node:fs';
14
+ import { resolve, dirname } from 'node:path';
15
+ import { die, CliError, EXIT } from '../lib/errors.mjs';
16
+ import { detectStack } from '../lib/detect-stack.mjs';
17
+ import { loadConfig, saveConfig } from '../lib/state.mjs';
18
+ import { execGit, getGitRoot } from '../lib/git.mjs';
19
+ import { parseCommandArgs } from '../lib/command-helpers.mjs';
20
+
21
+ const SUBCOMMANDS = ['create', 'list', 'prune', 'remove'];
22
+
23
+ export default async function worktreeCommand(args) {
24
+ const { json, targetDir } = parseCommandArgs(args);
25
+ const sub = args.subcommand;
26
+
27
+ if (!sub || !SUBCOMMANDS.includes(sub)) {
28
+ die(new CliError(`Usage: harness-dev worktree ${SUBCOMMANDS.join('|')}`, EXIT.USAGE_ERROR), json);
29
+ return;
30
+ }
31
+
32
+ const gitRoot = getGitRoot(targetDir);
33
+ if (!gitRoot) {
34
+ const msg = 'Not inside a git repository. Run: git init first or harness-dev init';
35
+ if (json) {
36
+ process.stdout.write(JSON.stringify({ command: 'worktree', subcommand: sub, status: 'error', message: msg }) + '\n');
37
+ } else {
38
+ process.stderr.write(`Error: ${msg}\n`);
39
+ }
40
+ return;
41
+ }
42
+
43
+ // ── create ───────────────────────────────────────────────────────────────
44
+ if (sub === 'create') {
45
+ const name = args.positionals[0];
46
+ if (!name) {
47
+ die(new CliError('Usage: harness-dev worktree create <name>', EXIT.USAGE_ERROR), json);
48
+ return;
49
+ }
50
+
51
+ const branchName = `feat/${name}`;
52
+ const worktreePath = resolve(dirname(gitRoot), `feat-${name}`);
53
+
54
+ // Check branch doesn't already exist
55
+ const branchCheck = execGit(`git show-ref --verify --quiet refs/heads/${branchName}`, gitRoot);
56
+ if (branchCheck.ok) {
57
+ const msg = `Branch "${branchName}" already exists. Choose a different name.`;
58
+ if (json) {
59
+ process.stdout.write(JSON.stringify({ command: 'worktree', subcommand: 'create', name, branch: branchName, status: 'error', message: msg }) + '\n');
60
+ } else {
61
+ process.stderr.write(`Error: ${msg}\n`);
62
+ }
63
+ return;
64
+ }
65
+
66
+ // Check target directory doesn't exist
67
+ if (existsSync(worktreePath)) {
68
+ const msg = `Target directory already exists: ${worktreePath}`;
69
+ if (json) {
70
+ process.stdout.write(JSON.stringify({ command: 'worktree', subcommand: 'create', name, branch: branchName, path: worktreePath, status: 'error', message: msg }) + '\n');
71
+ } else {
72
+ process.stderr.write(`Error: ${msg}\n`);
73
+ }
74
+ return;
75
+ }
76
+
77
+ // Create the worktree
78
+ const addResult = execGit(`git worktree add "${worktreePath}" -b "${branchName}"`, gitRoot);
79
+ if (!addResult.ok) {
80
+ const msg = `Failed to create worktree: ${addResult.stderr || addResult.stdout}`;
81
+ if (json) {
82
+ process.stdout.write(JSON.stringify({ command: 'worktree', subcommand: 'create', name, branch: branchName, path: worktreePath, status: 'error', message: msg }) + '\n');
83
+ } else {
84
+ process.stderr.write(`Error: ${msg}\n`);
85
+ }
86
+ return;
87
+ }
88
+
89
+ // Scaffold harness in the new worktree — run full init with parent's detected stack
90
+ const stack = detectStack(worktreePath).name;
91
+ const harnessDevPath = new URL('../harness-dev.mjs', import.meta.url).pathname;
92
+ const initResult = execGit(
93
+ `node "${harnessDevPath}" init --stack "${stack}" --force --no-git --json`,
94
+ worktreePath,
95
+ );
96
+ let filesCreated = 0;
97
+ let initErrors = [];
98
+ if (initResult.ok && initResult.stdout) {
99
+ try {
100
+ const initJson = JSON.parse(initResult.stdout);
101
+ filesCreated = initJson.filesCreated || 0;
102
+ initErrors = initJson.errors || [];
103
+ } catch { /* ignore parse error — fall through */ }
104
+ }
105
+
106
+ // Overwrite harness-config.json with worktree context (keep init settings)
107
+ const cfg = {
108
+ version: '1.0',
109
+ stack,
110
+ mode: 'copilot',
111
+ currentPhase: null,
112
+ paused: false,
113
+ features: { remaining: 0, passing: 0, total: 0 },
114
+ gates: { enabled: false, checks: ['all'] },
115
+ git: { autoCommit: false, autoTag: false, resetOnRetry: false, branch: branchName, clean: true, hasUpstream: false, lastCommitMessage: null },
116
+ phases: { enabled: ['define', 'plan', 'build', 'verify', 'review', 'ship'] },
117
+ agents: { tone: { planner: 'Analytical and precise. Define clear boundaries.', generator: 'Focused and practical. Build what\'s specified, nothing more.', evaluator: 'Skeptical and thorough. Accept only compelling evidence.', simplifier: 'Relentless about clarity. Delete more than you add.' } },
118
+ maxRetries: 3,
119
+ gateHistory: [],
120
+ worktree: { parent: gitRoot, name, branch: branchName },
121
+ };
122
+ saveConfig(worktreePath, cfg);
123
+
124
+ if (json) {
125
+ process.stdout.write(JSON.stringify({
126
+ command: 'worktree',
127
+ subcommand: 'create',
128
+ name,
129
+ branch: branchName,
130
+ path: worktreePath,
131
+ stacked: true,
132
+ status: initErrors.length > 0 ? 'partial' : 'ok',
133
+ message: `Worktree created at ${worktreePath} on branch ${branchName}`,
134
+ filesCreated,
135
+ errors: initErrors,
136
+ }) + '\n');
137
+ } else {
138
+ process.stdout.write(`✓ Worktree created at ${worktreePath}\n`);
139
+ process.stdout.write(` Branch: ${branchName}\n`);
140
+ process.stdout.write(` Harness scaffolded with stack "${stack}" (${filesCreated} files)\n`);
141
+ for (const e of initErrors) {
142
+ process.stderr.write(` ⚠ ${e}\n`);
143
+ }
144
+ }
145
+ return;
146
+ }
147
+
148
+ // ── list ─────────────────────────────────────────────────────────────────
149
+ if (sub === 'list') {
150
+ const result = execGit('git worktree list', gitRoot);
151
+ if (!result.ok) {
152
+ const msg = `Failed to list worktrees: ${result.stderr || result.stdout}`;
153
+ if (json) {
154
+ process.stdout.write(JSON.stringify({ command: 'worktree', subcommand: 'list', status: 'error', message: msg }) + '\n');
155
+ } else {
156
+ process.stderr.write(`Error: ${msg}\n`);
157
+ }
158
+ return;
159
+ }
160
+
161
+ const lines = result.stdout.split('\n').filter(Boolean);
162
+ const worktrees = lines.map(line => {
163
+ const parts = line.split(/\s+/);
164
+ const path = parts[0];
165
+ const hash = parts[1];
166
+ const branch = parts.slice(2).join(' ').replace(/^\[|\]$/g, '') || '(detached HEAD)';
167
+ return { path, hash, branch };
168
+ });
169
+
170
+ // Try to enrich with harness phase info
171
+ for (const wt of worktrees) {
172
+ const cfg = loadConfig(wt.path);
173
+ if (cfg.ok && cfg.config) {
174
+ wt.phase = cfg.config.currentPhase || null;
175
+ wt.stack = cfg.config.stack || null;
176
+ } else {
177
+ wt.phase = null;
178
+ wt.stack = null;
179
+ }
180
+ }
181
+
182
+ if (json) {
183
+ process.stdout.write(JSON.stringify({
184
+ command: 'worktree',
185
+ subcommand: 'list',
186
+ status: 'ok',
187
+ message: `${worktrees.length} worktree(s)`,
188
+ worktrees,
189
+ }) + '\n');
190
+ } else {
191
+ if (worktrees.length === 0) {
192
+ process.stdout.write('No worktrees found.\n');
193
+ } else {
194
+ process.stdout.write(`${'Path'.padEnd(50)} ${'Branch'.padEnd(30)} Phase\n`);
195
+ process.stdout.write(`${''.padEnd(50, '-')} ${''.padEnd(30, '-')} ${''.padEnd(10, '-')}\n`);
196
+ for (const wt of worktrees) {
197
+ const phase = wt.phase || '—';
198
+ process.stdout.write(`${wt.path.padEnd(50)} ${wt.branch.padEnd(30)} ${phase}\n`);
199
+ }
200
+ }
201
+ }
202
+ return;
203
+ }
204
+
205
+ // ── prune ────────────────────────────────────────────────────────────────
206
+ if (sub === 'prune') {
207
+ const result = execGit('git worktree prune', gitRoot);
208
+ if (!result.ok) {
209
+ const msg = `Failed to prune worktrees: ${result.stderr || result.stdout}`;
210
+ if (json) {
211
+ process.stdout.write(JSON.stringify({ command: 'worktree', subcommand: 'prune', status: 'error', message: msg }) + '\n');
212
+ } else {
213
+ process.stderr.write(`Error: ${msg}\n`);
214
+ }
215
+ return;
216
+ }
217
+
218
+ if (json) {
219
+ process.stdout.write(JSON.stringify({
220
+ command: 'worktree',
221
+ subcommand: 'prune',
222
+ status: 'ok',
223
+ message: 'Orphaned worktree metadata pruned',
224
+ }) + '\n');
225
+ } else {
226
+ process.stdout.write('✓ Orphaned worktree metadata pruned\n');
227
+ }
228
+ return;
229
+ }
230
+
231
+ // ── remove ───────────────────────────────────────────────────────────────
232
+ if (sub === 'remove') {
233
+ const name = args.positionals[0];
234
+ if (!name) {
235
+ die(new CliError('Usage: harness-dev worktree remove <name>', EXIT.USAGE_ERROR), json);
236
+ return;
237
+ }
238
+
239
+ const worktreePath = resolve(dirname(gitRoot), `feat-${name}`);
240
+ const branchName = `feat/${name}`;
241
+
242
+ if (!existsSync(worktreePath)) {
243
+ const msg = `Worktree path not found: ${worktreePath}. It may have been deleted manually.`;
244
+ if (json) {
245
+ process.stdout.write(JSON.stringify({ command: 'worktree', subcommand: 'remove', name, status: 'error', message: msg }) + '\n');
246
+ } else {
247
+ process.stderr.write(`Error: ${msg}\n`);
248
+ }
249
+ return;
250
+ }
251
+
252
+ // Remove worktree
253
+ const forceRemove = args.flags?.force === true || args.flags?.force === 'true';
254
+ let removeResult;
255
+ if (forceRemove) {
256
+ removeResult = execGit(`git worktree remove --force "${worktreePath}"`, gitRoot);
257
+ } else {
258
+ removeResult = execGit(`git worktree remove "${worktreePath}"`, gitRoot);
259
+ }
260
+ if (!removeResult.ok) {
261
+ const msg = `Failed to remove worktree: ${removeResult.stderr || removeResult.stdout}`;
262
+ if (json) {
263
+ process.stdout.write(JSON.stringify({ command: 'worktree', subcommand: 'remove', name, status: 'error', message: msg }) + '\n');
264
+ } else {
265
+ process.stderr.write(`Error: ${msg}\n`);
266
+ }
267
+ return;
268
+ }
269
+
270
+ // Optionally delete the branch (--delete-branch flag)
271
+ const deleteBranch = args.flags?.['delete-branch'] === true || args.flags?.['delete-branch'] === 'true';
272
+ let branchDeleted = false;
273
+ if (deleteBranch) {
274
+ const branchResult = execGit(`git branch -D "${branchName}"`, gitRoot);
275
+ if (branchResult.ok) {
276
+ branchDeleted = true;
277
+ }
278
+ }
279
+
280
+ if (json) {
281
+ process.stdout.write(JSON.stringify({
282
+ command: 'worktree',
283
+ subcommand: 'remove',
284
+ name,
285
+ path: worktreePath,
286
+ branchDeleted,
287
+ status: 'ok',
288
+ message: `Worktree removed from ${worktreePath}`,
289
+ }) + '\n');
290
+ } else {
291
+ process.stdout.write(`✓ Worktree removed from ${worktreePath}\n`);
292
+ if (branchDeleted) {
293
+ process.stdout.write(` Branch "${branchName}" deleted\n`);
294
+ }
295
+ }
296
+ return;
297
+ }
298
+ }
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * harness-dev — Agent-agnostic development harness CLI.
4
+ *
5
+ * Entry point. Parses args, routes to command handler,
6
+ * formats output (human or JSON), handles errors.
7
+ */
8
+
9
+ import { parseArgs } from './lib/args.mjs';
10
+ import { CliError, EXIT, die } from './lib/errors.mjs';
11
+ import { helpText, versionText, commandHelpText } from './lib/help.mjs';
12
+
13
+ /** Map of command names to their implementation modules. */
14
+ const COMMANDS = {
15
+ init: () => import('./commands/init.mjs'),
16
+ status: () => import('./commands/status.mjs'),
17
+ phase: () => import('./commands/phase.mjs'),
18
+ validate: () => import('./commands/validate.mjs'),
19
+ 'set-mode': () => import('./commands/set-mode.mjs'),
20
+ config: () => import('./commands/config.mjs'),
21
+ pause: () => import('./commands/pause.mjs'),
22
+ resume: () => import('./commands/resume.mjs'),
23
+ learn: () => import('./commands/learn.mjs'),
24
+ contract: () => import('./commands/contract.mjs'),
25
+ worktree: () => import('./commands/worktree.mjs'),
26
+ rollback: () => import('./commands/rollback.mjs'),
27
+ checkpoint: () => import('./commands/checkpoint.mjs'),
28
+ 'detect-tool': () => import('./commands/detect-tool.mjs'),
29
+ help: null, // handled inline — prints help text, registers as valid command
30
+ };
31
+
32
+ async function main() {
33
+ const args = parseArgs(process.argv);
34
+ const json = args.json;
35
+
36
+ // --help with no command
37
+ if (args.help && !args.command) {
38
+ process.stdout.write(helpText(json) + '\n');
39
+ return;
40
+ }
41
+
42
+ // --version
43
+ if (args.version) {
44
+ process.stdout.write(versionText(json) + '\n');
45
+ return;
46
+ }
47
+
48
+ // --help with a command → per-command help (falls back to global help)
49
+ if (args.help && args.command) {
50
+ const perCmd = commandHelpText(args.command, json);
51
+ process.stdout.write((perCmd ?? helpText(json)) + '\n');
52
+ return;
53
+ }
54
+
55
+ // No command → help (exit 0, not an error)
56
+ if (!args.command) {
57
+ process.stdout.write(helpText(json) + '\n');
58
+ return;
59
+ }
60
+
61
+ // "help" command alias — redirects to --help
62
+ if (args.command === 'help') {
63
+ process.stdout.write(helpText(json) + '\n');
64
+ return;
65
+ }
66
+
67
+ // Resolve command module
68
+ const loader = COMMANDS[args.command];
69
+ if (!loader) {
70
+ throw new CliError(
71
+ `Unknown command "${args.command}". See harness-dev --help`,
72
+ EXIT.USAGE_ERROR
73
+ );
74
+ }
75
+
76
+ // Load and execute
77
+ const mod = await loader();
78
+ await mod.default(args);
79
+ }
80
+
81
+ main().catch((err) => {
82
+ const isCli = err instanceof CliError;
83
+ // Detect --json from argv since parseArgs already ran
84
+ const json = process.argv.includes('--json');
85
+ die(err, json);
86
+ // die() calls process.exit, but keep this as safety net
87
+ process.exit(isCli ? EXIT.USAGE_ERROR : EXIT.INTERNAL_ERROR);
88
+ });