@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.
- package/LICENSE +21 -0
- package/README.md +48 -13
- package/bin/agent-harness.js +272 -68
- package/package.json +7 -2
- package/templates/.agents/AGENTS.md +93 -91
- package/templates/.agents/memory/domain-map.md +2 -2
- package/templates/.agents/memory/history.md +7 -0
- package/templates/.agents/memory/model-routing.md +22 -6
- package/templates/.agents/memory/stack-versions.md +25 -2
- package/templates/.agents/rules/00-operating.md +29 -85
- package/templates/.agents/skills/data-engineer/SKILL.md +2 -2
- package/templates/.agents/skills/devops-engineer/SKILL.md +1 -1
- package/templates/.agents/skills/diagnostics-expert/SKILL.md +8 -9
- package/templates/.agents/skills/handoff/SKILL.md +2 -2
- package/templates/.agents/skills/mobile-engineer/SKILL.md +2 -2
- package/templates/.agents/skills/product-manager/SKILL.md +8 -8
- package/templates/.agents/skills/qa-architect/SKILL.md +5 -0
- package/templates/.agents/skills/security-engineer/SKILL.md +5 -5
- package/templates/.agents/skills/system-architect/SKILL.md +4 -4
- package/templates/.opencode/agents/data-engineer.md +1 -0
- package/templates/.opencode/agents/devops-engineer.md +1 -0
- package/templates/.opencode/agents/diagnostics-expert.md +2 -0
- package/templates/.opencode/agents/frontend-engineer.md +1 -0
- package/templates/.opencode/agents/handoff.md +5 -0
- package/templates/.opencode/agents/mobile-engineer.md +1 -0
- package/templates/.opencode/agents/planner.md +6 -0
- package/templates/.opencode/agents/product-manager.md +4 -0
- package/templates/.opencode/agents/qa-architect.md +4 -0
- package/templates/.opencode/agents/qa-runner.md +2 -0
- package/templates/.opencode/agents/security-engineer.md +1 -0
- package/templates/.opencode/agents/system-architect.md +1 -0
- package/templates/.opencode/agents/ui-designer.md +1 -0
- package/templates/AGENTS.md +6 -13
- package/templates/opencode.json +2 -1
- 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 (
|
|
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,
|
|
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
|
|
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
|
```
|
package/bin/agent-harness.js
CHANGED
|
@@ -1,31 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
/*
|
|
4
|
-
* agent-harness — deploy the
|
|
5
|
-
*
|
|
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
|
|
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`
|
|
13
|
-
* PROJECT_NAME }} / {{ PROJECT_DOMAIN }}.
|
|
14
|
-
*
|
|
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
|
-
|
|
47
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 (
|
|
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) => {
|
|
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
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
135
|
+
return JSON.stringify(merged, null, 2) + "\n";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------- deploy
|
|
98
139
|
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
224
|
+
if (!dryRun) {
|
|
225
|
+
fs.writeFileSync(path.join(target, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
226
|
+
}
|
|
126
227
|
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
144
|
-
const
|
|
145
|
-
|
|
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 =
|
|
148
|
-
|
|
149
|
-
|
|
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) => {
|
|
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) => {
|
|
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.
|
|
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.
|
|
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",
|