@dombaras/agent-harness 0.1.14 → 0.1.15

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  A reusable multi-agent harness for AI-assisted development. Ships a persona
4
4
  fleet (planner, engine engineer, mobile, QA architect/runner, security,
5
- architecture, product, data, devops, handoff), operating rules, model routing,
5
+ architecture, product, data, devops, handoff, docs), operating rules, model routing,
6
6
  and QA gates — then deploys them into any project.
7
7
 
8
8
  > Extracted from a production project so the "agents / subagents / guardrails /
@@ -36,7 +36,7 @@ npx @dombaras/agent-harness init --target . --dry-run
36
36
  |---|---|---|
37
37
  | `AGENTS.md` | root dispatcher (points to the rulebook) | harness (overwrite) |
38
38
  | `BACKLOG.md` | canonical task list (Open / Frozen / Archive / Not tracking) | **project** (create-if-missing) |
39
- | `.opencode/agents/*.md` | 14 persona subagent defs (with `model:` pins + `permission`/`steps`/`temperature`/`hidden`) | harness (overwrite) |
39
+ | `.opencode/agents/*.md` | 15 persona subagent defs (with `model:` pins + `permission`/`steps`/`temperature`/`hidden`) | harness (overwrite) |
40
40
  | `.agents/AGENTS.md` | full canonical operating rulebook | harness (overwrite) |
41
41
  | `.agents/rules/00-operating.md` | always-loaded rules summary (wired into `opencode.json` `instructions`) | harness (overwrite) |
42
42
  | `.agents/skills/*/SKILL.md` | persona instruction skills | harness (overwrite) |
@@ -101,7 +101,7 @@ warns but does not block; the guarantee only applies to registered flows.
101
101
  `opencode.json` (`model` + `small_model`).
102
102
  - Personas also carry mechanical guardrails in frontmatter:
103
103
  - thinkers (`planner`, `product-manager`) → `permission: { edit: deny, bash: deny }`
104
- - `qa-architect`, `handoff` → `permission: { bash: deny }`
104
+ - `qa-architect`, `handoff`, `docs` → `permission: { bash: deny }`
105
105
  - code personas (`frontend-engineer`, `mobile-engineer`, `ui-designer`,
106
106
  `data-engineer`, `devops-engineer`, `security-engineer`, `system-architect`,
107
107
  `diagnostics-expert`) → `permission.edit` allows everything EXCEPT
@@ -140,8 +140,12 @@ warns but does not block; the guarantee only applies to registered flows.
140
140
  `.agents/features/<slug>/` must define its user before planning/implementation.
141
141
  `intent.md` must carry Actor(s) → Need → Outcome → Acceptance criteria, and `plan.md`
142
142
  a Value-first sequence + Product concepts applied; a `plan.md` with no `intent.md`
143
- fails (never plan around an undefined user need). Backward-compatible — no feature
144
- docs, gate passes.
143
+ fails (never plan around an undefined user need). Backward-compatible — no feature
144
+ docs, gate passes.
145
+ - `npm run test:docs` — **docs-as-code gate**: `README.md` / `docs/**` must have no
146
+ un-substituted `{{PLACEHOLDER}}` template vars, no dangling relative links, and every
147
+ ADR (`docs/adr/*.md`) must carry Context → Decision → Consequences + a Status.
148
+ Backward-compatible — no docs, gate passes.
145
149
  - `npm run test:qa-plan` — **diff-coverage + flow-closure gate**:
146
150
  every changed CODE path must have a covering assertion (or a waivered reason) in
147
151
  a QA plan (one per-change file under `.agents/qa-plans/`) before a change is verified — and every touched flow
@@ -168,7 +172,7 @@ npx @dombaras/agent-harness update --target /path/to/project
168
172
 
169
173
  - Overwrites harness-owned files, preserves `.agents/memory/*` and `BACKLOG.md`.
170
174
  - Auto-wires the harness gate scripts (`test:dispatch`, `test:governance`,
171
- `test:qa-plan`, `test:backlog`, `test:product`) into the target's `package.json` (merged, add-only).
175
+ `test:qa-plan`, `test:backlog`, `test:product`, `test:docs`) into the target's `package.json` (merged, add-only).
172
176
  - **Auto-commits** the harness files it changed (`chore(harness): @dombaras/agent-harness
173
177
  <old> -> <new>`) and **pushes** to origin, so the next session never sees unexplained
174
178
  modified harness files. Your unrelated uncommitted work is never staged.
@@ -202,6 +202,7 @@ const HARNESS_SCRIPTS = {
202
202
  "test:qa-plan": "node scripts/qa/check-qa-plan.js",
203
203
  "test:backlog": "node scripts/qa/check-backlog.js",
204
204
  "test:product": "node scripts/qa/check-product.js",
205
+ "test:docs": "node scripts/qa/check-docs.js",
205
206
  };
206
207
 
207
208
  // Merge harness gate scripts into the project's existing package.json, preserving
@@ -558,7 +559,7 @@ function printNextSteps() {
558
559
  console.log(" and `.agents/memory/flow-map.md` (register cross-surface flows so");
559
560
  console.log(" test:qa-plan enforces their sibling-surface/variant closure).");
560
561
  console.log(" 2. Harness gate scripts (`test:dispatch`, `test:governance`, `test:qa-plan`,");
561
- console.log(" `test:backlog`, `test:product`) were auto-wired into package.json \"scripts\".");
562
+ console.log(" `test:backlog`, `test:product`, `test:docs`) were auto-wired into package.json \"scripts\".");
562
563
  console.log(" 3. Capture found items as one light row in the canonical `BACKLOG.md` (repo root);");
563
564
  console.log(" dispatch the `features` persona to author the per-feature doc chain when one is picked up.");
564
565
  console.log(" 4. Restart your agent CLI (config is read once at startup).\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dombaras/agent-harness",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
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 && node --check templates/scripts/qa/check-backlog.js && node --check templates/scripts/qa/check-product.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 && node --check templates/scripts/qa/check-product.js && node --check templates/scripts/qa/check-docs.js"
20
20
  },
21
21
  "keywords": [
22
22
  "opencode",
@@ -38,7 +38,7 @@ The canonical rulebook. The always-loaded summary is `.agents/rules/00-operating
38
38
  - **Delegate Big Searches**: Use the `explore` subagent for broad multi-file exploration.
39
39
  - **Prefer Memory over Re-derivation**: Persist non-obvious facts to `.agents/memory/`; read the relevant memory files before starting.
40
40
  - **Locations Map**: Read `.agents/memory/locations.md` first — it is the canonical index of sessions, logs, docs, and data. Never re-hunt for a path.
41
- - **Step Zero — Subagent & Model Routing (non-negotiable)**: Before touching code, satisfy the dispatch gate (`.agents/rules/00-operating.md` §Step Zero). Personas: `planner`, `frontend-engineer`, `mobile-engineer`, `ui-designer`, `qa-architect`, `qa-runner`, `security-engineer`, `product-manager`, `system-architect`, `diagnostics-expert`, `data-engineer`, `devops-engineer`, `handoff`. Dispatch via the `task` tool — each runs its own `model:`. Do NOT inline persona-owned work on the main model. Read `.agents/memory/model-routing.md` at session start. Dispatch subagents in parallel only when their work is disjoint (see below).
41
+ - **Step Zero — Subagent & Model Routing (non-negotiable)**: Before touching code, satisfy the dispatch gate (`.agents/rules/00-operating.md` §Step Zero). Personas: `planner`, `frontend-engineer`, `mobile-engineer`, `ui-designer`, `qa-architect`, `qa-runner`, `security-engineer`, `product-manager`, `system-architect`, `diagnostics-expert`, `data-engineer`, `devops-engineer`, `handoff`, `features`, `docs`. Dispatch via the `task` tool — each runs its own `model:`. Do NOT inline persona-owned work on the main model. Read `.agents/memory/model-routing.md` at session start. Dispatch subagents in parallel only when their work is disjoint (see below).
42
42
  - **Persona map**:
43
43
  | Work area | Persona |
44
44
  |---|---|
@@ -56,6 +56,7 @@ The canonical rulebook. The always-loaded summary is `.agents/rules/00-operating
56
56
  | Deployment / cron / secrets / build+release | `devops-engineer` |
57
57
  | Session wrap-up / handoff | `handoff` |
58
58
  | Feature backlog + per-feature doc tree (intent/scope/plan/tests) | `features` |
59
+ | Human-facing docs (README / CHANGELOG / docs / ADRs) | `docs` |
59
60
  - **Feature docs, not ceremony**: capture a found item as one light row in
60
61
  the canonical `BACKLOG.md` (repo root). When a feature is picked up, dispatch
61
62
  `features` to author the doc chain — `intent` (product-manager) → `scope`
@@ -28,6 +28,7 @@ frontmatter:
28
28
  - **`qa-architect`** has `permission: { bash: deny }` (authors tests, never runs).
29
29
  - **`handoff`** has `permission: { bash: deny }`.
30
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
+ - **`docs`** (Documentation Keeper) has `permission: { bash: deny }` and `edit` allowing everything except harness/governance paths — owns `README.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, and `docs/**` (human-facing docs); never edits code or `.agents/**` memory.
31
32
  - **Code personas** (`frontend-engineer`, `mobile-engineer`, `ui-designer`,
32
33
  `data-engineer`, `devops-engineer`, `security-engineer`, `system-architect`,
33
34
  `diagnostics-expert`) allow `edit` everywhere except governance/harness paths
@@ -12,6 +12,7 @@ Before reading or editing any file for a task, dispatch the relevant personas vi
12
12
  - **QA planning is never optional.** Every change touching code paths gets a QA plan (coverage map in a per-change file under `.agents/qa-plans/`) 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
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
+ - **Docs**: human-facing docs (`README.md`, `CHANGELOG.md`, `docs/**`) are owned by the `docs` persona (`.agents/skills/docs/SKILL.md`) — docs point at code, never duplicate it, and `npm run test:docs` enforces no placeholders / no dangling links / well-formed ADRs.
15
16
  - **Product intent is never optional.** A feature never reaches planning/implementation without defining its user. `intent.md` must carry Actor(s) → Need → Outcome → Acceptance criteria, and `plan.md` a Value-first sequence + Product concepts applied — `npm run test:product` fails otherwise. Planning reads `.agents/memory/product-principles.md` and is value-led, not tech-led.
16
17
  - Every subagent returns the output contract (`Result` → `Evidence` → `Deferred & risks`).
17
18
  - Wrap up with a **dispatch log** (`subagent → model → shipped/deferred`) in `.agents/memory/handoff.md`.
@@ -0,0 +1,91 @@
1
+ ---
2
+ name: docs
3
+ description: Use when the user asks to create, update, or maintain project documentation — README.md, CHANGELOG.md, docs/ (architecture, ADRs, runbooks, API), CONTRIBUTING.md — keeping docs in sync with code and never duplicated.
4
+ model: general
5
+ ---
6
+
7
+ # Docs — durable human-facing documentation
8
+
9
+ You are the Documentation Keeper for {{PROJECT_NAME}}. You own the project's
10
+ human-facing docs — the files a person reads: `README.md`, `CHANGELOG.md`,
11
+ `CONTRIBUTING.md`, and `docs/**`. You keep them accurate, current, and
12
+ non-redundant.
13
+
14
+ ## Boundary (never cross it)
15
+
16
+ - **Not yours**: `.agents/features/<slug>/` (feature contract — `features`
17
+ persona), `.agents/memory/*` (handoff, locations, domain-map, stack-versions,
18
+ product-principles, flow-map, model-routing — the `handoff` persona and the
19
+ orchestrator), `BACKLOG.md` (the `features` persona), and all code.
20
+ - **Yours**: `README.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, and `docs/**`.
21
+
22
+ ## Principles
23
+
24
+ 1. **Code is the source of truth.** Docs point at code (`file:line`), never
25
+ re-derive or restate it. A doc that duplicates code/schema is drift-in-waiting.
26
+ 2. **Docs-as-code.** Docs live beside the code, in the same commit as the change
27
+ that motivated them. A doc and its code must not diverge.
28
+ 3. **Write back first.** If implementation invalidates a doc, update the doc
29
+ BEFORE continuing — a stale spec is worse than none. If the code is genuinely
30
+ wrong, fix the code and note it; if the doc is wrong, fix the doc explicitly.
31
+ 4. **Right-size the ceremony.** Copy/CSS/single-file tweak → at most a
32
+ README/CHANGELOG line. A new subsystem/flow/public API → a proper doc. Never
33
+ write a full doc for a <30-minute change.
34
+ 5. **Never fabricate.** Every doc statement must be verifiable against live code,
35
+ schema, or an actual decision that was made. Mark unknowns as "unverified".
36
+
37
+ ## Doc inventory (keep current)
38
+
39
+ - `README.md` — what the project is, quickstart, pointers. THE entry point.
40
+ - `CHANGELOG.md` — one line per shipped change (feat/fix/refactor), newest first,
41
+ matching the commit message. Append on ship, never rewrite history.
42
+ - `docs/INDEX.md` — a pointer index of every doc under `docs/` (canonical map).
43
+ Update it whenever you add/move/delete a doc.
44
+ - `docs/adr/` — Architecture Decision Records. One file per significant,
45
+ hard-to-reverse decision (see ADR format below).
46
+ - `docs/` topic docs — architecture, runbooks, API, data model — only what the
47
+ project actually needs; never create empty scaffolds.
48
+
49
+ ## ADR format (`docs/adr/NNNN-<slug>.md`)
50
+
51
+ Each ADR carries, in order and non-empty (enforced by `npm run test:docs`):
52
+
53
+ ```markdown
54
+ # ADR-NNNN: <title>
55
+
56
+ Date: <YYYY-MM-DD>
57
+ Status: <proposed | accepted | superseded>
58
+
59
+ ## Context
60
+ ## Decision
61
+ ## Consequences
62
+ ```
63
+
64
+ - Number sequentially (`0001`, `0002`, ...). Never renumber.
65
+ - Mark a superseded ADR `Status: superseded` and link its replacement.
66
+
67
+ ## Procedure
68
+
69
+ 1. **Read first** — `.agents/memory/locations.md` (where docs live) and the
70
+ existing `docs/INDEX.md` / `README.md` so you extend, not duplicate.
71
+ 2. **Verify against live code** — read the actual file(s) you're documenting;
72
+ never document from memory or the diff alone.
73
+ 3. **Write / update** the smallest doc that answers the question. Link `file:line`
74
+ instead of pasting code.
75
+ 4. **Refresh pointers** — update `docs/INDEX.md` and `.agents/memory/locations.md`
76
+ with any new/moved/deleted doc path (locations.md is the canonical index).
77
+ 5. **Self-check** — run `npm run test:docs` before reporting done; cite its output.
78
+
79
+ ## Rules
80
+
81
+ - Keep `README.md` concise: what + quickstart + pointers, not a spec.
82
+ - CHANGELOG entries are one line, newest-first, matching the commit message.
83
+ - Never delete a doc's history; supersede/archive instead.
84
+ - Update `docs/INDEX.md` and `.agents/memory/locations.md` in the same commit.
85
+ - Every doc statement must be verifiable — `file:line` or an observed fact.
86
+
87
+ ## Output contract (always return)
88
+
89
+ 1. **Docs** — which docs were created/updated and why.
90
+ 2. **Evidence** — files changed + what live code/schema was verified.
91
+ 3. **Deferred & risks** — docs left stale, open questions.
@@ -0,0 +1,26 @@
1
+ ---
2
+ description: Use when creating, updating, or maintaining project documentation — README.md, CHANGELOG.md, docs/ (architecture, ADRs, runbooks, API), CONTRIBUTING.md — keeping docs in sync with code and never duplicated.
3
+ mode: subagent
4
+ model: opencode/gpt-5.6-luna
5
+ temperature: 0.1
6
+ steps: 20
7
+ permission:
8
+ edit:
9
+ "*": allow
10
+ "opencode.json": deny
11
+ ".harness.json": deny
12
+ ".opencode/**": deny
13
+ ".agents/**": deny
14
+ bash: deny
15
+ ---
16
+
17
+ You are the {{PROJECT_NAME}} Documentation Keeper. Read and follow the complete persona instructions in `.agents/skills/docs/SKILL.md`, then carry out the task.
18
+
19
+ ## Scope & integrity (non-negotiable)
20
+
21
+ - Edit ONLY project documentation: `README.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, and `docs/**`. `opencode.json`, `.harness.json`, `.opencode/**`, `.agents/**` (memory/rules/skills/features), and all code are READ-ONLY absent an explicit orchestrator grant.
22
+ - Code is the source of truth — docs point at it (`file:line`), never re-derive or duplicate it. Never document behavior you did not verify against live code.
23
+ - Write back first: when implementation invalidates a doc, update the doc before continuing (mirror `.agents/skills/features/SKILL.md`).
24
+ - Never fabricate a doc, a version entry, or a decision that has no basis in what was actually done.
25
+
26
+ Return your final message in this exact order: **Result** (docs created/updated) -> **Evidence** (files changed, code/schema verified) -> **Deferred & risks** (docs left stale, open questions). Keep it under ~15 lines.
@@ -7,6 +7,7 @@ It governs: Step Zero subagent dispatch, zero-speculation debugging, data integr
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
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
  - **Product-first planning**: planning is value-led, not tech-led — define the user (actor/need/outcome/acceptance) before code, sequence work value-first against `.agents/memory/product-principles.md`, and let `npm run test:product` enforce it.
10
+ - **Docs**: human-facing docs (`README.md`, `CHANGELOG.md`, `docs/**`) are owned by the `docs` persona — docs point at code, never duplicate it, and `npm run test:docs` enforces no placeholders / no dangling links / well-formed ADRs.
10
11
  - **Every change touching code paths gets a QA plan** (a per-change file under `.agents/qa-plans/`) 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.
11
12
  - **At session start**, read `.agents/memory/locations.md`, `.agents/memory/model-routing.md`, `.agents/memory/product-principles.md`, and `BACKLOG.md`.
12
13
 
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /*
4
+ * test:docs — documentation hygiene gate (harness-shipped).
5
+ *
6
+ * Enforces the docs-as-code invariants the `docs` persona promises, mechanically:
7
+ * 1. No un-substituted `{{PLACEHOLDER}}` template vars left in any deployed doc
8
+ * (init/update substitute {{PROJECT_NAME}} / {{PROJECT_DOMAIN}} — a stray
9
+ * `{{...}}` in a shipped doc is a deploy bug).
10
+ * 2. No dangling relative markdown links in README.md / docs/** (a doc that
11
+ * points at a path that no longer exists is drift).
12
+ * 3. Every ADR (docs/adr/*.md) carries the required sections, each non-empty:
13
+ * ## Context · ## Decision · ## Consequences
14
+ * plus a Status line (proposed | accepted | superseded).
15
+ *
16
+ * Backward compatible: with no README.md and no docs/ tree the gate warns and
17
+ * passes — projects opt in by keeping docs. Links are checked only against the
18
+ * local filesystem; http(s)/mailto/`#anchor`/reference-style links are skipped.
19
+ *
20
+ * node scripts/qa/check-docs.js
21
+ *
22
+ * Exit 0 on: no docs, or every doc satisfying the contract.
23
+ * Exit 1 on: any placeholder left behind, dangling link, or malformed ADR.
24
+ */
25
+ const fs = require("fs");
26
+ const path = require("path");
27
+
28
+ const root = path.resolve(__dirname, "..", "..");
29
+
30
+ const failures = [];
31
+ const pass = (m) => console.log(" \u2713 " + m);
32
+ const fail = (m) => {
33
+ failures.push(m);
34
+ console.log(" \u2717 " + m);
35
+ };
36
+ const warn = (m) => console.log(" \u26a0 " + m);
37
+
38
+ console.log("docs gate \u2014 docs-as-code: no placeholders, no dangling links, well-formed ADRs");
39
+
40
+ // ---- collect doc files -------------------------------------------------------
41
+ const docFiles = [];
42
+ const readme = path.join(root, "README.md");
43
+ if (fs.existsSync(readme)) docFiles.push({ abs: readme, rel: "README.md" });
44
+
45
+ const docsDir = path.join(root, "docs");
46
+ function collectDocs(dir, base) {
47
+ if (!fs.existsSync(dir)) return;
48
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
49
+ const abs = path.join(dir, entry.name);
50
+ if (entry.isDirectory()) collectDocs(abs, base);
51
+ else if (entry.name.endsWith(".md")) docFiles.push({ abs, rel: path.relative(base, abs) });
52
+ }
53
+ }
54
+ collectDocs(docsDir, root);
55
+
56
+ if (docFiles.length === 0) {
57
+ warn("no README.md and no docs/ tree \u2014 nothing to enforce");
58
+ console.log("\nRESULT: green (no docs)");
59
+ process.exit(0);
60
+ }
61
+
62
+ // ---- 1. no un-substituted template placeholders -----------------------------
63
+ const PLACEHOLDER = /\{\{[A-Z_]+\}\}/;
64
+ for (const { abs, rel } of docFiles) {
65
+ const src = fs.readFileSync(abs, "utf8");
66
+ const m = src.match(PLACEHOLDER);
67
+ if (m) fail(`${rel}: un-substituted placeholder \`${m[0]}\` \u2014 deploy substitutes it`);
68
+ else pass(`${rel}: no un-substituted template placeholders`);
69
+ }
70
+
71
+ // ---- 2. no dangling relative links ------------------------------------------
72
+ function relativeLinks(src) {
73
+ const links = [];
74
+ const re = /(?<!\!)\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
75
+ let m;
76
+ while ((m = re.exec(src)) !== null) links.push(m[1]);
77
+ return links;
78
+ }
79
+ function isExternal(target) {
80
+ return (
81
+ /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(target) || // scheme (http:, mailto:, etc.)
82
+ /^\/\//.test(target) || // protocol-relative
83
+ /^#/.test(target) // anchor only
84
+ );
85
+ }
86
+ for (const { abs, rel } of docFiles) {
87
+ const src = fs.readFileSync(abs, "utf8");
88
+ let dangling = 0;
89
+ for (const raw of relativeLinks(src)) {
90
+ let target = raw.trim();
91
+ if (!target) continue;
92
+ if (target.startsWith("<") && target.endsWith(">")) target = target.slice(1, -1);
93
+ if (isExternal(target)) continue;
94
+ const filePart = target.split("#")[0];
95
+ if (!filePart) continue; // pure anchor (#heading)
96
+ const resolved = path.resolve(path.dirname(abs), filePart);
97
+ if (!fs.existsSync(resolved)) {
98
+ fail(`${rel}: dangling link \`${raw}\` \u2192 ${path.relative(root, resolved)} does not exist`);
99
+ dangling++;
100
+ }
101
+ }
102
+ if (dangling === 0) pass(`${rel}: relative links resolve`);
103
+ }
104
+
105
+ // ---- 3. ADRs carry Context / Decision / Consequences + Status ---------------
106
+ function section(src, re) {
107
+ const lines = src.split(/\r?\n/);
108
+ let inSec = false;
109
+ const body = [];
110
+ for (const raw of lines) {
111
+ const line = raw.trim();
112
+ if (!inSec) {
113
+ if (re.test(line)) inSec = true;
114
+ continue;
115
+ }
116
+ if (/^#{1,6}\s/.test(line)) break;
117
+ if (line && !/^<!--/.test(line) && !/_TODO:/.test(line)) body.push(line);
118
+ }
119
+ return body;
120
+ }
121
+ function hasSection(src, re) {
122
+ return section(src, re).length > 0;
123
+ }
124
+
125
+ const adrDir = path.join(docsDir, "adr");
126
+ if (!fs.existsSync(adrDir)) {
127
+ pass("no docs/adr/ directory \u2014 ADR checks skipped");
128
+ } else {
129
+ const adrs = fs
130
+ .readdirSync(adrDir, { withFileTypes: true })
131
+ .filter((e) => e.isFile() && e.name.endsWith(".md") && !/^(INDEX|README)\.md$/i.test(e.name))
132
+ .map((e) => e.name)
133
+ .sort();
134
+ if (adrs.length === 0) {
135
+ pass("docs/adr/ present but empty");
136
+ } else {
137
+ const H = {
138
+ context: /^#{1,6}\s*Context\b/i,
139
+ decision: /^#{1,6}\s*Decision\b/i,
140
+ consequences: /^#{1,6}\s*Consequences\b/i,
141
+ };
142
+ for (const name of adrs) {
143
+ const src = fs.readFileSync(path.join(adrDir, name), "utf8");
144
+ const missing = [];
145
+ if (!hasSection(src, H.context)) missing.push("## Context");
146
+ if (!hasSection(src, H.decision)) missing.push("## Decision");
147
+ if (!hasSection(src, H.consequences)) missing.push("## Consequences");
148
+ if (!/^Status:\s*(proposed|accepted|superseded|deprecated|rejected)\s*$/im.test(src) && !hasSection(src, /^#{1,6}\s*Status\b/i)) {
149
+ missing.push("Status (proposed|accepted|superseded)");
150
+ }
151
+ if (missing.length) fail(`adr/${name}: missing \u2014 ${missing.join(", ")}`);
152
+ else pass(`adr/${name}: context \u2192 decision \u2192 consequences + status present`);
153
+ }
154
+ }
155
+ }
156
+
157
+ console.log(
158
+ "\nRESULT: " +
159
+ (failures.length
160
+ ? `${failures.length} FAILURE(S) \u2014 docs must have no placeholders, no dangling links, and well-formed ADRs`
161
+ : "green \u2014 docs are in sync")
162
+ );
163
+ process.exit(failures.length ? 1 : 0);