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,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-registry — Central registry of all supported agentic coding tools.
|
|
3
|
+
*
|
|
4
|
+
* Maps each tool to:
|
|
5
|
+
* - file: the tool-specific file to generate (or null if tool reads AGENTS.md natively)
|
|
6
|
+
* - header: optional prefix prepended to AGENTS.md content when generating tool file
|
|
7
|
+
* - detectionFiles: files whose presence indicates this tool is in use
|
|
8
|
+
* - label: human-readable name
|
|
9
|
+
* - notes: short description
|
|
10
|
+
* - special: true for tools with full adapter directories (e.g. Hermes)
|
|
11
|
+
*
|
|
12
|
+
* AGENTS.md is the canonical source. Tool-specific files (CLAUDE.md, .cursorrules,
|
|
13
|
+
* etc.) are generated by copying AGENTS.md content + optional header — no separate
|
|
14
|
+
* templates needed. This keeps content DRY: one source, many filenames.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* import { TOOL_REGISTRY, KNOWN_TOOLS, getToolFile, getToolDetectionFiles } from './tool-registry.mjs';
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} ToolEntry
|
|
22
|
+
* @property {string} label - Human-readable tool name
|
|
23
|
+
* @property {string|null} file - Tool-specific filename to generate (null = AGENTS.md only)
|
|
24
|
+
* @property {string|null} header - Optional header prepended to AGENTS.md content
|
|
25
|
+
* @property {string[]} detectionFiles - Files whose presence indicates this tool
|
|
26
|
+
* @property {string} notes - Short description
|
|
27
|
+
* @property {boolean} [special] - True if tool has a full adapter directory
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** @type {Record<string, ToolEntry>} */
|
|
31
|
+
export const TOOL_REGISTRY = {
|
|
32
|
+
// ── AGENTS.md-native tools (no extra file needed) ────────────────────────
|
|
33
|
+
'generic': {
|
|
34
|
+
label: 'Generic',
|
|
35
|
+
file: null,
|
|
36
|
+
header: null,
|
|
37
|
+
detectionFiles: [],
|
|
38
|
+
notes: 'Default — AGENTS.md only. Works with any tool that reads AGENTS.md.',
|
|
39
|
+
},
|
|
40
|
+
'codex': {
|
|
41
|
+
label: 'Codex CLI',
|
|
42
|
+
file: null,
|
|
43
|
+
header: null,
|
|
44
|
+
detectionFiles: [],
|
|
45
|
+
notes: 'OpenAI Codex CLI reads AGENTS.md from project root natively.',
|
|
46
|
+
},
|
|
47
|
+
'opencode': {
|
|
48
|
+
label: 'OpenCode',
|
|
49
|
+
file: null,
|
|
50
|
+
header: null,
|
|
51
|
+
detectionFiles: [],
|
|
52
|
+
notes: 'OpenCode reads AGENTS.md natively.',
|
|
53
|
+
},
|
|
54
|
+
'continue': {
|
|
55
|
+
label: 'Continue',
|
|
56
|
+
file: null,
|
|
57
|
+
header: null,
|
|
58
|
+
detectionFiles: ['.continue/config.json', 'continue.json'],
|
|
59
|
+
notes: 'Continue reads AGENTS.md from project root.',
|
|
60
|
+
},
|
|
61
|
+
'aider': {
|
|
62
|
+
label: 'Aider',
|
|
63
|
+
file: null,
|
|
64
|
+
header: null,
|
|
65
|
+
detectionFiles: ['.aider.conf.yml', '.aider.conf.yaml', '.aider.conf.json'],
|
|
66
|
+
notes: 'Aider auto-discovers AGENTS.md (or via --read CONVENTIONS.md).',
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// ── Tools with a specific rules file (generated from AGENTS.md content) ──
|
|
70
|
+
'claude-code': {
|
|
71
|
+
label: 'Claude Code',
|
|
72
|
+
file: 'CLAUDE.md',
|
|
73
|
+
header: '> Read by Claude Code automatically. Mirrors AGENTS.md.\n',
|
|
74
|
+
detectionFiles: ['CLAUDE.md'],
|
|
75
|
+
notes: 'Claude Code reads CLAUDE.md on startup (falls back to AGENTS.md).',
|
|
76
|
+
},
|
|
77
|
+
'cursor': {
|
|
78
|
+
label: 'Cursor',
|
|
79
|
+
file: '.cursorrules',
|
|
80
|
+
header: '# Cursor rules — mirrors AGENTS.md\n',
|
|
81
|
+
detectionFiles: ['.cursorrules', '.cursor/rules'],
|
|
82
|
+
notes: 'Cursor reads .cursorrules as system context.',
|
|
83
|
+
},
|
|
84
|
+
'windsurf': {
|
|
85
|
+
label: 'Windsurf',
|
|
86
|
+
file: '.windsurfrules',
|
|
87
|
+
header: '# Windsurf rules — mirrors AGENTS.md\n',
|
|
88
|
+
detectionFiles: ['.windsurfrules'],
|
|
89
|
+
notes: 'Windsurf (Codeium) reads .windsurfrules.',
|
|
90
|
+
},
|
|
91
|
+
'gemini': {
|
|
92
|
+
label: 'Gemini CLI',
|
|
93
|
+
file: 'GEMINI.md',
|
|
94
|
+
header: '> Read by Gemini CLI. Mirrors AGENTS.md.\n',
|
|
95
|
+
detectionFiles: ['GEMINI.md'],
|
|
96
|
+
notes: 'Google Gemini CLI reads GEMINI.md.',
|
|
97
|
+
},
|
|
98
|
+
'copilot': {
|
|
99
|
+
label: 'GitHub Copilot',
|
|
100
|
+
file: '.github/copilot-instructions.md',
|
|
101
|
+
header: '# GitHub Copilot instructions — mirrors AGENTS.md\n',
|
|
102
|
+
detectionFiles: ['.github/copilot-instructions.md'],
|
|
103
|
+
notes: 'GitHub Copilot reads .github/copilot-instructions.md.',
|
|
104
|
+
},
|
|
105
|
+
'cline': {
|
|
106
|
+
label: 'Cline',
|
|
107
|
+
file: '.clinerules',
|
|
108
|
+
header: '# Cline rules — mirrors AGENTS.md\n',
|
|
109
|
+
detectionFiles: ['.clinerules'],
|
|
110
|
+
notes: 'Cline (VS Code extension) reads .clinerules.',
|
|
111
|
+
},
|
|
112
|
+
'roo': {
|
|
113
|
+
label: 'Roo Code',
|
|
114
|
+
file: '.roorules',
|
|
115
|
+
header: '# Roo Code rules — mirrors AGENTS.md\n',
|
|
116
|
+
detectionFiles: ['.roorules'],
|
|
117
|
+
notes: 'Roo Code reads .roorules.',
|
|
118
|
+
},
|
|
119
|
+
'kilo-code': {
|
|
120
|
+
label: 'Kilo Code',
|
|
121
|
+
file: '.kilocoderules',
|
|
122
|
+
header: '# Kilo Code rules — mirrors AGENTS.md\n',
|
|
123
|
+
detectionFiles: ['.kilocoderules'],
|
|
124
|
+
notes: 'Kilo Code reads .kilocoderules.',
|
|
125
|
+
},
|
|
126
|
+
'amazon-q': {
|
|
127
|
+
label: 'Amazon Q Developer',
|
|
128
|
+
file: '.amazonq/rules.md',
|
|
129
|
+
header: '# Amazon Q Developer rules — mirrors AGENTS.md\n',
|
|
130
|
+
detectionFiles: ['.amazonq/rules.md', '.amazonq/rules'],
|
|
131
|
+
notes: 'Amazon Q Developer reads .amazonq/rules.',
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// ── Tools assumed to read AGENTS.md (format not yet confirmed) ───────────
|
|
135
|
+
'antigravity': {
|
|
136
|
+
label: 'Antigravity 2',
|
|
137
|
+
file: null,
|
|
138
|
+
header: null,
|
|
139
|
+
detectionFiles: ['.antigravity'],
|
|
140
|
+
notes: 'Antigravity 2 (IDE/CLI/SDK) — assumed to read AGENTS.md. Adjust if needed.',
|
|
141
|
+
},
|
|
142
|
+
'openclaw': {
|
|
143
|
+
label: 'OpenClaw',
|
|
144
|
+
file: null,
|
|
145
|
+
header: null,
|
|
146
|
+
detectionFiles: ['.openclaw'],
|
|
147
|
+
notes: 'OpenClaw — assumed to read AGENTS.md. Adjust if needed.',
|
|
148
|
+
},
|
|
149
|
+
'pi': {
|
|
150
|
+
label: 'Pi',
|
|
151
|
+
file: null,
|
|
152
|
+
header: null,
|
|
153
|
+
detectionFiles: ['.pi'],
|
|
154
|
+
notes: 'Pi — assumed to read AGENTS.md. Adjust if needed.',
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// ── Special: full adapter directory ──────────────────────────────────────
|
|
158
|
+
'hermes': {
|
|
159
|
+
label: 'Hermes',
|
|
160
|
+
file: null,
|
|
161
|
+
header: null,
|
|
162
|
+
detectionFiles: ['adapters/hermes/SKILL.md', 'hermes/skill/dev-harness/SKILL.md'],
|
|
163
|
+
special: true,
|
|
164
|
+
notes: 'Hermes uses SKILL.md manifest with wrapper scripts in adapters/hermes/.',
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/** All known tool names (keys of TOOL_REGISTRY). */
|
|
169
|
+
export const KNOWN_TOOLS = Object.keys(TOOL_REGISTRY);
|
|
170
|
+
|
|
171
|
+
/** Tools that read AGENTS.md natively (no tool-specific file). */
|
|
172
|
+
export const AGENTS_MD_TOOLS = Object.entries(TOOL_REGISTRY)
|
|
173
|
+
.filter(([, t]) => t.file === null && !t.special)
|
|
174
|
+
.map(([name]) => name);
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get the tool-specific file info for a tool, or null.
|
|
178
|
+
* @param {string} toolName
|
|
179
|
+
* @returns {ToolEntry|null}
|
|
180
|
+
*/
|
|
181
|
+
export function getToolEntry(toolName) {
|
|
182
|
+
return TOOL_REGISTRY[toolName] || null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get the filename to generate for a tool, or null (AGENTS.md only).
|
|
187
|
+
* @param {string} toolName
|
|
188
|
+
* @returns {string|null}
|
|
189
|
+
*/
|
|
190
|
+
export function getToolFile(toolName) {
|
|
191
|
+
const entry = TOOL_REGISTRY[toolName];
|
|
192
|
+
return entry ? entry.file : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get detection files for a tool.
|
|
197
|
+
* @param {string} toolName
|
|
198
|
+
* @returns {string[]}
|
|
199
|
+
*/
|
|
200
|
+
export function getToolDetectionFiles(toolName) {
|
|
201
|
+
const entry = TOOL_REGISTRY[toolName];
|
|
202
|
+
return entry ? entry.detectionFiles : [];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get all detection signatures across all tools (for detect-tool scan).
|
|
207
|
+
* Returns array of { tool, file } pairs.
|
|
208
|
+
* @returns {Array<{tool: string, file: string}>}
|
|
209
|
+
*/
|
|
210
|
+
export function getAllDetectionSignatures() {
|
|
211
|
+
const sigs = [];
|
|
212
|
+
for (const [tool, entry] of Object.entries(TOOL_REGISTRY)) {
|
|
213
|
+
for (const file of entry.detectionFiles) {
|
|
214
|
+
sigs.push({ tool, file });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return sigs;
|
|
218
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validate-schema — Lightweight JSON-schema validator (no external deps).
|
|
3
|
+
*
|
|
4
|
+
* Supports the subset of JSON Schema draft-07 used by this project's schemas:
|
|
5
|
+
* - type (string, number, integer, boolean, array, object, null)
|
|
6
|
+
* - type as union array (e.g. ["string", "null"])
|
|
7
|
+
* - required (array of property names)
|
|
8
|
+
* - enum (array of allowed values)
|
|
9
|
+
* - properties (nested object schema)
|
|
10
|
+
* - items (schema for array elements)
|
|
11
|
+
* - minimum (number)
|
|
12
|
+
*
|
|
13
|
+
* Intentionally minimal — not a general-purpose validator. Schemas live in
|
|
14
|
+
* /schema at the project root and are loaded by absolute path.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* import { validateAgainstSchema } from './validate-schema.mjs';
|
|
18
|
+
* const result = validateAgainstSchema(obj, '/path/to/schema.json');
|
|
19
|
+
* if (!result.ok) console.error(result.errors);
|
|
20
|
+
*/
|
|
21
|
+
import { readFileSync } from 'node:fs';
|
|
22
|
+
|
|
23
|
+
// Cache loaded schemas by path to avoid re-reading on every load.
|
|
24
|
+
const schemaCache = new Map();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Load a JSON schema from disk (cached).
|
|
28
|
+
* @param {string} schemaPath — absolute path to the .schema.json file
|
|
29
|
+
* @returns {object|null}
|
|
30
|
+
*/
|
|
31
|
+
function loadSchema(schemaPath) {
|
|
32
|
+
if (schemaCache.has(schemaPath)) {
|
|
33
|
+
return schemaCache.get(schemaPath);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const raw = readFileSync(schemaPath, 'utf-8');
|
|
37
|
+
const schema = JSON.parse(raw);
|
|
38
|
+
schemaCache.set(schemaPath, schema);
|
|
39
|
+
return schema;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check a value against a single type string.
|
|
47
|
+
* @param {*} value
|
|
48
|
+
* @param {string} type
|
|
49
|
+
* @returns {boolean}
|
|
50
|
+
*/
|
|
51
|
+
function matchesType(value, type) {
|
|
52
|
+
switch (type) {
|
|
53
|
+
case 'string': return typeof value === 'string';
|
|
54
|
+
case 'number': return typeof value === 'number' && !Number.isNaN(value);
|
|
55
|
+
case 'integer': return typeof value === 'number' && Number.isInteger(value);
|
|
56
|
+
case 'boolean': return typeof value === 'boolean';
|
|
57
|
+
case 'array': return Array.isArray(value);
|
|
58
|
+
case 'object': return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
59
|
+
case 'null': return value === null;
|
|
60
|
+
default: return true; // unknown types pass (forward-compat)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Validate a value against a schema node, collecting errors.
|
|
66
|
+
* @param {*} value
|
|
67
|
+
* @param {object} schema
|
|
68
|
+
* @param {string} path — dotted path for error messages (e.g. "root.gates.enabled")
|
|
69
|
+
* @param {string[]} errors — accumulator
|
|
70
|
+
*/
|
|
71
|
+
function validateNode(value, schema, path, errors) {
|
|
72
|
+
// type
|
|
73
|
+
if (schema.type) {
|
|
74
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
75
|
+
const ok = types.some((t) => matchesType(value, t));
|
|
76
|
+
if (!ok) {
|
|
77
|
+
errors.push(`${path}: expected type ${types.join('|')}, got ${Array.isArray(value) ? 'array' : typeof value}`);
|
|
78
|
+
return; // no point checking further on a type mismatch
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// enum
|
|
83
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
84
|
+
errors.push(`${path}: value "${value}" not in enum [${schema.enum.join(', ')}]`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// minimum
|
|
88
|
+
if (schema.minimum !== undefined && typeof value === 'number' && value < schema.minimum) {
|
|
89
|
+
errors.push(`${path}: value ${value} below minimum ${schema.minimum}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// object properties + required
|
|
93
|
+
if (matchesType(value, 'object') && schema.properties) {
|
|
94
|
+
if (schema.required) {
|
|
95
|
+
for (const req of schema.required) {
|
|
96
|
+
if (!(req in value)) {
|
|
97
|
+
errors.push(`${path}: missing required property "${req}"`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const [key, subSchema] of Object.entries(schema.properties)) {
|
|
102
|
+
if (key in value) {
|
|
103
|
+
validateNode(value[key], subSchema, `${path}.${key}`, errors);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// array items
|
|
109
|
+
if (matchesType(value, 'array') && schema.items) {
|
|
110
|
+
for (let i = 0; i < value.length; i++) {
|
|
111
|
+
validateNode(value[i], schema.items, `${path}[${i}]`, errors);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Validate an object against a JSON schema loaded from disk.
|
|
118
|
+
* @param {object} obj — the value to validate
|
|
119
|
+
* @param {string} schemaPath — absolute path to the schema file
|
|
120
|
+
* @returns {{ ok: boolean, errors: string[] }}
|
|
121
|
+
*/
|
|
122
|
+
export function validateAgainstSchema(obj, schemaPath) {
|
|
123
|
+
const schema = loadSchema(schemaPath);
|
|
124
|
+
if (!schema) {
|
|
125
|
+
// Schema missing/unreadable — fail open (don't block on missing schema).
|
|
126
|
+
return { ok: true, errors: [] };
|
|
127
|
+
}
|
|
128
|
+
const errors = [];
|
|
129
|
+
validateNode(obj, schema, 'config', errors);
|
|
130
|
+
return { ok: errors.length === 0, errors };
|
|
131
|
+
}
|
package/cli/lib/vars.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vars — Stack-aware variable loader.
|
|
3
|
+
*
|
|
4
|
+
* Loads stack metadata from stacks.json and returns a flat variable map
|
|
5
|
+
* suitable for {{VAR}} substitution in templates.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { getStackVars } from './vars.mjs';
|
|
9
|
+
* const vars = getStackVars('python');
|
|
10
|
+
* // → { stack: 'python', testCmd: 'python3 -m pytest', ... }
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { STACKS_SCHEMA_PATH } from './paths.mjs';
|
|
15
|
+
import { loadConfig } from './state.mjs';
|
|
16
|
+
|
|
17
|
+
const STACKS_PATH = STACKS_SCHEMA_PATH;
|
|
18
|
+
|
|
19
|
+
/** @type {Record<string, object>|null} */
|
|
20
|
+
let _stacksCache = null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load and cache the stacks schema.
|
|
24
|
+
* @returns {Record<string, object>}
|
|
25
|
+
*/
|
|
26
|
+
function loadStacks() {
|
|
27
|
+
if (_stacksCache) {return _stacksCache;}
|
|
28
|
+
const raw = readFileSync(STACKS_PATH, 'utf-8');
|
|
29
|
+
_stacksCache = JSON.parse(raw);
|
|
30
|
+
return _stacksCache;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fields in stacks.json that carry through as template variables.
|
|
35
|
+
* Ordered for deterministic output.
|
|
36
|
+
*/
|
|
37
|
+
const VAR_FIELDS = [
|
|
38
|
+
'label',
|
|
39
|
+
'testCmd',
|
|
40
|
+
'lintCmd',
|
|
41
|
+
'typeCheckCmd',
|
|
42
|
+
'buildCmd',
|
|
43
|
+
'installCmd',
|
|
44
|
+
'versionFile',
|
|
45
|
+
'configFile',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get template variables for a given stack.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} stackName — stack name (built-in or custom)
|
|
52
|
+
* @param {object} [overrides] — optional extra variables to merge in (e.g. agent-provided settings)
|
|
53
|
+
* @param {string} [targetDir] — optional project dir (enables config.stackMeta override)
|
|
54
|
+
* @returns {Record<string, string>}
|
|
55
|
+
*/
|
|
56
|
+
export function getStackVars(stackName, overrides = {}, targetDir) {
|
|
57
|
+
const meta = getEffectiveStackMeta(stackName, targetDir);
|
|
58
|
+
|
|
59
|
+
const vars = {
|
|
60
|
+
stack: stackName,
|
|
61
|
+
stackLabel: meta.label || stackName,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for (const field of VAR_FIELDS) {
|
|
65
|
+
vars[field] = (meta[field] || '').toString();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Merge overrides (overwrites any computed vars)
|
|
69
|
+
Object.assign(vars, overrides);
|
|
70
|
+
|
|
71
|
+
return vars;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get effective stack metadata: config.stackMeta overrides built-in stacks.json.
|
|
76
|
+
* Priority: config.stackMeta > built-in stacks.json[stackName] > generic fallback.
|
|
77
|
+
* @param {string} stackName
|
|
78
|
+
* @param {string} [targetDir] — project dir to read config from
|
|
79
|
+
* @returns {object}
|
|
80
|
+
*/
|
|
81
|
+
export function getEffectiveStackMeta(stackName, targetDir) {
|
|
82
|
+
const stacks = loadStacks();
|
|
83
|
+
const builtIn = stacks[stackName] || stacks.generic || {};
|
|
84
|
+
|
|
85
|
+
// If targetDir given, check config.stackMeta for user/agent overrides
|
|
86
|
+
if (targetDir) {
|
|
87
|
+
try {
|
|
88
|
+
const { config, ok } = loadConfig(targetDir);
|
|
89
|
+
if (ok && config.stackMeta && typeof config.stackMeta === 'object') {
|
|
90
|
+
return { ...builtIn, ...config.stackMeta };
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// config unreadable — use built-in
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return builtIn;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* List all available stack names.
|
|
102
|
+
* @returns {string[]}
|
|
103
|
+
*/
|
|
104
|
+
export function listStacks() {
|
|
105
|
+
const stacks = loadStacks();
|
|
106
|
+
return Object.keys(stacks).sort();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Reset internal cache (for testing).
|
|
111
|
+
*/
|
|
112
|
+
export function _resetCache() {
|
|
113
|
+
_stacksCache = null;
|
|
114
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dev-harness-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Agent-agnostic software development harness CLI — scaffold, phase orchestration, gate validation for any coding agent",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"harness-dev": "./cli/harness-dev.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"cli/",
|
|
11
|
+
"templates/",
|
|
12
|
+
"schema/",
|
|
13
|
+
"adapters/",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "Bakr Bagaber",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/bakr-bagaber/dev-harness"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/bakr-bagaber/dev-harness#readme",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/bakr-bagaber/dev-harness/issues"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"harness",
|
|
32
|
+
"developer-tools",
|
|
33
|
+
"ai-agents",
|
|
34
|
+
"phase-pipeline",
|
|
35
|
+
"gate-validation",
|
|
36
|
+
"scaffold",
|
|
37
|
+
"cli"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"check": "node --check cli/harness-dev.mjs && echo 'Syntax OK'",
|
|
41
|
+
"postinstall": "node -e \"try{process.stdout.write('harness-dev installed. Run: npx harness-dev --help\\n')}catch(e){}\""
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@eslint/js": "^10.0.1",
|
|
45
|
+
"eslint": "^10.5.0"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Harness Config",
|
|
4
|
+
"description": "Schema for harness-config.json — project state, phase tracking, and agent configuration",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["version", "mode", "currentPhase", "gates", "git", "phases", "maxRetries"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"version": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "Schema version (not CLI version)",
|
|
11
|
+
"default": "1.0"
|
|
12
|
+
},
|
|
13
|
+
"stack": {
|
|
14
|
+
"type": ["string", "null"],
|
|
15
|
+
"description": "Detected or explicitly set project stack"
|
|
16
|
+
},
|
|
17
|
+
"stackMeta": {
|
|
18
|
+
"type": ["object", "null"],
|
|
19
|
+
"description": "User/agent-supplied stack metadata (overrides built-in stacks.json). Filled during DEFINE phase for custom/unknown stacks.",
|
|
20
|
+
"properties": {
|
|
21
|
+
"label": { "type": "string" },
|
|
22
|
+
"testCmd": { "type": "string" },
|
|
23
|
+
"lintCmd": { "type": "string" },
|
|
24
|
+
"typeCheckCmd": { "type": "string" },
|
|
25
|
+
"buildCmd": { "type": "string" },
|
|
26
|
+
"installCmd": { "type": "string" },
|
|
27
|
+
"coverageCmd": { "type": "string" },
|
|
28
|
+
"versionFile": { "type": "string" },
|
|
29
|
+
"configFile": { "type": "string" },
|
|
30
|
+
"extensions": { "type": "array", "items": { "type": "string" } },
|
|
31
|
+
"detectFiles": { "type": "array", "items": { "type": "string" } }
|
|
32
|
+
},
|
|
33
|
+
"additionalProperties": false
|
|
34
|
+
},
|
|
35
|
+
"agentTool": {
|
|
36
|
+
"type": ["string", "null"],
|
|
37
|
+
"enum": [null, "generic", "claude-code", "codex", "cursor", "windsurf", "gemini", "copilot", "cline", "roo", "kilo-code", "aider", "continue", "opencode", "amazon-q", "antigravity", "openclaw", "pi", "hermes"],
|
|
38
|
+
"default": null,
|
|
39
|
+
"description": "Which agentic coding tool the project uses (null = unspecified, tools auto-detect via AGENTS.md)"
|
|
40
|
+
},
|
|
41
|
+
"mode": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"enum": ["copilot", "autopilot"],
|
|
44
|
+
"default": "copilot",
|
|
45
|
+
"description": "Execution mode"
|
|
46
|
+
},
|
|
47
|
+
"currentPhase": {
|
|
48
|
+
"type": ["string", "null"],
|
|
49
|
+
"enum": [null, "init", "define", "plan", "build", "verify", "simplify", "review", "ship"],
|
|
50
|
+
"default": null,
|
|
51
|
+
"description": "Current pipeline phase"
|
|
52
|
+
},
|
|
53
|
+
"paused": {
|
|
54
|
+
"type": "boolean",
|
|
55
|
+
"default": false,
|
|
56
|
+
"description": "Autopilot paused state"
|
|
57
|
+
},
|
|
58
|
+
"features": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"properties": {
|
|
61
|
+
"remaining": { "type": "integer", "minimum": 0, "default": 0 },
|
|
62
|
+
"passing": { "type": "integer", "minimum": 0, "default": 0 },
|
|
63
|
+
"total": { "type": "integer", "minimum": 0, "default": 0 }
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"gates": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"properties": {
|
|
69
|
+
"enabled": { "type": "boolean", "default": false },
|
|
70
|
+
"checks": { "type": "array", "items": { "type": "string" }, "default": ["all"] }
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"git": {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"properties": {
|
|
76
|
+
"autoCommit": { "type": "boolean", "default": false },
|
|
77
|
+
"autoTag": { "type": "boolean", "default": false },
|
|
78
|
+
"resetOnRetry": { "type": "boolean", "default": false },
|
|
79
|
+
"branch": { "type": ["string", "null"], "default": null },
|
|
80
|
+
"clean": { "type": "boolean", "default": true },
|
|
81
|
+
"hasUpstream": { "type": "boolean", "default": false },
|
|
82
|
+
"lastCommitMessage": { "type": ["string", "null"], "default": null }
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"phases": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"properties": {
|
|
88
|
+
"enabled": {
|
|
89
|
+
"type": "array",
|
|
90
|
+
"items": { "type": "string" },
|
|
91
|
+
"default": ["define", "plan", "build", "verify", "review", "ship"]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"agents": {
|
|
96
|
+
"type": "object",
|
|
97
|
+
"properties": {
|
|
98
|
+
"tone": {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"properties": {
|
|
101
|
+
"planner": { "type": "string", "default": "Analytical and precise. Define clear boundaries." },
|
|
102
|
+
"generator": { "type": "string", "default": "Focused and practical. Build what's specified, nothing more." },
|
|
103
|
+
"evaluator": { "type": "string", "default": "Skeptical and thorough. Accept only compelling evidence." },
|
|
104
|
+
"simplifier": { "type": "string", "default": "Relentless about clarity. Delete more than you add." }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"maxRetries": {
|
|
110
|
+
"type": "integer",
|
|
111
|
+
"minimum": 1,
|
|
112
|
+
"default": 3
|
|
113
|
+
},
|
|
114
|
+
"gateHistory": {
|
|
115
|
+
"type": "array",
|
|
116
|
+
"items": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"properties": {
|
|
119
|
+
"phase": { "type": "string" },
|
|
120
|
+
"result": { "type": "string", "enum": ["pass", "fail"] },
|
|
121
|
+
"timestamp": { "type": "string" }
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"default": []
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|