@dombaras/agent-harness 0.1.0 → 0.1.1

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 (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -13
  3. package/bin/agent-harness.js +272 -68
  4. package/package.json +7 -2
  5. package/templates/.agents/AGENTS.md +93 -91
  6. package/templates/.agents/memory/domain-map.md +2 -2
  7. package/templates/.agents/memory/history.md +7 -0
  8. package/templates/.agents/memory/model-routing.md +22 -6
  9. package/templates/.agents/memory/stack-versions.md +25 -2
  10. package/templates/.agents/rules/00-operating.md +29 -85
  11. package/templates/.agents/skills/data-engineer/SKILL.md +2 -2
  12. package/templates/.agents/skills/devops-engineer/SKILL.md +1 -1
  13. package/templates/.agents/skills/diagnostics-expert/SKILL.md +8 -9
  14. package/templates/.agents/skills/handoff/SKILL.md +2 -2
  15. package/templates/.agents/skills/mobile-engineer/SKILL.md +2 -2
  16. package/templates/.agents/skills/product-manager/SKILL.md +8 -8
  17. package/templates/.agents/skills/qa-architect/SKILL.md +5 -0
  18. package/templates/.agents/skills/security-engineer/SKILL.md +5 -5
  19. package/templates/.agents/skills/system-architect/SKILL.md +4 -4
  20. package/templates/.opencode/agents/data-engineer.md +1 -0
  21. package/templates/.opencode/agents/devops-engineer.md +1 -0
  22. package/templates/.opencode/agents/diagnostics-expert.md +2 -0
  23. package/templates/.opencode/agents/frontend-engineer.md +1 -0
  24. package/templates/.opencode/agents/handoff.md +5 -0
  25. package/templates/.opencode/agents/mobile-engineer.md +1 -0
  26. package/templates/.opencode/agents/planner.md +6 -0
  27. package/templates/.opencode/agents/product-manager.md +4 -0
  28. package/templates/.opencode/agents/qa-architect.md +4 -0
  29. package/templates/.opencode/agents/qa-runner.md +2 -0
  30. package/templates/.opencode/agents/security-engineer.md +1 -0
  31. package/templates/.opencode/agents/system-architect.md +1 -0
  32. package/templates/.opencode/agents/ui-designer.md +1 -0
  33. package/templates/AGENTS.md +6 -13
  34. package/templates/opencode.json +2 -1
  35. package/templates/scripts/qa/check-qa-scripts.js +75 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 @dombaras/agent-harness contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -24,51 +24,86 @@ npx @dombaras/agent-harness init --target /path/to/project --name "MyApp" --doma
24
24
 
25
25
  # non-interactive
26
26
  npx @dombaras/agent-harness init --target . --yes
27
+
28
+ # preview what would change, without writing anything
29
+ npx @dombaras/agent-harness init --target . --dry-run
27
30
  ```
28
31
 
29
32
  `init` writes:
30
33
 
31
34
  | Path | Content | Ownership |
32
35
  |---|---|---|
33
- | `AGENTS.md` | root dispatcher | harness (overwrite) |
34
- | `.opencode/agents/*.md` | 13 persona subagent defs (with `model:` pins) | harness (overwrite) |
35
- | `.agents/AGENTS.md` | full operating rulebook | harness (overwrite) |
36
- | `.agents/rules/00-operating.md` | always-loaded rules summary | harness (overwrite) |
36
+ | `AGENTS.md` | root dispatcher (points to the rulebook) | harness (overwrite) |
37
+ | `.opencode/agents/*.md` | 13 persona subagent defs (with `model:` pins + `permission`/`steps`/`temperature`/`hidden`) | harness (overwrite) |
38
+ | `.agents/AGENTS.md` | full canonical operating rulebook | harness (overwrite) |
39
+ | `.agents/rules/00-operating.md` | always-loaded rules summary (wired into `opencode.json` `instructions`) | harness (overwrite) |
37
40
  | `.agents/skills/*/SKILL.md` | persona instruction skills | harness (overwrite) |
38
- | `opencode.json` | main/small model routing | harness (overwrite) |
39
- | `scripts/qa/*` | `test:dispatch` / `test:governance` gates | harness (overwrite) |
40
- | `.agents/memory/*` | project data (domain-map, stack-versions, handoff, locations, model-routing) | **project** (create-if-missing) |
41
- | `.harness.json` | deployed version + project profile | harness |
41
+ | `opencode.json` | main/small model routing + `instructions` | harness (**merged**, see below) |
42
+ | `scripts/qa/*` | `test:dispatch` / `test:governance` gates + QA-script wiring check | harness (overwrite) |
43
+ | `.agents/memory/*` | project data (domain-map, stack-versions, handoff, locations, model-routing, history) | **project** (create-if-missing) |
44
+ | `.harness.json` | deployed version + project profile + per-file checksums | harness |
45
+
46
+ ### `opencode.json` is merged, not clobbered
47
+
48
+ The harness owns only `$schema`, `model`, `small_model`, and its own
49
+ `instructions` entry (`.agents/rules/00-operating.md`). Every other key your
50
+ project adds — `permission`, `mcp`, `plugin`, custom `instructions`, etc. — is
51
+ preserved across `init`/`update`.
42
52
 
43
53
  ## Placeholders
44
54
 
45
55
  Templates use `{{PROJECT_NAME}}` and `{{PROJECT_DOMAIN}}`; `init` substitutes
46
56
  them from `--name` / `--domain` (or prompts, or `.harness.json`). Domain facts
47
- that are genuinely project-specific (entity model, catalog sources, visibility
57
+ that are genuinely project-specific (entity model, external sources, visibility
48
58
  tiers, trust tiers) live in `.agents/memory/domain-map.md`, which `init`
49
- scaffolds for you to fill in — the skills reference it instead of hardcoding
50
- domain assumptions.
59
+ scaffolds for you to fill in — the skills and rules reference memory instead of
60
+ hardcoding domain assumptions.
51
61
 
52
- ## Model routing
62
+ ## Model routing & enforcement
53
63
 
54
64
  - Concrete models are set in `.opencode/agents/<name>.md` (`model:` field) and
55
65
  `opencode.json` (`model` + `small_model`).
66
+ - Personas also carry mechanical guardrails in frontmatter:
67
+ - thinkers (`planner`, `product-manager`) → `permission: { edit: deny, bash: deny }`
68
+ - `qa-architect`, `handoff` → `permission: { bash: deny }`
69
+ - deterministic personas → `temperature: 0.1`
70
+ - all personas → a `steps:` cap (cost ceiling)
71
+ - `planner`, `handoff` → `hidden: true`
56
72
  - `.agents/skills/*/SKILL.md` carries a `model:` label only (informational).
57
73
  - `npx @dombaras/agent-harness list` prints the persona → model mapping.
58
74
  - `npm run test:dispatch` mechanically verifies model pins against
59
75
  `scripts/qa/models.allowlist.txt`.
60
76
 
77
+ ## QA gates
78
+
79
+ - `npm run test:dispatch` — persona model-pin preflight + persona↔skill parity.
80
+ - `npm run test:governance` — session wrap-up dispatch-log enforcement.
81
+ - `node scripts/qa/check-qa-scripts.js` — warns (or `--strict` fails) when the
82
+ DoD-referenced runtime QA tiers aren't wired into `package.json`.
83
+
84
+ The harness ships only these gates. The runtime QA tiers (`test:quick`,
85
+ `test:routes`, `test:api`, `test:verify`, `test:security`) and their backing
86
+ scripts are **project-provided** — the rulebook references them, and
87
+ `check-qa-scripts.js` reminds you if they're missing.
88
+
61
89
  ## Update
62
90
 
63
91
  ```bash
64
92
  npx @dombaras/agent-harness update --target /path/to/project
65
93
  ```
66
94
 
67
- Overwrites harness-owned files, preserves `.agents/memory/*`.
95
+ - Overwrites harness-owned files, preserves `.agents/memory/*`.
96
+ - **Non-destructive**: if a harness-owned file was modified locally since the
97
+ last deploy (tracked by checksum in `.harness.json`), it is backed up to
98
+ `.harness-backup/<timestamp>/` before overwrite.
99
+ - `opencode.json` is merged (project keys preserved).
100
+ - `--dry-run` previews the plan without writing.
68
101
 
69
102
  ## Develop
70
103
 
71
104
  ```bash
105
+ npm test # node --test
106
+ npm run lint # syntax-check the CLI + QA scripts
72
107
  node bin/agent-harness.js list
73
108
  node bin/agent-harness.js init --target /tmp/demo --name Demo --yes
74
109
  ```
@@ -1,31 +1,43 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /*
4
- * agent-harness — deploy the LibraryOS agent harness (personas, skills, rules,
5
- * model routing, QA gates) into a target project.
4
+ * agent-harness — deploy the agent harness (personas, skills, rules, model
5
+ * routing, QA gates) into a target project.
6
6
  *
7
7
  * Commands:
8
- * init [--target <dir>] [--name <project>] [--domain <desc>] [--yes]
9
- * update [--target <dir>]
10
- * list (list personas + models)
8
+ * init [--target <dir>] [--name <project>] [--domain <desc>] [--yes] [--dry-run]
9
+ * update [--target <dir>] [--dry-run]
10
+ * list
11
+ * --version | -v
12
+ * --help | -h
11
13
  *
12
- * `init` materializes templates/ into the target and substitutes {{
13
- * PROJECT_NAME }} / {{ PROJECT_DOMAIN }}. `.agents/memory/*` files are only
14
- * created when absent (project data). Harness-owned files are overwritten.
14
+ * `init`/`update` materialize templates/ into the target and substitute {{
15
+ * PROJECT_NAME }} / {{ PROJECT_DOMAIN }}.
16
+ *
17
+ * Ownership model:
18
+ * - `.agents/memory/*` is project data -> create-if-missing (never overwrite).
19
+ * - `opencode.json` is MERGED: harness manages $schema/model/small_model/
20
+ * instructions; every other key the project adds is preserved.
21
+ * - all other harness-owned files are overwritten. If a harness-owned file was
22
+ * modified locally since the last deploy (tracked by checksum in
23
+ * `.harness.json`), it is backed up to `.harness-backup/<timestamp>/` first.
15
24
  */
16
25
 
17
26
  const fs = require("fs");
18
27
  const path = require("path");
28
+ const crypto = require("crypto");
19
29
  const readline = require("readline");
20
30
 
21
31
  const PKG = require("../package.json");
22
32
  const TEMPLATES_DIR = path.resolve(__dirname, "..", "templates");
23
33
  const CONFIG_FILE = ".harness.json";
34
+ const BACKUP_DIR = ".harness-backup";
35
+
36
+ // ---------------------------------------------------------------- helpers
24
37
 
25
- // Files the harness owns and overwrites on every run.
26
- // `.agents/memory/*` is project data -> create-if-missing only.
27
38
  function listFiles(dir, base = dir) {
28
39
  const out = [];
40
+ if (!fs.existsSync(dir)) return out;
29
41
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
30
42
  const abs = path.join(dir, entry.name);
31
43
  if (entry.isDirectory()) out.push(...listFiles(abs, base));
@@ -37,82 +49,167 @@ function listFiles(dir, base = dir) {
37
49
  function substitute(text, vars) {
38
50
  let out = text;
39
51
  for (const [key, value] of Object.entries(vars)) {
40
- out = out.split("{{" + key + "}}").join(value);
52
+ out = out.split("{{" + key + "}}").join(value == null ? "" : String(value));
41
53
  }
42
54
  return out;
43
55
  }
44
56
 
57
+ function relKey(rel) {
58
+ return rel.split(path.sep).join("/");
59
+ }
60
+
45
61
  function isMemoryFile(rel) {
46
- return rel.split(path.sep).includes(".agents") &&
47
- rel.split(path.sep).includes("memory");
62
+ const parts = rel.split(path.sep);
63
+ return parts.includes(".agents") && parts.includes("memory");
64
+ }
65
+
66
+ function isOpencodeJson(rel) {
67
+ return relKey(rel) === "opencode.json";
48
68
  }
49
69
 
50
- function writeFileIfAbsent(targetAbs, content) {
51
- if (fs.existsSync(targetAbs)) return "skip";
52
- fs.mkdirSync(path.dirname(targetAbs), { recursive: true });
53
- fs.writeFileSync(targetAbs, content, "utf8");
54
- return "create";
70
+ function sha256(str) {
71
+ return crypto.createHash("sha256").update(str, "utf8").digest("hex");
72
+ }
73
+
74
+ function safeJson(text, fallback) {
75
+ if (text == null) return fallback;
76
+ try {
77
+ return JSON.parse(text);
78
+ } catch (_) {
79
+ return fallback;
80
+ }
55
81
  }
56
82
 
57
83
  function readConfig(target) {
58
84
  const p = path.join(target, CONFIG_FILE);
59
- if (fs.existsSync(p)) {
60
- try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch (_) { return {}; }
61
- }
85
+ if (fs.existsSync(p)) return safeJson(fs.readFileSync(p, "utf8"), {});
62
86
  return {};
63
87
  }
64
88
 
65
- function ask(question, fallback) {
66
- if (process.argv.includes("--yes")) return Promise.resolve(fallback || "");
89
+ function ask(question, fallback, yes) {
90
+ if (yes) return Promise.resolve(fallback || "");
67
91
  if (!process.stdin.isTTY) return Promise.resolve(fallback || "");
68
92
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
69
93
  return new Promise((resolve) => {
70
- rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); });
94
+ rl.question(question, (answer) => {
95
+ rl.close();
96
+ resolve(answer.trim());
97
+ });
71
98
  });
72
99
  }
73
100
 
74
101
  function resolveVars(target, args) {
75
102
  const existing = readConfig(target);
76
- const nameFlag = args["--name"];
77
- const domainFlag = args["--domain"];
78
- const projectName = nameFlag || existing.projectName || path.basename(target);
79
- return { projectName, domainFlag, existing, VARIABLES: { PROJECT_NAME: projectName, PROJECT_DOMAIN: domainFlag || existing.projectDomain || "" } };
103
+ const projectName = args["--name"] || existing.projectName || path.basename(target);
104
+ const projectDomain = args["--domain"] || existing.projectDomain || "";
105
+ return {
106
+ projectName,
107
+ projectDomain,
108
+ existing,
109
+ VARIABLES: { PROJECT_NAME: projectName, PROJECT_DOMAIN: projectDomain },
110
+ };
80
111
  }
81
112
 
82
- function printPersonas(templatesDir) {
83
- const agents = path.join(templatesDir, ".opencode", "agents");
84
- console.log("Personas (model routing):\n");
85
- for (const f of fs.readdirSync(agents).sort()) {
86
- const src = fs.readFileSync(path.join(agents, f), "utf8");
87
- const model = (src.match(/^model:\s*(.+)$/m) || [])[1] || "?";
88
- console.log(` ${f.replace(/\.md$/, "").padEnd(22)} -> ${model}`);
89
- }
113
+ // ---------------------------------------------------------------- opencode.json
114
+
115
+ function templateOpencodeJson() {
116
+ const p = path.join(TEMPLATES_DIR, "opencode.json");
117
+ return safeJson(fs.readFileSync(p, "utf8"), {});
90
118
  }
91
119
 
92
- async function init(target, args) {
93
- if (!fs.existsSync(TEMPLATES_DIR)) {
94
- console.error("templates/ directory not found next to this CLI.");
95
- process.exit(1);
120
+ // Merge harness-managed keys into the project's existing opencode.json,
121
+ // preserving every other key the project has added.
122
+ function mergeOpencodeJson(target, variables) {
123
+ const harness = safeJson(
124
+ substitute(fs.readFileSync(path.join(TEMPLATES_DIR, "opencode.json"), "utf8"), variables),
125
+ {}
126
+ );
127
+ const p = path.join(target, "opencode.json");
128
+ const existing = fs.existsSync(p) ? safeJson(fs.readFileSync(p, "utf8"), {}) : {};
129
+
130
+ const merged = { ...existing, ...harness };
131
+ if (Array.isArray(harness.instructions)) {
132
+ const prior = Array.isArray(existing.instructions) ? existing.instructions : [];
133
+ merged.instructions = [...new Set([...prior, ...harness.instructions])];
96
134
  }
97
- const { projectName, domainFlag, VARIABLES } = resolveVars(target, args);
135
+ return JSON.stringify(merged, null, 2) + "\n";
136
+ }
137
+
138
+ // ---------------------------------------------------------------- deploy
98
139
 
99
- let projectDomain = VARIABLES.PROJECT_DOMAIN;
100
- if (!domainFlag && !readConfig(target).projectDomain) {
140
+ function backup(target, rel, content) {
141
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
142
+ const dest = path.join(target, BACKUP_DIR, stamp, relKey(rel));
143
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
144
+ fs.writeFileSync(dest, content, "utf8");
145
+ }
146
+
147
+ async function deploy(target, opts) {
148
+ const { name, domain, yes, dryRun, isUpdate } = opts || {};
149
+ const existing = readConfig(target);
150
+ const projectName = name || existing.projectName || path.basename(target);
151
+ let projectDomain = domain || existing.projectDomain || "";
152
+ if (!projectDomain && !isUpdate && !yes && process.stdin.isTTY) {
101
153
  projectDomain = await ask(`Project domain (one line) for "${projectName}": `, "");
102
154
  }
103
- VARIABLES.PROJECT_DOMAIN = projectDomain;
155
+ const VARIABLES = { PROJECT_NAME: projectName, PROJECT_DOMAIN: projectDomain };
156
+
157
+ const previousFiles = existing.files || {};
158
+ const actions = [];
159
+ const manifest = {};
104
160
 
105
- const stats = { overwritten: 0, created: 0, skipped: 0 };
106
161
  for (const { abs, rel } of listFiles(TEMPLATES_DIR)) {
107
- const content = substitute(fs.readFileSync(abs, "utf8"), VARIABLES);
162
+ const key = relKey(rel);
108
163
  const targetAbs = path.join(target, rel);
164
+
165
+ if (isOpencodeJson(rel)) {
166
+ const merged = mergeOpencodeJson(target, VARIABLES);
167
+ const current = fs.existsSync(targetAbs) ? fs.readFileSync(targetAbs, "utf8") : null;
168
+ if (current == null) actions.push({ rel: key, kind: "create" });
169
+ else if (current !== merged) actions.push({ rel: key, kind: "merge" });
170
+ else actions.push({ rel: key, kind: "unchanged" });
171
+ manifest[key] = sha256(merged);
172
+ if (!dryRun) {
173
+ fs.mkdirSync(path.dirname(targetAbs), { recursive: true });
174
+ fs.writeFileSync(targetAbs, merged, "utf8");
175
+ }
176
+ continue;
177
+ }
178
+
109
179
  if (isMemoryFile(rel)) {
110
- const r = writeFileIfAbsent(targetAbs, content);
111
- if (r === "create") stats.created++; else stats.skipped++;
180
+ if (fs.existsSync(targetAbs)) {
181
+ actions.push({ rel: key, kind: "preserve" });
182
+ } else {
183
+ actions.push({ rel: key, kind: "create" });
184
+ if (!dryRun) {
185
+ fs.mkdirSync(path.dirname(targetAbs), { recursive: true });
186
+ fs.writeFileSync(targetAbs, substitute(fs.readFileSync(abs, "utf8"), VARIABLES), "utf8");
187
+ }
188
+ }
189
+ continue; // project data: never tracked in the manifest
190
+ }
191
+
192
+ // harness-owned file
193
+ const content = substitute(fs.readFileSync(abs, "utf8"), VARIABLES);
194
+ if (fs.existsSync(targetAbs)) {
195
+ const current = fs.readFileSync(targetAbs, "utf8");
196
+ if (current !== content) {
197
+ const deployedHash = previousFiles[key];
198
+ const locallyModified = !!deployedHash && deployedHash !== sha256(current);
199
+ actions.push({ rel: key, kind: locallyModified ? "overwrite (backup)" : "overwrite" });
200
+ if (locallyModified && !dryRun) backup(target, rel, current);
201
+ } else {
202
+ actions.push({ rel: key, kind: "unchanged" });
203
+ manifest[key] = previousFiles[key] || sha256(content);
204
+ continue;
205
+ }
112
206
  } else {
207
+ actions.push({ rel: key, kind: "create" });
208
+ }
209
+ manifest[key] = sha256(content);
210
+ if (!dryRun) {
113
211
  fs.mkdirSync(path.dirname(targetAbs), { recursive: true });
114
212
  fs.writeFileSync(targetAbs, content, "utf8");
115
- stats.overwritten++;
116
213
  }
117
214
  }
118
215
 
@@ -120,12 +217,39 @@ async function init(target, args) {
120
217
  version: PKG.version,
121
218
  projectName,
122
219
  projectDomain,
123
- initializedAt: new Date().toISOString(),
220
+ initializedAt: existing.initializedAt || new Date().toISOString(),
221
+ updatedAt: new Date().toISOString(),
222
+ files: manifest,
124
223
  };
125
- fs.writeFileSync(path.join(target, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n", "utf8");
224
+ if (!dryRun) {
225
+ fs.writeFileSync(path.join(target, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n", "utf8");
226
+ }
126
227
 
127
- console.log(`\nagent-harness v${PKG.version} initialized "${projectName}" in ${target}`);
128
- console.log(` ${stats.overwritten} harness files written, ${stats.created} memory files created, ${stats.skipped} existing memory files preserved.\n`);
228
+ return { projectName, projectDomain, actions, dryRun: !!dryRun };
229
+ }
230
+
231
+ function summarize(actions) {
232
+ const counts = {};
233
+ for (const a of actions) counts[a.kind] = (counts[a.kind] || 0) + 1;
234
+ return counts;
235
+ }
236
+
237
+ function printSummary(result, isUpdate) {
238
+ const { projectName, actions, dryRun } = result;
239
+ const verb = isUpdate
240
+ ? (dryRun ? "Would update" : "Updated")
241
+ : (dryRun ? "Would initialize" : "Initialized");
242
+ console.log(`\nagent-harness v${PKG.version} ${verb.toLowerCase()} "${projectName}"` + (dryRun ? " (dry run)" : ""));
243
+ for (const a of actions) console.log(` [${a.kind}] ${a.rel}`);
244
+ const counts = summarize(actions);
245
+ console.log(
246
+ `\n ${counts.create || 0} created, ${counts.overwrite || 0} overwritten, ` +
247
+ `${counts["overwrite (backup)"] || 0} backed up (locally modified), ` +
248
+ `${counts.merge || 0} merged, ${counts.preserve || 0} preserved, ${counts.unchanged || 0} unchanged.`
249
+ );
250
+ }
251
+
252
+ function printNextSteps() {
129
253
  console.log(" Next steps:");
130
254
  console.log(" 1. Fill in `.agents/memory/domain-map.md` and `.agents/memory/stack-versions.md`.");
131
255
  console.log(" 2. Add QA gate scripts to package.json:");
@@ -134,36 +258,116 @@ async function init(target, args) {
134
258
  console.log(" 3. Restart your agent CLI (config is read once at startup).\n");
135
259
  }
136
260
 
137
- function update(target) {
138
- const { projectName, VARIABLES } = resolveVars(target, {});
139
- console.log(`Updating harness in ${target} (project "${projectName}")...`);
140
- return init(target, { "--name": projectName, "--domain": VARIABLES.PROJECT_DOMAIN });
261
+ function printUsage() {
262
+ console.log(
263
+ `agent-harness v${PKG.version}\n\n` +
264
+ `Usage:\n` +
265
+ ` agent-harness init [--target <dir>] [--name <project>] [--domain <desc>] [--yes] [--dry-run]\n` +
266
+ ` agent-harness update [--target <dir>] [--dry-run]\n` +
267
+ ` agent-harness list\n` +
268
+ ` agent-harness --version | -v\n` +
269
+ ` agent-harness --help | -h\n`
270
+ );
141
271
  }
142
272
 
143
- function main() {
144
- const args = process.argv.slice(2);
145
- const cmd = args[0] || "help";
273
+ function printPersonas(templatesDir) {
274
+ const agents = path.join(templatesDir, ".opencode", "agents");
275
+ console.log("Personas (model routing):\n");
276
+ for (const f of fs.readdirSync(agents).sort()) {
277
+ const src = fs.readFileSync(path.join(agents, f), "utf8");
278
+ const model = (src.match(/^model:\s*(.+)$/m) || [])[1] || "?";
279
+ console.log(` ${f.replace(/\.md$/, "").padEnd(22)} -> ${model}`);
280
+ }
281
+ }
282
+
283
+ // ---------------------------------------------------------------- commands
284
+
285
+ async function init(target, flags) {
286
+ if (!fs.existsSync(TEMPLATES_DIR)) {
287
+ console.error("templates/ directory not found next to this CLI.");
288
+ process.exit(1);
289
+ }
290
+ const result = await deploy(target, {
291
+ name: flags["--name"],
292
+ domain: flags["--domain"],
293
+ yes: !!flags["--yes"],
294
+ dryRun: !!flags["--dry-run"],
295
+ isUpdate: false,
296
+ });
297
+ printSummary(result, false);
298
+ if (!result.dryRun) printNextSteps();
299
+ }
300
+
301
+ async function update(target, flags) {
302
+ if (!fs.existsSync(TEMPLATES_DIR)) {
303
+ console.error("templates/ directory not found next to this CLI.");
304
+ process.exit(1);
305
+ }
306
+ const result = await deploy(target, {
307
+ yes: true,
308
+ dryRun: !!flags["--dry-run"],
309
+ isUpdate: true,
310
+ });
311
+ printSummary(result, true);
312
+ }
313
+
314
+ function parseArgs(argv) {
146
315
  const flags = {};
147
- for (let i = 1; i < args.length; i++) {
148
- if (args[i] === "--target" || args[i] === "--name" || args[i] === "--domain") {
149
- flags[args[i]] = args[i + 1]; i++;
150
- }
316
+ for (let i = 0; i < argv.length; i++) {
317
+ const a = argv[i];
318
+ if (a === "--target" || a === "--name" || a === "--domain") flags[a] = argv[++i];
319
+ else if (a === "--dry-run" || a === "--yes" || a === "-y") flags[a] = true;
151
320
  }
321
+ return flags;
322
+ }
323
+
324
+ function main() {
325
+ const args = process.argv.slice(2);
326
+ const cmd = args[0];
327
+ if (cmd === "--version" || cmd === "-v") return console.log(PKG.version);
328
+ if (cmd === "--help" || cmd === "-h" || !cmd || cmd === "help") return printUsage();
329
+
330
+ const flags = parseArgs(args.slice(1));
152
331
  const target = path.resolve(flags["--target"] || process.cwd());
153
332
 
154
333
  switch (cmd) {
155
334
  case "init":
156
- init(target, flags).catch((e) => { console.error(e); process.exit(1); });
335
+ init(target, flags).catch((e) => {
336
+ console.error(e);
337
+ process.exit(1);
338
+ });
157
339
  break;
158
340
  case "update":
159
- update(target).catch((e) => { console.error(e); process.exit(1); });
341
+ update(target, flags).catch((e) => {
342
+ console.error(e);
343
+ process.exit(1);
344
+ });
160
345
  break;
161
346
  case "list":
162
347
  printPersonas(TEMPLATES_DIR);
163
348
  break;
164
349
  default:
165
- console.log(`agent-harness v${PKG.version}\n\nUsage:\n agent-harness init [--target <dir>] [--name <project>] [--domain <desc>] [--yes]\n agent-harness update [--target <dir>]\n agent-harness list`);
350
+ console.error(`Unknown command: ${cmd}\n`);
351
+ printUsage();
352
+ process.exitCode = 1;
166
353
  }
167
354
  }
168
355
 
169
- main();
356
+ if (require.main === module) main();
357
+
358
+ module.exports = {
359
+ listFiles,
360
+ substitute,
361
+ isMemoryFile,
362
+ isOpencodeJson,
363
+ sha256,
364
+ readConfig,
365
+ resolveVars,
366
+ mergeOpencodeJson,
367
+ deploy,
368
+ printPersonas,
369
+ TEMPLATES_DIR,
370
+ CONFIG_FILE,
371
+ BACKUP_DIR,
372
+ PKG,
373
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dombaras/agent-harness",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Reusable multi-agent harness for AI-assisted development: personas, skills, operating rules, model routing, and QA gates. Deploy into any project with `npx @dombaras/agent-harness init`.",
5
5
  "bin": {
6
6
  "agent-harness": "bin/agent-harness.js"
@@ -11,8 +11,13 @@
11
11
  "files": [
12
12
  "bin/",
13
13
  "templates/",
14
- "README.md"
14
+ "README.md",
15
+ "LICENSE"
15
16
  ],
17
+ "scripts": {
18
+ "test": "node --test",
19
+ "lint": "node --check bin/agent-harness.js && node --check templates/scripts/qa/check-dispatch-config.js && node --check templates/scripts/qa/governance.js && node --check templates/scripts/qa/check-qa-scripts.js"
20
+ },
16
21
  "keywords": [
17
22
  "opencode",
18
23
  "agents",