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,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* phase — Invoke a phase by name.
|
|
3
|
+
*
|
|
4
|
+
* Transitions the state machine, runs the inner loop,
|
|
5
|
+
* then triggers the outer loop for autopilot advancement.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* harness-dev phase <name>
|
|
9
|
+
* harness-dev phase <name> --json
|
|
10
|
+
*/
|
|
11
|
+
import { CliError, EXIT, die } from '../lib/errors.mjs';
|
|
12
|
+
import { transitionPhase, getPhaseOrder, loadConfig } from '../lib/state.mjs';
|
|
13
|
+
import { runPhase, getPhaseType } from '../lib/ralph-inner.mjs';
|
|
14
|
+
import { continuePipeline } from '../lib/ralph-outer.mjs';
|
|
15
|
+
import { promptYesNo, shouldConfirmGates, shouldAutoPrompt } from '../lib/modes.mjs';
|
|
16
|
+
import { parseCommandArgs, phaseLabel } from '../lib/command-helpers.mjs';
|
|
17
|
+
|
|
18
|
+
export default async function phaseCommand(args) {
|
|
19
|
+
const { json, targetDir, gitOps } = parseCommandArgs(args);
|
|
20
|
+
const phase = args.subcommand;
|
|
21
|
+
|
|
22
|
+
// Load config to get enabled phases (e.g. simplify may be enabled)
|
|
23
|
+
const { config: cfg, ok: cfgOk } = loadConfig(targetDir);
|
|
24
|
+
const enabledPhases = cfgOk ? cfg.phases?.enabled : undefined;
|
|
25
|
+
|
|
26
|
+
// Validate phase name
|
|
27
|
+
const validPhases = getPhaseOrder(enabledPhases);
|
|
28
|
+
if (!phase) {
|
|
29
|
+
die(
|
|
30
|
+
new CliError(
|
|
31
|
+
`Phase name required.\nValid phases: ${validPhases.join(', ')}`,
|
|
32
|
+
EXIT.USAGE_ERROR,
|
|
33
|
+
),
|
|
34
|
+
json,
|
|
35
|
+
);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!validPhases.includes(phase)) {
|
|
40
|
+
die(
|
|
41
|
+
new CliError(
|
|
42
|
+
`Invalid phase "${phase}". Valid: ${validPhases.join(', ')}`,
|
|
43
|
+
EXIT.USAGE_ERROR,
|
|
44
|
+
),
|
|
45
|
+
json,
|
|
46
|
+
);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Pre-transition pause check for autopilot
|
|
51
|
+
const { config: preConfig, ok: preOk } = loadConfig(targetDir);
|
|
52
|
+
const preMode = preOk ? (preConfig.mode ?? 'copilot') : 'copilot';
|
|
53
|
+
if (preOk && preConfig.paused && preMode === 'autopilot') {
|
|
54
|
+
const msg = 'Pipeline is paused. Run: harness-dev resume';
|
|
55
|
+
if (json) {
|
|
56
|
+
process.stdout.write(JSON.stringify({
|
|
57
|
+
command: 'phase',
|
|
58
|
+
phase,
|
|
59
|
+
status: 'paused',
|
|
60
|
+
message: msg,
|
|
61
|
+
currentPhase: preConfig.currentPhase,
|
|
62
|
+
mode: preMode,
|
|
63
|
+
}) + '\n');
|
|
64
|
+
} else {
|
|
65
|
+
process.stdout.write(` ⏸ ${msg}\n`);
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Attempt phase transition
|
|
71
|
+
const transitionResult = transitionPhase(targetDir, phase);
|
|
72
|
+
|
|
73
|
+
if (!transitionResult.ok) {
|
|
74
|
+
die(
|
|
75
|
+
new CliError(transitionResult.error || 'Phase transition failed', EXIT.VALIDATION_FAILURE),
|
|
76
|
+
json,
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const mode = transitionResult.config?.mode ?? 'copilot';
|
|
82
|
+
const phaseType = getPhaseType(phase);
|
|
83
|
+
|
|
84
|
+
// Run the inner loop for this phase
|
|
85
|
+
const loopResult = runPhase(targetDir, phase, { json, gitOps });
|
|
86
|
+
|
|
87
|
+
if (!loopResult.ok) {
|
|
88
|
+
die(
|
|
89
|
+
new CliError(loopResult.message, EXIT.VALIDATION_FAILURE),
|
|
90
|
+
json,
|
|
91
|
+
);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Phase complete — trigger outer loop ──────────────────────────────
|
|
96
|
+
const order = getPhaseOrder(transitionResult.config?.phases?.enabled);
|
|
97
|
+
const phaseIdx = order.indexOf(phase);
|
|
98
|
+
const nextPhase = (phaseIdx >= 0 && phaseIdx < order.length - 1) ? order[phaseIdx + 1] : null;
|
|
99
|
+
|
|
100
|
+
if (json) {
|
|
101
|
+
// Build JSON output
|
|
102
|
+
const out = {
|
|
103
|
+
command: 'phase',
|
|
104
|
+
phase,
|
|
105
|
+
status: loopResult.status,
|
|
106
|
+
message: loopResult.message,
|
|
107
|
+
currentPhase: phase,
|
|
108
|
+
mode,
|
|
109
|
+
phaseType,
|
|
110
|
+
iteration: loopResult.iteration,
|
|
111
|
+
nextPhase,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (loopResult.details) {
|
|
115
|
+
Object.assign(out, loopResult.details);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// In autopilot mode with complete status — continue pipeline
|
|
119
|
+
if (mode === 'autopilot' && loopResult.status === 'complete') {
|
|
120
|
+
const pipelineResult = continuePipeline(targetDir, phase, { json, verbose: false });
|
|
121
|
+
out.pipeline = {
|
|
122
|
+
status: pipelineResult.status,
|
|
123
|
+
message: pipelineResult.message,
|
|
124
|
+
phasesRemaining: pipelineResult.phasesRemaining,
|
|
125
|
+
nextPhase: pipelineResult.nextPhase || null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
process.stdout.write(JSON.stringify(out) + '\n');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Human output ────────────────────────────────────────────────────
|
|
134
|
+
if (loopResult.status === 'complete') {
|
|
135
|
+
process.stdout.write(`\n${phaseLabel(phase)} phase complete.\n`);
|
|
136
|
+
|
|
137
|
+
if (mode === 'autopilot') {
|
|
138
|
+
// Autopilot: continue pipeline automatically
|
|
139
|
+
const pipelineResult = continuePipeline(targetDir, phase, { json: false, verbose: true });
|
|
140
|
+
if (pipelineResult.status === 'complete') {
|
|
141
|
+
process.stdout.write(`\n✓ Pipeline complete. All phases done.\n`);
|
|
142
|
+
} else if (pipelineResult.status === 'instruction') {
|
|
143
|
+
process.stdout.write(`\nNext: harness-dev phase ${pipelineResult.nextPhase}\n`);
|
|
144
|
+
}
|
|
145
|
+
} else if (nextPhase) {
|
|
146
|
+
// Copilot: print next step
|
|
147
|
+
process.stdout.write(`Next: harness-dev phase ${nextPhase}\n`);
|
|
148
|
+
// Auto-prompt: controlled by two independent flags:
|
|
149
|
+
// autoPrompt=true → show the prompt
|
|
150
|
+
// confirmGates=true → require y/n answer before continuing
|
|
151
|
+
if (shouldAutoPrompt(targetDir)) {
|
|
152
|
+
if (shouldConfirmGates(targetDir)) {
|
|
153
|
+
const answer = await promptYesNo(`Advance to ${nextPhase.toUpperCase()}?`);
|
|
154
|
+
if (answer === true) {
|
|
155
|
+
process.stdout.write(`\n ● Advancing to "${nextPhase}"...\n`);
|
|
156
|
+
const pipelineResult = continuePipeline(targetDir, phase, { json: false, verbose: true });
|
|
157
|
+
if (pipelineResult.status === 'complete') {
|
|
158
|
+
process.stdout.write(`\n✓ Pipeline complete. All phases done.\n`);
|
|
159
|
+
}
|
|
160
|
+
} else if (answer === false) {
|
|
161
|
+
process.stdout.write(` Staying in ${phase.toUpperCase()}. Run: harness-dev phase ${nextPhase} when ready.\n`);
|
|
162
|
+
}
|
|
163
|
+
// null = no TTY, skipped
|
|
164
|
+
} else {
|
|
165
|
+
// confirmGates disabled — auto-advance without waiting for input
|
|
166
|
+
process.stdout.write(` ● Auto-advancing to "${nextPhase}"...\n`);
|
|
167
|
+
const pipelineResult = continuePipeline(targetDir, phase, { json: false, verbose: true });
|
|
168
|
+
if (pipelineResult.status === 'complete') {
|
|
169
|
+
process.stdout.write(`\n✓ Pipeline complete. All phases done.\n`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
process.stdout.write('Pipeline complete.\n');
|
|
175
|
+
}
|
|
176
|
+
} else if (loopResult.status === 'instruction') {
|
|
177
|
+
// runPhase already printed the task instructions
|
|
178
|
+
if (mode === 'autopilot' && nextPhase) {
|
|
179
|
+
process.stdout.write(`After gate passes, autopilot will continue to "${nextPhase}".\n`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resume — Resume autopilot execution.
|
|
3
|
+
*
|
|
4
|
+
* Sets config.paused = false. Allows autopilot to continue.
|
|
5
|
+
*
|
|
6
|
+
* Usage: harness-dev resume [--json]
|
|
7
|
+
*/
|
|
8
|
+
import { set } from '../lib/state.mjs';
|
|
9
|
+
import { parseCommandArgs } from '../lib/command-helpers.mjs';
|
|
10
|
+
import { emitJson, emitHuman, emitHumanError } from '../lib/output.mjs';
|
|
11
|
+
|
|
12
|
+
export default async function resumeCommand(args) {
|
|
13
|
+
const { json, targetDir } = parseCommandArgs(args);
|
|
14
|
+
|
|
15
|
+
const result = set(targetDir, 'paused', false);
|
|
16
|
+
|
|
17
|
+
if (json) {
|
|
18
|
+
emitJson({
|
|
19
|
+
command: 'resume',
|
|
20
|
+
status: result.ok ? 'ok' : 'error',
|
|
21
|
+
message: result.ok
|
|
22
|
+
? 'Pipeline resumed. Run: harness-dev phase <name> to continue.'
|
|
23
|
+
: (result.error || 'Failed to resume'),
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (result.ok) {
|
|
29
|
+
emitHuman('✓ Pipeline resumed. Run: harness-dev phase <name> to continue.\n');
|
|
30
|
+
} else {
|
|
31
|
+
emitHumanError(`✗ ${result.error}\n`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* rollback — Checkpoint recovery (list/to/branch).
|
|
4
|
+
*
|
|
5
|
+
* T18 implementation:
|
|
6
|
+
* list — List available checkpoints (tags)
|
|
7
|
+
* to <checkpoint> — Restore working tree to checkpoint state
|
|
8
|
+
* branch <checkpoint> — Create recovery branch at checkpoint
|
|
9
|
+
*
|
|
10
|
+
* Supports tag patterns:
|
|
11
|
+
* phase/<name> — Phase completion tags (set by outer loop)
|
|
12
|
+
* iter/<N> — Iteration tags (set by inner loop)
|
|
13
|
+
* manual/<label> — User-created manual checkpoints
|
|
14
|
+
* recovery/* — Recovery branches (for informational display)
|
|
15
|
+
*
|
|
16
|
+
* Usage: harness-dev rollback <subcommand> [checkpoint]
|
|
17
|
+
*/
|
|
18
|
+
import { die, CliError, EXIT } from '../lib/errors.mjs';
|
|
19
|
+
import { execGit, getGitRoot } from '../lib/git.mjs';
|
|
20
|
+
import { parseCommandArgs } from '../lib/command-helpers.mjs';
|
|
21
|
+
|
|
22
|
+
const SUBCOMMANDS = ['list', 'to', 'branch'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse git tag list and return structured checkpoint data.
|
|
26
|
+
*/
|
|
27
|
+
function listCheckpoints(gitRoot) {
|
|
28
|
+
// Get all harness-related tags with their dates
|
|
29
|
+
const r = execGit(
|
|
30
|
+
'git tag --list "phase/*" "iter/*" "manual/*" --sort=-taggerdate --format="%(refname:short)|%(taggerdate:iso)|%(objectname)"',
|
|
31
|
+
gitRoot,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (!r.ok || !r.stdout) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const checkpoints = [];
|
|
39
|
+
for (const line of r.stdout.split('\n').filter(Boolean)) {
|
|
40
|
+
const [ref, date, hash] = line.split('|');
|
|
41
|
+
if (!ref) {continue;}
|
|
42
|
+
const segments = ref.split('/');
|
|
43
|
+
const type = segments[0]; // phase, iter, manual
|
|
44
|
+
const name = segments.slice(1).join('/');
|
|
45
|
+
checkpoints.push({ ref, type, name, date: date || 'unknown', hash: hash || '—' });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Also add annotated tags that may not have taggerdate (fallback to *-taggerdate)
|
|
49
|
+
// Sort reverse chronologically (newest first)
|
|
50
|
+
return checkpoints;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get short description for a checkpoint type.
|
|
55
|
+
*/
|
|
56
|
+
function checkpointTypeLabel(type) {
|
|
57
|
+
switch (type) {
|
|
58
|
+
case 'phase': return 'Phase gate pass';
|
|
59
|
+
case 'iter': return 'Iteration checkpoint';
|
|
60
|
+
case 'manual': return 'Manual checkpoint';
|
|
61
|
+
default: return 'Checkpoint';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default async function rollbackCommand(args) {
|
|
66
|
+
const { json, targetDir } = parseCommandArgs(args);
|
|
67
|
+
const sub = args.subcommand;
|
|
68
|
+
|
|
69
|
+
if (!sub || !SUBCOMMANDS.includes(sub)) {
|
|
70
|
+
die(new CliError(`Usage: harness-dev rollback ${SUBCOMMANDS.join('|')}`, EXIT.USAGE_ERROR), json);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (sub !== 'list' && args.positionals.length < 1) {
|
|
75
|
+
die(new CliError(`Usage: harness-dev rollback ${sub} <checkpoint>`, EXIT.USAGE_ERROR), json);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const gitRoot = getGitRoot(targetDir);
|
|
80
|
+
if (!gitRoot) {
|
|
81
|
+
const msg = 'Not inside a git repository. This command requires a git repo with checkpoint tags.';
|
|
82
|
+
if (json) {
|
|
83
|
+
process.stdout.write(JSON.stringify({ command: 'rollback', subcommand: sub, status: 'error', message: msg }) + '\n');
|
|
84
|
+
} else {
|
|
85
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const checkpoint = args.positionals[0] || null;
|
|
91
|
+
|
|
92
|
+
// ── list ─────────────────────────────────────────────────────────────────
|
|
93
|
+
if (sub === 'list') {
|
|
94
|
+
const checkpoints = listCheckpoints(gitRoot);
|
|
95
|
+
|
|
96
|
+
// Also list recovery branches
|
|
97
|
+
const branchR = execGit(
|
|
98
|
+
'git branch --list "recovery/*" --format="%(refname:short)|%(subject)|%(objectname:short)"',
|
|
99
|
+
gitRoot,
|
|
100
|
+
);
|
|
101
|
+
const recoveryBranches = [];
|
|
102
|
+
if (branchR.ok && branchR.stdout) {
|
|
103
|
+
for (const line of branchR.stdout.split('\n').filter(Boolean)) {
|
|
104
|
+
const [ref, subject, hash] = line.split('|');
|
|
105
|
+
recoveryBranches.push({ branch: ref || line, subject: subject || '', hash: hash || '' });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (json) {
|
|
110
|
+
process.stdout.write(JSON.stringify({
|
|
111
|
+
command: 'rollback',
|
|
112
|
+
subcommand: 'list',
|
|
113
|
+
status: 'ok',
|
|
114
|
+
message: `${checkpoints.length} checkpoint(s) found`,
|
|
115
|
+
checkpoints,
|
|
116
|
+
recoveryBranches,
|
|
117
|
+
}) + '\n');
|
|
118
|
+
} else {
|
|
119
|
+
if (checkpoints.length === 0 && recoveryBranches.length === 0) {
|
|
120
|
+
process.stdout.write('No checkpoints found. Phase tags (phase/*) and iteration tags (iter/*) are created automatically when auto-tagging is enabled.\n');
|
|
121
|
+
process.stdout.write('Manual checkpoints: harness-dev checkpoint create <label>\n');
|
|
122
|
+
} else {
|
|
123
|
+
if (checkpoints.length > 0) {
|
|
124
|
+
process.stdout.write('Checkpoints:\n');
|
|
125
|
+
process.stdout.write(`${''.padEnd(32)} Type Date Hash\n`);
|
|
126
|
+
process.stdout.write(`${''.padEnd(32, '-')} ${''.padEnd(26, '-')} ${''.padEnd(26, '-')} ${''.padEnd(10, '-')}\n`);
|
|
127
|
+
for (const cp of checkpoints) {
|
|
128
|
+
const typeLabel = checkpointTypeLabel(cp.type).padEnd(26);
|
|
129
|
+
const date = (cp.date || '—').padEnd(26);
|
|
130
|
+
process.stdout.write(`${cp.ref.padEnd(32)} ${typeLabel} ${date} ${cp.hash}\n`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (recoveryBranches.length > 0) {
|
|
134
|
+
process.stdout.write('\nRecovery branches:\n');
|
|
135
|
+
for (const rb of recoveryBranches) {
|
|
136
|
+
process.stdout.write(` ${rb.branch} (${rb.hash})\n`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── to ───────────────────────────────────────────────────────────────────
|
|
145
|
+
if (sub === 'to') {
|
|
146
|
+
// Verify the tag exists
|
|
147
|
+
const tagCheck = execGit(`git rev-parse --verify "${checkpoint}^{commit}"`, gitRoot);
|
|
148
|
+
if (!tagCheck.ok) {
|
|
149
|
+
const msg = `Checkpoint "${checkpoint}" not found. Run: harness-dev rollback list`;
|
|
150
|
+
if (json) {
|
|
151
|
+
process.stdout.write(JSON.stringify({ command: 'rollback', subcommand: 'to', checkpoint, status: 'error', message: msg }) + '\n');
|
|
152
|
+
} else {
|
|
153
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const targetHash = tagCheck.stdout;
|
|
158
|
+
|
|
159
|
+
// Stash any uncommitted changes
|
|
160
|
+
execGit('git stash push -m "rollback-auto-stash"', gitRoot);
|
|
161
|
+
|
|
162
|
+
// Restore all files from the checkpoint
|
|
163
|
+
const restoreFiles = [
|
|
164
|
+
'harness-config.json',
|
|
165
|
+
'progress.md',
|
|
166
|
+
'feature_list.json',
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
// First, restore the whole working tree from the tag
|
|
170
|
+
const restoreResult = execGit(`git checkout "${checkpoint}" -- .`, gitRoot);
|
|
171
|
+
if (!restoreResult.ok) {
|
|
172
|
+
const msg = `Failed to restore files from ${checkpoint}: ${restoreResult.stderr || restoreResult.stdout}`;
|
|
173
|
+
if (json) {
|
|
174
|
+
process.stdout.write(JSON.stringify({ command: 'rollback', subcommand: 'to', checkpoint, status: 'error', message: msg }) + '\n');
|
|
175
|
+
} else {
|
|
176
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Also explicitly restore harness state files from the tag
|
|
182
|
+
for (const file of restoreFiles) {
|
|
183
|
+
execGit(`git checkout "${checkpoint}" -- "${file}"`, gitRoot);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (json) {
|
|
187
|
+
process.stdout.write(JSON.stringify({
|
|
188
|
+
command: 'rollback',
|
|
189
|
+
subcommand: 'to',
|
|
190
|
+
checkpoint,
|
|
191
|
+
hash: targetHash,
|
|
192
|
+
status: 'ok',
|
|
193
|
+
message: `Working tree restored to checkpoint "${checkpoint}" (${targetHash.slice(0, 8)})`,
|
|
194
|
+
}) + '\n');
|
|
195
|
+
} else {
|
|
196
|
+
process.stdout.write(`✓ Working tree restored to checkpoint "${checkpoint}" (${targetHash.slice(0, 8)})\n`);
|
|
197
|
+
process.stdout.write(` Files restored from ${checkpoint}\n`);
|
|
198
|
+
process.stdout.write(' Note: Uncommitted changes were stashed (git stash list)\n');
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── branch ───────────────────────────────────────────────────────────────
|
|
204
|
+
if (sub === 'branch') {
|
|
205
|
+
// Verify the tag exists
|
|
206
|
+
const tagCheck = execGit(`git rev-parse --verify "${checkpoint}^{commit}"`, gitRoot);
|
|
207
|
+
if (!tagCheck.ok) {
|
|
208
|
+
const msg = `Checkpoint "${checkpoint}" not found. Run: harness-dev rollback list`;
|
|
209
|
+
if (json) {
|
|
210
|
+
process.stdout.write(JSON.stringify({ command: 'rollback', subcommand: 'branch', checkpoint, status: 'error', message: msg }) + '\n');
|
|
211
|
+
} else {
|
|
212
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const targetHash = tagCheck.stdout;
|
|
217
|
+
|
|
218
|
+
// Generate a safe branch name
|
|
219
|
+
const safeName = checkpoint.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/\//g, '-');
|
|
220
|
+
const branchName = `recovery/from-${safeName}`;
|
|
221
|
+
|
|
222
|
+
// Check branch doesn't already exist
|
|
223
|
+
const branchCheck = execGit(`git show-ref --verify --quiet refs/heads/${branchName}`, gitRoot);
|
|
224
|
+
if (branchCheck.ok) {
|
|
225
|
+
const msg = `Recovery branch "${branchName}" already exists. Check it out directly: git checkout ${branchName}`;
|
|
226
|
+
if (json) {
|
|
227
|
+
process.stdout.write(JSON.stringify({ command: 'rollback', subcommand: 'branch', checkpoint, branch: branchName, status: 'error', message: msg }) + '\n');
|
|
228
|
+
} else {
|
|
229
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Create the recovery branch
|
|
235
|
+
const branchResult = execGit(`git checkout -b "${branchName}" "${checkpoint}"`, gitRoot);
|
|
236
|
+
if (!branchResult.ok) {
|
|
237
|
+
const msg = `Failed to create recovery branch: ${branchResult.stderr || branchResult.stdout}`;
|
|
238
|
+
if (json) {
|
|
239
|
+
process.stdout.write(JSON.stringify({ command: 'rollback', subcommand: 'branch', checkpoint, branch: branchName, status: 'error', message: msg }) + '\n');
|
|
240
|
+
} else {
|
|
241
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (json) {
|
|
247
|
+
process.stdout.write(JSON.stringify({
|
|
248
|
+
command: 'rollback',
|
|
249
|
+
subcommand: 'branch',
|
|
250
|
+
checkpoint,
|
|
251
|
+
branch: branchName,
|
|
252
|
+
hash: targetHash,
|
|
253
|
+
status: 'ok',
|
|
254
|
+
message: `Recovery branch "${branchName}" created at checkpoint "${checkpoint}"`,
|
|
255
|
+
}) + '\n');
|
|
256
|
+
} else {
|
|
257
|
+
process.stdout.write(`✓ Recovery branch "${branchName}" created at checkpoint "${checkpoint}" (${targetHash.slice(0, 8)})\n`);
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* set-mode — Switch between copilot and autopilot.
|
|
3
|
+
*
|
|
4
|
+
* Usage: harness-dev set-mode <mode>
|
|
5
|
+
* harness-dev set-mode autopilot
|
|
6
|
+
* harness-dev set-mode copilot
|
|
7
|
+
* harness-dev set-mode autopilot --json
|
|
8
|
+
*/
|
|
9
|
+
import { die, CliError, EXIT } from '../lib/errors.mjs';
|
|
10
|
+
import { set, loadConfig, getPhaseOrder } from '../lib/state.mjs';
|
|
11
|
+
import { ensureCopilotConfig } from '../lib/modes.mjs';
|
|
12
|
+
import { parseCommandArgs } from '../lib/command-helpers.mjs';
|
|
13
|
+
import { emitJson, emitHuman, emitHumanError } from '../lib/output.mjs';
|
|
14
|
+
|
|
15
|
+
export default async function setModeCommand(args) {
|
|
16
|
+
const { json, targetDir, subcommand: mode } = parseCommandArgs(args);
|
|
17
|
+
const valid = ['copilot', 'autopilot'];
|
|
18
|
+
|
|
19
|
+
if (!mode || !valid.includes(mode)) {
|
|
20
|
+
die(
|
|
21
|
+
new CliError(
|
|
22
|
+
`Mode required. Valid: ${valid.join(', ')}.\n Example: harness-dev set-mode autopilot`,
|
|
23
|
+
EXIT.USAGE_ERROR,
|
|
24
|
+
),
|
|
25
|
+
json,
|
|
26
|
+
);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Require DEFINE phase or later for autopilot
|
|
31
|
+
if (mode === 'autopilot') {
|
|
32
|
+
const { config, ok } = loadConfig(targetDir);
|
|
33
|
+
if (ok) {
|
|
34
|
+
const order = getPhaseOrder(config.phases?.enabled);
|
|
35
|
+
const defineIdx = order.indexOf('define');
|
|
36
|
+
const currentIdx = config.currentPhase ? order.indexOf(config.currentPhase) : -1;
|
|
37
|
+
if (currentIdx < 0 || currentIdx < defineIdx) {
|
|
38
|
+
const phase = config.currentPhase || 'start';
|
|
39
|
+
die(
|
|
40
|
+
new CliError(
|
|
41
|
+
`Autopilot requires DEFINE phase or later (current: "${phase}").\n Complete INIT and DEFINE first, then switch to autopilot.`,
|
|
42
|
+
EXIT.VALIDATION_FAILURE,
|
|
43
|
+
),
|
|
44
|
+
json,
|
|
45
|
+
);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = set(targetDir, 'mode', mode);
|
|
52
|
+
|
|
53
|
+
// Ensure copilot config block exists when switching to copilot
|
|
54
|
+
if (result.ok && mode === 'copilot') {
|
|
55
|
+
ensureCopilotConfig(targetDir);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (json) {
|
|
59
|
+
emitJson({
|
|
60
|
+
command: 'set-mode',
|
|
61
|
+
mode,
|
|
62
|
+
status: result.ok ? 'ok' : 'error',
|
|
63
|
+
message: result.ok
|
|
64
|
+
? `Mode set to "${mode}"`
|
|
65
|
+
: (result.error || 'Failed to set mode'),
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (result.ok) {
|
|
71
|
+
emitHuman(`✓ Mode set to "${mode}"\n`);
|
|
72
|
+
} else {
|
|
73
|
+
emitHumanError(`✗ ${result.error}\n`);
|
|
74
|
+
}
|
|
75
|
+
}
|