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,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "dev-harness"
|
|
3
|
+
description: "Hermes adapter for the dev-harness CLI — phase-based development pipeline with scaffold, stack detection, gate validation, inner/outer loops, sprint contracts, git worktree management, and rollback/checkpoint. Load this skill when working inside a harness-scaffolded project."
|
|
4
|
+
license: MIT
|
|
5
|
+
category: software-development
|
|
6
|
+
risk: high
|
|
7
|
+
source: self
|
|
8
|
+
date_added: "2026-06-20"
|
|
9
|
+
metadata:
|
|
10
|
+
version: 1.1.0
|
|
11
|
+
paradigm: "Phase-based agentic development pipeline"
|
|
12
|
+
changelog:
|
|
13
|
+
- "1.1.0 — Moved to adapters/hermes/ (tool-agnostic adapter structure)."
|
|
14
|
+
- "1.0.0 — Hermes skill wrapper for dev-harness CLI. Wraps init, phase, and validate commands."
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Dev Harness — Hermes Skill Wrapper
|
|
18
|
+
|
|
19
|
+
Triggers on: "harness-dev", "harness init", "harness scaffold", "new project", "phase pipeline"
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
- Node.js >= 18
|
|
24
|
+
- `harness-dev` CLI accessible via `node cli/harness-dev.mjs` from the project root
|
|
25
|
+
- A git repository (for worktree, rollback, and checkpoint commands)
|
|
26
|
+
|
|
27
|
+
## Actions
|
|
28
|
+
|
|
29
|
+
### `harness-dev init [--stack <name>] [--target <dir>] [--force]`
|
|
30
|
+
|
|
31
|
+
Scaffold harness in current project. If stack not specified, auto-detects from target directory.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
node cli/harness-dev.mjs init --stack python --target my-project
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### `harness-dev status [--json] [--target <dir>]`
|
|
38
|
+
|
|
39
|
+
Show current project state — phase, mode, gate status, recent lessons.
|
|
40
|
+
|
|
41
|
+
### `harness-dev phase <name>`
|
|
42
|
+
|
|
43
|
+
Run a phase: `define` → `plan` → `build` → `verify` → `simplify` → `review` → `ship`
|
|
44
|
+
|
|
45
|
+
### `harness-dev validate [--json] [--phase <name>] [--feature <id>] [--task <id>]`
|
|
46
|
+
|
|
47
|
+
Run gate checks for current or specified phase.
|
|
48
|
+
|
|
49
|
+
### `harness-dev set-mode copilot|autopilot`
|
|
50
|
+
|
|
51
|
+
Switch between one-phase-at-a-time (copilot) and auto-advance (autopilot).
|
|
52
|
+
|
|
53
|
+
### `harness-dev learn <message>`
|
|
54
|
+
|
|
55
|
+
Append a lesson to progress.md.
|
|
56
|
+
|
|
57
|
+
### `harness-dev config get <key>` / `harness-dev config set <key> <value>`
|
|
58
|
+
|
|
59
|
+
Read/write harness-config.json via dot-notation.
|
|
60
|
+
|
|
61
|
+
### `harness-dev contract propose|review|status|escalate`
|
|
62
|
+
|
|
63
|
+
Sprint contract negotiation workflow.
|
|
64
|
+
|
|
65
|
+
### `harness-dev worktree create|list|prune|remove <name>`
|
|
66
|
+
|
|
67
|
+
Git worktree management with harness scaffold.
|
|
68
|
+
|
|
69
|
+
### `harness-dev rollback list|to|branch`
|
|
70
|
+
|
|
71
|
+
Checkpoint recovery with tag-based state restoration.
|
|
72
|
+
|
|
73
|
+
### `harness-dev checkpoint create <label>`
|
|
74
|
+
|
|
75
|
+
Manual checkpoint tagging.
|
|
76
|
+
|
|
77
|
+
## Scripts
|
|
78
|
+
|
|
79
|
+
Thin wrapper scripts are provided under `scripts/` that resolve the CLI relative to the project root:
|
|
80
|
+
|
|
81
|
+
- `scripts/init.mjs` — wrapper for `harness-dev init`
|
|
82
|
+
- `scripts/phase.mjs` — wrapper for `harness-dev phase`
|
|
83
|
+
- `scripts/validate.mjs` — wrapper for `harness-dev validate`
|
|
84
|
+
|
|
85
|
+
Use these when you want to invoke harness commands from other Hermes skill scripts or automation.
|
|
86
|
+
|
|
87
|
+
## Templates
|
|
88
|
+
|
|
89
|
+
The `templates/` directory is a symlink to `../../../templates` (the project's main templates directory). Template files use `{{VAR}}` substitution for stack-aware scaffolding.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Hermes skill wrapper — harness-dev init
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper that resolves the CLI relative to this skill's location
|
|
6
|
+
* and forwards arguments to the init command.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node hermes/skill/dev-harness/scripts/init.mjs [--stack <name>] [--target <dir>] [--force] [--no-git] [--json]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { dirname, resolve } from 'node:path';
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const projectRoot = resolve(__dirname, '..', '..', '..', '..');
|
|
17
|
+
const cliPath = resolve(projectRoot, 'cli/harness-dev.mjs');
|
|
18
|
+
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
|
|
21
|
+
const result = spawnSync('node', [cliPath, 'init', ...args], {
|
|
22
|
+
stdio: 'inherit',
|
|
23
|
+
cwd: projectRoot,
|
|
24
|
+
env: { ...process.env },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Hermes skill wrapper — harness-dev phase
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper that resolves the CLI relative to this skill's location
|
|
6
|
+
* and forwards arguments to the phase command.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node hermes/skill/dev-harness/scripts/phase.mjs <phase-name> [--json] [--target <dir>]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { dirname, resolve } from 'node:path';
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const projectRoot = resolve(__dirname, '..', '..', '..', '..');
|
|
17
|
+
const cliPath = resolve(projectRoot, 'cli/harness-dev.mjs');
|
|
18
|
+
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
|
|
21
|
+
const result = spawnSync('node', [cliPath, 'phase', ...args], {
|
|
22
|
+
stdio: 'inherit',
|
|
23
|
+
cwd: projectRoot,
|
|
24
|
+
env: { ...process.env },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Hermes skill wrapper — harness-dev validate
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper that resolves the CLI relative to this skill's location
|
|
6
|
+
* and forwards arguments to the validate command.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node hermes/skill/dev-harness/scripts/validate.mjs [--json] [--phase <name>] [--feature <id>] [--task <id>] [--target <dir>]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { dirname, resolve } from 'node:path';
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const projectRoot = resolve(__dirname, '..', '..', '..', '..');
|
|
17
|
+
const cliPath = resolve(projectRoot, 'cli/harness-dev.mjs');
|
|
18
|
+
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
|
|
21
|
+
const result = spawnSync('node', [cliPath, 'validate', ...args], {
|
|
22
|
+
stdio: 'inherit',
|
|
23
|
+
cwd: projectRoot,
|
|
24
|
+
env: { ...process.env },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Kilo Code Adapter
|
|
2
|
+
|
|
3
|
+
Kilo Code reads .kilocoderules.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
harness-dev init --stack node --agent-tool kilo-code --target my-project
|
|
9
|
+
cd my-project
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Files Generated
|
|
13
|
+
|
|
14
|
+
- `AGENTS.md` — canonical harness conventions (always generated)
|
|
15
|
+
- `.kilocoderules` — Kilo Code-specific rules file (generated from AGENTS.md content)
|
|
16
|
+
- `harness-config.json` — with `agentTool: "kilo-code"`
|
|
17
|
+
|
|
18
|
+
## How It Works
|
|
19
|
+
|
|
20
|
+
The harness generates AGENTS.md as the canonical conventions file. For tools
|
|
21
|
+
with a specific rules file, the harness copies AGENTS.md content to the
|
|
22
|
+
tool-specific filename (e.g. .kilocoderules) with an optional header. The tool then
|
|
23
|
+
reads its native file and follows the harness phase instructions.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# OpenClaw Adapter
|
|
2
|
+
|
|
3
|
+
OpenClaw — assumed to read AGENTS.md.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
harness-dev init --stack node --agent-tool openclaw --target my-project
|
|
9
|
+
cd my-project
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Files Generated
|
|
13
|
+
|
|
14
|
+
- `AGENTS.md` — canonical harness conventions (always generated)
|
|
15
|
+
- `harness-config.json` — with `agentTool: "openclaw"`
|
|
16
|
+
|
|
17
|
+
## How It Works
|
|
18
|
+
|
|
19
|
+
The harness generates AGENTS.md as the canonical conventions file. For tools
|
|
20
|
+
with a specific rules file, the harness copies AGENTS.md content to the
|
|
21
|
+
tool-specific filename (e.g. AGENTS.md) with an optional header. The tool then
|
|
22
|
+
reads its native file and follows the harness phase instructions.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Pi Adapter
|
|
2
|
+
|
|
3
|
+
Pi — assumed to read AGENTS.md.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
harness-dev init --stack node --agent-tool pi --target my-project
|
|
9
|
+
cd my-project
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Files Generated
|
|
13
|
+
|
|
14
|
+
- `AGENTS.md` — canonical harness conventions (always generated)
|
|
15
|
+
- `harness-config.json` — with `agentTool: "pi"`
|
|
16
|
+
|
|
17
|
+
## How It Works
|
|
18
|
+
|
|
19
|
+
The harness generates AGENTS.md as the canonical conventions file. For tools
|
|
20
|
+
with a specific rules file, the harness copies AGENTS.md content to the
|
|
21
|
+
tool-specific filename (e.g. AGENTS.md) with an optional header. The tool then
|
|
22
|
+
reads its native file and follows the harness phase instructions.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Roo Code Adapter
|
|
2
|
+
|
|
3
|
+
Roo Code reads .roorules.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
harness-dev init --stack node --agent-tool roo --target my-project
|
|
9
|
+
cd my-project
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Files Generated
|
|
13
|
+
|
|
14
|
+
- `AGENTS.md` — canonical harness conventions (always generated)
|
|
15
|
+
- `.roorules` — Roo Code-specific rules file (generated from AGENTS.md content)
|
|
16
|
+
- `harness-config.json` — with `agentTool: "roo"`
|
|
17
|
+
|
|
18
|
+
## How It Works
|
|
19
|
+
|
|
20
|
+
The harness generates AGENTS.md as the canonical conventions file. For tools
|
|
21
|
+
with a specific rules file, the harness copies AGENTS.md content to the
|
|
22
|
+
tool-specific filename (e.g. .roorules) with an optional header. The tool then
|
|
23
|
+
reads its native file and follows the harness phase instructions.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Windsurf Adapter
|
|
2
|
+
|
|
3
|
+
Windsurf (Codeium) reads .windsurfrules as system context.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
harness-dev init --stack node --agent-tool windsurf --target my-project
|
|
9
|
+
cd my-project
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Files Generated
|
|
13
|
+
|
|
14
|
+
- `AGENTS.md` — canonical harness conventions (always generated)
|
|
15
|
+
- `.windsurfrules` — Windsurf-specific rules file (generated from AGENTS.md content)
|
|
16
|
+
- `harness-config.json` — with `agentTool: "windsurf"`
|
|
17
|
+
|
|
18
|
+
## How It Works
|
|
19
|
+
|
|
20
|
+
The harness generates AGENTS.md as the canonical conventions file. For tools
|
|
21
|
+
with a specific rules file, the harness copies AGENTS.md content to the
|
|
22
|
+
tool-specific filename (e.g. .windsurfrules) with an optional header. The tool then
|
|
23
|
+
reads its native file and follows the harness phase instructions.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* checkpoint — Manual checkpoint tagging (create).
|
|
4
|
+
*
|
|
5
|
+
* T18 implementation:
|
|
6
|
+
* create <label> — git tag -a manual/<label> -m "checkpoint: <label>"
|
|
7
|
+
*
|
|
8
|
+
* This complements the rollback system. Manual checkpoints let users
|
|
9
|
+
* save named recovery points (e.g. "before-refactor") that appear
|
|
10
|
+
* in `rollback list` and can be used with `rollback to/branch`.
|
|
11
|
+
*
|
|
12
|
+
* Usage: harness-dev checkpoint create <label>
|
|
13
|
+
*/
|
|
14
|
+
import { die, CliError, EXIT } from '../lib/errors.mjs';
|
|
15
|
+
import { execGit, getGitRoot, gitTagExists, createGitTag } from '../lib/git.mjs';
|
|
16
|
+
import { parseCommandArgs } from '../lib/command-helpers.mjs';
|
|
17
|
+
|
|
18
|
+
export default async function checkpointCommand(args) {
|
|
19
|
+
const { json, targetDir, force } = parseCommandArgs(args);
|
|
20
|
+
const sub = args.subcommand;
|
|
21
|
+
const label = args.positionals[0];
|
|
22
|
+
|
|
23
|
+
if (sub !== 'create' || !label) {
|
|
24
|
+
die(new CliError('Usage: harness-dev checkpoint create <label> [--force]', EXIT.USAGE_ERROR), json);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const gitRoot = getGitRoot(targetDir);
|
|
29
|
+
if (!gitRoot) {
|
|
30
|
+
const msg = 'Not inside a git repository. Checkpoints require git tags.';
|
|
31
|
+
if (json) {
|
|
32
|
+
process.stdout.write(JSON.stringify({ command: 'checkpoint', subcommand: 'create', label, status: 'error', message: msg }) + '\n');
|
|
33
|
+
} else {
|
|
34
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Verify working tree is clean (encourage checkpointing at known states).
|
|
40
|
+
// --force skips this check for users who intentionally checkpoint dirty states.
|
|
41
|
+
const cleanCheck = execGit('git status --porcelain', gitRoot);
|
|
42
|
+
if (!force && cleanCheck.ok && cleanCheck.stdout.length > 0) {
|
|
43
|
+
if (json) {
|
|
44
|
+
process.stdout.write(JSON.stringify({ command: 'checkpoint', subcommand: 'create', label, status: 'error', message: 'Working tree is not clean. Commit or stash changes, or use --force to checkpoint anyway.' }) + '\n');
|
|
45
|
+
} else {
|
|
46
|
+
process.stderr.write('Error: Working tree is not clean. Commit or stash changes, or use --force to checkpoint anyway.\n');
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const tagName = `manual/${label}`;
|
|
52
|
+
|
|
53
|
+
// Check tag doesn't already exist
|
|
54
|
+
if (gitTagExists(tagName, gitRoot)) {
|
|
55
|
+
const msg = `Tag "${tagName}" already exists. Use a different label.`;
|
|
56
|
+
if (json) {
|
|
57
|
+
process.stdout.write(JSON.stringify({ command: 'checkpoint', subcommand: 'create', label, tag: tagName, status: 'error', message: msg }) + '\n');
|
|
58
|
+
} else {
|
|
59
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Create the annotated tag
|
|
65
|
+
const created = createGitTag(tagName, `checkpoint: ${label}`, gitRoot);
|
|
66
|
+
|
|
67
|
+
if (!created) {
|
|
68
|
+
const msg = `Failed to create checkpoint tag: ${tagName}`;
|
|
69
|
+
if (json) {
|
|
70
|
+
process.stdout.write(JSON.stringify({ command: 'checkpoint', subcommand: 'create', label, tag: tagName, status: 'error', message: msg }) + '\n');
|
|
71
|
+
} else {
|
|
72
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Get the hash the tag points to
|
|
78
|
+
const hashR = execGit(`git rev-parse --short "${tagName}"`, gitRoot);
|
|
79
|
+
const hash = hashR.ok ? hashR.stdout : '—';
|
|
80
|
+
|
|
81
|
+
if (json) {
|
|
82
|
+
process.stdout.write(JSON.stringify({
|
|
83
|
+
command: 'checkpoint',
|
|
84
|
+
subcommand: 'create',
|
|
85
|
+
label,
|
|
86
|
+
tag: tagName,
|
|
87
|
+
hash,
|
|
88
|
+
status: 'ok',
|
|
89
|
+
message: `Checkpoint "${tagName}" created (${hash})`,
|
|
90
|
+
}) + '\n');
|
|
91
|
+
} else {
|
|
92
|
+
process.stdout.write(`✓ Checkpoint "${tagName}" created (${hash})\n`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config — Get/set/list harness-config.json values.
|
|
3
|
+
*
|
|
4
|
+
* Uses state.mjs for dot-notation read/write with persistence.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* harness-dev config list — list all parameters with descriptions
|
|
8
|
+
* harness-dev config get [key] — get a value or all config
|
|
9
|
+
* harness-dev config set <key> <value> — set a value
|
|
10
|
+
*
|
|
11
|
+
* Examples:
|
|
12
|
+
* harness-dev config list
|
|
13
|
+
* harness-dev config list --json
|
|
14
|
+
* harness-dev config get gates.enabled
|
|
15
|
+
* harness-dev config set gates.enabled true
|
|
16
|
+
* harness-dev config set maxRetries 5
|
|
17
|
+
*/
|
|
18
|
+
import { resolve } from 'node:path';
|
|
19
|
+
import { die, CliError, EXIT } from '../lib/errors.mjs';
|
|
20
|
+
import { get as stateGet, set as stateSet, loadConfig } from '../lib/state.mjs';
|
|
21
|
+
import { CONFIG_PARAMS, getGroups, getParamsByGroup, getParamMeta } from '../lib/config-registry.mjs';
|
|
22
|
+
|
|
23
|
+
export default async function configCommand(args) {
|
|
24
|
+
const json = !!(args.json || args.flags?.json);
|
|
25
|
+
const sub = args.subcommand; // 'get', 'set', or 'list'
|
|
26
|
+
const pos = args.positionals; // key[, value] for set
|
|
27
|
+
const rawTarget = args.flags?.target;
|
|
28
|
+
const targetDir = (typeof rawTarget === 'string') ? resolve(rawTarget) : process.cwd();
|
|
29
|
+
|
|
30
|
+
if (!sub || (sub !== 'get' && sub !== 'set' && sub !== 'list')) {
|
|
31
|
+
die(new CliError(
|
|
32
|
+
'Usage: harness-dev config list | config get [key] | config set <key> <value>',
|
|
33
|
+
EXIT.USAGE_ERROR,
|
|
34
|
+
), json);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── list ─────────────────────────────────────────────────────────────────
|
|
39
|
+
if (sub === 'list') {
|
|
40
|
+
const { config, ok } = loadConfig(targetDir);
|
|
41
|
+
|
|
42
|
+
// Helper to resolve current value from config via dot-notation
|
|
43
|
+
function resolveValue(key) {
|
|
44
|
+
if (!ok) { return null; }
|
|
45
|
+
const parts = key.split('.');
|
|
46
|
+
let val = config;
|
|
47
|
+
for (const p of parts) {
|
|
48
|
+
val = val?.[p];
|
|
49
|
+
}
|
|
50
|
+
return val ?? null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (json) {
|
|
54
|
+
const params = CONFIG_PARAMS.map(p => ({
|
|
55
|
+
key: p.key,
|
|
56
|
+
group: p.group,
|
|
57
|
+
label: p.label,
|
|
58
|
+
type: p.type,
|
|
59
|
+
description: p.description,
|
|
60
|
+
default: p.default,
|
|
61
|
+
options: p.options || null,
|
|
62
|
+
editable: p.editable,
|
|
63
|
+
value: resolveValue(p.key),
|
|
64
|
+
}));
|
|
65
|
+
process.stdout.write(JSON.stringify({
|
|
66
|
+
command: 'config',
|
|
67
|
+
subcommand: 'list',
|
|
68
|
+
status: 'ok',
|
|
69
|
+
message: `${params.length} configuration parameters`,
|
|
70
|
+
params,
|
|
71
|
+
}, null, 2) + '\n');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Human output — grouped table
|
|
76
|
+
process.stdout.write('═══ Harness Configuration ═══\n\n');
|
|
77
|
+
if (!ok) {
|
|
78
|
+
process.stdout.write(' No harness-config.json found. Run: harness-dev init\n\n');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const group of getGroups()) {
|
|
83
|
+
const params = getParamsByGroup(group);
|
|
84
|
+
const readOnly = params[0]?.editable === false;
|
|
85
|
+
process.stdout.write(`── ${group}${readOnly ? ' (read-only)' : ''} ──\n`);
|
|
86
|
+
|
|
87
|
+
// Calculate column widths
|
|
88
|
+
const maxKey = Math.max(...params.map(p => p.key.length), 10);
|
|
89
|
+
const maxVal = Math.max(...params.map(p => {
|
|
90
|
+
const v = resolveValue(p.key);
|
|
91
|
+
return JSON.stringify(v)?.length ?? 4;
|
|
92
|
+
}), 7);
|
|
93
|
+
|
|
94
|
+
for (const p of params) {
|
|
95
|
+
const val = resolveValue(p.key);
|
|
96
|
+
const valStr = JSON.stringify(val) ?? 'null';
|
|
97
|
+
const opts = p.options ? `[${p.options.filter(o => o !== null).slice(0, 4).join('|')}${p.options.length > 4 ? '|...' : ''}]` : `[${p.type}]`;
|
|
98
|
+
const padKey = p.key.padEnd(maxKey);
|
|
99
|
+
const padVal = valStr.padEnd(Math.min(maxVal, 20));
|
|
100
|
+
process.stdout.write(` ${padKey} ${padVal} ${opts.padEnd(16)} ${p.description.slice(0, 60)}\n`);
|
|
101
|
+
}
|
|
102
|
+
process.stdout.write('\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
process.stdout.write('Edit with: harness-dev config set <key> <value>\n');
|
|
106
|
+
process.stdout.write('Full docs: docs/CONFIGURATION.md\n');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── get ──────────────────────────────────────────────────────────────────
|
|
111
|
+
if (sub === 'get') {
|
|
112
|
+
const key = pos[0] || null;
|
|
113
|
+
const { value, ok, error } = stateGet(targetDir, key);
|
|
114
|
+
|
|
115
|
+
if (json) {
|
|
116
|
+
process.stdout.write(JSON.stringify({
|
|
117
|
+
command: 'config',
|
|
118
|
+
subcommand: 'get',
|
|
119
|
+
key,
|
|
120
|
+
value: ok ? value : null,
|
|
121
|
+
status: ok ? 'ok' : 'error',
|
|
122
|
+
message: ok ? null : (error || 'Unknown error'),
|
|
123
|
+
}) + '\n');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Human output
|
|
128
|
+
if (!ok) {
|
|
129
|
+
process.stdout.write(`Config not available: ${error}\n`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (key === null) {
|
|
133
|
+
process.stdout.write(JSON.stringify(value, null, 2) + '\n');
|
|
134
|
+
} else {
|
|
135
|
+
process.stdout.write(`${key} = ${JSON.stringify(value)}\n`);
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── set ──────────────────────────────────────────────────────────────────
|
|
141
|
+
if (sub === 'set') {
|
|
142
|
+
if (pos.length < 2) {
|
|
143
|
+
die(new CliError(
|
|
144
|
+
'Usage: harness-dev config set <key> <value>\n' +
|
|
145
|
+
' String values: config set mode copilot\n' +
|
|
146
|
+
' Boolean values: config set gates.enabled true\n' +
|
|
147
|
+
' Numeric values: config set maxRetries 5',
|
|
148
|
+
EXIT.USAGE_ERROR,
|
|
149
|
+
), json);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const key = pos[0];
|
|
154
|
+
const rawValue = pos.slice(1).join(' '); // support multi-word values
|
|
155
|
+
|
|
156
|
+
// Get parameter metadata for type-aware coercion
|
|
157
|
+
const meta = getParamMeta(key);
|
|
158
|
+
|
|
159
|
+
// Type coercion: try JSON for arrays/objects, then numbers and booleans
|
|
160
|
+
let parsedValue;
|
|
161
|
+
if (meta && (meta.type === 'array' || meta.type === 'object')) {
|
|
162
|
+
// Array/object types: parse as JSON
|
|
163
|
+
try {
|
|
164
|
+
parsedValue = JSON.parse(rawValue);
|
|
165
|
+
} catch {
|
|
166
|
+
// If JSON parse fails, keep as string (let type check below catch it)
|
|
167
|
+
parsedValue = rawValue;
|
|
168
|
+
}
|
|
169
|
+
} else if (rawValue === 'true') {
|
|
170
|
+
parsedValue = true;
|
|
171
|
+
} else if (rawValue === 'false') {
|
|
172
|
+
parsedValue = false;
|
|
173
|
+
} else if (/^-?\d+(\.\d+)?$/.test(rawValue)) {
|
|
174
|
+
parsedValue = Number(rawValue);
|
|
175
|
+
} else if (rawValue === 'null') {
|
|
176
|
+
parsedValue = null;
|
|
177
|
+
} else {
|
|
178
|
+
parsedValue = rawValue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Validate against config-registry if parameter is known
|
|
182
|
+
if (meta) {
|
|
183
|
+
// Check editability
|
|
184
|
+
if (!meta.editable) {
|
|
185
|
+
const msg = `"${key}" is read-only (managed by harness). Cannot set manually.`;
|
|
186
|
+
if (json) {
|
|
187
|
+
process.stdout.write(JSON.stringify({ command: 'config', subcommand: 'set', key, status: 'error', message: msg }) + '\n');
|
|
188
|
+
} else {
|
|
189
|
+
process.stderr.write(`✗ ${msg}\n`);
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// Check enum options
|
|
194
|
+
if (meta.options && !meta.options.includes(parsedValue)) {
|
|
195
|
+
const opts = meta.options.map(o => o === null ? 'null' : `"${o}"`).join(', ');
|
|
196
|
+
const msg = `Invalid value for "${key}". Allowed: ${opts}`;
|
|
197
|
+
if (json) {
|
|
198
|
+
process.stdout.write(JSON.stringify({ command: 'config', subcommand: 'set', key, value: parsedValue, status: 'error', message: msg }) + '\n');
|
|
199
|
+
} else {
|
|
200
|
+
process.stderr.write(`✗ ${msg}\n`);
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Check type for integers
|
|
205
|
+
if (meta.type === 'integer' && typeof parsedValue !== 'number') {
|
|
206
|
+
const msg = `"${key}" expects an integer, got ${typeof parsedValue}`;
|
|
207
|
+
if (json) {
|
|
208
|
+
process.stdout.write(JSON.stringify({ command: 'config', subcommand: 'set', key, value: parsedValue, status: 'error', message: msg }) + '\n');
|
|
209
|
+
} else {
|
|
210
|
+
process.stderr.write(`✗ ${msg}\n`);
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Check type for booleans
|
|
215
|
+
if (meta.type === 'boolean' && typeof parsedValue !== 'boolean') {
|
|
216
|
+
const msg = `"${key}" expects a boolean (true/false), got ${typeof parsedValue}`;
|
|
217
|
+
if (json) {
|
|
218
|
+
process.stdout.write(JSON.stringify({ command: 'config', subcommand: 'set', key, value: parsedValue, status: 'error', message: msg }) + '\n');
|
|
219
|
+
} else {
|
|
220
|
+
process.stderr.write(`✗ ${msg}\n`);
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
// Check type for arrays
|
|
225
|
+
if (meta.type === 'array' && !Array.isArray(parsedValue)) {
|
|
226
|
+
const msg = `"${key}" expects a JSON array, got ${typeof parsedValue}`;
|
|
227
|
+
if (json) {
|
|
228
|
+
process.stdout.write(JSON.stringify({ command: 'config', subcommand: 'set', key, value: parsedValue, status: 'error', message: msg }) + '\n');
|
|
229
|
+
} else {
|
|
230
|
+
process.stderr.write(`✗ ${msg}\n`);
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
// Check type for objects
|
|
235
|
+
if (meta.type === 'object' && (typeof parsedValue !== 'object' || Array.isArray(parsedValue) || parsedValue === null)) {
|
|
236
|
+
const msg = `"${key}" expects a JSON object, got ${Array.isArray(parsedValue) ? 'array' : typeof parsedValue}`;
|
|
237
|
+
if (json) {
|
|
238
|
+
process.stdout.write(JSON.stringify({ command: 'config', subcommand: 'set', key, value: parsedValue, status: 'error', message: msg }) + '\n');
|
|
239
|
+
} else {
|
|
240
|
+
process.stderr.write(`✗ ${msg}\n`);
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const result = stateSet(targetDir, key, parsedValue);
|
|
247
|
+
|
|
248
|
+
if (json) {
|
|
249
|
+
process.stdout.write(JSON.stringify({
|
|
250
|
+
command: 'config',
|
|
251
|
+
subcommand: 'set',
|
|
252
|
+
key,
|
|
253
|
+
value: parsedValue,
|
|
254
|
+
status: result.ok ? 'ok' : 'error',
|
|
255
|
+
message: result.ok
|
|
256
|
+
? `Set ${key} = ${JSON.stringify(parsedValue)}`
|
|
257
|
+
: (result.error || 'Unknown error'),
|
|
258
|
+
}) + '\n');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (result.ok) {
|
|
263
|
+
process.stdout.write(`✓ ${key} = ${JSON.stringify(parsedValue)}\n`);
|
|
264
|
+
} else {
|
|
265
|
+
process.stderr.write(`✗ ${result.error}\n`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|