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.
- package/LICENSE +21 -0
- package/README.md +299 -0
- package/adapters/amazon-q/README.md +23 -0
- package/adapters/antigravity/README.md +22 -0
- package/adapters/claude-code/README.md +30 -0
- package/adapters/cline/README.md +23 -0
- package/adapters/codex/README.md +31 -0
- package/adapters/copilot/README.md +23 -0
- package/adapters/cursor/README.md +29 -0
- package/adapters/gemini/README.md +23 -0
- package/adapters/generic/README.md +40 -0
- package/adapters/hermes/README.md +31 -0
- package/adapters/hermes/SKILL.md +89 -0
- package/adapters/hermes/scripts/init.mjs +27 -0
- package/adapters/hermes/scripts/phase.mjs +27 -0
- package/adapters/hermes/scripts/validate.mjs +27 -0
- package/adapters/kilo-code/README.md +23 -0
- package/adapters/openclaw/README.md +22 -0
- package/adapters/pi/README.md +22 -0
- package/adapters/roo/README.md +23 -0
- package/adapters/windsurf/README.md +23 -0
- package/cli/commands/checkpoint.mjs +94 -0
- package/cli/commands/config.mjs +268 -0
- package/cli/commands/contract.mjs +155 -0
- package/cli/commands/detect-tool.mjs +112 -0
- package/cli/commands/init.mjs +351 -0
- package/cli/commands/learn.mjs +47 -0
- package/cli/commands/pause.mjs +34 -0
- package/cli/commands/phase.mjs +182 -0
- package/cli/commands/resume.mjs +33 -0
- package/cli/commands/rollback.mjs +261 -0
- package/cli/commands/set-mode.mjs +75 -0
- package/cli/commands/status.mjs +168 -0
- package/cli/commands/validate.mjs +118 -0
- package/cli/commands/worktree.mjs +298 -0
- package/cli/harness-dev.mjs +88 -0
- package/cli/lib/args.mjs +111 -0
- package/cli/lib/command-helpers.mjs +50 -0
- package/cli/lib/config-registry.mjs +329 -0
- package/cli/lib/constants.mjs +30 -0
- package/cli/lib/contract.mjs +306 -0
- package/cli/lib/detect-stack.mjs +235 -0
- package/cli/lib/errors.mjs +71 -0
- package/cli/lib/file-io.mjs +90 -0
- package/cli/lib/gates.mjs +492 -0
- package/cli/lib/git.mjs +144 -0
- package/cli/lib/help.mjs +246 -0
- package/cli/lib/modes.mjs +92 -0
- package/cli/lib/output.mjs +49 -0
- package/cli/lib/paths.mjs +75 -0
- package/cli/lib/phases.mjs +58 -0
- package/cli/lib/platform.mjs +78 -0
- package/cli/lib/progress.mjs +357 -0
- package/cli/lib/ralph-inner.mjs +314 -0
- package/cli/lib/ralph-outer.mjs +249 -0
- package/cli/lib/ralph-output.mjs +178 -0
- package/cli/lib/scaffold.mjs +431 -0
- package/cli/lib/schemas/stacks.json +477 -0
- package/cli/lib/state.mjs +333 -0
- package/cli/lib/templates.mjs +264 -0
- package/cli/lib/tool-registry.mjs +218 -0
- package/cli/lib/validate-schema.mjs +131 -0
- package/cli/lib/vars.mjs +114 -0
- package/package.json +50 -0
- package/schema/harness-config.schema.json +127 -0
- package/templates/AGENTS.md +63 -0
- package/templates/ci/github-actions.yml +78 -0
- package/templates/ci/gitlab-ci.yml +59 -0
- package/templates/docs/agents/evaluator.md +14 -0
- package/templates/docs/agents/generator.md +13 -0
- package/templates/docs/agents/planner.md +13 -0
- package/templates/docs/agents/simplifier.md +13 -0
- package/templates/docs/phases/build.md +41 -0
- package/templates/docs/phases/define.md +51 -0
- package/templates/docs/phases/plan.md +36 -0
- package/templates/docs/phases/review.md +42 -0
- package/templates/docs/phases/ship.md +43 -0
- package/templates/docs/phases/simplify.md +40 -0
- package/templates/docs/phases/verify.md +38 -0
- package/templates/evaluator-rubric.md +28 -0
- package/templates/init.ps1 +97 -0
- package/templates/init.sh +102 -0
- 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
|
+
});
|