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
package/cli/lib/help.mjs
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Help text builder — centralized to keep all formatting in one place.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const USAGE = `Usage: harness-dev <command> [options]
|
|
6
|
+
|
|
7
|
+
Pipeline commands:
|
|
8
|
+
init Scaffold full harness in current directory
|
|
9
|
+
phase <name> Invoke a phase (define|plan|build|verify|simplify|review|ship)
|
|
10
|
+
validate Run gate checks for current phase
|
|
11
|
+
validate --feature X --task Y Validate a single task (feature-iterate phases)
|
|
12
|
+
|
|
13
|
+
State commands:
|
|
14
|
+
status Show current phase + gate state + detected stack
|
|
15
|
+
config list List all config parameters with descriptions
|
|
16
|
+
config get [key] Get config value (omit key for all)
|
|
17
|
+
config set <key> <val> Set config value (e.g. config set gates.enabled true)
|
|
18
|
+
pause Pause autopilot execution
|
|
19
|
+
resume Resume autopilot execution
|
|
20
|
+
learn <message> Append a lesson to progress.md
|
|
21
|
+
|
|
22
|
+
Agent workflow commands:
|
|
23
|
+
contract propose Write/update sprint-contract.md
|
|
24
|
+
contract review Evaluator reviews contract, sets status
|
|
25
|
+
contract status Show current contract state
|
|
26
|
+
contract escalate Human adjudication when agents can't agree
|
|
27
|
+
|
|
28
|
+
Git workflow commands:
|
|
29
|
+
worktree create <name> Create isolated worktree for a feature
|
|
30
|
+
worktree list List active worktrees
|
|
31
|
+
worktree prune Remove orphaned worktrees
|
|
32
|
+
worktree remove <name> Clean up worktree (optionally merge branch)
|
|
33
|
+
rollback list Show available checkpoints
|
|
34
|
+
rollback to <tag> Restore state to a checkpoint
|
|
35
|
+
rollback branch <tag> Branch off a good iteration
|
|
36
|
+
checkpoint create <label> Force a manual checkpoint tag
|
|
37
|
+
detect-tool Detect which agent tools are configured
|
|
38
|
+
|
|
39
|
+
Mode:
|
|
40
|
+
set-mode <mode> Switch mode (copilot|autopilot)
|
|
41
|
+
|
|
42
|
+
Other:
|
|
43
|
+
help Alias for --help
|
|
44
|
+
|
|
45
|
+
Global flags:
|
|
46
|
+
--json Machine-parseable JSON output
|
|
47
|
+
--help, -h Show this help message
|
|
48
|
+
--version Show version
|
|
49
|
+
|
|
50
|
+
Exit codes:
|
|
51
|
+
0 Success
|
|
52
|
+
1 Validation failure (gate check failed)
|
|
53
|
+
2 Usage error (bad arguments)
|
|
54
|
+
3 Internal error`;
|
|
55
|
+
|
|
56
|
+
const VERSION = '0.2.0';
|
|
57
|
+
|
|
58
|
+
// Help text for JSON output
|
|
59
|
+
function buildJsonHelp() {
|
|
60
|
+
return {
|
|
61
|
+
help: true,
|
|
62
|
+
version: VERSION,
|
|
63
|
+
usage: 'harness-dev <command> [options]',
|
|
64
|
+
commands: {
|
|
65
|
+
init: 'Scaffold full harness in current directory',
|
|
66
|
+
status: 'Show current phase + gate state + detected stack',
|
|
67
|
+
phase: 'Invoke a phase (define|plan|build|verify|simplify|review|ship)',
|
|
68
|
+
validate: 'Run gate checks for current phase (--feature --task for per-task check)',
|
|
69
|
+
'set-mode': 'Switch mode (copilot|autopilot)',
|
|
70
|
+
config: 'Get/set config values',
|
|
71
|
+
pause: 'Pause autopilot execution',
|
|
72
|
+
resume: 'Resume autopilot execution',
|
|
73
|
+
learn: 'Append a lesson to progress.md',
|
|
74
|
+
contract: 'Sprint Contract workflow (propose/review/status/escalate)',
|
|
75
|
+
worktree: 'Git worktree management (create/list/prune/remove)',
|
|
76
|
+
rollback: 'Checkpoint recovery (list/to/branch)',
|
|
77
|
+
checkpoint: 'Manual checkpoint tagging (create)',
|
|
78
|
+
'detect-tool': 'Detect which agent coding tools are configured in the project',
|
|
79
|
+
help: 'Alias for --help',
|
|
80
|
+
},
|
|
81
|
+
flags: {
|
|
82
|
+
json: 'Machine-parseable JSON output',
|
|
83
|
+
help: 'Show this help message',
|
|
84
|
+
version: 'Show version',
|
|
85
|
+
},
|
|
86
|
+
exitCodes: {
|
|
87
|
+
0: 'Success',
|
|
88
|
+
1: 'Validation failure (gate check failed)',
|
|
89
|
+
2: 'Usage error (bad arguments)',
|
|
90
|
+
3: 'Internal error',
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {boolean} json
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
export function helpText(json = false) {
|
|
100
|
+
if (json) {
|
|
101
|
+
return JSON.stringify(buildJsonHelp());
|
|
102
|
+
}
|
|
103
|
+
return USAGE;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {boolean} json
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
110
|
+
export function versionText(json = false) {
|
|
111
|
+
if (json) {
|
|
112
|
+
return JSON.stringify({ version: VERSION });
|
|
113
|
+
}
|
|
114
|
+
return `harness-dev v${VERSION}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Per-command help text (for `harness-dev <command> --help`).
|
|
118
|
+
const COMMAND_HELP = {
|
|
119
|
+
init: `Usage: harness-dev init [--stack <name>] [--target <dir>] [--force] [--no-git] [--json]
|
|
120
|
+
|
|
121
|
+
Scaffold a full harness in the target directory:
|
|
122
|
+
- Detects stack (or use --stack)
|
|
123
|
+
- Generates AGENTS.md, harness-config.json, init.sh, progress.md,
|
|
124
|
+
sprint-contract.md, feature_list.json, docs/, ci/, evaluator-rubric.md
|
|
125
|
+
- Initializes git repo + initial commit (unless --no-git)
|
|
126
|
+
|
|
127
|
+
Flags:
|
|
128
|
+
--stack <name> Override stack detection (node|python|go|rust|java|...)
|
|
129
|
+
--target <dir> Target directory (default: cwd)
|
|
130
|
+
--force Overwrite existing harness files
|
|
131
|
+
--no-git Skip git init
|
|
132
|
+
--json JSON output`,
|
|
133
|
+
|
|
134
|
+
status: `Usage: harness-dev status [--target <dir>] [--json]
|
|
135
|
+
|
|
136
|
+
Show current phase, gate state, detected stack, recent lessons, and next action.
|
|
137
|
+
|
|
138
|
+
Flags:
|
|
139
|
+
--target <dir> Project directory (default: cwd)
|
|
140
|
+
--json JSON output`,
|
|
141
|
+
|
|
142
|
+
phase: `Usage: harness-dev phase <name> [--target <dir>] [--git-ops] [--json]
|
|
143
|
+
|
|
144
|
+
Invoke a phase. Valid phases: define, plan, build, verify, simplify, review, ship.
|
|
145
|
+
|
|
146
|
+
Flags:
|
|
147
|
+
--target <dir> Project directory (default: cwd)
|
|
148
|
+
--git-ops Execute git reset --hard + clean on retry (fresh context)
|
|
149
|
+
--json JSON output`,
|
|
150
|
+
|
|
151
|
+
validate: `Usage: harness-dev validate [--phase <name>] [--feature <id> --task <id>] [--target <dir>] [--json]
|
|
152
|
+
|
|
153
|
+
Run gate checks for the current (or specified) phase.
|
|
154
|
+
|
|
155
|
+
Flags:
|
|
156
|
+
--phase <name> Override current phase
|
|
157
|
+
--feature <id> Validate a single feature (feature-iterate phases)
|
|
158
|
+
--task <id> Validate a single task within a feature
|
|
159
|
+
--target <dir> Project directory (default: cwd)
|
|
160
|
+
--json JSON output`,
|
|
161
|
+
|
|
162
|
+
'set-mode': `Usage: harness-dev set-mode <copilot|autopilot> [--target <dir>] [--json]
|
|
163
|
+
|
|
164
|
+
Switch execution mode. Autopilot requires DEFINE phase or later.`,
|
|
165
|
+
|
|
166
|
+
config: `Usage: harness-dev config list [--target <dir>] [--json]
|
|
167
|
+
harness-dev config get [key] [--target <dir>] [--json]
|
|
168
|
+
harness-dev config set <key> <value> [--target <dir>] [--json]
|
|
169
|
+
|
|
170
|
+
List all parameters with descriptions, or get/set values via dot-notation.
|
|
171
|
+
Use 'config list' to see all configurable parameters, their current values,
|
|
172
|
+
types, allowed options, and descriptions.
|
|
173
|
+
|
|
174
|
+
Examples:
|
|
175
|
+
harness-dev config list
|
|
176
|
+
harness-dev config list --json
|
|
177
|
+
harness-dev config get gates.enabled
|
|
178
|
+
harness-dev config set gates.enabled true
|
|
179
|
+
harness-dev config set mode autopilot
|
|
180
|
+
harness-dev config set maxRetries 5`,
|
|
181
|
+
|
|
182
|
+
pause: `Usage: harness-dev pause [--target <dir>] [--json]
|
|
183
|
+
|
|
184
|
+
Pause autopilot execution. Autopilot stops after the current phase gate.`,
|
|
185
|
+
|
|
186
|
+
resume: `Usage: harness-dev resume [--target <dir>] [--json]
|
|
187
|
+
|
|
188
|
+
Resume autopilot execution.`,
|
|
189
|
+
|
|
190
|
+
learn: `Usage: harness-dev learn "<message>" [--target <dir>] [--json]
|
|
191
|
+
|
|
192
|
+
Append a lesson to the Lessons section of progress.md.`,
|
|
193
|
+
|
|
194
|
+
contract: `Usage: harness-dev contract <subcommand> [options] [--target <dir>] [--json]
|
|
195
|
+
|
|
196
|
+
Subcommands:
|
|
197
|
+
propose --scope "..." [--exclusions "..."] [--criteria "..."] Generator proposes
|
|
198
|
+
review --decision <agreed|needs-revision> [--notes "..."] Evaluator reviews
|
|
199
|
+
status Show contract state
|
|
200
|
+
escalate [--reason "..."] Human adjudication`,
|
|
201
|
+
|
|
202
|
+
worktree: `Usage: harness-dev worktree <subcommand> [options] [--target <dir>] [--json]
|
|
203
|
+
|
|
204
|
+
Subcommands:
|
|
205
|
+
create <name> Create isolated worktree for a feature
|
|
206
|
+
list List active worktrees
|
|
207
|
+
prune Remove orphaned worktrees
|
|
208
|
+
remove <name> Clean up worktree (optionally merge branch)`,
|
|
209
|
+
|
|
210
|
+
rollback: `Usage: harness-dev rollback <subcommand> [checkpoint] [--target <dir>] [--json]
|
|
211
|
+
|
|
212
|
+
Subcommands:
|
|
213
|
+
list Show available checkpoints
|
|
214
|
+
to <tag> Restore state to a checkpoint
|
|
215
|
+
branch <tag> Branch off a good iteration`,
|
|
216
|
+
|
|
217
|
+
checkpoint: `Usage: harness-dev checkpoint create <label> [--force] [--target <dir>] [--json]
|
|
218
|
+
|
|
219
|
+
Create a manual checkpoint tag (manual/<label>). Requires clean working tree
|
|
220
|
+
unless --force is given.`,
|
|
221
|
+
|
|
222
|
+
'detect-tool': `Usage: harness-dev detect-tool [--target <dir>] [--json]
|
|
223
|
+
|
|
224
|
+
Scan the project for agent-tool files (CLAUDE.md, .cursorrules, AGENTS.md, etc.)
|
|
225
|
+
and report which coding agents are available. Recommends a tool based on config
|
|
226
|
+
and detected files.`,
|
|
227
|
+
|
|
228
|
+
help: `Usage: harness-dev help
|
|
229
|
+
|
|
230
|
+
Show the global help message. Alias for --help.`,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get per-command help text.
|
|
235
|
+
* @param {string} command
|
|
236
|
+
* @param {boolean} json
|
|
237
|
+
* @returns {string|null} — null if no per-command help exists
|
|
238
|
+
*/
|
|
239
|
+
export function commandHelpText(command, json = false) {
|
|
240
|
+
const text = COMMAND_HELP[command];
|
|
241
|
+
if (!text) { return null; }
|
|
242
|
+
if (json) {
|
|
243
|
+
return JSON.stringify({ command, help: text });
|
|
244
|
+
}
|
|
245
|
+
return text;
|
|
246
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* modes — Copilot/Autopilot mode configuration and behavior.
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for determining mode, reading copilot config,
|
|
5
|
+
* and handling interactive prompts.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { getMode, shouldAutoPrompt, shouldConfirmGates, promptYesNo } from './modes.mjs';
|
|
9
|
+
*/
|
|
10
|
+
import { loadConfig, set } from './state.mjs';
|
|
11
|
+
import * as readline from 'node:readline';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the current mode for a project.
|
|
15
|
+
* @param {string} targetDir
|
|
16
|
+
* @returns {'copilot'|'autopilot'}
|
|
17
|
+
*/
|
|
18
|
+
export function getMode(targetDir) {
|
|
19
|
+
const { config } = loadConfig(targetDir);
|
|
20
|
+
return (config.mode === 'autopilot') ? 'autopilot' : 'copilot';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if copilot should auto-prompt after gate passes.
|
|
25
|
+
* @param {string} targetDir
|
|
26
|
+
* @returns {boolean}
|
|
27
|
+
*/
|
|
28
|
+
export function shouldAutoPrompt(targetDir) {
|
|
29
|
+
const { config } = loadConfig(targetDir);
|
|
30
|
+
if (config.mode !== 'copilot') {return false;}
|
|
31
|
+
return config.copilot?.autoPrompt !== false; // default true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if copilot should confirm before advancing gates.
|
|
36
|
+
* @param {string} targetDir
|
|
37
|
+
* @returns {boolean}
|
|
38
|
+
*/
|
|
39
|
+
export function shouldConfirmGates(targetDir) {
|
|
40
|
+
const { config } = loadConfig(targetDir);
|
|
41
|
+
if (config.mode !== 'copilot') {return false;}
|
|
42
|
+
return config.copilot?.confirmGates !== false; // default true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Prompt the user with a yes/no question. Returns true for y/yes.
|
|
47
|
+
* In non-interactive contexts (stdin not a TTY), returns null (no answer).
|
|
48
|
+
* @param {string} question — question text to display
|
|
49
|
+
* @returns {Promise<boolean|null>} true=y, false=n, null=no answer
|
|
50
|
+
*/
|
|
51
|
+
export function promptYesNo(question) {
|
|
52
|
+
// Use readline for interactive prompt
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
try {
|
|
55
|
+
const rl = readline.createInterface({
|
|
56
|
+
input: process.stdin,
|
|
57
|
+
output: process.stdout,
|
|
58
|
+
});
|
|
59
|
+
rl.question(`${question} (y/n) `, (answer) => {
|
|
60
|
+
rl.close();
|
|
61
|
+
const trimmed = answer.trim().toLowerCase();
|
|
62
|
+
if (trimmed === 'y' || trimmed === 'yes') {
|
|
63
|
+
resolve(true);
|
|
64
|
+
} else if (trimmed === 'n' || trimmed === 'no') {
|
|
65
|
+
resolve(false);
|
|
66
|
+
} else {
|
|
67
|
+
// Invalid input — treat as no
|
|
68
|
+
resolve(false);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
} catch {
|
|
72
|
+
// Not interactive or readline unavailable
|
|
73
|
+
resolve(null);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Ensure copilot config block exists in project config.
|
|
80
|
+
* @param {string} targetDir
|
|
81
|
+
* @param {object} [overrides]
|
|
82
|
+
*/
|
|
83
|
+
export function ensureCopilotConfig(targetDir, overrides = {}) {
|
|
84
|
+
const { config } = loadConfig(targetDir);
|
|
85
|
+
if (!config.copilot) {
|
|
86
|
+
config.copilot = { autoPrompt: true, confirmGates: true };
|
|
87
|
+
}
|
|
88
|
+
// Apply overrides
|
|
89
|
+
if (overrides.autoPrompt !== undefined) {config.copilot.autoPrompt = overrides.autoPrompt;}
|
|
90
|
+
if (overrides.confirmGates !== undefined) {config.copilot.confirmGates = overrides.confirmGates;}
|
|
91
|
+
set(targetDir, 'copilot', config.copilot);
|
|
92
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* output — Centralized CLI output helpers.
|
|
3
|
+
*
|
|
4
|
+
* Standardizes the JSON output contract { command, status, message, ... }
|
|
5
|
+
* and human-mode text emission so every command emits output the same way.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { emitJson, emitHuman, emitError } from '../lib/output.mjs';
|
|
9
|
+
* emitJson({ command: 'status', status: 'ok', message: '...', ...extras });
|
|
10
|
+
* emitHuman('✓ Done\n');
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Write a JSON object to stdout followed by a newline.
|
|
15
|
+
* @param {object} obj
|
|
16
|
+
*/
|
|
17
|
+
export function emitJson(obj) {
|
|
18
|
+
process.stdout.write(JSON.stringify(obj) + '\n');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Write human-readable text to stdout.
|
|
23
|
+
* @param {string} text
|
|
24
|
+
*/
|
|
25
|
+
export function emitHuman(text) {
|
|
26
|
+
process.stdout.write(text);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Write human-readable error text to stderr.
|
|
31
|
+
* @param {string} text
|
|
32
|
+
*/
|
|
33
|
+
export function emitHumanError(text) {
|
|
34
|
+
process.stderr.write(text);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Emit a standard command result in JSON or human form.
|
|
39
|
+
*
|
|
40
|
+
* @param {object} opts
|
|
41
|
+
* @param {string} opts.command — command name
|
|
42
|
+
* @param {boolean} opts.json — JSON mode
|
|
43
|
+
* @param {boolean} opts.ok — success flag
|
|
44
|
+
* @param {string} opts.message — status message
|
|
45
|
+
* @param {string} [opts.okText] — human success text (defaults to message)
|
|
46
|
+
* @param {string} [opts.errText] — human error text (defaults to message)
|
|
47
|
+
* @param {object} [opts.extras] — additional JSON fields
|
|
48
|
+
*/
|
|
49
|
+
// emitResult and emitFatalError removed — commands use emitJson/emitHuman directly.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paths — Centralized path resolution for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the lib directory location and all
|
|
5
|
+
* project-relative file paths (config, feature list, contract, progress,
|
|
6
|
+
* schemas, templates). Eliminates 4× duplicated __dirname boilerplate and
|
|
7
|
+
* scattered resolve(targetDir, '...') calls.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import { LIB_DIR, TEMPLATES_DIR, SCHEMA_DIR, CONFIG_PATH, FEATURE_LIST_PATH } from './paths.mjs';
|
|
11
|
+
*/
|
|
12
|
+
import { resolve, dirname } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
/** Directory containing cli/lib/ (this module's directory). */
|
|
16
|
+
export const LIB_DIR = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
/** Project root (parent of cli/). */
|
|
19
|
+
export const PROJECT_ROOT = resolve(LIB_DIR, '..', '..');
|
|
20
|
+
|
|
21
|
+
/** Templates directory (project_root/templates). */
|
|
22
|
+
export const TEMPLATES_DIR = resolve(PROJECT_ROOT, 'templates');
|
|
23
|
+
|
|
24
|
+
/** Adapters directory (project_root/adapters) — tool-specific integration files. */
|
|
25
|
+
export const ADAPTERS_DIR = resolve(PROJECT_ROOT, 'adapters');
|
|
26
|
+
|
|
27
|
+
/** JSON schemas directory (project_root/schema). */
|
|
28
|
+
export const SCHEMA_DIR = resolve(PROJECT_ROOT, 'schema');
|
|
29
|
+
|
|
30
|
+
/** harness-config.json schema path. */
|
|
31
|
+
export const CONFIG_SCHEMA_PATH = resolve(SCHEMA_DIR, 'harness-config.schema.json');
|
|
32
|
+
|
|
33
|
+
/** feature_list.json schema path. */
|
|
34
|
+
export const FEATURE_LIST_SCHEMA_PATH = resolve(SCHEMA_DIR, 'feature-list.schema.json');
|
|
35
|
+
|
|
36
|
+
/** stacks.json metadata path (cli/lib/schemas/stacks.json). */
|
|
37
|
+
export const STACKS_SCHEMA_PATH = resolve(LIB_DIR, 'schemas', 'stacks.json');
|
|
38
|
+
|
|
39
|
+
// ── Project-relative paths (target project, not this CLI) ────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Path to a project's harness-config.json.
|
|
43
|
+
* @param {string} targetDir
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
export function CONFIG_PATH(targetDir) {
|
|
47
|
+
return resolve(targetDir, 'harness-config.json');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Path to a project's feature_list.json.
|
|
52
|
+
* @param {string} targetDir
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
export function FEATURE_LIST_PATH(targetDir) {
|
|
56
|
+
return resolve(targetDir, 'feature_list.json');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Path to a project's sprint-contract.md.
|
|
61
|
+
* @param {string} targetDir
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
export function CONTRACT_PATH(targetDir) {
|
|
65
|
+
return resolve(targetDir, 'sprint-contract.md');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Path to a project's progress.md.
|
|
70
|
+
* @param {string} targetDir
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
export function PROGRESS_PATH(targetDir) {
|
|
74
|
+
return resolve(targetDir, 'progress.md');
|
|
75
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* phases — Phase pipeline definitions and pure transition logic.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from state.mjs to separate the phase state machine (pure logic,
|
|
5
|
+
* no I/O) from config file I/O. state.mjs re-exports these for backward
|
|
6
|
+
* compatibility.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { PHASE_ORDER, getPhaseOrder, isValidTransition } from './phases.mjs';
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Canonical phase pipeline order. */
|
|
13
|
+
export const PHASE_ORDER = [
|
|
14
|
+
'init',
|
|
15
|
+
'define',
|
|
16
|
+
'plan',
|
|
17
|
+
'build',
|
|
18
|
+
'verify',
|
|
19
|
+
'simplify',
|
|
20
|
+
'review',
|
|
21
|
+
'ship',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the ordered list of enabled phases.
|
|
26
|
+
* Filters out SIMPLIFY unless explicitly enabled.
|
|
27
|
+
* @param {string[]} [enabled]
|
|
28
|
+
* @returns {string[]}
|
|
29
|
+
*/
|
|
30
|
+
export function getPhaseOrder(enabled) {
|
|
31
|
+
if (enabled === undefined || enabled === null) {
|
|
32
|
+
// Default: all phases except simplify
|
|
33
|
+
return PHASE_ORDER.filter(p => p !== 'simplify');
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(enabled)) {
|
|
36
|
+
return PHASE_ORDER.filter(p => enabled.includes(p));
|
|
37
|
+
}
|
|
38
|
+
// Fallback: default
|
|
39
|
+
return PHASE_ORDER.filter(p => p !== 'simplify');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if a phase transition is valid.
|
|
44
|
+
* @param {string|null} fromPhase — current phase (null = start)
|
|
45
|
+
* @param {string} toPhase — target phase
|
|
46
|
+
* @param {string[]} [enabled]
|
|
47
|
+
* @returns {boolean}
|
|
48
|
+
*/
|
|
49
|
+
export function isValidTransition(fromPhase, toPhase, enabled) {
|
|
50
|
+
const order = getPhaseOrder(enabled);
|
|
51
|
+
if (!order.includes(toPhase)) {return false;}
|
|
52
|
+
if (fromPhase === null) {return order[0] === toPhase;}
|
|
53
|
+
// Re-running the same phase is always valid
|
|
54
|
+
if (fromPhase === toPhase) {return true;}
|
|
55
|
+
const fromIdx = order.indexOf(fromPhase);
|
|
56
|
+
const toIdx = order.indexOf(toPhase);
|
|
57
|
+
return toIdx === fromIdx + 1;
|
|
58
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* platform — Cross-platform detection and execution helpers.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { getPlatform, isWindows, crossExec } from './platform.mjs';
|
|
6
|
+
*/
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Detect the current platform.
|
|
11
|
+
* @returns {'linux'|'darwin'|'win32'}
|
|
12
|
+
*/
|
|
13
|
+
export function getPlatform() {
|
|
14
|
+
return process.platform;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if running on Windows.
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
export function isWindows() {
|
|
22
|
+
return process.platform === 'win32';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if running on macOS.
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
export function isMacOS() {
|
|
30
|
+
return process.platform === 'darwin';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Shell-quote a string for the current platform.
|
|
35
|
+
* On Windows, uses double quotes. On Unix, wraps in single quotes (or double if shell chars present).
|
|
36
|
+
* @param {string} str
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
export function shellQuote(str) {
|
|
40
|
+
if (isWindows()) {
|
|
41
|
+
// Windows CMD: double quotes, doubled for literal
|
|
42
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
43
|
+
}
|
|
44
|
+
// Unix: single quotes, escaped for any single quotes in the string
|
|
45
|
+
if (str.includes("'")) {
|
|
46
|
+
return `"${str.replace(/"/g, '\\"')}"`;
|
|
47
|
+
}
|
|
48
|
+
return `'${str}'`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Execute a command using the platform-appropriate shell.
|
|
53
|
+
* On Windows, uses 'cmd /c'. On Unix, uses 'sh -c'.
|
|
54
|
+
* @param {string} cmd
|
|
55
|
+
* @param {object} [options]
|
|
56
|
+
* @returns {{ stdout: string, stderr: string, exitCode: number }}
|
|
57
|
+
*/
|
|
58
|
+
export function crossExec(cmd, options = {}) {
|
|
59
|
+
const shell = isWindows() ? 'cmd' : 'sh';
|
|
60
|
+
const shellFlag = isWindows() ? '/c' : '-c';
|
|
61
|
+
try {
|
|
62
|
+
// execSync with encoding:'utf-8' returns the stdout string directly
|
|
63
|
+
// (not an object with .stdout/.stderr).
|
|
64
|
+
const stdout = execSync(`${shell} ${shellFlag} ${shellQuote(cmd)}`, {
|
|
65
|
+
stdio: 'pipe',
|
|
66
|
+
encoding: 'utf-8',
|
|
67
|
+
timeout: options.timeout || 60000,
|
|
68
|
+
...options,
|
|
69
|
+
});
|
|
70
|
+
return { stdout: stdout || '', stderr: '', exitCode: 0 };
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return {
|
|
73
|
+
stdout: typeof err.stdout === 'string' ? err.stdout : (err.stdout?.toString() || ''),
|
|
74
|
+
stderr: typeof err.stderr === 'string' ? err.stderr : (err.stderr?.toString() || err.message),
|
|
75
|
+
exitCode: err.status || 1,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|