doctrina-cli 0.6.0 → 0.9.0

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.
@@ -0,0 +1,97 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+ import { watch as fsWatch } from "node:fs";
4
+ import { exists } from "../lib/fs-ops.js";
5
+ import { flagBool } from "../lib/args.js";
6
+ import { c } from "../lib/colors.js";
7
+ import * as validate from "./validate.js";
8
+ import * as next from "./next.js";
9
+
10
+ // Continuous keep-in-sync (review 2026-06-27 passive-user feature #4): re-run
11
+ // `validate --fix` (heal drift, migrate the stamp) and reprint `doctrina next`
12
+ // whenever an artifact changes, so the index stays synced and the agent stays
13
+ // oriented without anyone invoking a command. Passive by construction. The
14
+ // pass is debounced and ignores writes to index.json (which `--fix` itself
15
+ // makes) so it never loops on its own output. `--once` runs a single pass and
16
+ // exits — the testable, scriptable form.
17
+
18
+ export async function run(_positional, flags) {
19
+ const projectRoot = process.cwd();
20
+ const dot = path.join(projectRoot, ".doctrina");
21
+ if (!exists(dot)) {
22
+ throw new Error("not a Doctrina project (no .doctrina/ in cwd). Run `doctrina init` first.");
23
+ }
24
+
25
+ if (flagBool(flags, "once", false)) {
26
+ return pass(projectRoot);
27
+ }
28
+
29
+ console.log(c.bold("doctrina watch") + c.gray(" — validate --fix + next on every change. Ctrl-C to stop."));
30
+ await pass(projectRoot);
31
+
32
+ let timer = null;
33
+ let running = false;
34
+ const schedule = () => {
35
+ if (timer) clearTimeout(timer);
36
+ timer = setTimeout(async () => {
37
+ if (running) return;
38
+ running = true;
39
+ console.log("");
40
+ console.log(c.gray(`──── ${new Date().toLocaleTimeString()} change detected`));
41
+ await pass(projectRoot);
42
+ running = false;
43
+ }, 300);
44
+ };
45
+
46
+ let watcher;
47
+ try {
48
+ watcher = fsWatch(dot, { recursive: true }, (_event, filename) => {
49
+ const name = (filename ?? "").replace(/\\/g, "/");
50
+ // Ignore the index the fix itself rewrites (would self-trigger forever).
51
+ if (name.endsWith("index.json")) return;
52
+ schedule();
53
+ });
54
+ } catch (err) {
55
+ console.error(c.red("error:") + ` could not start the watcher: ${err.message}`);
56
+ return 1;
57
+ }
58
+
59
+ // Keep the process alive until interrupted.
60
+ return await new Promise((resolve) => {
61
+ const stop = () => {
62
+ if (watcher) watcher.close();
63
+ console.log("");
64
+ console.log(c.gray("watch stopped."));
65
+ resolve(0);
66
+ };
67
+ process.on("SIGINT", stop);
68
+ process.on("SIGTERM", stop);
69
+ });
70
+ }
71
+
72
+ // One pass: heal drift, then reprint the next actions. Never throws.
73
+ async function pass(projectRoot) {
74
+ try {
75
+ await validate.run([], new Map([["fix", true]]));
76
+ console.log("");
77
+ await next.run([], new Map());
78
+ } catch (err) {
79
+ console.error(c.red("error:") + ` ${err.message}`);
80
+ return 1;
81
+ }
82
+ return 0;
83
+ }
84
+
85
+ export const help = `
86
+ Usage: doctrina watch [--once]
87
+
88
+ Watch the .doctrina/ tree and, on every change, run \`doctrina validate
89
+ --fix\` (heal index drift, migrate the framework stamp) and reprint
90
+ \`doctrina next\` — so the index stays in sync and the agent stays oriented
91
+ without anyone invoking a command. Debounced; ignores the index.json the
92
+ fix itself rewrites. Runs until interrupted (Ctrl-C).
93
+
94
+ Options:
95
+ --once Run a single pass (validate --fix + next) and exit, instead of
96
+ watching. The scriptable / testable form.
97
+ `;
@@ -0,0 +1,182 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+ import { readdirSync } from "node:fs";
4
+ import { exists, isDir, isFile, read } from "../lib/fs-ops.js";
5
+ import { specHeader, listHeader } from "../lib/scan.js";
6
+ import { c } from "../lib/colors.js";
7
+ import { suggest } from "../lib/suggest.js";
8
+
9
+ // Provenance explainer (review 2026-06-27 passive-user feature #5): answer
10
+ // "why does this capability exist / why was it built this way?" by assembling
11
+ // the chain the framework already records — product intent (the [SC1] anchors
12
+ // it Realizes) → the capability's purpose → the acceptance criteria that prove
13
+ // it → the accepted ADRs that decided it — into one read. The human (or agent)
14
+ // asks; the tool answers from the artifacts, instead of grepping by hand.
15
+ // Read-only. Builds on the Realizes:/trace provenance (ADR 0006/0011).
16
+
17
+ export async function run(positional, _flags) {
18
+ const cap = positional[0];
19
+ const projectRoot = process.cwd();
20
+ if (!exists(path.join(projectRoot, ".doctrina"))) {
21
+ throw new Error("not a Doctrina project (no .doctrina/ in cwd). Run `doctrina init` first.");
22
+ }
23
+ const specsDir = path.join(projectRoot, ".doctrina", "specs");
24
+ const known = isDir(specsDir) ? readdirSync(specsDir).filter((e) => isFile(path.join(specsDir, e, "spec.md"))) : [];
25
+
26
+ if (!cap) {
27
+ console.error(c.red("error:") + " why requires a <capability>");
28
+ if (known.length) console.error(c.gray("known: ") + known.sort().join(", "));
29
+ return 2;
30
+ }
31
+ const specPath = path.join(specsDir, cap, "spec.md");
32
+ if (!isFile(specPath)) {
33
+ console.error(c.red("error:") + ` no spec for capability "${cap}"`);
34
+ const guess = suggest(cap, known);
35
+ if (guess) console.error(c.gray("hint: ") + `did you mean "${guess}"?`);
36
+ else if (known.length) console.error(c.gray("known: ") + known.sort().join(", "));
37
+ return 1;
38
+ }
39
+
40
+ const text = read(specPath);
41
+ console.log(c.bold(`Why "${cap}"`) + c.gray(" — provenance chain (intent → capability → proof → decisions)"));
42
+ console.log("");
43
+
44
+ // 1. Product intent it realizes.
45
+ const realizesRaw = specHeader(text, "Realizes");
46
+ const anchorIds = realizesRaw ? (realizesRaw.match(/[A-Z]+\d+/g) ?? []) : [];
47
+ console.log(c.bold(" Product intent"));
48
+ if (realizesRaw && /^n\/a\b/i.test(realizesRaw.trim())) {
49
+ console.log(` ${c.gray("n/a — " + realizesRaw.trim().replace(/^n\/a\s*[—-]?\s*/i, ""))}`);
50
+ } else if (anchorIds.length === 0) {
51
+ console.log(` ${c.yellow("none declared")} ${c.gray("— add a **Realizes:** header (doctrina trace)")}`);
52
+ } else {
53
+ const anchors = productAnchors(projectRoot);
54
+ for (const id of anchorIds) {
55
+ const line = anchors.get(id);
56
+ console.log(` ${c.cyan(id)} ${line ? line : c.yellow("(no matching anchor in product.md)")}`);
57
+ }
58
+ }
59
+
60
+ // 2. The capability itself.
61
+ console.log("");
62
+ console.log(c.bold(" Capability"));
63
+ const status = specHeader(text, "Status") ?? "—";
64
+ const impl = specHeader(text, "Implementation") ?? "—";
65
+ console.log(` ${c.gray("status:")} ${status} ${c.gray("implementation:")} ${impl}`);
66
+ const purpose = sectionParagraph(text, "Purpose");
67
+ if (purpose) console.log(" " + purpose.replace(/\s+/g, " ").trim());
68
+
69
+ // 3. The proof (acceptance criteria with cited evidence).
70
+ console.log("");
71
+ console.log(c.bold(" Proof"));
72
+ const criteria = acceptanceWithEvidence(text);
73
+ if (criteria.length === 0) {
74
+ console.log(` ${c.gray("no acceptance criteria declared")}`);
75
+ } else {
76
+ for (const cr of criteria) {
77
+ const proof = cr.proof.length ? c.gray(cr.proof.map((p) => `\`${p}\``).join(", ")) : c.yellow("no evidence cited");
78
+ console.log(` ${cr.mark} #${cr.n} ${proof}`);
79
+ }
80
+ }
81
+
82
+ // 4. The decisions that shaped it (accepted ADRs naming the capability).
83
+ console.log("");
84
+ console.log(c.bold(" Decisions"));
85
+ const adrs = decisionsMentioning(projectRoot, cap);
86
+ if (adrs.length === 0) {
87
+ console.log(` ${c.gray("no accepted ADR names this capability")}`);
88
+ } else {
89
+ for (const a of adrs) console.log(` ${c.cyan("ADR " + a.id)} ${a.title}`);
90
+ }
91
+
92
+ return 0;
93
+ }
94
+
95
+ // Map of anchor id -> the product.md bullet text that declares it.
96
+ function productAnchors(projectRoot) {
97
+ const out = new Map();
98
+ const p = path.join(projectRoot, ".doctrina", "product.md");
99
+ if (!isFile(p)) return out;
100
+ for (const line of read(p).split(/\r?\n/)) {
101
+ const m = line.match(/^\s*[-*]\s+\[([A-Z]+\d+)\]\s+(.+)$/);
102
+ if (m && !out.has(m[1])) out.set(m[1], m[2].trim());
103
+ }
104
+ return out;
105
+ }
106
+
107
+ // First non-comment paragraph under a "## <name>" heading.
108
+ function sectionParagraph(text, name) {
109
+ const lines = text.split(/\r?\n/);
110
+ let inSection = false;
111
+ const buf = [];
112
+ for (const line of lines) {
113
+ if (/^##\s+/.test(line)) {
114
+ if (inSection) break;
115
+ if (new RegExp(`^##\\s+${name}\\b`, "i").test(line)) inSection = true;
116
+ continue;
117
+ }
118
+ if (!inSection) continue;
119
+ if (/^\s*<!--/.test(line) || /-->/.test(line)) continue;
120
+ if (line.trim() === "") {
121
+ if (buf.length) break;
122
+ continue;
123
+ }
124
+ buf.push(line.trim());
125
+ }
126
+ return buf.join(" ");
127
+ }
128
+
129
+ // Numbered acceptance criteria with their [mark] and any backtick proof paths.
130
+ function acceptanceWithEvidence(text) {
131
+ const lines = text.split(/\r?\n/);
132
+ let inSection = false;
133
+ const out = [];
134
+ for (const line of lines) {
135
+ if (/^##\s+/.test(line)) {
136
+ inSection = /^##\s+Acceptance criteria\b/i.test(line);
137
+ continue;
138
+ }
139
+ if (!inSection) continue;
140
+ const m = line.match(/^\s*(\d+)\.\s+(.*)$/);
141
+ if (!m) continue;
142
+ const body = m[2];
143
+ const markMatch = body.match(/^\[([^\]]+)\]/);
144
+ const mark = markMatch
145
+ ? (/^verified/i.test(markMatch[1]) ? c.green("✓") : c.yellow("○"))
146
+ : c.gray("·");
147
+ const proof = [...body.matchAll(/`([^`]+)`/g)].map((x) => x[1]).filter((s) => s.includes("/") || /\.[a-z0-9]{1,8}$/i.test(s));
148
+ out.push({ n: m[1], mark, proof });
149
+ }
150
+ return out;
151
+ }
152
+
153
+ // Accepted ADRs whose body names the capability (word-ish boundary).
154
+ function decisionsMentioning(projectRoot, cap) {
155
+ const adrDir = path.join(projectRoot, ".doctrina", "decisions");
156
+ const out = [];
157
+ if (!isDir(adrDir)) return out;
158
+ const re = new RegExp(`\\b${cap.replace(/[-]/g, "[- ]?")}\\b`, "i");
159
+ for (const f of readdirSync(adrDir).sort()) {
160
+ const m = f.match(/^(\d{4})-.*\.md$/);
161
+ if (!m) continue;
162
+ const text = read(path.join(adrDir, f));
163
+ const status = (listHeader(text, "Status") ?? "").toLowerCase();
164
+ if (status !== "accepted") continue;
165
+ if (!re.test(text)) continue;
166
+ const titleMatch = text.match(/^#\s+ADR\s+\d{4}\s*[—-]\s*(.+)$/m);
167
+ out.push({ id: m[1], title: titleMatch ? titleMatch[1].trim() : f });
168
+ }
169
+ return out;
170
+ }
171
+
172
+ export const help = `
173
+ Usage: doctrina why <capability>
174
+
175
+ Explain a capability's provenance by assembling the chain Doctrina already
176
+ records into one read: the product intent it **Realizes:** ([SC1] anchors),
177
+ the capability's purpose and status, the acceptance criteria that prove it
178
+ (with cited evidence), and the accepted ADRs that name it. Read-only.
179
+
180
+ Builds on the intent provenance (ADR 0006/0011) — answer "why was X built,
181
+ and built this way?" without grepping the tree by hand.
182
+ `;
@@ -333,6 +333,8 @@ function printChorePlaybook(projectRoot, { id, prompt }) {
333
333
  console.log(` spec delta, it is not a chore — reopen with ${c.cyan("doctrina work \"<prompt>\"")}.`);
334
334
  console.log(`4. ${c.cyan(`doctrina change apply ${id}`)} (zero deltas → flips to applied).`);
335
335
  console.log(`5. ${c.cyan(`doctrina change archive ${id}`)}, then ${c.cyan("doctrina validate")}.`);
336
+ console.log(c.gray(" If this fix taught a lesson worth not relearning, capture it: ") +
337
+ c.cyan("doctrina skill new <slug>") + c.gray("."));
336
338
  }
337
339
 
338
340
  function printPlaybook(projectRoot, { id, prompt, pinned, matches, capability, clarity, fromDiff = false, diffMatches = [] }) {
@@ -385,6 +387,10 @@ function printPlaybook(projectRoot, { id, prompt, pinned, matches, capability, c
385
387
  console.log("");
386
388
  console.log("2. If the capability has no spec yet:");
387
389
  console.log(` ${c.cyan("doctrina spec new <capability>")}`);
390
+ console.log(c.gray(" Trace it to product intent: tag the product.md success-criteria bullet"));
391
+ console.log(c.gray(" it serves with an anchor (\"- [SC1] ...\") and set the spec's"));
392
+ console.log(c.gray(" \"**Realizes:** SC1\" header (or \"n/a — <why>\"). `doctrina validate` warns"));
393
+ console.log(c.gray(" on an active spec with no Realizes; `doctrina trace` reports the link."));
388
394
  console.log("");
389
395
  console.log("3. Write one delta per affected capability at");
390
396
  console.log(` .doctrina/changes/${id}/specs/<capability>/delta.md:`);
@@ -413,11 +419,14 @@ function printPlaybook(projectRoot, { id, prompt, pinned, matches, capability, c
413
419
  console.log("8. Prove it before declaring done (archive refuses unchecked boxes):");
414
420
  console.log(` ${c.cyan("doctrina verify")} — the project's typecheck/test/build gate`);
415
421
  console.log(` ${c.cyan("doctrina coverage")} — each acceptance criterion cites a real test`);
422
+ console.log(` ${c.cyan("doctrina trace")} — the capability traces to product intent`);
416
423
  console.log(" Then check the proposal's ## Verification boxes and every task,");
417
424
  console.log(" closing steps included, and bump Implementation to verified.");
418
425
  console.log("");
419
426
  console.log(`9. ${c.cyan(`doctrina change archive ${id}`)}`);
420
427
  console.log(`10. ${c.cyan("doctrina validate")}, then ${c.cyan("doctrina next")} for the follow-up.`);
428
+ console.log(c.gray(" If this change taught a reusable lesson (a fix you'd hate to relearn,"));
429
+ console.log(c.gray(" a recurring convention), capture it: ") + c.cyan("doctrina skill new <slug>") + c.gray("."));
421
430
  }
422
431
 
423
432
  export const help = `
package/src/index.js CHANGED
@@ -26,11 +26,17 @@ import * as coverage from "./commands/coverage.js";
26
26
  import * as verify from "./commands/verify.js";
27
27
  import * as contract from "./commands/contract.js";
28
28
  import * as trace from "./commands/trace.js";
29
+ import * as status from "./commands/status.js";
30
+ import * as close from "./commands/close.js";
31
+ import * as review from "./commands/review.js";
32
+ import * as watch from "./commands/watch.js";
33
+ import * as why from "./commands/why.js";
29
34
 
30
35
  const COMMANDS = {
31
36
  init, spec, change, decision, validate, hooks, analyze, clarify,
32
37
  templates, skill, index: indexCmd, next, metrics, context, search,
33
38
  intake, work, coverage, verify, contract, trace,
39
+ status, close, review, watch, why,
34
40
  };
35
41
 
36
42
  const TOP_HELP = `
@@ -56,6 +62,7 @@ Commands:
56
62
  skill new Scaffold an on-demand procedural memory skill
57
63
  skill list List skills with their descriptions
58
64
  skill sync Mirror skill frontmatter descriptions into index.json
65
+ skill suggest Surface fix-shaped lessons worth a skill (--write scaffolds)
59
66
  analyze Inspect a change folder before applying
60
67
  clarify Smell-test a Markdown file for ambiguity (--all for the tree)
61
68
  context Print the context pack for a task in read order
@@ -63,12 +70,17 @@ Commands:
63
70
  validate Run schema and structural checks
64
71
  coverage Report acceptance criteria with linked evidence (--strict gates)
65
72
  trace Report intent provenance: product intent → specs (--strict gates)
73
+ review Conformance review of your changes vs specs/ADRs/contracts
66
74
  verify Run project-declared typecheck/test/build checks (the real gate)
75
+ close Run the whole close sequence for a change in one pass
76
+ status One-glance project health dashboard
77
+ why Explain a capability's provenance (intent → proof → ADRs)
67
78
  templates list List the templates shipped by the installed CLI
68
79
  templates check Compare the project against the recommended template shape
69
80
  hooks install Install the pre-commit hook
70
81
  index rebuild Regenerate index.json from the artifacts on disk
71
82
  next Print the recommended next workflow actions
83
+ watch Re-run validate --fix + next on every change (--once for one pass)
72
84
  metrics Local git-derived adoption metrics (no network)
73
85
 
74
86
  Global flags:
@@ -78,7 +90,7 @@ Global flags:
78
90
 
79
91
  async function main(argv) {
80
92
  const { positional, flags } = parseArgs(argv, {
81
- boolean: ["help", "h", "version", "v", "force", "non-interactive", "check", "save", "bug", "write", "all", "concat", "archive", "strict", "list", "init"],
93
+ boolean: ["help", "h", "version", "v", "force", "non-interactive", "check", "save", "bug", "write", "all", "concat", "archive", "strict", "list", "init", "fix", "once", "clean"],
82
94
  });
83
95
 
84
96
  if (flags.get("version") || flags.get("v")) {
@@ -11,20 +11,47 @@ This repository uses the Doctrina framework for spec-driven, AGENTS.md
11
11
  native multi-agent development. The full framework artifacts live under
12
12
  `.doctrina/`.
13
13
 
14
- ## Working from intent
15
-
16
- - **Bootstrap.** If `.doctrina/intake.md` exists with `Status: pending`,
17
- the specs have not been written yet. Before anything else, run
18
- `doctrina intake` and execute the printed bootstrap playbook in one
19
- linear pass: fill `product.md`, derive capabilities, author one EARS
20
- spec each, run the quality gates (`doctrina clarify --all`,
21
- `doctrina validate`), then flip the intake to `Status: converted`. Ask
22
- the user only when the intake is genuinely ambiguous. After conversion
23
- the specs are the only source of truth — never edit `intake.md` to
24
- change requirements.
25
- - **Day-to-day.** Turn a brief request into a scaffolded change with
26
- `doctrina work "<prompt>"`, then follow the printed work playbook
27
- (spec delta → tasks → implement → analyze → apply → archive → validate).
14
+ ## Working from intent (you drive; the human stays passive)
15
+
16
+ You — the AI agent — run the Doctrina commands. The human gives a brief
17
+ prompt and approves; do not make them run the CLI or hand-author artifacts.
18
+
19
+ - **Bootstrap.** If `.doctrina/intake.md` is `Status: pending`, the specs
20
+ are not written yet: run `doctrina intake` and execute the printed
21
+ playbook in one pass (fill `product.md`, derive capabilities, one EARS
22
+ spec each, `doctrina clarify --all` + `doctrina validate`, then flip the
23
+ intake to `Status: converted`). After conversion the specs are the only
24
+ source of truth — never edit `intake.md` to change requirements.
25
+ - **Day-to-day.** Turn any request into a change with `doctrina work
26
+ "<prompt>"` and follow the printed playbook (spec delta → tasks →
27
+ implement → analyze → apply → archive → validate). Unsure what to do
28
+ next? Run `doctrina next`. Need to read? Run `doctrina context`. Ask the
29
+ human only on genuine ambiguity.
30
+
31
+ ## Doctrina command surface (reach for these — don't hand-author)
32
+
33
+ The CLI scaffolds from canonical templates and keeps `index.json` in sync,
34
+ so prefer it over writing artifacts by hand. Full list: `doctrina --help`.
35
+
36
+ - **Read / orient (any task, first & last):** `doctrina status` (health) ·
37
+ `doctrina context [<cap>] --concat` (read pack) · `doctrina next` (what's
38
+ next) · `doctrina why <cap>` (provenance) · `doctrina spec list` ·
39
+ `doctrina decision list` · `doctrina search <query>`
40
+ - **Start:** `doctrina work "<prompt>"` · `--from-diff` (backfill from
41
+ existing code) · `--chore` (no-spec infra/docs/build)
42
+ - **Scaffold (manual creation stays available):** `doctrina spec new <cap>`
43
+ (`--bug`) · `doctrina decision new "<title>"` · `doctrina change new <id>`
44
+ · `doctrina contract new <id>` · `doctrina skill new <slug>`
45
+ - **Advance / close:** `doctrina change apply|archive|diff <id>` · `doctrina
46
+ decision accept|land|supersede <n>` · `doctrina spec set <cap> [...]` ·
47
+ `doctrina close <id>` (whole close in one pass)
48
+ - **Gates (ground truth, not guesses):** `doctrina validate` (`--fix`) ·
49
+ `doctrina verify` · `doctrina review` (conformance vs specs/ADRs) ·
50
+ `doctrina coverage --strict` · `doctrina trace --strict` · `doctrina
51
+ contract check` · `doctrina clarify --all` · `doctrina analyze <id>`
52
+
53
+ Continuous: `doctrina watch` re-syncs and re-orients on every save.
54
+ Capture lessons: `doctrina skill suggest` surfaces skills worth writing.
28
55
 
29
56
  ## Stack and tooling
30
57
 
@@ -86,14 +113,14 @@ before considering work done.
86
113
 
87
114
  ## How to read context efficiently
88
115
 
89
- 1. Read this `AGENTS.md`.
90
- 2. Read `.doctrina/product.md`.
91
- 3. Read the relevant `.doctrina/specs/<capability>/spec.md`.
92
- 4. Read any open `.doctrina/changes/<id>/`.
93
- 5. Read `.doctrina/decisions/` filtered by `Status: accepted`.
94
- 6. If the task matches a skill in `.doctrina/skills/`, load the
95
- relevant skill on demand (read its `description:` / `when:`
96
- frontmatter; load the full body only when the trigger fires).
116
+ `doctrina context [<capability>] --concat` assembles the read pack in one
117
+ call, in order: this `AGENTS.md` → `.doctrina/product.md` → the capability
118
+ spec → open `.doctrina/changes/<id>/` → `.doctrina/decisions/` filtered to
119
+ `Status: accepted`. Run it for ANY task, not only `doctrina work`; it skips
120
+ `changes/archive/` (history — read only when debugging it).
121
+
122
+ On demand only: if the task matches a skill in `.doctrina/skills/`, read its
123
+ `description:` / `when:` frontmatter; load the full body when the trigger fires.
97
124
 
98
125
  Keep this file under 150 lines. Density beats prose. Use exact commands.
99
126
 
@@ -1,7 +1,24 @@
1
1
  #!/usr/bin/env sh
2
2
  # Doctrina pre-commit hook.
3
- # Installed by `doctrina hooks install`. Runs validate and blocks the
4
- # commit if it exits non-zero. Edit freely after install; the CLI will
5
- # not overwrite without --force.
3
+ # Installed by `doctrina hooks install`. Edit freely after install; the CLI
4
+ # will not overwrite without --force.
5
+ #
6
+ # Runs `doctrina validate --fix`:
7
+ # 1. --fix regenerates index.json from the tree (repairs metadata drift and
8
+ # migrates the framework_version stamp) BEFORE checking, so the single
9
+ # most common gate failure — a hand-edited header that drifted the index —
10
+ # is healed at commit time instead of blocking the commit.
11
+ # 2. validate still exits non-zero (and blocks the commit) on real errors a
12
+ # rebuild cannot heal: missing artifacts, duplicate ADR numbers, broken
13
+ # EARS, ledger/index disagreement, and so on.
14
+ #
15
+ # To gate without auto-repairing (CI-style, fail on any drift), replace the
16
+ # line below with a bare `doctrina validate`.
6
17
 
7
- doctrina validate
18
+ doctrina validate --fix || exit 1
19
+
20
+ # Re-stage the index if --fix rewrote it, so the repaired index lands in this
21
+ # commit rather than as a leftover unstaged change. Harmless when unchanged.
22
+ if [ -f .doctrina/index.json ]; then
23
+ git add .doctrina/index.json
24
+ fi
@@ -3,6 +3,7 @@
3
3
  **Capability:** {{CAPABILITY}}
4
4
  **Status:** draft
5
5
  **Implementation:** planned
6
+ **Realizes:** <product.md success-criteria anchors this delivers, or "n/a — <why>">
6
7
  **Last updated:** {{DATE}}
7
8
  **Version:** 0.1.0
8
9
 
@@ -18,9 +19,12 @@ behind it, and `doctrina validate` warns. To record a deliberate gap and
18
19
  silence the warning, add a note after the value, e.g.
19
20
  "**Implementation:** planned — backend deferred to Q3, see ADR 0007".
20
21
 
21
- Optional: trace this capability to product intent by adding a header
22
- "**Realizes:** SC1, SC3" that names the product.md anchors it delivers
23
- (`doctrina trace`; ADR 0006).
22
+ Realizes ties this capability to product intent (`doctrina trace`;
23
+ ADR 0006). Tag product.md success-criteria bullets with an anchor —
24
+ "- [SC1] ..." — and list the ones this spec delivers above. An "active"
25
+ spec with no Realizes: header warns under `doctrina validate`; if the
26
+ capability genuinely maps to no product anchor (internal tooling), keep
27
+ the header and write "n/a — <why>" to record that on purpose.
24
28
  -->
25
29
 
26
30
  ## Purpose