@dombaras/agent-harness 0.1.8 → 0.1.10

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
@@ -98,11 +100,23 @@ warns but does not block; the guarantee only applies to registered flows.
98
100
  - `npm run test:dispatch` mechanically verifies model pins against
99
101
  `scripts/qa/models.allowlist.txt` and that `model-routing.md` does not name a
100
102
  stale model no longer pinned anywhere (doc↔config drift).
103
+ - **`/switch-model` command** — every deploy ships an opencode command
104
+ (`.opencode/command/switch-model.md`) plus `scripts/switch-reasoning-model.ps1`
105
+ that rewrites the main `opencode.json` `model` and the six reasoning-tier
106
+ agents (planner, system-architect, security-engineer, diagnostics-expert,
107
+ product-manager, qa-architect) in one step. Aliases: `deepseek`/`direct` →
108
+ `deepseek/deepseek-v4-pro` (default, direct API key), `zen`/`opencode` →
109
+ `opencode/deepseek-v4-pro` (gateway), `pickle` → `opencode/big-pickle`, or any
110
+ explicit `provider/model`. Restart opencode after switching (config reads once).
101
111
 
102
112
  ## QA gates
103
113
 
104
114
  - `npm run test:dispatch` — persona model-pin preflight + persona↔skill parity + model-routing↔pin drift check.
105
115
  - `npm run test:governance` — session wrap-up dispatch-log enforcement.
116
+ - `npm run test:backlog` — canonical single-backlog gate: `BACKLOG.md` is the one
117
+ source of truth; status/priority enums, frozen-reopen conditions, non-empty
118
+ priority/evidence, no duplicate IDs, and `INDEX.md`/`todo.md` must be pointers (not
119
+ live lists).
106
120
  - `npm run test:qa-plan` — **diff-coverage + flow-closure gate**:
107
121
  every changed CODE path must have a covering assertion (or a waivered reason) in
108
122
  `.agents/memory/qa-plan.md` before a change is verified — and every touched flow
@@ -127,9 +141,9 @@ scripts are **project-provided** — the rulebook references them, and
127
141
  npx @dombaras/agent-harness update --target /path/to/project
128
142
  ```
129
143
 
130
- - Overwrites harness-owned files, preserves `.agents/memory/*`.
144
+ - Overwrites harness-owned files, preserves `.agents/memory/*` and `BACKLOG.md`.
131
145
  - Auto-wires the harness gate scripts (`test:dispatch`, `test:governance`,
132
- `test:qa-plan`) into the target's `package.json` (merged, add-only).
146
+ `test:qa-plan`, `test:backlog`) into the target's `package.json` (merged, add-only).
133
147
  - **Auto-commits** only the harness files it changed (`chore(harness): @dombaras/agent-harness
134
148
  <old> -> <new>`) and **pushes** to origin, so the next session never sees unexplained
135
149
  modified harness files. Your unrelated uncommitted work is never staged.
@@ -154,6 +168,18 @@ npx @dombaras/agent-harness update --target /path/to/project
154
168
  - `opencode.json` is merged (project keys preserved).
155
169
  - `--dry-run` previews the plan without writing.
156
170
 
171
+ ### Backlog migration (`migrate-backlog`)
172
+
173
+ ```bash
174
+ npx @dombaras/agent-harness migrate-backlog --target /path/to/project
175
+ ```
176
+
177
+ Consolidates any live `.agents/features/INDEX.md` F-rows and
178
+ `.agents/memory/todo.md` list items into the canonical `BACKLOG.md`, then rewrites
179
+ both as pointers. Idempotent — re-running never duplicates rows (it only merges into
180
+ an empty board) — and never deletes a source before `BACKLOG.md` is written and
181
+ verified. Logs a summary: `N open · M frozen · K archived`.
182
+
157
183
  ## Develop
158
184
 
159
185
  ```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 {
@@ -473,7 +489,9 @@ function printSummary(result, isUpdate) {
473
489
 
474
490
  /* Best-effort warning: when this CLI's version is not the latest published on
475
491
  * the registry, flag it. This catches the "bumped + ran update from a local
476
- * checkout, but never published" silent gap. */
492
+ * checkout, but never published" silent gap. Publish guidance stays inline so a
493
+ * failed `npm publish` (401/404 auth/permission) never leaves the developer
494
+ * staring at a raw npm error — it points them at `npm login`. */
477
495
  function printUnpublishedWarning() {
478
496
  const published = publishedLatestVersion();
479
497
  if (published == null || published === "" || published === PKG.version) return;
@@ -485,6 +503,12 @@ function printUnpublishedWarning() {
485
503
  `NOT published — Publish first, then \`update\` other projects, or they will ` +
486
504
  `branch off an older harness.`
487
505
  );
506
+ console.log(
507
+ ` To publish: run \`npm publish\` from the harness repo. If it fails with ` +
508
+ `E401/E404 (auth or permission), you are not logged in as the package owner ` +
509
+ `(\`npm owner ls ${PKG.name}\` shows who owns it) — run \`npm login\` first, ` +
510
+ `then publish again. Login: https://www.npmjs.com/login`
511
+ );
488
512
  }
489
513
 
490
514
  function printNextSteps() {
@@ -492,11 +516,11 @@ function printNextSteps() {
492
516
  console.log(" 1. Fill in `.agents/memory/domain-map.md`, `.agents/memory/stack-versions.md`,");
493
517
  console.log(" and `.agents/memory/flow-map.md` (register cross-surface flows so");
494
518
  console.log(" test:qa-plan enforces their sibling-surface/variant closure).");
495
- console.log(" 2. Harness gate scripts (`test:dispatch`, `test:governance`, `test:qa-plan`) were");
496
- console.log(' auto-wired into package.json "scripts".');
497
- console.log(" 3. Capture found items as rows in `.agents/features/INDEX.md`; dispatch the `features`");
498
- console.log(" persona to author the per-feature doc chain when one is picked up.");
499
- 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");
500
524
  }
501
525
 
502
526
  function printUsage() {
@@ -505,11 +529,17 @@ function printUsage() {
505
529
  `Usage:\n` +
506
530
  ` agent-harness init [--target <dir>] [--name <project>] [--domain <desc>] [--yes] [--dry-run]\n` +
507
531
  ` agent-harness update [--target <dir>] [--dry-run] [--no-commit] [--no-push]\n` +
532
+ ` agent-harness migrate-backlog [--target <dir>]\n` +
508
533
  `\n` +
509
534
  ` update auto-commits only the harness files it changes (chore(harness): ...) and\n` +
510
535
  ` pushes to origin by default. Use --no-commit to skip commit+push, or --no-push\n` +
511
536
  ` to commit but not push. Your unrelated uncommitted work is never staged.\n` +
512
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` +
513
543
  ` agent-harness list\n` +
514
544
  ` agent-harness --version | -v\n` +
515
545
  ` agent-harness --help | -h\n`
@@ -526,6 +556,222 @@ function printPersonas(templatesDir) {
526
556
  }
527
557
  }
528
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
+
529
775
  // ---------------------------------------------------------------- commands
530
776
 
531
777
  async function init(target, flags) {
@@ -620,6 +866,24 @@ function main() {
620
866
  process.exit(1);
621
867
  });
622
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;
623
887
  case "list":
624
888
  printPersonas(TEMPLATES_DIR);
625
889
  break;
@@ -642,6 +906,11 @@ module.exports = {
642
906
  resolveVars,
643
907
  mergeOpencodeJson,
644
908
  deploy,
909
+ migrateBacklog,
910
+ splitCells,
911
+ parseTableRows,
912
+ parseListItems,
913
+ mapIndexRow,
645
914
  printPersonas,
646
915
  TEMPLATES_DIR,
647
916
  CONFIG_FILE,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dombaras/agent-harness",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
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,
@@ -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
 
@@ -31,4 +31,4 @@ Right-sized QA tier → security lens (if the change touches data/auth/input/sec
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: 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.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Use when debugging errors, performance regressions, or API anomalies — log-first investigation, ExternalServiceLog, cold-start profiling, authentic reproduction.
3
3
  mode: subagent
4
- model: opencode/deepseek-v4-pro
4
+ model: deepseek/deepseek-v4-pro
5
5
  temperature: 0.1
6
6
  steps: 30
7
7
  permission:
@@ -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.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Use to plan/decompose a task and emit the Step Zero subagent dispatch plan (personas, models, order, parallel batches) before any file is touched.
3
3
  mode: subagent
4
- model: opencode/deepseek-v4-pro
4
+ model: deepseek/deepseek-v4-pro
5
5
  temperature: 0.1
6
6
  steps: 15
7
7
  hidden: true
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Use when designing features, gamification, or user journeys — maps DB logic to UX with the 5 UI states.
3
3
  mode: subagent
4
- model: opencode/deepseek-v4-pro
4
+ model: deepseek/deepseek-v4-pro
5
5
  steps: 20
6
6
  permission:
7
7
  edit: deny
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Use to design the QA plan for a change — inspects the diff, assesses risk, selects the minimal tier, and authors progression tests (thinker only; does not run suites).
3
3
  mode: subagent
4
- model: opencode/deepseek-v4-pro
4
+ model: deepseek/deepseek-v4-pro
5
5
  temperature: 0.1
6
6
  steps: 20
7
7
  permission:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Use when a change touches auth, secrets, input validation, external services, or dependencies — security review + audit.
3
3
  mode: subagent
4
- model: opencode/deepseek-v4-pro
4
+ model: deepseek/deepseek-v4-pro
5
5
  steps: 30
6
6
  permission:
7
7
  edit:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Use when changing the Prisma schema, data model, multi-tenant boundaries, privacy tiers, or sync protocol.
3
3
  mode: subagent
4
- model: opencode/deepseek-v4-pro
4
+ model: deepseek/deepseek-v4-pro
5
5
  steps: 40
6
6
  permission:
7
7
  edit:
@@ -0,0 +1,25 @@
1
+ ---
2
+ description: Switch the reasoning-tier model (default deepseek/deepseek-v4-pro direct).
3
+ ---
4
+
5
+ Change the reasoning-tier model to `$ARGUMENTS`.
6
+
7
+ Valid values:
8
+ - `deepseek` / `direct` (default) -> `deepseek/deepseek-v4-pro` (direct API key)
9
+ - `zen` / `opencode` -> `opencode/deepseek-v4-pro` (Zen gateway)
10
+ - `pickle` / `big-pickle` -> `opencode/big-pickle`
11
+ - any other explicit model id (e.g. `opencode/gpt-5.6-luna`)
12
+
13
+ Run:
14
+
15
+ ```
16
+ powershell -ExecutionPolicy Bypass -File scripts/switch-reasoning-model.ps1 -Target $ARGUMENTS
17
+ ```
18
+
19
+ This rewrites the `model` field in `opencode.json` and the `model:` frontmatter
20
+ of the six reasoning-tier agents (planner, system-architect, security-engineer,
21
+ diagnostics-expert, product-manager, qa-architect).
22
+
23
+ Report the old model -> new model delta, and remind the user that opencode
24
+ does not hot-reload config, so they must quit and restart for it to take
25
+ effect.
@@ -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._
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://opencode.ai/config.json",
3
- "model": "opencode/deepseek-v4-pro",
3
+ "model": "deepseek/deepseek-v4-pro",
4
4
  "small_model": "opencode/gpt-5-nano",
5
5
  "instructions": [".agents/rules/00-operating.md"]
6
6
  }
@@ -0,0 +1,189 @@
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
+ */
20
+ const fs = require("fs");
21
+ const path = require("path");
22
+
23
+ const root = path.resolve(__dirname, "..", "..");
24
+
25
+ const PRIORITY = new Set(["P1", "P2", "P3", "P4"]);
26
+
27
+ const failures = [];
28
+ const warnings = [];
29
+ const check = (ok, msg) => {
30
+ console.log(` ${ok ? "\u2713" : "\u2717"} ${msg}`);
31
+ if (!ok) failures.push(msg);
32
+ };
33
+
34
+ console.log("backlog gate \u2014 canonical single-file BACKLOG.md");
35
+
36
+ // ---- 1. Only ONE canonical backlog file ------------------------------------
37
+ const canonical = path.join(root, "BACKLOG.md");
38
+ if (!fs.existsSync(canonical)) {
39
+ console.error("FATAL: missing BACKLOG.md at repo root (the canonical task list)");
40
+ process.exit(2);
41
+ }
42
+ check(true, "canonical BACKLOG.md present at repo root");
43
+
44
+ const src = fs.readFileSync(canonical, "utf8");
45
+ const lines = src.split(/\r?\n/);
46
+
47
+ // ---- table parsing ----------------------------------------------------------
48
+ // Split a markdown table row on unescaped `|` (respects `\|` inside cells).
49
+ function splitCells(row) {
50
+ const cells = [];
51
+ let cur = "";
52
+ let esc = false;
53
+ for (const ch of row) {
54
+ if (esc) {
55
+ cur += ch;
56
+ esc = false;
57
+ } else if (ch === "\\") {
58
+ esc = true;
59
+ cur += ch;
60
+ } else if (ch === "|") {
61
+ cells.push(cur.trim());
62
+ cur = "";
63
+ } else {
64
+ cur += ch;
65
+ }
66
+ }
67
+ cells.push(cur.trim());
68
+ return cells;
69
+ }
70
+
71
+ // Group table rows by the last `##`/`###` section heading that preceded them.
72
+ function parseSectionRows(lines) {
73
+ const buckets = {};
74
+ let current = null;
75
+ let rowCount = 0;
76
+ for (const raw of lines) {
77
+ const heading = raw.match(/^#{2,3}\s+(.*)$/);
78
+ if (heading) {
79
+ current = heading[1].trim();
80
+ buckets[current] = buckets[current] || [];
81
+ continue;
82
+ }
83
+ if (!current) continue;
84
+ const row = raw.trim();
85
+ if (!/^\|.*\|$/.test(row)) continue;
86
+ const inner = row.slice(1, -1);
87
+ const cells = splitCells(inner);
88
+ // skip empty rows and separators like `|---|---|`
89
+ if (cells.every((c) => /^[-:|\s]*$/.test(c) && c.trim().length === 0)) continue;
90
+ if (cells.every((c) => /^[-:|\s]*$/.test(c))) continue;
91
+ const joined = cells.join("|").trim();
92
+ if (/^([-:|\s]*)$/.test(joined)) continue;
93
+ // skip header rows named ID/P/Task/...
94
+ if (cells.some((c) => /^(ID|[Ii]d|P|Task|Evidence|Why frozen \/ reopen when|Closed)$/.test(c))) continue;
95
+ buckets[current].push(cells);
96
+ rowCount++;
97
+ }
98
+ return { buckets, rowCount };
99
+ }
100
+
101
+ const { buckets, rowCount } = parseSectionRows(lines);
102
+
103
+ // Column indexes per section (from the BACKLOG template schema).
104
+ const COLS = {
105
+ open: { id: 0, priority: 1, evidence: 3 },
106
+ frozen: { id: 0, reopen: 2 },
107
+ "archive (shipped · done)": { id: 0 },
108
+ };
109
+
110
+ function bucketRows(key) {
111
+ // tolerate loose heading variants
112
+ return buckets[key] || [];
113
+ }
114
+
115
+ const openRows = bucketRows("Open");
116
+ const frozenRows = bucketRows("Frozen");
117
+ const archiveRows = bucketRows("Archive (shipped · done)") || bucketRows("Archive");
118
+
119
+ // ---- 3. Frozen requires a reopen condition ---------------------------------
120
+ for (const r of frozenRows) {
121
+ const reopen = (r[COLS.frozen.reopen] || "").trim();
122
+ check(
123
+ reopen.length > 0,
124
+ `frozen row ${r[COLS.frozen.id]}: must carry a "why frozen / reopen when" condition`
125
+ );
126
+ }
127
+
128
+ // ---- 2 + 4. Open rows: valid priority + non-empty evidence -----------------
129
+ for (const r of openRows) {
130
+ const id = r[COLS.open.id] || "?";
131
+ const priority = (r[COLS.open.priority] || "").trim();
132
+ const evidence = (r[COLS.open.evidence] || "").trim();
133
+ if (priority.length === 0) {
134
+ check(false, `open row ${id}: priority must not be blank (use P1..P4, or \`?\` if unknown)`);
135
+ } else if (!PRIORITY.has(priority)) {
136
+ check(false, `open row ${id}: priority \`${priority}\` not in {P1,P2,P3,P4}`);
137
+ } else {
138
+ check(true, `open row ${id}: priority \`${priority}\` is valid`);
139
+ }
140
+ check(
141
+ evidence.length > 0,
142
+ `open row ${id}: evidence must not be blank (pointer, or \`?\` if unknown)`
143
+ );
144
+ }
145
+
146
+ // ---- 5. No duplicate IDs across the whole file -----------------------------
147
+ const allRows = [...openRows.map((r, i) => ({ cells: r, tag: "Open" })),
148
+ ...frozenRows.map((r, i) => ({ cells: r, tag: "Frozen" })),
149
+ ...archiveRows.map((r, i) => ({ cells: r, tag: "Archive" }))];
150
+ const totalRows = allRows.length;
151
+ if (totalRows > 0) {
152
+ check(true, `parsed ${totalRows} row(s) across Open/Frozen/Archive`);
153
+ const seen = new Map();
154
+ for (const { cells, tag } of allRows) {
155
+ const id = (cells[0] || "").trim();
156
+ if (!id) {
157
+ check(false, `${tag}: a row has no ID — rows must carry an ID (F-001, ...)`);
158
+ continue;
159
+ }
160
+ if (seen.has(id)) check(false, `duplicate ID \`${id}\` (${seen.get(id)} and ${tag})`);
161
+ else seen.set(id, tag);
162
+ }
163
+ } else {
164
+ check(true, "empty board (no Open/Frozen/Archive rows yet) — valid for a fresh project");
165
+ }
166
+
167
+ // ---- 6. INDEX.md / todo.md are pointers, not live lists --------------------
168
+ for (const rel of [".agents/features/INDEX.md", ".agents/memory/todo.md"]) {
169
+ const p = path.join(root, rel);
170
+ if (!fs.existsSync(p)) {
171
+ check(true, `${rel} absent`);
172
+ continue;
173
+ }
174
+ const content = fs.readFileSync(p, "utf8");
175
+ const isPointer = /BACKLOG\.md/.test(content);
176
+ const hasList = /^\s*\|.+\|/m.test(content);
177
+ if (isPointer) {
178
+ check(true, `${rel} is a pointer to BACKLOG.md`);
179
+ } else if (hasList) {
180
+ check(false, `${rel} looks like a live list — point it at BACKLOG.md (or remove it)`);
181
+ } else {
182
+ check(true, `${rel} present and not a live list`);
183
+ }
184
+ }
185
+
186
+ console.log(
187
+ `\nRESULT: ${failures.length ? `${failures.length} FAILURE(S)` : "backlog green"}`
188
+ );
189
+ process.exit(failures.length ? 1 : 0);
@@ -0,0 +1,70 @@
1
+ param(
2
+ [Parameter(Mandatory = $true)][string]$Target
3
+ )
4
+
5
+ $ErrorActionPreference = "Stop"
6
+
7
+ $root = Split-Path -Parent $PSScriptRoot
8
+
9
+ $aliases = @{
10
+ "deepseek" = "deepseek/deepseek-v4-pro"
11
+ "deepseek-v4-pro" = "deepseek/deepseek-v4-pro"
12
+ "direct" = "deepseek/deepseek-v4-pro"
13
+ "zen" = "opencode/deepseek-v4-pro"
14
+ "opencode" = "opencode/deepseek-v4-pro"
15
+ "big-pickle" = "opencode/big-pickle"
16
+ "pickle" = "opencode/big-pickle"
17
+ "bigpickle" = "opencode/big-pickle"
18
+ }
19
+
20
+ $targetModel = $Target
21
+ if ($aliases.ContainsKey($Target.ToLowerInvariant())) {
22
+ $targetModel = $aliases[$Target.ToLowerInvariant()]
23
+ }
24
+ elseif (-not ($targetModel -match "^[a-zA-Z0-9._-]+/")) {
25
+ $targetModel = "deepseek/$targetModel"
26
+ }
27
+
28
+ $reasoningModels = @(
29
+ "deepseek/deepseek-v4-pro",
30
+ "opencode/deepseek-v4-pro",
31
+ "opencode/big-pickle"
32
+ )
33
+
34
+ $files = @(
35
+ "opencode.json",
36
+ ".opencode/agents/planner.md",
37
+ ".opencode/agents/system-architect.md",
38
+ ".opencode/agents/security-engineer.md",
39
+ ".opencode/agents/diagnostics-expert.md",
40
+ ".opencode/agents/product-manager.md",
41
+ ".opencode/agents/qa-architect.md"
42
+ )
43
+
44
+ $changed = @()
45
+ foreach ($rel in $files) {
46
+ $path = Join-Path $root $rel
47
+ if (-not (Test-Path -LiteralPath $path)) {
48
+ Write-Warning "missing: $rel"
49
+ continue
50
+ }
51
+ $content = Get-Content -LiteralPath $path -Raw
52
+ $new = $content
53
+ foreach ($m in $reasoningModels) {
54
+ $new = $new -replace [regex]::Escape($m), $targetModel
55
+ }
56
+ if ($new -ne $content) {
57
+ Set-Content -LiteralPath $path -Value $new -NoNewline
58
+ $changed += $rel
59
+ }
60
+ }
61
+
62
+ Write-Output ("Switched reasoning-tier model -> {0}" -f $targetModel)
63
+ if ($changed.Count -gt 0) {
64
+ Write-Output "Updated:"
65
+ $changed | ForEach-Object { Write-Output " - $_" }
66
+ }
67
+ else {
68
+ Write-Output "No files changed (already set)."
69
+ }
70
+ Write-Output "Restart opencode for the change to take effect."