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,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
+ }