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,155 @@
1
+ /**
2
+ * contract — Sprint Contract negotiation (propose/review/status/escalate).
3
+ *
4
+ * Manages the generator-evaluator agreement loop.
5
+ *
6
+ * Usage:
7
+ * harness-dev contract propose [--scope "msg"] [--exclusions "msg"]
8
+ * harness-dev contract review [--agreed|--needs-revision]
9
+ * harness-dev contract status
10
+ * harness-dev contract escalate [--reason "msg"]
11
+ */
12
+ import { resolve } from 'node:path';
13
+ import { die, CliError, EXIT } from '../lib/errors.mjs';
14
+ import { proposeContract, reviewContract, getContractStatus, escalateContract } from '../lib/contract.mjs';
15
+
16
+ const SUBCOMMANDS = ['propose', 'review', 'status', 'escalate'];
17
+
18
+ export default async function contractCommand(args) {
19
+ const json = !!(args.json || args.flags?.json);
20
+ const rawTarget = args.flags?.target;
21
+ const targetDir = (typeof rawTarget === 'string') ? resolve(rawTarget) : process.cwd();
22
+ const sub = args.subcommand;
23
+
24
+ if (!sub || !SUBCOMMANDS.includes(sub)) {
25
+ die(new CliError(`Usage: harness-dev contract ${SUBCOMMANDS.join('|')}`, EXIT.USAGE_ERROR), json);
26
+ return;
27
+ }
28
+
29
+ // ── propose ──────────────────────────────────────────────────────────────
30
+ if (sub === 'propose') {
31
+ const scope = args.flags?.scope || args.positionals.join(' ');
32
+ const exclusions = args.flags?.exclusions || null;
33
+ const criteria = args.flags?.criteria ? args.flags.criteria.split('|') : null;
34
+
35
+ if (!scope) {
36
+ die(new CliError(
37
+ 'Usage: harness-dev contract propose --scope "I will build X" [--exclusions "W"] [--criteria "test1|test2"]',
38
+ EXIT.USAGE_ERROR,
39
+ ), json);
40
+ return;
41
+ }
42
+
43
+ const result = proposeContract(targetDir, { scope, exclusions, criteria });
44
+
45
+ if (json) {
46
+ process.stdout.write(JSON.stringify({
47
+ command: 'contract',
48
+ subcommand: 'propose',
49
+ status: result.ok ? 'ok' : 'error',
50
+ message: result.ok ? 'Contract proposed. Evaluator review needed.' : result.error,
51
+ }) + '\n');
52
+ return;
53
+ }
54
+
55
+ if (result.ok) {
56
+ process.stdout.write('✓ Contract proposed. Run: harness-dev contract review\n');
57
+ } else {
58
+ process.stderr.write(`✗ ${result.error}\n`);
59
+ }
60
+ return;
61
+ }
62
+
63
+ // ── review ───────────────────────────────────────────────────────────────
64
+ if (sub === 'review') {
65
+ const agreed = args.flags?.agreed === true || args.flags?.agreed === 'true';
66
+ const needsRevision = args.flags?.['needs-revision'] === true || args.flags?.['needs-revision'] === 'true';
67
+ const notes = args.flags?.notes || null;
68
+
69
+ if (!agreed && !needsRevision) {
70
+ die(new CliError(
71
+ 'Usage: harness-dev contract review --agreed [--notes "msg"] OR --needs-revision [--notes "msg"]',
72
+ EXIT.USAGE_ERROR,
73
+ ), json);
74
+ return;
75
+ }
76
+
77
+ const decision = agreed ? 'agreed' : 'needs-revision';
78
+ const result = reviewContract(targetDir, decision, notes);
79
+
80
+ if (json) {
81
+ process.stdout.write(JSON.stringify({
82
+ command: 'contract',
83
+ subcommand: 'review',
84
+ status: result.ok ? 'ok' : 'error',
85
+ message: result.ok
86
+ ? result.escalated
87
+ ? 'Max negotiation rounds reached. Contract escalated to human.'
88
+ : `Contract ${decision}.`
89
+ : result.error,
90
+ escalated: result.escalated,
91
+ }) + '\n');
92
+ return;
93
+ }
94
+
95
+ if (result.ok) {
96
+ const msg = result.escalated
97
+ ? '✓ Max negotiation rounds reached. Contract escalated to human.'
98
+ : `✓ Contract marked as "${decision}"`;
99
+ process.stdout.write(msg + '\n');
100
+ } else {
101
+ process.stderr.write(`✗ ${result.error}\n`);
102
+ }
103
+ return;
104
+ }
105
+
106
+ // ── status ───────────────────────────────────────────────────────────────
107
+ if (sub === 'status') {
108
+ const { status, rounds } = getContractStatus(targetDir);
109
+
110
+ if (json) {
111
+ process.stdout.write(JSON.stringify({
112
+ command: 'contract',
113
+ subcommand: 'status',
114
+ status: status ? 'ok' : 'error',
115
+ contractStatus: status || 'not_found',
116
+ rounds,
117
+ message: status
118
+ ? `Contract ${status} (round ${rounds}/5)`
119
+ : 'No sprint-contract.md found. Run: harness-dev contract propose',
120
+ }) + '\n');
121
+ return;
122
+ }
123
+
124
+ if (status) {
125
+ process.stdout.write(`Contract status: ${status} (round ${rounds}/5)\n`);
126
+ } else {
127
+ process.stdout.write('No sprint-contract.md found. Run: harness-dev contract propose\n');
128
+ }
129
+ return;
130
+ }
131
+
132
+ // ── escalate ─────────────────────────────────────────────────────────────
133
+ if (sub === 'escalate') {
134
+ const reason = args.flags?.reason || args.positionals.join(' ') || null;
135
+
136
+ const result = escalateContract(targetDir, reason);
137
+
138
+ if (json) {
139
+ process.stdout.write(JSON.stringify({
140
+ command: 'contract',
141
+ subcommand: 'escalate',
142
+ status: result.ok ? 'ok' : 'error',
143
+ message: result.ok ? 'Contract escalated to human.' : result.error,
144
+ }) + '\n');
145
+ return;
146
+ }
147
+
148
+ if (result.ok) {
149
+ process.stdout.write('✓ Contract escalated to human.\n');
150
+ } else {
151
+ process.stderr.write(`✗ ${result.error}\n`);
152
+ }
153
+ return;
154
+ }
155
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * detect-tool — Detect which agent coding tools are configured in a project.
3
+ *
4
+ * Scans the project directory for tool-specific files:
5
+ * - CLAUDE.md → claude-code
6
+ * - .cursorrules → cursor
7
+ * - AGENTS.md → generic (read by Codex, Aider, Continue, OpenCode)
8
+ * - .aider.conf* → aider
9
+ * - continue.json → continue
10
+ * - hermes/ skill → hermes
11
+ *
12
+ * Also reads harness-config.json agentTool field if present.
13
+ *
14
+ * Usage:
15
+ * harness-dev detect-tool [--target <dir>] [--json]
16
+ */
17
+ import { existsSync } from 'node:fs';
18
+ import { resolve } from 'node:path';
19
+ import { parseCommandArgs } from '../lib/command-helpers.mjs';
20
+ import { emitJson, emitHuman } from '../lib/output.mjs';
21
+ import { loadConfig } from '../lib/state.mjs';
22
+ import { getAllDetectionSignatures, AGENTS_MD_TOOLS, TOOL_REGISTRY } from '../lib/tool-registry.mjs';
23
+
24
+ export default async function detectToolCommand(args) {
25
+ const { json, targetDir } = parseCommandArgs(args);
26
+
27
+ const detected = [];
28
+ const hasAgentsMd = existsSync(resolve(targetDir, 'AGENTS.md'));
29
+
30
+ // 1. Scan for tool-specific detection files (from registry)
31
+ for (const { tool, file } of getAllDetectionSignatures()) {
32
+ if (existsSync(resolve(targetDir, file))) {
33
+ if (!detected.includes(tool)) {
34
+ detected.push(tool);
35
+ }
36
+ }
37
+ }
38
+
39
+ // 2. AGENTS.md present → tools that read it natively are "available"
40
+ if (hasAgentsMd) {
41
+ for (const tool of AGENTS_MD_TOOLS) {
42
+ if (!detected.includes(tool)) {
43
+ detected.push(tool);
44
+ }
45
+ }
46
+ }
47
+
48
+ // 3. Read config.agentTool if present
49
+ const { config, ok } = loadConfig(targetDir);
50
+ const configuredTool = ok && config.agentTool ? config.agentTool : null;
51
+
52
+ // 4. Recommend: configured tool > first detected tool-specific > 'generic'
53
+ let recommended = configuredTool;
54
+ if (!recommended && detected.length > 0) {
55
+ // Prefer tool-specific (has a file) over AGENTS.md-native
56
+ const specific = detected.find(t => {
57
+ const entry = TOOL_REGISTRY[t];
58
+ return entry && entry.file !== null;
59
+ });
60
+ recommended = specific || detected[0];
61
+ }
62
+ if (!recommended) {
63
+ recommended = 'generic';
64
+ }
65
+
66
+ // 5. Build per-tool details for richer output
67
+ const toolDetails = detected.map(t => {
68
+ const entry = TOOL_REGISTRY[t];
69
+ return {
70
+ tool: t,
71
+ label: entry?.label || t,
72
+ file: entry?.file || null,
73
+ notes: entry?.notes || '',
74
+ };
75
+ });
76
+
77
+ const result = {
78
+ command: 'detect-tool',
79
+ status: 'ok',
80
+ message: detected.length > 0
81
+ ? `Detected ${detected.length} agent tool(s): ${detected.join(', ')}`
82
+ : 'No agent tools detected. Run: harness-dev init',
83
+ available: detected,
84
+ configured: configuredTool,
85
+ recommended,
86
+ hasAgentsMd,
87
+ tools: toolDetails,
88
+ };
89
+
90
+ if (json) {
91
+ emitJson(result);
92
+ return;
93
+ }
94
+
95
+ emitHuman(`═══ Agent Tool Detection ═══\n\n`);
96
+ if (detected.length > 0) {
97
+ emitHuman(`Available tools:\n`);
98
+ for (const t of toolDetails) {
99
+ const marker = t.tool === recommended ? ' ← recommended' : '';
100
+ const source = t.tool === configuredTool ? ' (from config)' : '';
101
+ const fileInfo = t.file ? ` [${t.file}]` : ' [AGENTS.md]';
102
+ emitHuman(` • ${t.label}${source}${marker}${fileInfo}\n`);
103
+ }
104
+ } else {
105
+ emitHuman(` No agent tools detected.\n`);
106
+ emitHuman(` Run: harness-dev init to scaffold a project.\n`);
107
+ }
108
+ if (hasAgentsMd) {
109
+ emitHuman(`\n AGENTS.md present — tools that read it natively are available.\n`);
110
+ }
111
+ emitHuman(`\n Recommended: ${TOOL_REGISTRY[recommended]?.label || recommended}\n`);
112
+ }
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * init — Scaffold full harness in target directory.
4
+ *
5
+ * Creates all harness files: template-based (AGENTS.md, harness-config.json,
6
+ * init.sh, progress.md, sprint-contract.md) plus project files (feature_list.json,
7
+ * feature-list.schema.json, session-handoff.md, etc.), git init, .gitignore.
8
+ *
9
+ * Usage: harness-dev init [--stack <name>] [--target <dir>] [--agent-tool <name>] [--force] [--no-git] [--json]
10
+ */
11
+ import { resolve, join } from 'node:path';
12
+ import {
13
+ existsSync, writeFileSync, mkdirSync, readFileSync,
14
+ } from 'node:fs';
15
+ import { generateTemplates } from '../lib/templates.mjs';
16
+ import { detectStack, getStackMeta } from '../lib/detect-stack.mjs';
17
+ import { listStacks } from '../lib/vars.mjs';
18
+ import { CliError, EXIT, die } from '../lib/errors.mjs';
19
+ import { execGit } from '../lib/git.mjs';
20
+ import {
21
+ getExtraFiles,
22
+ getConfigFileContent,
23
+ getVersionFileContent,
24
+ getGitignoreContent,
25
+ KNOWN_AGENT_TOOLS,
26
+ } from '../lib/scaffold.mjs';
27
+ import { getToolEntry } from '../lib/tool-registry.mjs';
28
+
29
+
30
+ // ── Git helpers ──────────────────────────────────────────────────────────────
31
+
32
+ /**
33
+ * Check if targetDir is inside a git repository.
34
+ */
35
+ function isInsideGitRepo(targetDir) {
36
+ return execGit('git rev-parse --git-dir 2>/dev/null', targetDir).ok;
37
+ }
38
+
39
+ /**
40
+ * Check if git repo is empty (no commits yet).
41
+ */
42
+ function isGitRepoEmpty(targetDir) {
43
+ const r = execGit('git rev-list -n 1 HEAD 2>/dev/null', targetDir);
44
+ return !r.ok || r.stdout === '';
45
+ }
46
+
47
+ /**
48
+ * Init git repo and create initial commit.
49
+ */
50
+ function initGit(targetDir) {
51
+ const messages = [];
52
+
53
+ if (!isInsideGitRepo(targetDir)) {
54
+ execGit('git init', targetDir);
55
+ messages.push('Initialized empty git repo');
56
+ }
57
+
58
+ if (isGitRepoEmpty(targetDir)) {
59
+ // Stage everything and commit
60
+ execGit('git add -A', targetDir);
61
+ execGit('git commit -m "harness: initial scaffold" --allow-empty', targetDir);
62
+ messages.push('Created initial commit: harness: initial scaffold');
63
+ } else {
64
+ messages.push('Git repo already has commits — skipped initial commit');
65
+ }
66
+
67
+ return messages;
68
+ }
69
+
70
+ // ── Command handler ──────────────────────────────────────────────────────────
71
+
72
+ export default async function initCommand(args) {
73
+ const json = !!(args.json || args.flags?.json);
74
+ const force = args.flags?.force === true || args.flags?.force === 'true';
75
+ const noGit = args.flags?.['no-git'] === true || args.flags?.['no-git'] === 'true';
76
+ const agentTool = args.flags?.['agent-tool'] || null;
77
+
78
+ // Validate agent-tool if specified
79
+ if (agentTool && !KNOWN_AGENT_TOOLS.includes(agentTool)) {
80
+ die(
81
+ new CliError(
82
+ `Unknown agent tool "${agentTool}". Valid: ${KNOWN_AGENT_TOOLS.join(', ')}`,
83
+ EXIT.USAGE_ERROR,
84
+ ),
85
+ json,
86
+ );
87
+ return;
88
+ }
89
+ // Guard: --target without value passes boolean true — fall back to cwd
90
+ const rawTarget = args.flags?.target;
91
+ const targetDir = resolve((typeof rawTarget === 'string') ? rawTarget : process.cwd());
92
+
93
+ // Resolve stack — explicit or auto-detect
94
+ let stack;
95
+ const explicitStack = args.flags?.stack;
96
+ const validStacks = listStacks();
97
+
98
+ if (explicitStack) {
99
+ if (!validStacks.includes(explicitStack)) {
100
+ // Allow unknown stacks — user/agent will fill stackMeta in harness-config.json
101
+ // during DEFINE phase (testCmd, lintCmd, buildCmd, installCmd, etc.)
102
+ process.stderr.write(
103
+ `Note: stack "${explicitStack}" is not built-in. ` +
104
+ `Fill stackMeta in harness-config.json during DEFINE phase ` +
105
+ `(testCmd, lintCmd, buildCmd, installCmd, coverageCmd).\n`
106
+ );
107
+ }
108
+ stack = explicitStack;
109
+ } else {
110
+ // Auto-detect
111
+ const detected = detectStack(targetDir);
112
+ if (detected.name === 'generic') {
113
+ die(
114
+ new CliError(
115
+ 'Could not auto-detect project stack. Specify with --stack <name>. ' +
116
+ `Valid: ${validStacks.join(', ')}, or any custom stack name ` +
117
+ `(fill stackMeta during DEFINE phase).`,
118
+ EXIT.USAGE_ERROR,
119
+ ),
120
+ json,
121
+ );
122
+ return;
123
+ }
124
+ stack = detected.name;
125
+ }
126
+
127
+ // Build the full file manifest
128
+ const extraFiles = getExtraFiles(stack);
129
+
130
+ // Config file and version file from stacks.json metadata
131
+ const meta = getStackMeta(stack);
132
+ let configFileRel = null;
133
+ let versionFileRel = null;
134
+ if (meta && meta.configFile) {
135
+ configFileRel = meta.configFile; // e.g. "pyproject.toml"
136
+ }
137
+ if (meta && meta.versionFile) {
138
+ versionFileRel = meta.versionFile; // e.g. ".python-version"
139
+ }
140
+
141
+ // Collate all output paths for existence check
142
+ // We separate "harness files" (our scaffold — conflict = abort) from
143
+ // "project files" (user's own — skip silently if they exist).
144
+ const harnessPaths = [];
145
+ const projectPaths = [];
146
+
147
+ // Template files — known template names
148
+ const templateNames = [
149
+ 'AGENTS.md', 'harness-config.json', 'init.sh',
150
+ 'progress.md', 'sprint-contract.md', 'evaluator-rubric.md',
151
+ ];
152
+ for (const name of templateNames) {
153
+ harnessPaths.push(join(targetDir, name));
154
+ }
155
+
156
+ // Extra scaffold files
157
+ for (const relPath of Object.keys(extraFiles)) {
158
+ harnessPaths.push(join(targetDir, relPath));
159
+ }
160
+
161
+ // .gitignore
162
+ harnessPaths.push(join(targetDir, '.gitignore'));
163
+
164
+ // Stack config file and version file — these are the user's own project files.
165
+ // Skip if they exist (don't abort scaffold for them).
166
+ if (configFileRel) {
167
+ projectPaths.push({ rel: configFileRel, abs: join(targetDir, configFileRel) });
168
+ }
169
+ if (versionFileRel) {
170
+ projectPaths.push({ rel: versionFileRel, abs: join(targetDir, versionFileRel) });
171
+ }
172
+
173
+ // Check for existing harness files (unless --force)
174
+ if (!force) {
175
+ const conflicts = harnessPaths.filter(p => existsSync(p));
176
+ if (conflicts.length > 0) {
177
+ const msg = conflicts.length === 1
178
+ ? `File already exists: ${conflicts[0]}` +
179
+ '\nUse --force to overwrite existing files.'
180
+ : `${conflicts.length} harness file(s) already exist in ${targetDir}` +
181
+ `\nFirst conflict: ${conflicts[0]}` +
182
+ '\nUse --force to overwrite existing files.';
183
+ die(new CliError(msg, EXIT.VALIDATION_FAILURE), json);
184
+ return;
185
+ }
186
+ }
187
+
188
+ // ── Write phase ──────────────────────────────────────────────────────
189
+
190
+ // Ensure target directory exists
191
+ mkdirSync(targetDir, { recursive: true });
192
+
193
+ // Ensure docs/ directory exists
194
+ mkdirSync(join(targetDir, 'docs'), { recursive: true });
195
+
196
+ const created = [];
197
+ const errors = [];
198
+
199
+ // 1. Template files
200
+ try {
201
+ const tmplResult = generateTemplates({ stack, target: targetDir });
202
+ for (const f of tmplResult.files) {
203
+ created.push(f);
204
+ }
205
+ for (const e of tmplResult.errors) {
206
+ errors.push(e);
207
+ }
208
+ } catch (err) {
209
+ errors.push(`Template generation: ${err.message}`);
210
+ }
211
+
212
+ // 2. Extra files (inline content)
213
+ for (const [relPath, content] of Object.entries(extraFiles)) {
214
+ const absPath = join(targetDir, relPath);
215
+ // Ensure subdirectories exist
216
+ mkdirSync(resolve(absPath, '..'), { recursive: true });
217
+ try {
218
+ writeFileSync(absPath, content, 'utf-8');
219
+ created.push(absPath);
220
+ } catch (err) {
221
+ errors.push(`${relPath}: ${err.message}`);
222
+ }
223
+ }
224
+
225
+ // 3. Stack config file — skip if it already exists (user's project file)
226
+ if (configFileRel) {
227
+ const cfPath = join(targetDir, configFileRel);
228
+ if (force || !existsSync(cfPath)) {
229
+ const cfContent = getConfigFileContent(stack);
230
+ if (cfContent !== null) {
231
+ try {
232
+ writeFileSync(cfPath, cfContent, 'utf-8');
233
+ created.push(cfPath);
234
+ } catch (err) {
235
+ errors.push(`${configFileRel}: ${err.message}`);
236
+ }
237
+ }
238
+ } else {
239
+ // Skipped — already exists, will report in output
240
+ }
241
+ }
242
+
243
+ // 4. Stack version file — skip if it already exists
244
+ if (versionFileRel) {
245
+ const vfPath = join(targetDir, versionFileRel);
246
+ if (force || !existsSync(vfPath)) {
247
+ const vfContent = getVersionFileContent(stack);
248
+ if (vfContent !== null && vfContent !== '') {
249
+ try {
250
+ writeFileSync(vfPath, vfContent, 'utf-8');
251
+ created.push(vfPath);
252
+ } catch (err) {
253
+ errors.push(`${versionFileRel}: ${err.message}`);
254
+ }
255
+ }
256
+ } else {
257
+ // Skipped — already exists
258
+ }
259
+ }
260
+
261
+ // 5. .gitignore
262
+ const gitignorePath = join(targetDir, '.gitignore');
263
+ try {
264
+ writeFileSync(gitignorePath, getGitignoreContent(stack), 'utf-8');
265
+ created.push(gitignorePath);
266
+ } catch (err) {
267
+ errors.push(`.gitignore: ${err.message}`);
268
+ }
269
+
270
+ // 5b. Agent-tool file (e.g. CLAUDE.md, .cursorrules, .windsurfrules)
271
+ // Generated from the already-rendered AGENTS.md content + optional header.
272
+ // No separate templates needed — AGENTS.md is the canonical source.
273
+ if (agentTool) {
274
+ const toolEntry = getToolEntry(agentTool);
275
+ if (toolEntry && toolEntry.file) {
276
+ const agentsMdPath = join(targetDir, 'AGENTS.md');
277
+ if (existsSync(agentsMdPath)) {
278
+ try {
279
+ const agentsContent = readFileSync(agentsMdPath, 'utf-8');
280
+ const header = toolEntry.header || '';
281
+ const outPath = join(targetDir, toolEntry.file);
282
+ // Ensure subdirectory exists (e.g. .github/copilot-instructions.md)
283
+ mkdirSync(resolve(outPath, '..'), { recursive: true });
284
+ writeFileSync(outPath, header + '\n' + agentsContent, 'utf-8');
285
+ created.push(outPath);
286
+ } catch (err) {
287
+ errors.push(`${toolEntry.file}: ${err.message}`);
288
+ }
289
+ }
290
+ }
291
+
292
+ // Set agentTool in the generated harness-config.json
293
+ const configPath = join(targetDir, 'harness-config.json');
294
+ if (existsSync(configPath)) {
295
+ try {
296
+ const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
297
+ cfg.agentTool = agentTool;
298
+ writeFileSync(configPath, JSON.stringify(cfg, null, 2) + '\n', 'utf-8');
299
+ } catch (err) {
300
+ errors.push(`harness-config.json agentTool: ${err.message}`);
301
+ }
302
+ }
303
+ }
304
+
305
+ // 6. Git init (unless --no-git)
306
+ const gitMessages = [];
307
+ if (!noGit) {
308
+ try {
309
+ const msgs = initGit(targetDir);
310
+ gitMessages.push(...msgs);
311
+ } catch (err) {
312
+ errors.push(`Git init: ${err.message}`);
313
+ }
314
+ }
315
+
316
+ // ── Output ───────────────────────────────────────────────────────────
317
+
318
+ if (json) {
319
+ const status = errors.length > 0 ? 'partial' : 'ok';
320
+ const message = errors.length > 0
321
+ ? `Created ${created.length} file(s) with ${errors.length} error(s)`
322
+ : `Created ${created.length} file(s) for stack "${stack}"`;
323
+ process.stdout.write(JSON.stringify({
324
+ command: 'init',
325
+ status,
326
+ message,
327
+ stack,
328
+ target: targetDir,
329
+ filesCreated: created.length,
330
+ files: created,
331
+ git: gitMessages,
332
+ errors,
333
+ }) + '\n');
334
+ if (errors.length > 0) {
335
+ process.exit(EXIT.VALIDATION_FAILURE);
336
+ }
337
+ return;
338
+ }
339
+
340
+ // Human output
341
+ for (const f of created) {
342
+ process.stdout.write(` ✓ ${f}\n`);
343
+ }
344
+ for (const e of errors) {
345
+ process.stderr.write(` ✗ ${e}\n`);
346
+ }
347
+ for (const g of gitMessages) {
348
+ process.stdout.write(` ● ${g}\n`);
349
+ }
350
+ process.stdout.write(`\nCreated ${created.length} file(s) for stack "${stack}" in ${targetDir}\n`);
351
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * learn — Append a lesson to progress.md.
3
+ *
4
+ * Usage:
5
+ * harness-dev learn "Lesson text here"
6
+ * harness-dev learn "Lesson text" --json
7
+ */
8
+ import { CliError, EXIT, die } from '../lib/errors.mjs';
9
+ import { appendLesson } from '../lib/progress.mjs';
10
+ import { parseCommandArgs } from '../lib/command-helpers.mjs';
11
+ import { emitJson, emitHuman, emitHumanError } from '../lib/output.mjs';
12
+
13
+ export default async function learnCommand(args) {
14
+ const { json, targetDir, subcommand, positionals } = parseCommandArgs(args);
15
+ const message = subcommand || positionals.join(' ');
16
+
17
+ if (!message) {
18
+ die(
19
+ new CliError(
20
+ 'Lesson message required.\n Example: harness-dev learn "Token refresh gotcha — accepts access_token in body"',
21
+ EXIT.USAGE_ERROR,
22
+ ),
23
+ json,
24
+ );
25
+ return;
26
+ }
27
+
28
+ const result = appendLesson(targetDir, message);
29
+
30
+ if (json) {
31
+ emitJson({
32
+ command: 'learn',
33
+ lesson: message,
34
+ status: result.ok ? 'ok' : 'error',
35
+ message: result.ok
36
+ ? `Lesson saved: "${message}"`
37
+ : (result.error || 'Failed to save lesson'),
38
+ });
39
+ return;
40
+ }
41
+
42
+ if (result.ok) {
43
+ emitHuman(`✓ Lesson saved\n "${message}"\n`);
44
+ } else {
45
+ emitHumanError(`✗ ${result.error}\n`);
46
+ }
47
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * pause — Pause autopilot execution.
3
+ *
4
+ * Sets config.paused = true. The outer loop checks this
5
+ * before starting a new phase in autopilot mode.
6
+ *
7
+ * Usage: harness-dev pause [--json]
8
+ */
9
+ import { set } from '../lib/state.mjs';
10
+ import { parseCommandArgs } from '../lib/command-helpers.mjs';
11
+ import { emitJson, emitHuman, emitHumanError } from '../lib/output.mjs';
12
+
13
+ export default async function pauseCommand(args) {
14
+ const { json, targetDir } = parseCommandArgs(args);
15
+
16
+ const result = set(targetDir, 'paused', true);
17
+
18
+ if (json) {
19
+ emitJson({
20
+ command: 'pause',
21
+ status: result.ok ? 'ok' : 'error',
22
+ message: result.ok
23
+ ? 'Pipeline paused. Autopilot will stop after current phase gate.'
24
+ : (result.error || 'Failed to pause'),
25
+ });
26
+ return;
27
+ }
28
+
29
+ if (result.ok) {
30
+ emitHuman('✓ Pipeline paused. Autopilot will stop after current phase gate.\n');
31
+ } else {
32
+ emitHumanError(`✗ ${result.error}\n`);
33
+ }
34
+ }