@dombaras/agent-harness 0.1.9 → 0.1.11

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
@@ -34,11 +34,12 @@ npx @dombaras/agent-harness init --target . --dry-run
34
34
  | Path | Content | Ownership |
35
35
  |---|---|---|
36
36
  | `AGENTS.md` | root dispatcher (points to the rulebook) | harness (overwrite) |
37
+ | `BACKLOG.md` | canonical task list (Open / Frozen / Archive / Not tracking) | **project** (create-if-missing) |
37
38
  | `.opencode/agents/*.md` | 14 persona subagent defs (with `model:` pins + `permission`/`steps`/`temperature`/`hidden`) | harness (overwrite) |
38
39
  | `.agents/AGENTS.md` | full canonical operating rulebook | harness (overwrite) |
39
40
  | `.agents/rules/00-operating.md` | always-loaded rules summary (wired into `opencode.json` `instructions`) | harness (overwrite) |
40
41
  | `.agents/skills/*/SKILL.md` | persona instruction skills | harness (overwrite) |
41
- | `.agents/features/INDEX.md` | feature backlog + status board (durable feature doc tree root) | **project** (create-if-missing) |
42
+ | `.agents/features/INDEX.md` | pointer to `BACKLOG.md` (per-feature doc tree root) | **project** (create-if-missing) |
42
43
  | `opencode.json` | main/small model routing + `instructions` | harness (**merged**, see below) |
43
44
  | `scripts/qa/*` | `test:dispatch` / `test:governance` / `test:qa-plan` gates + QA-script wiring check | harness (overwrite) |
44
45
  | `.agents/memory/*` | project data (domain-map, stack-versions, handoff, locations, model-routing, history, flow-map, qa-plan) | **project** (create-if-missing) |
@@ -61,15 +62,16 @@ scaffolds for you to fill in — the skills and rules reference memory instead o
61
62
  hardcoding domain assumptions.
62
63
 
63
64
  **Features (`.agents/features/`)** — the durable, feature-centric plan of record. A
64
- found item is captured as one light row in `.agents/features/INDEX.md` (backlog: what's
65
- known, priority, context, status). When a feature is picked up, the `features` persona
66
- authors the per-feature doc chain (`.agents/features/<slug>/`), each layer routed to its
67
- author persona — `intent.md` (product-manager) → `scope.md` (system-architect, HIGH-risk
68
- only) → `plan.md` (planner) → `tests.md` (qa-architect, points to `qa-plan.md`). Ceremony
69
- is right-sized to blast radius: trivial → none, low single `intent.md`, high (auth/DB/
70
- public API/flow-siblings)full chain. Docs live beside the code, are written-back before
71
- continuing when implementation invalidates them, and are archived on ship. See
72
- `.agents/skills/features/SKILL.md`.
65
+ found item is captured as one light row in the canonical `BACKLOG.md` (repo root; the
66
+ single write-path for all open work: what's known, priority, evidence). When a feature
67
+ is picked up, the `features` persona authors the per-feature doc chain
68
+ (`.agents/features/<slug>/`), each layer routed to its author persona — `intent.md`
69
+ (product-manager) → `scope.md` (system-architect, HIGH-risk only) `plan.md` (planner)
70
+ `tests.md` (qa-architect, points to `qa-plan.md`). Ceremony is right-sized to blast
71
+ radius: trivialnone, low single `intent.md`, high (auth/DB/public API/flow-siblings)
72
+ full chain. Docs live beside the code, are written-back before continuing when
73
+ implementation invalidates them, and are archived on ship. `.agents/features/INDEX.md`
74
+ is a pointer to `BACKLOG.md`. See `.agents/skills/features/SKILL.md`.
73
75
 
74
76
  **Flow map (`.agents/memory/flow-map.md`)** — the project-owned registry powering the
75
77
  flow-closure half of `test:qa-plan`. Each user-facing flow lists the code surfaces that
@@ -106,11 +108,20 @@ warns but does not block; the guarantee only applies to registered flows.
106
108
  `deepseek/deepseek-v4-pro` (default, direct API key), `zen`/`opencode` →
107
109
  `opencode/deepseek-v4-pro` (gateway), `pickle` → `opencode/big-pickle`, or any
108
110
  explicit `provider/model`. Restart opencode after switching (config reads once).
111
+ - **`/board` command** — every deploy ships an opencode command
112
+ (`.opencode/command/board.md`) that refreshes the canonical `BACKLOG.md` board:
113
+ `git pull --ff-only` (safe, never forces), then renders Open (by priority),
114
+ Frozen, the last few shipped rows, and the newest `.agents/memory/handoff.md`
115
+ delta. Read-only — it never edits the board.
109
116
 
110
117
  ## QA gates
111
118
 
112
119
  - `npm run test:dispatch` — persona model-pin preflight + persona↔skill parity + model-routing↔pin drift check.
113
120
  - `npm run test:governance` — session wrap-up dispatch-log enforcement.
121
+ - `npm run test:backlog` — canonical single-backlog gate: `BACKLOG.md` is the one
122
+ source of truth; status/priority enums, frozen-reopen conditions, non-empty
123
+ priority/evidence, no duplicate IDs, and `INDEX.md`/`todo.md` must be pointers (not
124
+ live lists).
114
125
  - `npm run test:qa-plan` — **diff-coverage + flow-closure gate**:
115
126
  every changed CODE path must have a covering assertion (or a waivered reason) in
116
127
  `.agents/memory/qa-plan.md` before a change is verified — and every touched flow
@@ -135,9 +146,9 @@ scripts are **project-provided** — the rulebook references them, and
135
146
  npx @dombaras/agent-harness update --target /path/to/project
136
147
  ```
137
148
 
138
- - Overwrites harness-owned files, preserves `.agents/memory/*`.
149
+ - Overwrites harness-owned files, preserves `.agents/memory/*` and `BACKLOG.md`.
139
150
  - Auto-wires the harness gate scripts (`test:dispatch`, `test:governance`,
140
- `test:qa-plan`) into the target's `package.json` (merged, add-only).
151
+ `test:qa-plan`, `test:backlog`) into the target's `package.json` (merged, add-only).
141
152
  - **Auto-commits** only the harness files it changed (`chore(harness): @dombaras/agent-harness
142
153
  <old> -> <new>`) and **pushes** to origin, so the next session never sees unexplained
143
154
  modified harness files. Your unrelated uncommitted work is never staged.
@@ -162,6 +173,18 @@ npx @dombaras/agent-harness update --target /path/to/project
162
173
  - `opencode.json` is merged (project keys preserved).
163
174
  - `--dry-run` previews the plan without writing.
164
175
 
176
+ ### Backlog migration (`migrate-backlog`)
177
+
178
+ ```bash
179
+ npx @dombaras/agent-harness migrate-backlog --target /path/to/project
180
+ ```
181
+
182
+ Consolidates any live `.agents/features/INDEX.md` F-rows and
183
+ `.agents/memory/todo.md` list items into the canonical `BACKLOG.md`, then rewrites
184
+ both as pointers. Idempotent — re-running never duplicates rows (it only merges into
185
+ an empty board) — and never deletes a source before `BACKLOG.md` is written and
186
+ verified. Logs a summary: `N open · M frozen · K archived`.
187
+
165
188
  ## Develop
166
189
 
167
190
  ```bash
@@ -7,6 +7,7 @@
7
7
  * Commands:
8
8
  * init [--target <dir>] [--name <project>] [--domain <desc>] [--yes] [--dry-run]
9
9
  * update [--target <dir>] [--dry-run]
10
+ * migrate-backlog [--target <dir>]
10
11
  * list
11
12
  * --version | -v
12
13
  * --help | -h
@@ -16,6 +17,8 @@
16
17
  *
17
18
  * Ownership model:
18
19
  * - `.agents/memory/*` + `.agents/features/*` is project data -> create-if-missing (never overwrite).
20
+ * - `BACKLOG.md` (repo root) is project data -> create-if-missing (never overwrite); it is the
21
+ * canonical task list, and `update` must never clobber the project's live board.
19
22
  * - `opencode.json` is MERGED: harness manages $schema/model/small_model/
20
23
  * instructions; every other key the project adds is preserved.
21
24
  * - all other harness-owned files are overwritten. If a harness-owned file was
@@ -99,6 +102,18 @@ function isProjectScaffold(rel) {
99
102
  (parts.includes("memory") || parts.includes("features"))
100
103
  );
101
104
  }
105
+ /* The canonical `BACKLOG.md` at the repo root is also project data (a live task
106
+ * list the project owns): scaffolded create-if-missing, never overwritten so an
107
+ * `update` can never clobber the project's board, and never tracked in the
108
+ * manifest (its content is the project's, not the harness's). */
109
+ function isBacklogFile(rel) {
110
+ return relKey(rel) === "BACKLOG.md";
111
+ }
112
+ /* Combined: any file that must be scaffolded create-if-missing rather than
113
+ * overwritten. */
114
+ function isCreateIfMissing(rel) {
115
+ return isProjectScaffold(rel) || isBacklogFile(rel);
116
+ }
102
117
  /* Back-compat alias (kept for any external caller referencing the old name). */
103
118
  function isMemoryFile(rel) {
104
119
  return isProjectScaffold(rel);
@@ -183,6 +198,7 @@ const HARNESS_SCRIPTS = {
183
198
  "test:dispatch": "node scripts/qa/check-dispatch-config.js",
184
199
  "test:governance": "node scripts/qa/governance.js",
185
200
  "test:qa-plan": "node scripts/qa/check-qa-plan.js",
201
+ "test:backlog": "node scripts/qa/check-backlog.js",
186
202
  };
187
203
 
188
204
  // Merge harness gate scripts into the project's existing package.json, preserving
@@ -350,7 +366,7 @@ async function deploy(target, opts) {
350
366
  continue;
351
367
  }
352
368
 
353
- if (isProjectScaffold(rel)) {
369
+ if (isCreateIfMissing(rel)) {
354
370
  if (fs.existsSync(targetAbs)) {
355
371
  actions.push({ rel: key, kind: "preserve" });
356
372
  } else {
@@ -500,11 +516,11 @@ function printNextSteps() {
500
516
  console.log(" 1. Fill in `.agents/memory/domain-map.md`, `.agents/memory/stack-versions.md`,");
501
517
  console.log(" and `.agents/memory/flow-map.md` (register cross-surface flows so");
502
518
  console.log(" test:qa-plan enforces their sibling-surface/variant closure).");
503
- console.log(" 2. Harness gate scripts (`test:dispatch`, `test:governance`, `test:qa-plan`) were");
504
- console.log(' auto-wired into package.json "scripts".');
505
- console.log(" 3. Capture found items as rows in `.agents/features/INDEX.md`; dispatch the `features`");
506
- console.log(" persona to author the per-feature doc chain when one is picked up.");
507
- console.log(" 4. Restart your agent CLI (config is read once at startup).\n");
519
+ console.log(" 2. Harness gate scripts (`test:dispatch`, `test:governance`, `test:qa-plan`,");
520
+ console.log(" `test:backlog`) were auto-wired into package.json \"scripts\".");
521
+ console.log(" 3. Capture found items as one light row in the canonical `BACKLOG.md` (repo root);");
522
+ console.log(" dispatch the `features` persona to author the per-feature doc chain when one is picked up.");
523
+ console.log(" 4. Restart your agent CLI (config is read once at startup).\n");
508
524
  }
509
525
 
510
526
  function printUsage() {
@@ -513,11 +529,17 @@ function printUsage() {
513
529
  `Usage:\n` +
514
530
  ` agent-harness init [--target <dir>] [--name <project>] [--domain <desc>] [--yes] [--dry-run]\n` +
515
531
  ` agent-harness update [--target <dir>] [--dry-run] [--no-commit] [--no-push]\n` +
532
+ ` agent-harness migrate-backlog [--target <dir>]\n` +
516
533
  `\n` +
517
534
  ` update auto-commits only the harness files it changes (chore(harness): ...) and\n` +
518
535
  ` pushes to origin by default. Use --no-commit to skip commit+push, or --no-push\n` +
519
536
  ` to commit but not push. Your unrelated uncommitted work is never staged.\n` +
520
537
  `\n` +
538
+ ` migrate-backlog consolidates any live \`.agents/features/INDEX.md\` F-rows and\n` +
539
+ ` \`.agents/memory/todo.md\` into the canonical BACKLOG.md, then rewrites those\n` +
540
+ ` files as pointers. Idempotent: re-running never duplicates rows or deletes a\n` +
541
+ ` source before BACKLOG.md is written and verified.\n` +
542
+ `\n` +
521
543
  ` agent-harness list\n` +
522
544
  ` agent-harness --version | -v\n` +
523
545
  ` agent-harness --help | -h\n`
@@ -534,6 +556,222 @@ function printPersonas(templatesDir) {
534
556
  }
535
557
  }
536
558
 
559
+ // ---------------------------------------------------------------- backlog migration
560
+
561
+ /* Split a markdown table row on unescaped `|` (respects `\|` inside cells). */
562
+ function splitCells(row) {
563
+ const cells = [];
564
+ let cur = "";
565
+ let esc = false;
566
+ for (const ch of row) {
567
+ if (esc) {
568
+ cur += ch;
569
+ esc = false;
570
+ } else if (ch === "\\") {
571
+ esc = true;
572
+ cur += ch;
573
+ } else if (ch === "|") {
574
+ cells.push(cur.trim());
575
+ cur = "";
576
+ } else {
577
+ cur += ch;
578
+ }
579
+ }
580
+ cells.push(cur.trim());
581
+ return cells;
582
+ }
583
+
584
+ /* Parse markdown table data rows, skipping headers and separator/empty rows. */
585
+ function parseTableRows(src) {
586
+ const rows = [];
587
+ for (const raw of String(src || "").split(/\r?\n/)) {
588
+ const line = raw.trim();
589
+ if (!/^\|.*\|$/.test(line)) continue;
590
+ const cells = splitCells(line.slice(1, -1));
591
+ if (cells.every((c) => /^[-:|\s]*$/.test(c))) continue; // separator
592
+ if (cells.every((c) => c.trim() === "")) continue; // empty row
593
+ if (
594
+ cells.some((c) =>
595
+ /^(ID|[Ii]d|P|Task|Item|Priority|Context|Status|Evidence|Why frozen|Closed|Why frozen \/ reopen when)$/.test(c)
596
+ )
597
+ )
598
+ continue; // header
599
+ rows.push(cells);
600
+ }
601
+ return rows;
602
+ }
603
+
604
+ /* Parse markdown list items (bullets / checkboxes), preserving the text. */
605
+ function parseListItems(src) {
606
+ const items = [];
607
+ for (const raw of String(src || "").split(/\r?\n/)) {
608
+ const m = raw.match(/^\s*[-*]\s+(?:\[[ x]\]\s*)?(.+)$/);
609
+ if (m && m[1].trim()) items.push(m[1].trim());
610
+ }
611
+ return items;
612
+ }
613
+
614
+ /* Map an old INDEX.md F-row (ID | Item | Priority | Context | Status) into the
615
+ * BACKLOG schema. Returns null for rows that carry no usable identity. */
616
+ function mapIndexRow(cells) {
617
+ const id = (cells[0] || "").trim();
618
+ if (!id) return null;
619
+ const task = (cells[1] || "").trim() || id;
620
+ const rawPriority = (cells[2] || "").trim();
621
+ const context = (cells[3] || "").trim();
622
+ const status = (cells[4] || "").trim().toLowerCase();
623
+
624
+ // Reconcile the priority direction: old `P0` (blocking) == new `P1`.
625
+ let priority = rawPriority;
626
+ if (priority === "P0") priority = "P1";
627
+ if (!/^P[1-4]$/.test(priority)) priority = "?";
628
+
629
+ const evidence = context || "?";
630
+ const base = { id, task, priority, evidence };
631
+
632
+ if (/^(shipped|archived|done|closed)$/.test(status)) {
633
+ return { ...base, section: "archive", closed: status };
634
+ }
635
+ if (status === "frozen") {
636
+ return { ...base, section: "frozen", reopen: context || "frozen (migrated); reopen when evidence recurs" };
637
+ }
638
+ return { ...base, section: "open" };
639
+ }
640
+
641
+ /* Ensure the canonical BACKLOG.md exists (create-if-missing from the template),
642
+ * then — ONLY when it is still empty — consolidate a live INDEX.md F-rows list
643
+ * and a live todo.md list into it, and rewrite both as pointers. Idempotent:
644
+ * a re-run finds BACKLOG.md already populated and leaves it untouched. */
645
+ function migrateBacklog(target) {
646
+ const backlogPath = path.join(target, "BACKLOG.md");
647
+ const indexPath = path.join(target, ".agents", "features", "INDEX.md");
648
+ const todoPath = path.join(target, ".agents", "memory", "todo.md");
649
+ const templatePath = path.join(TEMPLATES_DIR, "BACKLOG.md");
650
+
651
+ const summary = { open: 0, frozen: 0, archived: 0, sources: [], createdBacklog: false, rewritten: [] };
652
+
653
+ // 1. Scaffold BACKLOG.md if missing (never overwrite an existing board).
654
+ if (!fs.existsSync(backlogPath)) {
655
+ if (!fs.existsSync(templatePath)) {
656
+ throw new Error("templates/BACKLOG.md not found next to this CLI.");
657
+ }
658
+ fs.mkdirSync(path.dirname(backlogPath), { recursive: true });
659
+ fs.writeFileSync(backlogPath, fs.readFileSync(templatePath, "utf8"), "utf8");
660
+ summary.createdBacklog = true;
661
+ }
662
+
663
+ // 2. Only merge into an empty board — idempotency guard against duplicates.
664
+ const existing = fs.readFileSync(backlogPath, "utf8");
665
+ const alreadyHasRows = parseTableRows(existing).length > 0;
666
+ if (alreadyHasRows) {
667
+ summary.skipped = "BACKLOG.md already has rows — nothing merged";
668
+ return summary;
669
+ }
670
+
671
+ // 3. Read the two sources.
672
+ const open = [];
673
+ const frozen = [];
674
+ const archive = [];
675
+ const seenIds = new Set();
676
+
677
+ const push = (row) => {
678
+ if (!row || seenIds.has(row.id)) return;
679
+ seenIds.add(row.id);
680
+ if (row.section === "archive") archive.push(row);
681
+ else if (row.section === "frozen") frozen.push(row);
682
+ else open.push(row);
683
+ };
684
+
685
+ if (fs.existsSync(indexPath)) {
686
+ const indexSrc = fs.readFileSync(indexPath, "utf8");
687
+ const isPointer = /BACKLOG\.md/.test(indexSrc);
688
+ if (!isPointer) {
689
+ summary.sources.push(".agents/features/INDEX.md");
690
+ for (const row of parseTableRows(indexSrc)) push(mapIndexRow(row));
691
+ }
692
+ }
693
+
694
+ let tCounter = 0;
695
+ if (fs.existsSync(todoPath)) {
696
+ const todoSrc = fs.readFileSync(todoPath, "utf8");
697
+ const isPointer = /BACKLOG\.md/.test(todoSrc);
698
+ if (!isPointer) {
699
+ summary.sources.push(".agents/memory/todo.md");
700
+ for (const item of parseListItems(todoSrc)) {
701
+ tCounter += 1;
702
+ push({
703
+ id: `T-${String(tCounter).padStart(3, "0")}`,
704
+ task: item,
705
+ priority: "?",
706
+ evidence: ".agents/memory/todo.md",
707
+ section: "open",
708
+ });
709
+ }
710
+ }
711
+ }
712
+
713
+ // 4. Emit BACKLOG.md only if there was something to migrate.
714
+ const total = open.length + frozen.length + archive.length;
715
+ if (total === 0) {
716
+ summary.skipped = "no live rows found in INDEX.md / todo.md";
717
+ return summary;
718
+ }
719
+
720
+ const toRow = (r) => `| ${r.id} | ${r.priority} | ${r.task} | ${r.evidence} |`;
721
+ const frozenRow = (r) => `| ${r.id} | ${r.task} | ${r.reopen} |`;
722
+ const archiveRow = (r) => `| ${r.id} | ${r.task} | ${r.closed} |`;
723
+
724
+ const table = (headers, rows) =>
725
+ [`| ${headers.join(" | ")} |`, `|${headers.map(() => "---").join("|")}|`, ...rows].join("\n");
726
+
727
+ const openBlock =
728
+ open.length === 0
729
+ ? ""
730
+ : `## Open\n\nSorted by priority (P1 → P4).\n\n${table(["ID", "P", "Task", "Evidence"], open.map(toRow))}`;
731
+ const frozenBlock =
732
+ frozen.length === 0
733
+ ? ""
734
+ : `## Frozen\n\nParked, not deleted. Reopen only when the stated condition recurs.\n\n${table(
735
+ ["ID", "Task", "Why frozen / reopen when"],
736
+ frozen.map(frozenRow)
737
+ )}`;
738
+ const archiveBlock =
739
+ archive.length === 0
740
+ ? ""
741
+ : `## Archive (shipped · done)\n\n${table(["ID", "Task", "Closed"], archive.map(archiveRow))}`;
742
+
743
+ // Rebuild the board: keep the template's usage header, replace the body.
744
+ const header = existing.split(/^## Active \/ next/m)[0].trim();
745
+ const body = [header, "## Active / next\n", openBlock, frozenBlock, archiveBlock, "## Not tracking (leave-in-place, no action)\n"]
746
+ .filter((b) => b.length > 0)
747
+ .join("\n\n") + "\n";
748
+
749
+ fs.writeFileSync(backlogPath, body, "utf8");
750
+
751
+ // 5. Verify BACKLOG.md is written and non-empty BEFORE rewriting sources.
752
+ const verify = fs.readFileSync(backlogPath, "utf8");
753
+ if (parseTableRows(verify).length === 0) {
754
+ throw new Error("refusing to rewrite sources: BACKLOG.md was not written with rows");
755
+ }
756
+
757
+ // 6. Rewrite sources as pointers (never before the board is verified).
758
+ const pointerFor = (name) =>
759
+ `# ${name}\n\nThe backlog lives in \`BACKLOG.md\` (repo root). Per-feature docs still\nlive in \`.agents/features/<slug>/\`. This file is a pointer, not a list.\n`;
760
+ if (fs.existsSync(indexPath) && !/BACKLOG\.md/.test(fs.readFileSync(indexPath, "utf8"))) {
761
+ fs.writeFileSync(indexPath, pointerFor("Features"), "utf8");
762
+ summary.rewritten.push(".agents/features/INDEX.md");
763
+ }
764
+ if (fs.existsSync(todoPath) && !/BACKLOG\.md/.test(fs.readFileSync(todoPath, "utf8"))) {
765
+ fs.writeFileSync(todoPath, pointerFor("Todo"), "utf8");
766
+ summary.rewritten.push(".agents/memory/todo.md");
767
+ }
768
+
769
+ summary.open = open.length;
770
+ summary.frozen = frozen.length;
771
+ summary.archived = archive.length;
772
+ return summary;
773
+ }
774
+
537
775
  // ---------------------------------------------------------------- commands
538
776
 
539
777
  async function init(target, flags) {
@@ -628,6 +866,24 @@ function main() {
628
866
  process.exit(1);
629
867
  });
630
868
  break;
869
+ case "migrate-backlog":
870
+ try {
871
+ const s = migrateBacklog(target);
872
+ if (s.skipped) {
873
+ console.log(`\n[migrate-backlog] ${s.skipped}`);
874
+ } else {
875
+ console.log(
876
+ `\n[migrate-backlog] consolidated ${s.open} open · ${s.frozen} frozen · ${s.archived} archived`
877
+ );
878
+ if (s.sources.length) console.log(` read from: ${s.sources.join(", ")}`);
879
+ if (s.rewritten.length) console.log(` rewritten as pointers: ${s.rewritten.join(", ")}`);
880
+ if (s.createdBacklog) console.log(" created BACKLOG.md from the template");
881
+ }
882
+ } catch (e) {
883
+ console.error(e);
884
+ process.exit(1);
885
+ }
886
+ break;
631
887
  case "list":
632
888
  printPersonas(TEMPLATES_DIR);
633
889
  break;
@@ -650,6 +906,11 @@ module.exports = {
650
906
  resolveVars,
651
907
  mergeOpencodeJson,
652
908
  deploy,
909
+ migrateBacklog,
910
+ splitCells,
911
+ parseTableRows,
912
+ parseListItems,
913
+ mapIndexRow,
653
914
  printPersonas,
654
915
  TEMPLATES_DIR,
655
916
  CONFIG_FILE,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dombaras/agent-harness",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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-plan.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 && node --check templates/scripts/qa/check-backlog.js"
20
20
  },
21
21
  "keywords": [
22
22
  "opencode",
@@ -57,7 +57,7 @@ The canonical rulebook. The always-loaded summary is `.agents/rules/00-operating
57
57
  | Session wrap-up / handoff | `handoff` |
58
58
  | Feature backlog + per-feature doc tree (intent/scope/plan/tests) | `features` |
59
59
  - **Feature docs, not ceremony**: capture a found item as one light row in
60
- `.agents/features/INDEX.md` (backlog). When a feature is picked up, dispatch
60
+ the canonical `BACKLOG.md` (repo root). When a feature is picked up, dispatch
61
61
  `features` to author the doc chain — `intent` (product-manager) → `scope`
62
62
  (system-architect, HIGH-risk only) → `plan` (planner) → `tests` (qa-architect) —
63
63
  each by its author persona. Match ceremony to blast radius: trivial → none,
@@ -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 passes this gate, in order:
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. **Commit** — concise `feat:` / `fix:` / `refactor:` message.
119
- 4. **Push** — `git push origin main`.
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`).
@@ -1,22 +1,8 @@
1
- # Features — backlog & status board
1
+ # Features — pointer
2
2
 
3
- Project-owned (like all `.agents/features/*`). This is the **collection box**, not a
4
- spec and not a status board. Every found item gets one terse row here; the per-feature
5
- doc chain (`.agents/features/<slug>/` intent · scope · plan · tests) is written only
6
- when the main agent focuses on the feature. Keep rows ultra-light — a note, not a promise.
3
+ The backlog lives in **`BACKLOG.md`** (repo root) the canonical task list. Per-feature
4
+ spec docs (`.agents/features/<slug>/` intent · scope · plan · tests) still live here in
5
+ this tree, owned by the `features` persona (`.agents/skills/features/SKILL.md`).
7
6
 
8
- Maintained by the `features` persona (`.agents/skills/features/SKILL.md`).
9
-
10
- ## Backlog
11
-
12
- One row per found item. `Status`: `backlog → ready → in-progress → testing → shipped → archived`.
13
-
14
- | ID | Item (what's known so far) | Priority | Context | Status |
15
- |----|----------------------------|----------|---------|--------|
16
- | F-001 | _replace: short, only-what-is-known description; open questions with `?`_ | P3 | _pointer to evidence (file/log/ticket/words), not a re-derivation_ | backlog |
17
-
18
- <!-- Add rows as items surface. Never invent status or priority — leave blank/`?` if unknown. -->
19
-
20
- ## Active / next
21
-
22
- - _when a feature is about to be worked, cite it here and route the docs to the author personas (see skill)._
7
+ This file is a pointer, not a list. Capture a found item as one row in `BACKLOG.md`;
8
+ when a feature is picked up, dispatch `features` to author the per-feature doc chain.
@@ -27,7 +27,7 @@ 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
- - **`features`** (Feature Registrar) has `permission: { bash: deny }` — owns `.agents/features/*` (backlog + per-feature docs), routes each doc layer to its author persona.
30
+ - **`features`** (Feature Registrar) has `permission: { bash: deny }` — owns `BACKLOG.md` (repo root) + `.agents/features/*` (per-feature docs), routes each doc layer to its author persona.
31
31
  - **Code personas** (`frontend-engineer`, `mobile-engineer`, `ui-designer`,
32
32
  `data-engineer`, `devops-engineer`, `security-engineer`, `system-architect`,
33
33
  `diagnostics-expert`) allow `edit` everywhere except governance/harness paths
@@ -11,7 +11,7 @@ Before reading or editing any file for a task, dispatch the relevant personas vi
11
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
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.
13
13
  - Persona map, waivers, and the dispatch-failure ladder: `.agents/AGENTS.md` §5 and `.agents/memory/model-routing.md`.
14
- - **Features**: capture a found item as one light row in `.agents/features/INDEX.md`; on pickup dispatch `features` to author the per-feature doc chain (`.agents/skills/features/SKILL.md`). Right-size ceremony to risk.
14
+ - **Features**: capture a found item as one light row in the canonical `BACKLOG.md` (repo root); on pickup dispatch `features` to author the per-feature doc chain (`.agents/skills/features/SKILL.md`). Right-size ceremony to risk.
15
15
  - Every subagent returns the output contract (`Result` → `Evidence` → `Deferred & risks`).
16
16
  - Wrap up with a **dispatch log** (`subagent → model → shipped/deferred`) in `.agents/memory/handoff.md`.
17
17
 
@@ -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
- Right-sized QA tier security lens (if the change touches data/auth/input/secrets) → commit → push.
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`. 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.
@@ -1,16 +1,17 @@
1
1
  ---
2
2
  name: features
3
- description: Use when the user asks to capture a found item, register a feature, manage the feature backlog, or prepare a feature for implementation — owns the .agents/features/ doc tree (INDEX + per-feature intent/scope/plan/tests).
3
+ description: Use when the user asks to capture a found item, register a feature, manage the feature backlog, or prepare a feature for implementation — owns the BACKLOG.md task list and the .agents/features/ doc tree (per-feature intent/scope/plan/tests).
4
4
  model: mechanical
5
5
  ---
6
6
 
7
7
  # Features — backlog & per-feature doc tree
8
8
 
9
- You are the Feature Registrar for {{PROJECT_NAME}}. You keep the
10
- `.agents/features/` tree as the durable, feature-centric plan of record — the
11
- backlog of found items plus, once a feature is picked up, the intent → scope →
12
- plan → tests docs that frame it. You coordinate the authors; you do NOT pull the
13
- implementation work onto yourself (that is dispatched to the code personas).
9
+ You are the Feature Registrar for {{PROJECT_NAME}}. You keep the canonical
10
+ `BACKLOG.md` (repo root) as the single task list of record — the backlog of found
11
+ items plus, once a feature is picked up, the `.agents/features/<slug>/` intent →
12
+ scope → plan → tests docs that frame it. You coordinate the authors; you do NOT
13
+ pull the implementation work onto yourself (that is dispatched to the code
14
+ personas).
14
15
 
15
16
  The doc tree is **complementary** to the session memory (`.agents/memory/*`):
16
17
  features are durable and survive across sessions; `handoff.md`/`qa-plan.md` track
@@ -20,7 +21,7 @@ per-session execution. Link between them, never duplicate.
20
21
 
21
22
  | Layer | What it is | Weight |
22
23
  |---|---|---|
23
- | **Backlog** (`.agents/features/INDEX.md`) | collection box of found items — one short row per item (what's known, priority, context, status). NOT a spec, NOT a status board. | ultra-light — every item gets one |
24
+ | **Backlog** (`BACKLOG.md`, repo root) | collection box of found items — one short row per item (what's known, priority, evidence, section). NOT a spec, NOT a status board. `.agents/features/INDEX.md` is a pointer to it. | ultra-light — every item gets one |
24
25
  | **Feature docs** (`.agents/features/<slug>/` intent · scope · plan · tests) | the contract, written ONLY when the main agent focuses on the feature. | right-sized to risk |
25
26
 
26
27
  A found item is **only** ever a backlog row until it is picked up. Do not
@@ -30,35 +31,49 @@ the heavy SDLC the harness is designed to avoid.
30
31
  ## Tree layout
31
32
 
32
33
  ```
34
+ BACKLOG.md # canonical task list (Open / Frozen / Archive / Not tracking)
33
35
  .agents/features/
34
- INDEX.md # backlog + status board (one row per item)
36
+ INDEX.md # pointer to BACKLOG.md
35
37
  <slug>/
36
- intent.md # what & why, boundaries, acceptance criteria (1 pager)
37
- scope.md # optional — only on HIGH-risk features
38
- plan.md # implementation decomposition (HOW)
39
- tests.md # intent-level acceptance + pointer to qa-plan.md
40
- archive/ # completed feature dirs moved here
38
+ intent.md # what & why, boundaries, acceptance criteria (1 pager)
39
+ scope.md # optional — only on HIGH-risk features
40
+ plan.md # implementation decomposition (HOW)
41
+ tests.md # intent-level acceptance + pointer to qa-plan.md
42
+ archive/ # completed feature dirs moved here
41
43
  ```
42
44
 
43
45
  Save docs beside the code they describe; keep them in the same commit as the
44
46
  code that implements them so a feature's intent and its delivery cannot drift.
45
47
 
46
- ## Backlog — `.agents/features/INDEX.md`
48
+ ## Backlog — `BACKLOG.md`
47
49
 
48
50
  One row per found item. Add a row when the user brings up a new idea/need/bug
49
- that is not yet being worked. Fields (keep each terse):
51
+ that is not yet being worked. One write-path only: capture → `BACKLOG.md`, update
52
+ it in the same session the work happens. Sections (see the `BACKLOG.md` header):
53
+
54
+ - **Active / next** (optional) — the next few picks, cited as `ID (P)`.
55
+ - **Open** — `| ID | P | Task | Evidence |`, sorted by priority.
56
+ - **Frozen** — parked, don't-do-but-don't-delete; MUST carry a *why* and a *reopen condition*.
57
+ - **Archive (shipped · done)** — completed rows, never deleted.
58
+ - **Not tracking** — leave-in-place features, one prose note.
59
+
60
+ Fields (keep each terse):
50
61
 
51
62
  - **What's known so far** — only what is actually known; mark open questions with `?`.
52
- - **Priority** — `P0` (blocking) / `P1` (important) / `P2` (nice-to-have) / `P3` (backlog).
53
- - **Context** — a pointer to evidence (file, log, ticket, the user's words), never a re-derivation.
54
- - **Status** — `backlog ready in-progress testing shipped archived`.
63
+ - **Priority** — `P1` (blocking) / `P2` (important) / `P3` (nice-to-have) / `P4` (later).
64
+ Applies to open rows only.
65
+ - **Evidence** — a pointer to evidence (file, log, ticket, the user's words), never a re-derivation.
66
+ - **Status ladder** — `backlog → ready → in-progress → testing → shipped → archived`, plus `frozen`.
55
67
 
56
- Never invent status or priority. If a field is unknown, leave it blank or `?` and
57
- say so; the row is a note, not a promise.
68
+ Never invent status or priority. If a field is unknown, write `?` an explicit
69
+ unknown, never a blank cell. `npm run test:backlog` fails on a blank priority or
70
+ evidence, a duplicate ID, an illegal status/priority, a `frozen` row without a
71
+ reopen condition, or a live `.agents/features/INDEX.md` / `.agents/memory/todo.md`
72
+ that isn't a pointer.
58
73
 
59
74
  ## Feature lifecycle (when the main agent focuses on a feature)
60
75
 
61
- Pick the ready item from `INDEX.md`, then author the docs **by routing each layer
76
+ Pick the ready item from `BACKLOG.md`, then author the docs **by routing each layer
62
77
  to the correct persona** via the `task` tool (Step Zero dispatch — see
63
78
  `.agents/rules/00-operating.md` §Step Zero):
64
79
 
@@ -72,7 +87,7 @@ to the correct persona** via the `task` tool (Step Zero dispatch — see
72
87
  ### Right-size the ceremony to blast radius
73
88
 
74
89
  - **Trivial** (copy/docs, CSS tweak, single-file bug, anything a human would fix
75
- in <30 minutes): no feature docs. Optionally note it in `INDEX.md` and close it.
90
+ in <30 minutes): no feature docs. Optionally note it in `BACKLOG.md` and close it.
76
91
  - **Low risk** (touches few files, no auth/DB/API contract): ONE `intent.md`
77
92
  (what & why, boundaries, acceptance) is enough. Fold scope into it.
78
93
  - **High risk** (auth/security, DB schema/migration, public API, payments, a flow
@@ -87,17 +102,18 @@ docs restating the same boundary is drift surface, not rigor.
87
102
  - **Write back first, then continue.** If implementation invalidates any doc,
88
103
  update the doc BEFORE coding continues. A stale spec is worse than none.
89
104
  - **Archive when it ships.** Move `.agents/features/<slug>/` to `archive/` and
90
- mark the `INDEX.md` row `shipped`/`archived`. Do not leave a pile of completed
91
- designs cluttering the live tree.
105
+ move the `BACKLOG.md` row to `Archive (shipped · done)` (or mark it
106
+ `shipped`/`archived`). Do not leave a pile of completed designs cluttering the
107
+ live tree.
92
108
  - Each doc starts with a `status:` line (`proposed → scoped → planned → testing →
93
109
  shipped → archived`) so an agent knows validity without reading the whole file.
94
110
  - When implementation drifts from a doc, fix the implementation to match the
95
111
  doc; when the doc is genuinely wrong, update the doc explicitly (with a one-line
96
- note in `INDEX.md`), then regenerate the derived layers — never let the code
97
- and the doc mutely diverge.
112
+ note in the `BACKLOG.md` row's evidence), then regenerate the derived layers —
113
+ never let the code and the doc mutely diverge.
98
114
 
99
115
  ## Output contract (always return)
100
116
 
101
- 1. **Backlog delta** — items added / status changes, in `.agents/features/INDEX.md`.
117
+ 1. **Backlog delta** — items added / status changes, in `BACKLOG.md`.
102
118
  2. **Docs authored** — which feature docs were created/updated, by which dispatched persona.
103
119
  3. **Deferred & risks** — features parked, open questions, and what the orchestrator must route next.
@@ -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.
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Use when the user asks to capture a found item, register/manage a feature, or prepare a feature for implementation — owns the .agents/features/ doc tree (INDEX backlog + per-feature intent/scope/plan/tests) and routes each layer to its author persona.
2
+ description: Use when the user asks to capture a found item, register/manage a feature, or prepare a feature for implementation — owns the BACKLOG.md task list and the .agents/features/ doc tree (per-feature intent/scope/plan/tests) and routes each layer to its author persona.
3
3
  mode: subagent
4
4
  model: opencode/gpt-5-nano
5
5
  temperature: 0.1
@@ -13,8 +13,8 @@ You are the {{PROJECT_NAME}} Feature Registrar. Read and follow the complete per
13
13
 
14
14
  ## Scope & integrity (non-negotiable)
15
15
 
16
- - Edit ONLY `.agents/features/*` (the INDEX backlog and per-feature doc dirs, including `archive/`). `opencode.json`, `.agents/memory/*`, `.agents/rules/*`, `.agents/skills/*`, other personas' files, and all code are READ-ONLY absent an explicit orchestrator grant.
16
+ - Edit ONLY `BACKLOG.md` (repo root) and `.agents/features/*` (the per-feature doc dirs, including `archive/`). `opencode.json`, `.agents/memory/*`, `.agents/rules/*`, `.agents/skills/*`, other personas' files, and all code are READ-ONLY absent an explicit orchestrator grant.
17
17
  - You coordinate authors; you do NOT implement. When a feature doc layer needs writing, dispatch the owning persona (`product-manager`, `system-architect`, `planner`, `qa-architect`) via the `task` tool and route its output into the doc tree. Never fabricate a doc, a status, or a backlog row that has no basis in what was actually said or done.
18
- - Keep the two layers separate: every found item is a light `INDEX.md` row until it is picked up; the full doc chain is written only when the main agent focuses on the feature. Right-size ceremony to risk (trivial → none, low → `intent.md` only, high → full chain).
18
+ - Keep the two layers separate: every found item is a light `BACKLOG.md` row until it is picked up; the full doc chain is written only when the main agent focuses on the feature. Right-size ceremony to risk (trivial → none, low → `intent.md` only, high → full chain).
19
19
 
20
20
  Return your final message in this exact order: **Result** (backlog delta + docs authored) -> **Evidence** (files changed, personas dispatched, observed output) -> **Deferred & risks** (features parked, open questions, next routing). Keep it under ~15 lines.
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: Refresh the task board — pull the latest, then show BACKLOG.md open/frozen/shipped plus the newest handoff delta.
3
+ ---
4
+
5
+ Refresh the canonical task board. Run these steps read-only, then report terse.
6
+
7
+ 1. Sync the workspace (never rewrite history, never force):
8
+ - `git fetch --all --prune`
9
+ - `git pull --ff-only`
10
+ - If the pull fails (uncommitted local changes / conflict), do NOT force — report the failure and show the board from the current on-disk state, flagged "possibly stale".
11
+
12
+ 2. Read `BACKLOG.md` (repo root) and `.agents/memory/handoff.md`.
13
+
14
+ 3. Render a compact board:
15
+ - `Active / next` picks (if any).
16
+ - `Open` grouped by priority P1 → P4, each row as `ID — task (evidence)`.
17
+ - `Frozen` rows with their reopen condition (one line each).
18
+ - `Archive` — only the last ~5 shipped rows (date/commit).
19
+ - Latest handoff delta (planned → shipped → deferred) from `.agents/memory/handoff.md`, plus any new `git log` commits from other sessions since the last refresh.
20
+
21
+ Rules:
22
+ - Read-only: never edit `BACKLOG.md`, `.agents/memory/*`, or any code here.
23
+ - This is a status snapshot, not a report. If a field is unknown write `?` — never invent.
@@ -5,8 +5,8 @@ The always-loaded rules summary lives in [`.agents/rules/00-operating.md`](.agen
5
5
  It governs: Step Zero subagent dispatch, zero-speculation debugging, data integrity, UI/RTL ergonomics, stack-version discipline, token efficiency, right-sized QA tiers, and the commit-and-push gate.
6
6
 
7
7
  - **Dispatch personas** via the `task` tool (see `.agents/rules/00-operating.md` §Step Zero); consult the relevant `.agents/skills/<persona>/SKILL.md`.
8
- - **Features**: capture a found item as one light row in `.agents/features/INDEX.md` (backlog). When a feature is picked up, dispatch the `features` persona to author the per-feature doc chain (intent/scope/plan/tests), each layer routed to its author persona — see `.agents/skills/features/SKILL.md`.
8
+ - **Features**: capture a found item as one light row in the canonical `BACKLOG.md` (repo root). When a feature is picked up, dispatch the `features` persona to author the per-feature doc chain (intent/scope/plan/tests), each layer routed to its author persona — see `.agents/skills/features/SKILL.md`.
9
9
  - **Every change touching code paths gets a QA plan** (`.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.
10
- - **At session start**, read `.agents/memory/locations.md` and `.agents/memory/model-routing.md`.
10
+ - **At session start**, read `.agents/memory/locations.md`, `.agents/memory/model-routing.md`, and `BACKLOG.md`.
11
11
 
12
12
  > Deployed and maintained by `@dombaras/agent-harness` (`npx @dombaras/agent-harness init` / `update`). Do not hand-edit harness-owned files — regenerate them and commit the deltas.
@@ -0,0 +1,52 @@
1
+ # BACKLOG — canonical task list
2
+
3
+ Single source of truth for **all** open work: features, bugs, follow-ups, audits,
4
+ roadmaps, ideas. Every other list (`.agents/features/INDEX.md`,
5
+ `.agents/memory/todo.md`, historical docs) points here. One row per task — if it's
6
+ not a row, it doesn't exist.
7
+
8
+ **Status ladder:** `backlog → ready → in-progress → testing → shipped → archived`,
9
+ plus `frozen` (parked — keep a *why* and a *reopen condition*, never delete).
10
+ **Priority (`P`):** `P1` blocking · `P2` important · `P3` nice-to-have · `P4` later.
11
+ Priority applies to open rows only. **Evidence** = a pointer (file/log/ticket/words),
12
+ never a re-derivation. Leave `?` where unknown — don't invent.
13
+
14
+ > Updated in the same session the work happens. Archive `shipped` rows; freeze
15
+ > don't-delete. Historical design docs are aspirational — verify against live code.
16
+
17
+ ## Active / next
18
+
19
+ - _the next few picks (optional): cite each as `ID (P)`._
20
+
21
+ ## Open
22
+
23
+ Sorted by priority (P1 → P4).
24
+
25
+ | ID | P | Task | Evidence |
26
+ |----|---|------|----------|
27
+ | | | | |
28
+
29
+ _Rows: `| F-001 | P3 | short desc; open questions with \`?\` | pointer, never a re-derivation |`_
30
+
31
+ ## Frozen
32
+
33
+ Parked, not deleted. Reopen only when the stated condition recurs.
34
+
35
+ | ID | Task | Why frozen / reopen when |
36
+ |----|------|--------------------------|
37
+ | | | |
38
+
39
+ _Rows: `| F-002 | short desc | why + reopen condition (required) |`_
40
+
41
+ ## Archive (shipped · done)
42
+
43
+ | ID | Task | Closed |
44
+ |----|------|--------|
45
+ | | | |
46
+
47
+ _Rows: `| F-003 | short desc | shipped/done + date/commit |`_
48
+
49
+ ## Not tracking (leave-in-place, no action)
50
+
51
+ _One prose note listing built-and-stable items deliberately left in place (no
52
+ investment, no removal). Omit the section if there is nothing to note._
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /*
4
+ * test:backlog — canonical-single-backlog gate (repo guard).
5
+ *
6
+ * Mechanically enforces the harness's ONE-write-path rule: `BACKLOG.md` at the
7
+ * repo root is the single source of truth for open work, and every other list
8
+ * (`.agents/features/INDEX.md`, `.agents/memory/todo.md`) is a pointer (or
9
+ * absent), never a live list.
10
+ *
11
+ * 1. Only ONE canonical backlog file exists: the repo-root `BACKLOG.md`.
12
+ * 2. Priority values are from {P1..P4} and apply to open rows only.
13
+ * 3. Every `frozen` row has a non-empty "why / reopen" condition.
14
+ * 4. Every `open` row has non-empty priority and evidence cells. `?` is
15
+ * allowed but must be explicit — a blank cell is a failure.
16
+ * 5. No duplicate IDs across the whole file.
17
+ * 6. `INDEX.md` / `todo.md`, if present, are pointers (content referencing
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.
21
+ */
22
+ const fs = require("fs");
23
+ const path = require("path");
24
+ const { execFileSync } = require("child_process");
25
+
26
+ const root = path.resolve(__dirname, "..", "..");
27
+
28
+ const PRIORITY = new Set(["P1", "P2", "P3", "P4"]);
29
+
30
+ const failures = [];
31
+ const warnings = [];
32
+ const check = (ok, msg) => {
33
+ console.log(` ${ok ? "\u2713" : "\u2717"} ${msg}`);
34
+ if (!ok) failures.push(msg);
35
+ };
36
+
37
+ console.log("backlog gate \u2014 canonical single-file BACKLOG.md");
38
+
39
+ // ---- 1. Only ONE canonical backlog file ------------------------------------
40
+ const canonical = path.join(root, "BACKLOG.md");
41
+ if (!fs.existsSync(canonical)) {
42
+ console.error("FATAL: missing BACKLOG.md at repo root (the canonical task list)");
43
+ process.exit(2);
44
+ }
45
+ check(true, "canonical BACKLOG.md present at repo root");
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
+
81
+ const src = fs.readFileSync(canonical, "utf8");
82
+ const lines = src.split(/\r?\n/);
83
+
84
+ // ---- table parsing ----------------------------------------------------------
85
+ // Split a markdown table row on unescaped `|` (respects `\|` inside cells).
86
+ function splitCells(row) {
87
+ const cells = [];
88
+ let cur = "";
89
+ let esc = false;
90
+ for (const ch of row) {
91
+ if (esc) {
92
+ cur += ch;
93
+ esc = false;
94
+ } else if (ch === "\\") {
95
+ esc = true;
96
+ cur += ch;
97
+ } else if (ch === "|") {
98
+ cells.push(cur.trim());
99
+ cur = "";
100
+ } else {
101
+ cur += ch;
102
+ }
103
+ }
104
+ cells.push(cur.trim());
105
+ return cells;
106
+ }
107
+
108
+ // Group table rows by the last `##`/`###` section heading that preceded them.
109
+ function parseSectionRows(lines) {
110
+ const buckets = {};
111
+ let current = null;
112
+ let rowCount = 0;
113
+ for (const raw of lines) {
114
+ const heading = raw.match(/^#{2,3}\s+(.*)$/);
115
+ if (heading) {
116
+ current = heading[1].trim();
117
+ buckets[current] = buckets[current] || [];
118
+ continue;
119
+ }
120
+ if (!current) continue;
121
+ const row = raw.trim();
122
+ if (!/^\|.*\|$/.test(row)) continue;
123
+ const inner = row.slice(1, -1);
124
+ const cells = splitCells(inner);
125
+ // skip empty rows and separators like `|---|---|`
126
+ if (cells.every((c) => /^[-:|\s]*$/.test(c) && c.trim().length === 0)) continue;
127
+ if (cells.every((c) => /^[-:|\s]*$/.test(c))) continue;
128
+ const joined = cells.join("|").trim();
129
+ if (/^([-:|\s]*)$/.test(joined)) continue;
130
+ // skip header rows named ID/P/Task/...
131
+ if (cells.some((c) => /^(ID|[Ii]d|P|Task|Evidence|Why frozen \/ reopen when|Closed)$/.test(c))) continue;
132
+ buckets[current].push(cells);
133
+ rowCount++;
134
+ }
135
+ return { buckets, rowCount };
136
+ }
137
+
138
+ const { buckets, rowCount } = parseSectionRows(lines);
139
+
140
+ // Column indexes per section (from the BACKLOG template schema).
141
+ const COLS = {
142
+ open: { id: 0, priority: 1, evidence: 3 },
143
+ frozen: { id: 0, reopen: 2 },
144
+ "archive (shipped · done)": { id: 0 },
145
+ };
146
+
147
+ function bucketRows(key) {
148
+ // tolerate loose heading variants
149
+ return buckets[key] || [];
150
+ }
151
+
152
+ const openRows = bucketRows("Open");
153
+ const frozenRows = bucketRows("Frozen");
154
+ const archiveRows = bucketRows("Archive (shipped · done)") || bucketRows("Archive");
155
+
156
+ // ---- 3. Frozen requires a reopen condition ---------------------------------
157
+ for (const r of frozenRows) {
158
+ const reopen = (r[COLS.frozen.reopen] || "").trim();
159
+ check(
160
+ reopen.length > 0,
161
+ `frozen row ${r[COLS.frozen.id]}: must carry a "why frozen / reopen when" condition`
162
+ );
163
+ }
164
+
165
+ // ---- 2 + 4. Open rows: valid priority + non-empty evidence -----------------
166
+ for (const r of openRows) {
167
+ const id = r[COLS.open.id] || "?";
168
+ const priority = (r[COLS.open.priority] || "").trim();
169
+ const evidence = (r[COLS.open.evidence] || "").trim();
170
+ if (priority.length === 0) {
171
+ check(false, `open row ${id}: priority must not be blank (use P1..P4, or \`?\` if unknown)`);
172
+ } else if (!PRIORITY.has(priority)) {
173
+ check(false, `open row ${id}: priority \`${priority}\` not in {P1,P2,P3,P4}`);
174
+ } else {
175
+ check(true, `open row ${id}: priority \`${priority}\` is valid`);
176
+ }
177
+ check(
178
+ evidence.length > 0,
179
+ `open row ${id}: evidence must not be blank (pointer, or \`?\` if unknown)`
180
+ );
181
+ }
182
+
183
+ // ---- 5. No duplicate IDs across the whole file -----------------------------
184
+ const allRows = [...openRows.map((r, i) => ({ cells: r, tag: "Open" })),
185
+ ...frozenRows.map((r, i) => ({ cells: r, tag: "Frozen" })),
186
+ ...archiveRows.map((r, i) => ({ cells: r, tag: "Archive" }))];
187
+ const totalRows = allRows.length;
188
+ if (totalRows > 0) {
189
+ check(true, `parsed ${totalRows} row(s) across Open/Frozen/Archive`);
190
+ const seen = new Map();
191
+ for (const { cells, tag } of allRows) {
192
+ const id = (cells[0] || "").trim();
193
+ if (!id) {
194
+ check(false, `${tag}: a row has no ID — rows must carry an ID (F-001, ...)`);
195
+ continue;
196
+ }
197
+ if (seen.has(id)) check(false, `duplicate ID \`${id}\` (${seen.get(id)} and ${tag})`);
198
+ else seen.set(id, tag);
199
+ }
200
+ } else {
201
+ check(true, "empty board (no Open/Frozen/Archive rows yet) — valid for a fresh project");
202
+ }
203
+
204
+ // ---- 6. INDEX.md / todo.md are pointers, not live lists --------------------
205
+ for (const rel of [".agents/features/INDEX.md", ".agents/memory/todo.md"]) {
206
+ const p = path.join(root, rel);
207
+ if (!fs.existsSync(p)) {
208
+ check(true, `${rel} absent`);
209
+ continue;
210
+ }
211
+ const content = fs.readFileSync(p, "utf8");
212
+ const isPointer = /BACKLOG\.md/.test(content);
213
+ const hasList = /^\s*\|.+\|/m.test(content);
214
+ if (isPointer) {
215
+ check(true, `${rel} is a pointer to BACKLOG.md`);
216
+ } else if (hasList) {
217
+ check(false, `${rel} looks like a live list — point it at BACKLOG.md (or remove it)`);
218
+ } else {
219
+ check(true, `${rel} present and not a live list`);
220
+ }
221
+ }
222
+
223
+ console.log(
224
+ `\nRESULT: ${failures.length ? `${failures.length} FAILURE(S)` : "backlog green"}`
225
+ );
226
+ process.exit(failures.length ? 1 : 0);