@dombaras/agent-harness 0.1.9 → 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 +30 -12
- package/bin/agent-harness.js +267 -6
- package/package.json +2 -2
- package/templates/.agents/AGENTS.md +1 -1
- package/templates/.agents/features/INDEX.md +6 -20
- package/templates/.agents/memory/model-routing.md +1 -1
- package/templates/.agents/rules/00-operating.md +2 -2
- package/templates/.agents/skills/features/SKILL.md +43 -27
- package/templates/.opencode/agents/features.md +3 -3
- package/templates/AGENTS.md +2 -2
- package/templates/BACKLOG.md +52 -0
- package/templates/scripts/qa/check-backlog.js +189 -0
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` |
|
|
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
|
|
65
|
-
known, priority,
|
|
66
|
-
authors the per-feature doc chain
|
|
67
|
-
author persona — `intent.md`
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
`.agents/
|
|
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: trivial → none, 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
|
|
@@ -111,6 +113,10 @@ warns but does not block; the guarantee only applies to registered flows.
|
|
|
111
113
|
|
|
112
114
|
- `npm run test:dispatch` — persona model-pin preflight + persona↔skill parity + model-routing↔pin drift check.
|
|
113
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).
|
|
114
120
|
- `npm run test:qa-plan` — **diff-coverage + flow-closure gate**:
|
|
115
121
|
every changed CODE path must have a covering assertion (or a waivered reason) in
|
|
116
122
|
`.agents/memory/qa-plan.md` before a change is verified — and every touched flow
|
|
@@ -135,9 +141,9 @@ scripts are **project-provided** — the rulebook references them, and
|
|
|
135
141
|
npx @dombaras/agent-harness update --target /path/to/project
|
|
136
142
|
```
|
|
137
143
|
|
|
138
|
-
- Overwrites harness-owned files, preserves `.agents/memory
|
|
144
|
+
- Overwrites harness-owned files, preserves `.agents/memory/*` and `BACKLOG.md`.
|
|
139
145
|
- Auto-wires the harness gate scripts (`test:dispatch`, `test:governance`,
|
|
140
|
-
`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).
|
|
141
147
|
- **Auto-commits** only the harness files it changed (`chore(harness): @dombaras/agent-harness
|
|
142
148
|
<old> -> <new>`) and **pushes** to origin, so the next session never sees unexplained
|
|
143
149
|
modified harness files. Your unrelated uncommitted work is never staged.
|
|
@@ -162,6 +168,18 @@ npx @dombaras/agent-harness update --target /path/to/project
|
|
|
162
168
|
- `opencode.json` is merged (project keys preserved).
|
|
163
169
|
- `--dry-run` previews the plan without writing.
|
|
164
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
|
+
|
|
165
183
|
## Develop
|
|
166
184
|
|
|
167
185
|
```bash
|
package/bin/agent-harness.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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.
|
|
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
|
-
|
|
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 —
|
|
1
|
+
# Features — pointer
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
spec
|
|
5
|
-
|
|
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
|
-
|
|
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/*` (
|
|
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
|
|
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 (
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
plan → tests docs that frame it. You coordinate the authors; you do NOT
|
|
13
|
-
implementation work onto yourself (that is dispatched to the code
|
|
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** (
|
|
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
|
|
36
|
+
INDEX.md # pointer to BACKLOG.md
|
|
35
37
|
<slug>/
|
|
36
|
-
intent.md
|
|
37
|
-
scope.md
|
|
38
|
-
plan.md
|
|
39
|
-
tests.md
|
|
40
|
-
archive/
|
|
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 —
|
|
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.
|
|
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** — `
|
|
53
|
-
|
|
54
|
-
- **
|
|
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,
|
|
57
|
-
|
|
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 `
|
|
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 `
|
|
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
|
-
|
|
91
|
-
designs cluttering the
|
|
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 `
|
|
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
|
|
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,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 (
|
|
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
|
|
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 `
|
|
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.
|
package/templates/AGENTS.md
CHANGED
|
@@ -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
|
|
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
|
|
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,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);
|