@dombaras/agent-harness 0.1.10 → 0.1.12
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/README.md +16 -4
- package/bin/agent-harness.js +52 -14
- package/package.json +1 -1
- package/templates/.agents/AGENTS.md +9 -6
- package/templates/.agents/rules/00-operating.md +3 -3
- package/templates/.agents/skills/handoff/SKILL.md +1 -0
- package/templates/.opencode/command/board.md +21 -0
- package/templates/scripts/qa/check-backlog.js +37 -0
package/README.md
CHANGED
|
@@ -11,8 +11,9 @@ and QA gates — then deploys them into any project.
|
|
|
11
11
|
|
|
12
12
|
## Why
|
|
13
13
|
|
|
14
|
-
- The agent harness is tooling, not application code.
|
|
15
|
-
|
|
14
|
+
- The agent harness is tooling, not application code. Its source lives in the
|
|
15
|
+
npm package — you deploy it with `init`/`update` rather than hand-copying it
|
|
16
|
+
into every repo.
|
|
16
17
|
- One canonical, versioned source of truth for personas/rules/QA tiers, deployed
|
|
17
18
|
on demand and updatable via `npx @dombaras/agent-harness update`.
|
|
18
19
|
|
|
@@ -44,6 +45,7 @@ npx @dombaras/agent-harness init --target . --dry-run
|
|
|
44
45
|
| `scripts/qa/*` | `test:dispatch` / `test:governance` / `test:qa-plan` gates + QA-script wiring check | harness (overwrite) |
|
|
45
46
|
| `.agents/memory/*` | project data (domain-map, stack-versions, handoff, locations, model-routing, history, flow-map, qa-plan) | **project** (create-if-missing) |
|
|
46
47
|
| `.harness.json` | deployed version + project profile + per-file checksums | harness |
|
|
48
|
+
| `.gitignore` | harness-managed section (`.harness-backup/`) merged in, project entries preserved | harness (merged) |
|
|
47
49
|
|
|
48
50
|
### `opencode.json` is merged, not clobbered
|
|
49
51
|
|
|
@@ -108,6 +110,10 @@ warns but does not block; the guarantee only applies to registered flows.
|
|
|
108
110
|
`deepseek/deepseek-v4-pro` (default, direct API key), `zen`/`opencode` →
|
|
109
111
|
`opencode/deepseek-v4-pro` (gateway), `pickle` → `opencode/big-pickle`, or any
|
|
110
112
|
explicit `provider/model`. Restart opencode after switching (config reads once).
|
|
113
|
+
- **`/board` command** — every deploy ships an opencode command
|
|
114
|
+
(`.opencode/command/board.md`) that reads the local `BACKLOG.md` and renders a
|
|
115
|
+
clean task list (Priority · Title · Short description). Read-only, no git — the
|
|
116
|
+
local file is the source of truth.
|
|
111
117
|
|
|
112
118
|
## QA gates
|
|
113
119
|
|
|
@@ -144,11 +150,13 @@ npx @dombaras/agent-harness update --target /path/to/project
|
|
|
144
150
|
- Overwrites harness-owned files, preserves `.agents/memory/*` and `BACKLOG.md`.
|
|
145
151
|
- Auto-wires the harness gate scripts (`test:dispatch`, `test:governance`,
|
|
146
152
|
`test:qa-plan`, `test:backlog`) into the target's `package.json` (merged, add-only).
|
|
147
|
-
- **Auto-commits**
|
|
153
|
+
- **Auto-commits** the harness files it changed (`chore(harness): @dombaras/agent-harness
|
|
148
154
|
<old> -> <new>`) and **pushes** to origin, so the next session never sees unexplained
|
|
149
155
|
modified harness files. Your unrelated uncommitted work is never staged.
|
|
150
156
|
- `--no-commit` to skip commit+push, `--no-push` to commit but not push.
|
|
151
|
-
- `init`
|
|
157
|
+
- `init` auto-commits + pushes by default too: it commits everything it scaffolds
|
|
158
|
+
(including `.agents/memory/*`, `.agents/features/*`, and `BACKLOG.md`) so a fresh
|
|
159
|
+
project starts fully tracked and no harness file is left untracked.
|
|
152
160
|
- Harness-owned paths are **force-added**, so a project that gitignores deployment
|
|
153
161
|
artifacts (an inherited `.agents/` `.opencode/` `AGENTS.md` pattern) still gets the
|
|
154
162
|
harness commit instead of a raw `git add` failure that silently skips it — force-added
|
|
@@ -166,6 +174,10 @@ npx @dombaras/agent-harness update --target /path/to/project
|
|
|
166
174
|
last deploy (tracked by checksum in `.harness.json`), it is backed up to
|
|
167
175
|
`.harness-backup/<timestamp>/` before overwrite.
|
|
168
176
|
- `opencode.json` is merged (project keys preserved).
|
|
177
|
+
- **Managed `.gitignore`**: the harness merges a marked section (`.harness-backup/`, its
|
|
178
|
+
local rollback artifacts) into the project's `.gitignore`, preserving every project entry.
|
|
179
|
+
New harness files are tracked (committed), not ignored — so they don't need a gitignore
|
|
180
|
+
change. If the harness ever adds a new local-only artifact, `update` re-merges the entry.
|
|
169
181
|
- `--dry-run` previews the plan without writing.
|
|
170
182
|
|
|
171
183
|
### Backlog migration (`migrate-backlog`)
|
package/bin/agent-harness.js
CHANGED
|
@@ -217,6 +217,25 @@ function mergePackageJson(target) {
|
|
|
217
217
|
return { text: JSON.stringify(merged, null, 2) + "\n", hadPkg };
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
// Harness-managed .gitignore entries. These are LOCAL-ONLY artifacts (never
|
|
221
|
+
// committed): everything else the harness deploys is tracked by git. Entries
|
|
222
|
+
// live in a marked section so `update` can re-merge them (and pick up new
|
|
223
|
+
// entries) without touching the project's own ignores.
|
|
224
|
+
const GITIGNORE_MARKER = "# agent-harness (managed below)";
|
|
225
|
+
const HARNESS_IGNORE_ENTRIES = [".harness-backup/"];
|
|
226
|
+
|
|
227
|
+
function mergeGitignore(target) {
|
|
228
|
+
const p = path.join(target, ".gitignore");
|
|
229
|
+
const existing = fs.existsSync(p) ? fs.readFileSync(p, "utf8") : "";
|
|
230
|
+
const lines = existing.split(/\r?\n/);
|
|
231
|
+
const idx = lines.findIndex((l) => l === GITIGNORE_MARKER);
|
|
232
|
+
const kept = idx === -1 ? lines : lines.slice(0, idx);
|
|
233
|
+
while (kept.length && kept[kept.length - 1].trim() === "") kept.pop();
|
|
234
|
+
const managed = [GITIGNORE_MARKER, ...HARNESS_IGNORE_ENTRIES, ""];
|
|
235
|
+
const out = kept.length ? [...kept, "", ...managed] : [...managed];
|
|
236
|
+
return out.join("\n");
|
|
237
|
+
}
|
|
238
|
+
|
|
220
239
|
// ---------------------------------------------------------------- git helpers
|
|
221
240
|
|
|
222
241
|
function runGit(target, args) {
|
|
@@ -348,6 +367,23 @@ async function deploy(target, opts) {
|
|
|
348
367
|
}
|
|
349
368
|
}
|
|
350
369
|
|
|
370
|
+
// Merge harness-managed gitignore entries (add-only, marked section).
|
|
371
|
+
{
|
|
372
|
+
const text = mergeGitignore(target);
|
|
373
|
+
const key = ".gitignore";
|
|
374
|
+
const targetAbs = path.join(target, ".gitignore");
|
|
375
|
+
const had = fs.existsSync(targetAbs);
|
|
376
|
+
const current = had ? fs.readFileSync(targetAbs, "utf8") : null;
|
|
377
|
+
if (!had) actions.push({ rel: key, kind: "create" });
|
|
378
|
+
else if (current !== text) actions.push({ rel: key, kind: "merge" });
|
|
379
|
+
else actions.push({ rel: key, kind: "unchanged" });
|
|
380
|
+
manifest[key] = sha256(text);
|
|
381
|
+
if (!dryRun) {
|
|
382
|
+
fs.mkdirSync(path.dirname(targetAbs), { recursive: true });
|
|
383
|
+
fs.writeFileSync(targetAbs, text, "utf8");
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
351
387
|
for (const { abs, rel } of listFiles(TEMPLATES_DIR)) {
|
|
352
388
|
const key = relKey(rel);
|
|
353
389
|
const targetAbs = path.join(target, rel);
|
|
@@ -425,15 +461,15 @@ async function deploy(target, opts) {
|
|
|
425
461
|
fs.writeFileSync(path.join(target, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
426
462
|
}
|
|
427
463
|
|
|
428
|
-
// Auto-commit
|
|
464
|
+
// Auto-commit the files this run created/changed so nothing the harness
|
|
465
|
+
// deploys is left untracked. Runs for both `init` and `update`. Excludes only
|
|
466
|
+
// rollback artifacts; freshly-scaffolded project-data files (memory/features/
|
|
467
|
+
// BACKLOG) are committed so they start tracked, while pre-existing project
|
|
468
|
+
// data is never touched (a "preserve" never enters `changedRelsAll`).
|
|
429
469
|
let commitResult = null;
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
!r.startsWith(".agents/memory/") &&
|
|
434
|
-
!r.startsWith(".agents/features/") &&
|
|
435
|
-
!r.startsWith(".harness-backup/")
|
|
436
|
-
);
|
|
470
|
+
const shouldCommit = !dryRun && (!isUpdate || !nothingToUpdate);
|
|
471
|
+
if (shouldCommit) {
|
|
472
|
+
const changedRels = changedRelsAll.filter((r) => !r.startsWith(".harness-backup/"));
|
|
437
473
|
changedRels.push(CONFIG_FILE); // .harness.json reflects the new deployed version
|
|
438
474
|
commitResult = commitHarnessChanges(target, changedRels, {
|
|
439
475
|
commit,
|
|
@@ -527,13 +563,14 @@ function printUsage() {
|
|
|
527
563
|
console.log(
|
|
528
564
|
`agent-harness v${PKG.version}\n\n` +
|
|
529
565
|
`Usage:\n` +
|
|
530
|
-
` agent-harness init [--target <dir>] [--name <project>] [--domain <desc>] [--yes] [--dry-run]\n` +
|
|
566
|
+
` agent-harness init [--target <dir>] [--name <project>] [--domain <desc>] [--yes] [--dry-run] [--no-commit] [--no-push]\n` +
|
|
531
567
|
` agent-harness update [--target <dir>] [--dry-run] [--no-commit] [--no-push]\n` +
|
|
532
568
|
` agent-harness migrate-backlog [--target <dir>]\n` +
|
|
533
569
|
`\n` +
|
|
534
|
-
` update auto-
|
|
535
|
-
`
|
|
536
|
-
` to commit but not push. Your unrelated
|
|
570
|
+
` init and update auto-commit only the harness files they create/change\n` +
|
|
571
|
+
` (chore(harness): ...) and push to origin by default. Use --no-commit to\n` +
|
|
572
|
+
` skip commit+push, or --no-push to commit but not push. Your unrelated\n` +
|
|
573
|
+
` uncommitted work is never staged.\n` +
|
|
537
574
|
`\n` +
|
|
538
575
|
` migrate-backlog consolidates any live \`.agents/features/INDEX.md\` F-rows and\n` +
|
|
539
576
|
` \`.agents/memory/todo.md\` into the canonical BACKLOG.md, then rewrites those\n` +
|
|
@@ -785,10 +822,11 @@ async function init(target, flags) {
|
|
|
785
822
|
yes: !!flags["--yes"],
|
|
786
823
|
dryRun: !!flags["--dry-run"],
|
|
787
824
|
isUpdate: false,
|
|
788
|
-
commit:
|
|
789
|
-
push:
|
|
825
|
+
commit: !flags["--no-commit"],
|
|
826
|
+
push: !flags["--no-commit"] && !flags["--no-push"],
|
|
790
827
|
});
|
|
791
828
|
printSummary(result, false);
|
|
829
|
+
printCommitResult(result);
|
|
792
830
|
if (!result.dryRun) printNextSteps();
|
|
793
831
|
}
|
|
794
832
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dombaras/agent-harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
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"
|
|
@@ -103,9 +103,11 @@ The canonical rulebook. The always-loaded summary is `.agents/rules/00-operating
|
|
|
103
103
|
|
|
104
104
|
## 7. Definition of Done & Continuous Backup
|
|
105
105
|
|
|
106
|
-
Every completed task
|
|
106
|
+
Every completed task closes with this gate. **QA gates dev work only:** steps 0-2
|
|
107
|
+
run when the session touched code paths — a backlog-only (or docs-only) change skips
|
|
108
|
+
them. Steps 3-5 are the unconditional session close; they are NOT gated by QA.
|
|
107
109
|
|
|
108
|
-
0. **Feature Completeness Gate
|
|
110
|
+
0. **Feature Completeness Gate** (code changes only):
|
|
109
111
|
- [ ] 5 UI states handled on every affected screen (list them).
|
|
110
112
|
- [ ] No `catch` blocks that only `console.warn` without user-facing feedback.
|
|
111
113
|
- [ ] Haptic/feedback consistency for every user-initiated action.
|
|
@@ -113,10 +115,11 @@ Every completed task passes this gate, in order:
|
|
|
113
115
|
- [ ] Cross-screen audit if a new UX pattern was introduced.
|
|
114
116
|
- [ ] No modified file over ~500 lines without extracting components.
|
|
115
117
|
- [ ] No blanket file-level `eslint-disable`/`@ts-nocheck`/`@ts-ignore` suppressions (line-level only, each with a reason).
|
|
116
|
-
1. **QA tier** — `qa-architect` inspects the diff, picks the tier BY COVERAGE (not path-label), records the coverage map in `.agents/memory/qa-plan.md`, and authors progression tests for any GAP; if `qa-architect` is not dispatched the orchestrator plans in its place. `qa-runner` executes and makes it pass (§6). A change is not verified until `npm run test:qa-plan` passes: every changed code path covered or waivered, **and** every touched flow (`.agents/memory/flow-map.md`) has every sibling surface and declared variant addressed in the plan's `## Parallel-surface & variant audit`, **AND** the executed tier exercised the modified path.
|
|
117
|
-
2. **Security check** — apply `security-engineer` when the change touches data/auth/input/secrets/deps.
|
|
118
|
-
3. **
|
|
119
|
-
4. **
|
|
118
|
+
1. **QA tier** (code changes only) — `qa-architect` inspects the diff, picks the tier BY COVERAGE (not path-label), records the coverage map in `.agents/memory/qa-plan.md`, and authors progression tests for any GAP; if `qa-architect` is not dispatched the orchestrator plans in its place. `qa-runner` executes and makes it pass (§6). A change is not verified until `npm run test:qa-plan` passes: every changed code path covered or waivered, **and** every touched flow (`.agents/memory/flow-map.md`) has every sibling surface and declared variant addressed in the plan's `## Parallel-surface & variant audit`, **AND** the executed tier exercised the modified path.
|
|
119
|
+
2. **Security check** (code changes only) — apply `security-engineer` when the change touches data/auth/input/secrets/deps.
|
|
120
|
+
3. **Backlog write-back** (always) — move every task this session shipped from `Open` to `Archive (shipped · done)` in `BACKLOG.md` (with date/commit), then stage it explicitly: `git add BACKLOG.md` (it starts untracked — `git commit -am` / `git commit` without `add` will NOT pick it up). The board syncs across sessions only through git, so this delta is committed with the code, never left uncommitted.
|
|
121
|
+
4. **Commit** (always) — concise `feat:` / `fix:` / `refactor:` message, including the `BACKLOG.md` delta.
|
|
122
|
+
5. **Push** (always) — `git push origin main`.
|
|
120
123
|
- **Mechanical floor**: a pre-push hook should run `npm run test:quick` (project-provided; the harness does not install git hooks).
|
|
121
124
|
- **Lean permissions**: grant only the narrowest `git`/`gh` action needed; escalate only after a command has actually failed.
|
|
122
125
|
- Git may not be on PATH in the default shell — use the full path (`.agents/memory/locations.md`).
|
|
@@ -23,12 +23,12 @@ Before reading or editing any file for a task, dispatch the relevant personas vi
|
|
|
23
23
|
4. **Verify stack versions** before writing framework code (`.agents/memory/stack-versions.md`).
|
|
24
24
|
5. **Token discipline** — search before read, targeted reads, batch reads, don't re-read unchanged files, right-size QA.
|
|
25
25
|
6. **Static ≠ runtime** — never declare verified from `tsc` alone; execute a real runtime/API path that exercises the MODIFIED code — a tier label never proves coverage. `test:qa-plan` gates every changed code path to a covering assertion or waiver, and every touched flow's sibling surfaces / variants to an audit entry (`.agents/memory/flow-map.md` — "fix one, fix all").
|
|
26
|
-
7. **Commit** with `feat:`/`fix:`/`refactor:` then push to main.
|
|
26
|
+
7. **Commit** with `feat:`/`fix:`/`refactor:` then push to main — including any `BACKLOG.md` delta.
|
|
27
27
|
|
|
28
28
|
## Definition of Done
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
QA tier + security lens gate **code changes only** (a backlog-only change skips them). Every session closes with **move shipped `BACKLOG.md` rows to `Archive` → commit → push** — the backlog write-back is never QA-gated.
|
|
31
31
|
|
|
32
32
|
## Session checklist
|
|
33
33
|
|
|
34
|
-
Start: read `.agents/memory/locations.md` + `.agents/memory/model-routing.md` + `BACKLOG.md`. End: record the dispatch log and update the handoff memory.
|
|
34
|
+
Start: read `.agents/memory/locations.md` + `.agents/memory/model-routing.md` + `BACKLOG.md`. End: move shipped `BACKLOG.md` rows to `Archive`, record the dispatch log, and update the handoff memory.
|
|
@@ -48,3 +48,4 @@ Date: <YYYY-MM-DD>
|
|
|
48
48
|
- Update this at session end even if the user doesn't ask, per the DoD in `.agents/AGENTS.md` §8.
|
|
49
49
|
- Keep it under ~40 lines. Code is the source of truth; the handoff is a pointer, not a spec.
|
|
50
50
|
- Keeping `.agents/memory/locations.md` current is part of this skill. Read it first, update it last.
|
|
51
|
+
- **You only touch handoff memory.** The board write-back — moving shipped `BACKLOG.md` rows to `Archive (shipped · done)` and committing them — is the orchestrator's DoD §7 step, not yours. If it hasn't been done, call it out in your output; never edit `BACKLOG.md` or run git yourself.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Show the local BACKLOG.md as a clean task list (title, short description, priority). Read-only — no git.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Read `BACKLOG.md` at the repo root and display a clean, human-readable task list.
|
|
6
|
+
The local file is the source of truth — do NOT run git, do NOT pull, do NOT edit
|
|
7
|
+
anything.
|
|
8
|
+
|
|
9
|
+
Render the `Open` rows (and the `Active / next` picks if present) as a table with
|
|
10
|
+
exactly these columns:
|
|
11
|
+
|
|
12
|
+
| Priority | Title | Short description |
|
|
13
|
+
|----------|-------|-------------------|
|
|
14
|
+
| P1 | F-001 | block something |
|
|
15
|
+
|
|
16
|
+
- **Priority** = the `P` cell.
|
|
17
|
+
- **Title** = the row's ID.
|
|
18
|
+
- **Short description** = the `Task` cell.
|
|
19
|
+
- Omit the `Evidence` column. Do not render the Frozen / Archive sections.
|
|
20
|
+
|
|
21
|
+
One row per open task. Never invent — show a `?` cell exactly as `?`.
|
|
@@ -16,9 +16,12 @@
|
|
|
16
16
|
* 5. No duplicate IDs across the whole file.
|
|
17
17
|
* 6. `INDEX.md` / `todo.md`, if present, are pointers (content referencing
|
|
18
18
|
* BACKLOG.md), not live row lists.
|
|
19
|
+
* 7. `BACKLOG.md` is git-tracked (committed), not untracked — the board syncs
|
|
20
|
+
* across sessions only through git. Skipped outside a git repo.
|
|
19
21
|
*/
|
|
20
22
|
const fs = require("fs");
|
|
21
23
|
const path = require("path");
|
|
24
|
+
const { execFileSync } = require("child_process");
|
|
22
25
|
|
|
23
26
|
const root = path.resolve(__dirname, "..", "..");
|
|
24
27
|
|
|
@@ -41,6 +44,40 @@ if (!fs.existsSync(canonical)) {
|
|
|
41
44
|
}
|
|
42
45
|
check(true, "canonical BACKLOG.md present at repo root");
|
|
43
46
|
|
|
47
|
+
// ---- 7. BACKLOG.md must be git-tracked (committed), not untracked ----------
|
|
48
|
+
// The board syncs across sessions only through git; an untracked BACKLOG.md
|
|
49
|
+
// never reaches the shared board. Skipped outside a git repo.
|
|
50
|
+
function insideGitRepo() {
|
|
51
|
+
try {
|
|
52
|
+
const r = execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
53
|
+
cwd: root,
|
|
54
|
+
stdio: "pipe",
|
|
55
|
+
});
|
|
56
|
+
return (r.toString() || "").trim() === "true";
|
|
57
|
+
} catch (_) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function gitTracked(rel) {
|
|
62
|
+
try {
|
|
63
|
+
const r = execFileSync("git", ["ls-files", "--error-unmatch", rel], {
|
|
64
|
+
cwd: root,
|
|
65
|
+
stdio: "pipe",
|
|
66
|
+
});
|
|
67
|
+
return (r.toString() || "").trim().length > 0;
|
|
68
|
+
} catch (_) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (insideGitRepo()) {
|
|
73
|
+
check(
|
|
74
|
+
gitTracked("BACKLOG.md"),
|
|
75
|
+
"BACKLOG.md is git-tracked (run `git add BACKLOG.md` then commit — an untracked board never syncs)"
|
|
76
|
+
);
|
|
77
|
+
} else {
|
|
78
|
+
check(true, "not a git repo \u2014 skipping BACKLOG.md tracking check");
|
|
79
|
+
}
|
|
80
|
+
|
|
44
81
|
const src = fs.readFileSync(canonical, "utf8");
|
|
45
82
|
const lines = src.split(/\r?\n/);
|
|
46
83
|
|