@dombaras/agent-harness 0.1.1 → 0.1.5

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 CHANGED
@@ -39,8 +39,8 @@ npx @dombaras/agent-harness init --target . --dry-run
39
39
  | `.agents/rules/00-operating.md` | always-loaded rules summary (wired into `opencode.json` `instructions`) | harness (overwrite) |
40
40
  | `.agents/skills/*/SKILL.md` | persona instruction skills | harness (overwrite) |
41
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) |
42
+ | `scripts/qa/*` | `test:dispatch` / `test:governance` / `test:qa-plan` gates + QA-script wiring check | harness (overwrite) |
43
+ | `.agents/memory/*` | project data (domain-map, stack-versions, handoff, locations, model-routing, history, flow-map, qa-plan) | **project** (create-if-missing) |
44
44
  | `.harness.json` | deployed version + project profile + per-file checksums | harness |
45
45
 
46
46
  ### `opencode.json` is merged, not clobbered
@@ -59,6 +59,13 @@ tiers, trust tiers) live in `.agents/memory/domain-map.md`, which `init`
59
59
  scaffolds for you to fill in — the skills and rules reference memory instead of
60
60
  hardcoding domain assumptions.
61
61
 
62
+ **Flow map (`.agents/memory/flow-map.md`)** — the project-owned registry powering the
63
+ flow-closure half of `test:qa-plan`. Each user-facing flow lists the code surfaces that
64
+ implement the same behavior across codebases/layers (e.g. web component, `mobile/` sheet,
65
+ API route) plus its optional variants (e.g. `condition-selector-present` vs `-absent`). Fill
66
+ it in so a change to one surface is forced to account for all the others. If empty, the gate
67
+ warns but does not block; the guarantee only applies to registered flows.
68
+
62
69
  ## Model routing & enforcement
63
70
 
64
71
  - Concrete models are set in `.opencode/agents/<name>.md` (`model:` field) and
@@ -66,18 +73,34 @@ hardcoding domain assumptions.
66
73
  - Personas also carry mechanical guardrails in frontmatter:
67
74
  - thinkers (`planner`, `product-manager`) → `permission: { edit: deny, bash: deny }`
68
75
  - `qa-architect`, `handoff` → `permission: { bash: deny }`
76
+ - code personas (`frontend-engineer`, `mobile-engineer`, `ui-designer`,
77
+ `data-engineer`, `devops-engineer`, `security-engineer`, `system-architect`,
78
+ `diagnostics-expert`) → `permission.edit` allows everything EXCEPT
79
+ `opencode.json`, `.harness.json`, `.opencode/**`, `.agents/**` (governance
80
+ and harness files are read-only), plus `bash: allow`
69
81
  - deterministic personas → `temperature: 0.1`
70
82
  - all personas → a `steps:` cap (cost ceiling)
71
83
  - `planner`, `handoff` → `hidden: true`
72
84
  - `.agents/skills/*/SKILL.md` carries a `model:` label only (informational).
73
85
  - `npx @dombaras/agent-harness list` prints the persona → model mapping.
74
86
  - `npm run test:dispatch` mechanically verifies model pins against
75
- `scripts/qa/models.allowlist.txt`.
87
+ `scripts/qa/models.allowlist.txt` and that `model-routing.md` does not name a
88
+ stale model no longer pinned anywhere (doc↔config drift).
76
89
 
77
90
  ## QA gates
78
91
 
79
- - `npm run test:dispatch` — persona model-pin preflight + persona↔skill parity.
92
+ - `npm run test:dispatch` — persona model-pin preflight + persona↔skill parity + model-routing↔pin drift check.
80
93
  - `npm run test:governance` — session wrap-up dispatch-log enforcement.
94
+ - `npm run test:qa-plan` — **diff-coverage + flow-closure gate**:
95
+ every changed CODE path must have a covering assertion (or a waivered reason) in
96
+ `.agents/memory/qa-plan.md` before a change is verified — and every touched flow
97
+ (`.agents/memory/flow-map.md`) must have every sibling surface and declared optional
98
+ variant addressed in the plan's `## Parallel-surface & variant audit`. Kills the
99
+ "verified by a suite that never touched the change" failure mode: a tier label never
100
+ proves coverage. It also makes **"Fix One, Fix All" mechanical**: a change to one
101
+ surface (e.g. web) cannot ship while the same flow in another codebase (e.g. `mobile/`)
102
+ silently keeps old behavior — the plan must name each sibling as covered (`->`) or
103
+ out-of-scope (`audited: reason`, e.g. `deferred — logged in handoff.md`).
81
104
  - `node scripts/qa/check-qa-scripts.js` — warns (or `--strict` fails) when the
82
105
  DoD-referenced runtime QA tiers aren't wired into `package.json`.
83
106
 
@@ -93,6 +116,13 @@ npx @dombaras/agent-harness update --target /path/to/project
93
116
  ```
94
117
 
95
118
  - Overwrites harness-owned files, preserves `.agents/memory/*`.
119
+ - Auto-wires the harness gate scripts (`test:dispatch`, `test:governance`,
120
+ `test:qa-plan`) into the target's `package.json` (merged, add-only).
121
+ - **Auto-commits** only the harness files it changed (`chore(harness): @dombaras/agent-harness
122
+ <old> -> <new>`) and **pushes** to origin, so the next session never sees unexplained
123
+ modified harness files. Your unrelated uncommitted work is never staged.
124
+ - `--no-commit` to skip commit+push, `--no-push` to commit but not push.
125
+ - `init` never commits.
96
126
  - **Non-destructive**: if a harness-owned file was modified locally since the
97
127
  last deploy (tracked by checksum in `.harness.json`), it is backed up to
98
128
  `.harness-backup/<timestamp>/` before overwrite.
@@ -27,6 +27,7 @@ const fs = require("fs");
27
27
  const path = require("path");
28
28
  const crypto = require("crypto");
29
29
  const readline = require("readline");
30
+ const { spawnSync } = require("child_process");
30
31
 
31
32
  const PKG = require("../package.json");
32
33
  const TEMPLATES_DIR = path.resolve(__dirname, "..", "templates");
@@ -35,6 +36,36 @@ const BACKUP_DIR = ".harness-backup";
35
36
 
36
37
  // ---------------------------------------------------------------- helpers
37
38
 
39
+ /* Numeric semver compare (major.minor.patch; prerelease tags ignored).
40
+ * Returns <0 / 0 / >0 like a normal comparator. */
41
+ function compareVersions(a, b) {
42
+ const pa = String(a || "0").split(".").map((n) => parseInt(n, 10) || 0);
43
+ const pb = String(b || "0").split(".").map((n) => parseInt(n, 10) || 0);
44
+ for (let i = 0; i < 3; i++) {
45
+ if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) > (pb[i] || 0) ? 1 : -1;
46
+ }
47
+ return 0;
48
+ }
49
+
50
+ /* Best-effort: the latest `dist-tags.latest` for PKG.name on the configured
51
+ * registry. Returns null when the registry is unreachable / package unknown,
52
+ * so the check never blocks a deploy. */
53
+ function publishedLatestVersion() {
54
+ const npmCli = process.env.npm_execpath
55
+ ? path.join(path.dirname(process.env.npm_execpath), "npm-cli.js")
56
+ : null;
57
+ const args = ["view", PKG.name, "version", "--json", "--fetch-timeout=5000", "--fetch-retries=0"];
58
+ const r = npmCli
59
+ ? spawnSync(process.execPath, [npmCli, ...args], { encoding: "utf8", timeout: 10000 })
60
+ : spawnSync("npm", args, { encoding: "utf8", timeout: 10000 });
61
+ if (r.status !== 0) return null;
62
+ try {
63
+ return JSON.parse(r.stdout.trim());
64
+ } catch (_) {
65
+ return r.stdout.trim() || null;
66
+ }
67
+ }
68
+
38
69
  function listFiles(dir, base = dir) {
39
70
  const out = [];
40
71
  if (!fs.existsSync(dir)) return out;
@@ -135,6 +166,95 @@ function mergeOpencodeJson(target, variables) {
135
166
  return JSON.stringify(merged, null, 2) + "\n";
136
167
  }
137
168
 
169
+ // Harness-shipped npm scripts auto-wired into the target project's package.json
170
+ // scripts (merged, not clobbered). Every other script the project has is preserved.
171
+ // These gates ship with the harness, so a deploy wires them up like opencode.json.
172
+ const HARNESS_SCRIPTS = {
173
+ "test:dispatch": "node scripts/qa/check-dispatch-config.js",
174
+ "test:governance": "node scripts/qa/governance.js",
175
+ "test:qa-plan": "node scripts/qa/check-qa-plan.js",
176
+ };
177
+
178
+ // Merge harness gate scripts into the project's existing package.json, preserving
179
+ // every other script. ADD-ONLY: an existing script is never overwritten, so a
180
+ // project's deliberate customization of a gate survives re-deploys. Returns
181
+ // { text, hadPkg }.
182
+ function mergePackageJson(target) {
183
+ const p = path.join(target, "package.json");
184
+ const hadPkg = fs.existsSync(p);
185
+ const existing = hadPkg ? safeJson(fs.readFileSync(p, "utf8"), {}) : {};
186
+ const scripts = Object.assign({}, existing.scripts || {});
187
+ for (const [name, cmd] of Object.entries(HARNESS_SCRIPTS)) {
188
+ if (!(name in scripts)) scripts[name] = cmd;
189
+ }
190
+ const merged = { ...existing, scripts };
191
+ return { text: JSON.stringify(merged, null, 2) + "\n", hadPkg };
192
+ }
193
+
194
+ // ---------------------------------------------------------------- git helpers
195
+
196
+ function runGit(target, args) {
197
+ const result = require("child_process").spawnSync("git", args, {
198
+ cwd: target,
199
+ encoding: "utf8",
200
+ });
201
+ return { ok: result.status === 0, status: result.status, out: (result.stdout || "").trim(), err: (result.stderr || "").trim() };
202
+ }
203
+
204
+ function isGitRepo(target) {
205
+ const r = runGit(target, ["rev-parse", "--is-inside-work-tree"]);
206
+ return r.ok;
207
+ }
208
+
209
+ // Auto-commit ONLY the harness-owned files an update just changed, so the target
210
+ // project's next session never sees "unexplained" modified harness files. Scoped
211
+ // to this run's changed harness files (never `git add -A`) so unrelated uncommitted
212
+ // work is NEVER swept in. Memory/backup files are excluded (project data / rollback).
213
+ // Returns { committed:boolean, staged:string[], skipped:string[], pushed:boolean|null }.
214
+ function commitHarnessChanges(target, changedRels, opts) {
215
+ const out = { committed: false, staged: [], skipped: [], pushed: null };
216
+ if (!opts.commit) return out;
217
+ if (!isGitRepo(target)) {
218
+ out.skipped.push("not a git repo");
219
+ return out;
220
+ }
221
+ if (changedRels.length === 0) {
222
+ out.skipped.push("no harness files changed");
223
+ return out;
224
+ }
225
+ const addR = runGit(target, ["add", "--", ...changedRels]);
226
+ if (!addR.ok) {
227
+ out.skipped.push("git add failed: " + addR.err);
228
+ return out;
229
+ }
230
+ const diffCached = runGit(target, ["diff", "--cached", "--name-only"]);
231
+ const staged = (diffCached.out || "").split(/\r?\n/).filter(Boolean).map(relKey);
232
+ if (staged.length === 0) {
233
+ out.skipped.push("no harness changes staged");
234
+ return out;
235
+ }
236
+ out.staged = staged;
237
+ const prior = opts.priorVersion ? opts.priorVersion + " -> " : "";
238
+ const message = `chore(harness): @dombaras/agent-harness ${prior}${PKG.version}`;
239
+ const commitR = runGit(target, ["commit", "-m", message]);
240
+ if (!commitR.ok) {
241
+ out.skipped.push("commit failed: " + commitR.err);
242
+ return out;
243
+ }
244
+ out.committed = true;
245
+ if (opts.push) {
246
+ const hasRemote = runGit(target, ["remote"]).ok && runGit(target, ["remote"]).out;
247
+ if (hasRemote) {
248
+ const pushR = runGit(target, ["push", "origin", "HEAD"]);
249
+ out.pushed = pushR.ok;
250
+ if (!pushR.ok) out.skipped.push("push failed: " + pushR.err);
251
+ } else {
252
+ out.skipped.push("no git remote to push");
253
+ }
254
+ }
255
+ return out;
256
+ }
257
+
138
258
  // ---------------------------------------------------------------- deploy
139
259
 
140
260
  function backup(target, rel, content) {
@@ -145,7 +265,7 @@ function backup(target, rel, content) {
145
265
  }
146
266
 
147
267
  async function deploy(target, opts) {
148
- const { name, domain, yes, dryRun, isUpdate } = opts || {};
268
+ const { name, domain, yes, dryRun, isUpdate, commit, push } = opts || {};
149
269
  const existing = readConfig(target);
150
270
  const projectName = name || existing.projectName || path.basename(target);
151
271
  let projectDomain = domain || existing.projectDomain || "";
@@ -158,6 +278,22 @@ async function deploy(target, opts) {
158
278
  const actions = [];
159
279
  const manifest = {};
160
280
 
281
+ // Auto-wire harness gate scripts into the target package.json (merged, add-only).
282
+ {
283
+ const { text, hadPkg } = mergePackageJson(target);
284
+ const key = "package.json";
285
+ const targetAbs = path.join(target, "package.json");
286
+ const current = hadPkg ? fs.readFileSync(targetAbs, "utf8") : null;
287
+ if (!hadPkg) actions.push({ rel: key, kind: "create" });
288
+ else if (current !== text) actions.push({ rel: key, kind: "merge" });
289
+ else actions.push({ rel: key, kind: "unchanged" });
290
+ manifest[key] = sha256(text);
291
+ if (!dryRun) {
292
+ fs.mkdirSync(path.dirname(targetAbs), { recursive: true });
293
+ fs.writeFileSync(targetAbs, text, "utf8");
294
+ }
295
+ }
296
+
161
297
  for (const { abs, rel } of listFiles(TEMPLATES_DIR)) {
162
298
  const key = relKey(rel);
163
299
  const targetAbs = path.join(target, rel);
@@ -225,7 +361,39 @@ async function deploy(target, opts) {
225
361
  fs.writeFileSync(path.join(target, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n", "utf8");
226
362
  }
227
363
 
228
- return { projectName, projectDomain, actions, dryRun: !!dryRun };
364
+ // Nothing-to-update detection: an update that made no effective change to any
365
+ // harness-owned file and was already running this exact version is a no-op —
366
+ // still report it clearly instead of printing a zero-delta "Updated".
367
+ const changedRelsAll = actions
368
+ .filter((a) => ["create", "overwrite", "overwrite (backup)", "merge"].includes(a.kind))
369
+ .map((a) => a.rel);
370
+ const nothingToUpdate =
371
+ !!isUpdate && changedRelsAll.length === 0 && compareVersions(existing.version || "0", PKG.version) === 0;
372
+
373
+ // Auto-commit only this run's changed harness-owned files (update only).
374
+ let commitResult = null;
375
+ if (isUpdate && !dryRun) {
376
+ const changedRels = changedRelsAll.filter(
377
+ (r) => !r.startsWith(".agents/memory/") && !r.startsWith(".harness-backup/")
378
+ );
379
+ changedRels.push(CONFIG_FILE); // .harness.json reflects the new deployed version
380
+ commitResult = commitHarnessChanges(target, changedRels, {
381
+ commit,
382
+ push,
383
+ priorVersion: existing.version || null,
384
+ });
385
+ }
386
+
387
+ return {
388
+ projectName,
389
+ projectDomain,
390
+ actions,
391
+ dryRun: !!dryRun,
392
+ commitResult,
393
+ isUpdate: !!isUpdate,
394
+ nothingToUpdate,
395
+ priorVersion: existing.version || null,
396
+ };
229
397
  }
230
398
 
231
399
  function summarize(actions) {
@@ -251,10 +419,11 @@ function printSummary(result, isUpdate) {
251
419
 
252
420
  function printNextSteps() {
253
421
  console.log(" Next steps:");
254
- console.log(" 1. Fill in `.agents/memory/domain-map.md` and `.agents/memory/stack-versions.md`.");
255
- console.log(" 2. Add QA gate scripts to package.json:");
256
- console.log(' "test:dispatch": "node scripts/qa/check-dispatch-config.js",');
257
- console.log(' "test:governance": "node scripts/qa/governance.js"');
422
+ console.log(" 1. Fill in `.agents/memory/domain-map.md`, `.agents/memory/stack-versions.md`,");
423
+ console.log(" and `.agents/memory/flow-map.md` (register cross-surface flows so");
424
+ console.log(" test:qa-plan enforces their sibling-surface/variant closure).");
425
+ console.log(" 2. Harness gate scripts (`test:dispatch`, `test:governance`, `test:qa-plan`) were");
426
+ console.log(' auto-wired into package.json "scripts".');
258
427
  console.log(" 3. Restart your agent CLI (config is read once at startup).\n");
259
428
  }
260
429
 
@@ -263,7 +432,12 @@ function printUsage() {
263
432
  `agent-harness v${PKG.version}\n\n` +
264
433
  `Usage:\n` +
265
434
  ` agent-harness init [--target <dir>] [--name <project>] [--domain <desc>] [--yes] [--dry-run]\n` +
266
- ` agent-harness update [--target <dir>] [--dry-run]\n` +
435
+ ` agent-harness update [--target <dir>] [--dry-run] [--no-commit] [--no-push]\n` +
436
+ `\n` +
437
+ ` update auto-commits only the harness files it changes (chore(harness): ...) and\n` +
438
+ ` pushes to origin by default. Use --no-commit to skip commit+push, or --no-push\n` +
439
+ ` to commit but not push. Your unrelated uncommitted work is never staged.\n` +
440
+ `\n` +
267
441
  ` agent-harness list\n` +
268
442
  ` agent-harness --version | -v\n` +
269
443
  ` agent-harness --help | -h\n`
@@ -293,6 +467,8 @@ async function init(target, flags) {
293
467
  yes: !!flags["--yes"],
294
468
  dryRun: !!flags["--dry-run"],
295
469
  isUpdate: false,
470
+ commit: false,
471
+ push: false,
296
472
  });
297
473
  printSummary(result, false);
298
474
  if (!result.dryRun) printNextSteps();
@@ -307,8 +483,29 @@ async function update(target, flags) {
307
483
  yes: true,
308
484
  dryRun: !!flags["--dry-run"],
309
485
  isUpdate: true,
486
+ commit: !flags["--no-commit"],
487
+ push: !flags["--no-commit"] && !flags["--no-push"],
310
488
  });
311
489
  printSummary(result, true);
490
+ printCommitResult(result);
491
+ }
492
+
493
+ function printCommitResult(result) {
494
+ const c = result.commitResult;
495
+ if (!c) return;
496
+ console.log("");
497
+ if (c.committed) {
498
+ console.log(" committed harness files: " + c.staged.join(", "));
499
+ console.log(
500
+ c.pushed === true
501
+ ? " pushed to origin."
502
+ : c.pushed === null
503
+ ? " push skipped (no --push / no remote)."
504
+ : " WARNING: commit made but push FAILED."
505
+ );
506
+ } else {
507
+ console.log(" no harness auto-commit: " + (c.skipped.join("; ") || "no-op"));
508
+ }
312
509
  }
313
510
 
314
511
  function parseArgs(argv) {
@@ -317,6 +514,7 @@ function parseArgs(argv) {
317
514
  const a = argv[i];
318
515
  if (a === "--target" || a === "--name" || a === "--domain") flags[a] = argv[++i];
319
516
  else if (a === "--dry-run" || a === "--yes" || a === "-y") flags[a] = true;
517
+ else if (a === "--no-commit" || a === "--no-push") flags[a] = true;
320
518
  }
321
519
  return flags;
322
520
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dombaras/agent-harness",
3
- "version": "0.1.1",
3
+ "version": "0.1.5",
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"
@@ -16,7 +16,7 @@
16
16
  ],
17
17
  "scripts": {
18
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"
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-plan.js && node --check templates/scripts/qa/check-qa-scripts.js"
20
20
  },
21
21
  "keywords": [
22
22
  "opencode",
@@ -20,7 +20,7 @@ The canonical rulebook. The always-loaded summary is `.agents/rules/00-operating
20
20
  - **Progressive Hydration & Cold-Start**: Never render empty states while waiting on async auth/sync. Hydrate from local cache so the UI renders immediately.
21
21
  - **The 5 Essential UI States**: Every screen/list component must handle Ideal, Empty (with CTA), Loading (skeleton), Error (retry), and Partial/single-item states.
22
22
  - **Error-Handling Contract (Mobile)**: Every `catch` block in a native screen must (1) fire an error haptic, (2) set a user-visible error state that renders inline — never `console.warn` alone, and (3) provide a retry or dismissal path. Follow the reference pattern in `.agents/skills/mobile-engineer/SKILL.md`.
23
- - **Cross-Screen Consistency ("Fix One, Fix All")**: When you implement a UX pattern in one screen, grep for analogous screens and apply it globally; if scope is too large, log a deferred item in the handoff with exact files/patterns. A local-only fix is a regression-in-waiting.
23
+ - **Cross-Screen Consistency ("Fix One, Fix All")**: When you implement a UX pattern in one screen, grep for analogous screens and apply it globally; if scope is too large, log a deferred item in the handoff with exact files/patterns. A local-only fix is a regression-in-waiting. Mechanically enforced by `test:qa-plan` via `.agents/memory/flow-map.md`: a change that touches a registered flow must address EVERY sibling surface (other codebases like `mobile/`, other layers) and every declared optional variant in the QA-plan audit — covered `->`, or `audited:` with a reason (including `deferred — logged in handoff`). A sibling the plan does not name fails the gate.
24
24
  - **Bidirectional & RTL**: Use logical properties (`ps-*`, `pe-*`, `ms-*`, `me-*`, `text-start`, `text-end`) never physical ones (`pl-*`, `left-*`); use inverted horizontal lists / direction-aware layout in native RTL; pass dynamic `dir` to UI primitives.
25
25
  - **Legacy Pruning & Physical Walkthrough**: When introducing a new flow/paradigm, explicitly audit and PRUNE obsolete mechanisms that contradict it — never bolt new steps on top of removed ones. Trace every physical/interaction flow step-by-step from each actor's perspective to eliminate unnecessary friction.
26
26
 
@@ -38,7 +38,7 @@ The canonical rulebook. The always-loaded summary is `.agents/rules/00-operating
38
38
  - **Delegate Big Searches**: Use the `explore` subagent for broad multi-file exploration.
39
39
  - **Prefer Memory over Re-derivation**: Persist non-obvious facts to `.agents/memory/`; read the relevant memory files before starting.
40
40
  - **Locations Map**: Read `.agents/memory/locations.md` first — it is the canonical index of sessions, logs, docs, and data. Never re-hunt for a path.
41
- - **Step Zero — Subagent & Model Routing (non-negotiable)**: Before touching code, satisfy the dispatch gate (`.agents/rules/00-operating.md` §Step Zero). Personas: `planner`, `frontend-engineer`, `mobile-engineer`, `ui-designer`, `qa-architect`, `qa-runner`, `security-engineer`, `product-manager`, `system-architect`, `diagnostics-expert`, `data-engineer`, `devops-engineer`, `handoff`. Dispatch via the `task` tool — each runs its own `model:`. Do NOT inline persona-owned work on the main model. Read `.agents/memory/model-routing.md` at session start. Dispatch independent subagents in parallel.
41
+ - **Step Zero — Subagent & Model Routing (non-negotiable)**: Before touching code, satisfy the dispatch gate (`.agents/rules/00-operating.md` §Step Zero). Personas: `planner`, `frontend-engineer`, `mobile-engineer`, `ui-designer`, `qa-architect`, `qa-runner`, `security-engineer`, `product-manager`, `system-architect`, `diagnostics-expert`, `data-engineer`, `devops-engineer`, `handoff`. Dispatch via the `task` tool — each runs its own `model:`. Do NOT inline persona-owned work on the main model. Read `.agents/memory/model-routing.md` at session start. Dispatch subagents in parallel only when their work is disjoint (see below).
42
42
  - **Persona map**:
43
43
  | Work area | Persona |
44
44
  |---|---|
@@ -55,8 +55,21 @@ The canonical rulebook. The always-loaded summary is `.agents/rules/00-operating
55
55
  | Data ingestion / entity resolution | `data-engineer` |
56
56
  | Deployment / cron / secrets / build+release | `devops-engineer` |
57
57
  | Session wrap-up / handoff | `handoff` |
58
+ - **QA planning is never optional (no silent skip)**: every change that touches
59
+ code paths MUST get a QA plan before it is verified. The plan is authored by
60
+ `qa-architect` (thinker). If `qa-architect` is not dispatched — for ANY reason,
61
+ including dispatch failure or simply not invoking it — the **main orchestrator
62
+ performs the qa-architect role itself** and writes the same coverage plan into
63
+ `.agents/memory/qa-plan.md`. The QA plan/copy escalation ladder NEVER leaves a
64
+ code change unplanned: `qa-architect` -> orchestrator fallback -> (never a silent
65
+ skip). `npm run test:qa-plan` enforces that every changed code path has a covering
66
+ assertion or a waivered reason.
58
67
  - **Waivers** (the only way to skip a persona): a persona may be skipped only when (a) the change is fully covered by an automated gate on push or in the QA tiers, AND (b) the skip is pre-audited in `.agents/memory/` with a cited pointer. Log every waiver as `waived: <persona>` with `reason: <gate|pointer>`.
59
68
  - **Dispatch failure ladder** (never silent): retry once (resume the same `task_id`); if it still fails, do the work inline and log `degraded: <persona> model: <reason>`; never silently skip.
69
+ - **Bound every dispatch (task spec)** — never hand a persona a bare metric ("get under N lines", "type everything"). Each dispatch prompt carries: `Objective` (one deliverable) → `Owned files` (exact paths) → `Read-only files` → `Shared contracts (owns|consumes)` → `Done = <gate command + observable metric>` → `Out-of-scope`.
70
+ - **Parallel = disjoint** — run personas in parallel only when their owned files AND shared contracts are disjoint; otherwise serialize, with the shared-contract owner dispatched first.
71
+ - **Orchestrator acceptance gate** — after a persona reports done, re-run the gate yourself (`tsc --noEmit`, `npm run lint:hooks`, `npm run test:quick`) and grep the metric before integrating or committing. A subagent's `Evidence` is a claim, not proof.
72
+ - **No suppression shortcuts** — never accept (or produce) file-level `/* eslint-disable */`, `@ts-nocheck`, or `@ts-ignore` to hit a metric. Suppressions are line-level only and each carries a reason — a file-level disable is a rules violation, not a fix.
60
73
  - **Synchronous Terminal Commands**: Prefer sync one-shot commands (build/test/lint) that return inline, over background servers/watchers.
61
74
  - **No Output Bloat**: Never paste large files/diffs into chat; apply edits with the edit tool, never print a code block of a change.
62
75
  - **Right-Size the QA Tier**: Never run the full regression suite for a CSS/copy tweak.
@@ -66,7 +79,8 @@ The canonical rulebook. The always-loaded summary is `.agents/rules/00-operating
66
79
  - Never run heavy regression suites blindly for small changes.
67
80
  - Split QA personas: `qa-architect` (thinks — inspects the diff, audits gaps, selects the minimal tier, authors progression tests) vs `qa-runner` (does — executes the chosen tier, reports pass/fail verbatim). The thinker never runs; the doer never designs.
68
81
  - **Static ≠ Runtime rule**: never declare UI/mobile/API changes verified from static typing alone — execute a real runtime path.
69
- - **Tiers** (commands are **project-provided** the target project must define them; the harness ships only `test:dispatch` and `test:governance`):
82
+ - **Coverage label rule**: a tier command verifies a change ONLY when its backing suite actually exercises the changed path. A suite that touches unrelated code proves nothing about this change. `npm run test:qa-plan` mechanically asserts every changed code path is covered by the QA plan (or waivered) before verification — and, via `flow-map.md`, that every touched flow's sibling surfaces and declared optional variants are addressed in the plan's audit (the "fix one, fix all" gate: a web-only diff must still say what the mobile twin does).
83
+ - **Tiers** (commands are **project-provided** — the target project must define them; the harness ships only `test:dispatch`, `test:governance`, and `test:qa-plan`):
70
84
  | Tier | Scope | Command |
71
85
  |---|---|---|
72
86
  | 1 | UI/CSS/mobile/copy | `npm run test:quick` |
@@ -75,7 +89,7 @@ The canonical rulebook. The always-loaded summary is `.agents/rules/00-operating
75
89
  | 4 | Data ingestion | the project's data E2E script |
76
90
  | 5 | Major release / breaking refactor | `npm run test:verify` |
77
91
  | 6 | Security / deps | `npm run test:security` |
78
- - **Progression Workflow**: for new routes/endpoints/states, (1) author a dedicated assertion path, (2) run the targeted progression test, (3) graduate it into the permanent regression suite.
92
+ - **Progression Workflow**: for new routes/endpoints/states, (1) author a dedicated assertion path, (2) run the targeted progression test, (3) graduate it into the permanent regression suite. A change is NOT verified if the executed tier's backing suite never exercised the modified path — author a progression test for that path first (this is the "GAP" rule).
79
93
 
80
94
  ## 7. Definition of Done & Continuous Backup
81
95
 
@@ -88,7 +102,8 @@ Every completed task passes this gate, in order:
88
102
  - [ ] RTL traced if layout touched; logical properties used.
89
103
  - [ ] Cross-screen audit if a new UX pattern was introduced.
90
104
  - [ ] No modified file over ~500 lines without extracting components.
91
- 1. **QA tier** `qa-architect` picks the tier (and authors progression tests); `qa-runner` executes and makes it pass (§6).
105
+ - [ ] No blanket file-level `eslint-disable`/`@ts-nocheck`/`@ts-ignore` suppressions (line-level only, each with a reason).
106
+ 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.
92
107
  2. **Security check** — apply `security-engineer` when the change touches data/auth/input/secrets/deps.
93
108
  3. **Commit** — concise `feat:` / `fix:` / `refactor:` message.
94
109
  4. **Push** — `git push origin main`.
@@ -99,6 +114,7 @@ Every completed task passes this gate, in order:
99
114
  ## 8. Session & Traceability Discipline
100
115
 
101
116
  - **One session = one coherent task**; keep a short checklist as the single source of truth.
117
+ - **Harness-update awareness**: `npx @dombaras/agent-harness update` auto-commits the harness files it changes as `chore(harness): @dombaras/agent-harness <old> -> <new>`. If you see such a commit (or modified `.agents/**`, `scripts/qa/**`, `AGENTS.md`, `opencode.json` you didn't touch), do NOT treat it as unexplained — run `git log -1 --stat` / `git show` to see exactly which harness files changed and why, then read the rulebook. Never guess at the cause.
102
118
  - **Planned vs. Shipped**: at session end, record a "planned → shipped → deferred" delta so aspirational docs are never mistaken for reality. Always verify against live code and schema.
103
119
  - **Dispatch Log (mandatory)**: every wrap-up opens with `subagent → model → shipped/deferred`. An empty log is non-compliant.
104
120
  - **Persist Continuity**: use the `handoff` skill to update `.agents/memory/handoff.md`. Project history lives in `.agents/memory/history.md`; stack facts in `.agents/memory/stack-versions.md`.
@@ -0,0 +1,23 @@
1
+ # Flow map — every user-facing flow and where it lives
2
+
3
+ Project-owned (like all `.agents/memory/*`). This is the registry that turns the
4
+ "Fix One, Fix All" rule (`AGENTS.md` §3) into a **gate**:
5
+ `npm run test:qa-plan` reads this file and, for every flow your diff touches,
6
+ requires the QA plan to explicitly address EVERY sibling surface — the same
7
+ behavior implemented anywhere else: another codebase (web vs `mobile/`), another
8
+ layer (API route), another page — and every OPTIONAL variant (state a surface can
9
+ be in, e.g. "condition selector present" vs "absent") before the change may be
10
+ verified.
11
+
12
+ The failure mode this kills: a change to one surface (e.g. web) ships while its
13
+ twin in another codebase silently keeps the old behavior. The diff is web-only so
14
+ per-file coverage looks green, but the flow is now inconsistent — the exact
15
+ "web/mobile return flow" miss.
16
+
17
+ Keep it current: every time a flow gains/removes a surface or variant, update its
18
+ entry here in the same change.
19
+
20
+ ## flow: return
21
+ behavior: "Mark as Returned" — return POST with an optional condition payload (condition selector)
22
+ surfaces: components/LendingActionDrawers.tsx, mobile/features/return/ReturnStatusSheet.tsx, app/api/transactions/[id]/return/route.ts
23
+ variants: condition-selector-present, condition-selector-absent
@@ -27,6 +27,11 @@ frontmatter:
27
27
  - **Thinkers** (`planner`, `product-manager`) have `permission: { edit: deny, bash: deny }`.
28
28
  - **`qa-architect`** has `permission: { bash: deny }` (authors tests, never runs).
29
29
  - **`handoff`** has `permission: { bash: deny }`.
30
+ - **Code personas** (`frontend-engineer`, `mobile-engineer`, `ui-designer`,
31
+ `data-engineer`, `devops-engineer`, `security-engineer`, `system-architect`,
32
+ `diagnostics-expert`) allow `edit` everywhere except governance/harness paths
33
+ — `opencode.json`, `.harness.json`, `.opencode/**`, `.agents/**` are denied —
34
+ so out-of-scope edits are blocked mechanically, not just by prose.
30
35
  - **Deterministic personas** (`planner`, `qa-architect`, `qa-runner`, `handoff`,
31
36
  `diagnostics-expert`) pin `temperature: 0.1`.
32
37
  - **`steps:`** caps iterations per persona (cost ceiling).
@@ -50,13 +55,18 @@ Every subagent must return a single final message with, in order:
50
55
  ## Gates
51
56
 
52
57
  - `npm run test:dispatch` — `scripts/qa/check-dispatch-config.js` verifies model
53
- pins, persona↔skill parity, and the output-contract marker.
58
+ pins, persona↔skill parity, the output-contract marker, and that this doc does
59
+ not name a concrete model no longer pinned anywhere (doc↔config drift).
54
60
  - `npm run test:governance` — `scripts/qa/governance.js` enforces the wrap-up
55
61
  dispatch log.
62
+ - `npm run test:qa-plan` — `scripts/qa/check-qa-plan.js` enforces diff coverage:
63
+ every changed CODE path must have a covering assertion (or a waivered reason)
64
+ in `.agents/memory/qa-plan.md` before the change is verified.
56
65
  - `scripts/qa/check-qa-scripts.js` — verifies the DoD-referenced QA tier scripts
57
66
  (project-provided) are wired into `package.json`.
58
67
 
59
- Run `test:dispatch` after editing any agent/skill path.
68
+ Run `test:dispatch` after editing any agent/skill path, and `test:qa-plan` on any
69
+ change that touches code paths.
60
70
 
61
71
  ## Gotchas
62
72
 
@@ -0,0 +1,49 @@
1
+ # QA plan — coverage-driven
2
+
3
+ One entry per SHIPPED change. Authored by `qa-architect` (thinker), or by the
4
+ main orchestrator as the mandatory fallback when qa-architect is not dispatched.
5
+ Gated mechanically by `npm run test:qa-plan` — a changed code path that is neither
6
+ covered nor waivered here fails, so the change cannot be declared verified.
7
+
8
+ Keep the LAST change on top. Remove the illustrative examples below; never leave
9
+ a real-looking path in Coverage map without the actual runtime assertion backing it.
10
+
11
+ ## Change intent
12
+
13
+ One sentence naming the SEMANTIC behavior change (what the user experiences), not
14
+ the file list. The flow-closure half of the gate judges sibling surfaces and
15
+ optional variant states against this intent.
16
+
17
+ - change: return flow no longer offers a condition selector — every return POSTs without a condition payload
18
+
19
+ ## Parallel-surface & variant audit
20
+
21
+ For every flow in `.agents/memory/flow-map.md` whose `surfaces:` intersect this
22
+ diff, address EVERY sibling surface and EVERY declared `variant` here — either
23
+ `->` a covering assertion or `audited:` with a reason. `test:qa-plan` fails on any
24
+ sibling surface or variant the plan does not NAME. A sibling that intentionally
25
+ keeps old behavior must say so (`audited: deferred — logged in handoff`), never be
26
+ silently omitted.
27
+
28
+ - components/LendingActionDrawers.tsx -> test:quick::return (click-through: Mark as Returned, no condition selector)
29
+ - mobile/features/return/ReturnStatusSheet.tsx audited: deferred — mobile still renders the selector; same simplification pending, logged in handoff.md
30
+ - app/api/transactions/[id]/return/route.ts -> test:api::return (POST without condition payload succeeds)
31
+ - flow: return variant: condition-selector-present audited: removed by this change — asserting absence only
32
+ - flow: return variant: condition-selector-absent -> test:quick::return (no-selector click-through renders no condition UI)
33
+
34
+ ## Coverage map
35
+
36
+ A `Changed path -> executing test/assertion` list. Every changed CODE path needs a
37
+ runtime assertion that ACTUALLY exercises it (never trust a tier label — verify the
38
+ backing suite reaches the path). Paths may be files or directories. Lines starting
39
+ with `#` are ignored.
40
+
41
+ # components/return/ReturnConfirmDrawer.tsx -> test:quick::progression (click-through: Mark as Returned -> confirm -> /api/transactions/[id]/return)
42
+ # app/api/transactions/[id]/return/route.ts -> test:quick::progression (return POST succeeds with no condition payload)
43
+
44
+ ## Waivers
45
+
46
+ Trivial diffs only (pure copy/docs, or a path whose behavior is already covered
47
+ elsewhere). Every waiver MUST carry a `reason:` on the same line.
48
+
49
+ # waived: src/some-screen.tsx reason: copy-only string swap, no runtime path changed
@@ -4,8 +4,12 @@ Always in effect. Full detail lives in `.agents/AGENTS.md`; read it before non-t
4
4
 
5
5
  ## Step Zero — subagent dispatch gate (non-negotiable)
6
6
 
7
- Before reading or editing any file for a task, dispatch the relevant personas via the `task` tool — each runs on its own `model:` (`.opencode/agents/<name>.md`). Dispatch independent subagents in parallel; serialize only on dependencies. Keep only coordination/mechanical work (reads, git, commits) on the main model.
7
+ Before reading or editing any file for a task, dispatch the relevant personas via the `task` tool — each runs on its own `model:` (`.opencode/agents/<name>.md`). Keep only coordination/mechanical work (reads, git, commits) on the main model.
8
8
 
9
+ - **Bound every task before dispatching.** No persona gets a bare metric ("get under N lines", "type everything"). Each dispatch prompt carries a task spec: `Objective` (one deliverable) → `Owned files` (exact paths) → `Read-only files` → `Shared contracts (owns|consumes)` → `Done = <gate command + observable metric>` → `Out-of-scope`.
10
+ - **Parallel = disjoint.** Dispatch two personas in parallel only when their owned files AND shared contracts are disjoint; otherwise serialize and put the shared-contract owner first.
11
+ - **Accept, don't trust.** After a code persona reports done, the orchestrator re-runs the gate itself (`tsc --noEmit`, `lint:hooks`, `test:quick`) and greps the metric before integrating — a subagent's `Evidence` is a claim, not proof.
12
+ - **QA planning is never optional.** Every change touching code paths gets a QA plan (coverage map in `.agents/memory/qa-plan.md`) from `qa-architect`; if it isn't dispatched, the main orchestrator plans in its place. `npm run test:qa-plan` fails on any changed code path with no covering assertion or waiver, and on any touched flow (`.agents/memory/flow-map.md`) whose sibling surfaces / declared variants the plan doesn't address.
9
13
  - Persona map, waivers, and the dispatch-failure ladder: `.agents/AGENTS.md` §5 and `.agents/memory/model-routing.md`.
10
14
  - Every subagent returns the output contract (`Result` → `Evidence` → `Deferred & risks`).
11
15
  - Wrap up with a **dispatch log** (`subagent → model → shipped/deferred`) in `.agents/memory/handoff.md`.
@@ -17,7 +21,7 @@ Before reading or editing any file for a task, dispatch the relevant personas vi
17
21
  3. **5 UI states** (ideal/empty/loading/error/partial), user-visible errors (never `console.warn`-only), and RTL-safe logical props.
18
22
  4. **Verify stack versions** before writing framework code (`.agents/memory/stack-versions.md`).
19
23
  5. **Token discipline** — search before read, targeted reads, batch reads, don't re-read unchanged files, right-size QA.
20
- 6. **Static ≠ runtime** — never declare verified from `tsc` alone; execute a real runtime/API path.
24
+ 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").
21
25
  7. **Commit** with `feat:`/`fix:`/`refactor:` then push to main.
22
26
 
23
27
  ## Definition of Done
@@ -57,6 +57,7 @@ Whenever you are writing frontend code or dealing with build errors, you MUST st
57
57
  - **500-Line Rule**: No single `.tsx` screen file should exceed ~500 lines. If it does, extract logically distinct sections (modals, list items, action handlers, sub-views) into separate component files in a co-located directory (e.g., `mobile/components/transactions/`). "Extract" means MOVE code into typed modules — never minify/compress JSX onto single lines to dodge the count, never swap real types for `any`, and never add `.d.ts` overrides or inline `require()` to silence tsc.
58
58
  - **Before adding code to a file that already exceeds 500 lines**: STOP. Refactor first, then add. Never grow a monolith.
59
59
  - **One component = one responsibility**: A screen file should orchestrate layout and state. Rendering logic for individual cards, list items, modals, or drawers should be in dedicated components.
60
+ - **No suppression shortcuts**: never silence lint/type errors with file-level `/* eslint-disable */`, `@ts-nocheck`, or `@ts-ignore` to hit a metric. Suppressions are line-level only, and each must carry a reason.
60
61
 
61
62
  ## Frontend Quality Exit Checklist
62
63
  Before declaring any UI work done, verify:
@@ -38,3 +38,7 @@ You own the native React Native layer and its gotchas — not shared web UI (tha
38
38
  config, handoff, other screens) to satisfy a metric.
39
39
  - **Verify before done**: run `tsc --noEmit`, `npm run lint:hooks`, `npm run test:quick`
40
40
  yourself and report the observed output — never claim success you did not run.
41
+ - **No suppression shortcuts**: never silence the gate with a file-level
42
+ `/* eslint-disable */`, `@ts-nocheck`, or `@ts-ignore`. Suppressions are
43
+ line-level only and each carries a reason — a file-level disable to hit a
44
+ metric is a rules violation, not a fix.