doctrina-cli 0.9.0 → 0.10.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.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > CLI for the Doctrina framework. The package is published as
4
4
  > `doctrina-cli`; the executable it installs is `doctrina`.
5
5
 
6
- **Status:** v0.6.0 — current release. Zero runtime dependencies
6
+ **Status:** v0.10.0 — current release. Zero runtime dependencies
7
7
  (Node.js standard library only).
8
8
 
9
9
  ## Install
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doctrina-cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Doctrina CLI — spec-driven, AGENTS.md-native framework for multi-agent AI development.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -0,0 +1,136 @@
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 { listHeader } from "../lib/scan.js";
6
+ import { c } from "../lib/colors.js";
7
+
8
+ // The project's standing rules in one read — Spec Kit parity for its
9
+ // `constitution.md`, but ASSEMBLED, not a new home for facts. Doctrina's
10
+ // constitution is its accepted ADRs (the immutable decisions that govern how
11
+ // the codebase evolves) plus the product's declared non-goals. Both already
12
+ // own those facts elsewhere; this is a read-only digest that gathers them so a
13
+ // human or agent (especially one migrating from Spec Kit, which expects a
14
+ // single constitution) can see "the rules" without reading the whole tree.
15
+ //
16
+ // To change a principle, supersede its ADR; to change a non-goal, edit
17
+ // product.md. The command never writes — it has nothing of its own to own.
18
+
19
+ export async function run(_positional, _flags) {
20
+ const projectRoot = process.cwd();
21
+ if (!exists(path.join(projectRoot, ".doctrina"))) {
22
+ throw new Error("not a Doctrina project (no .doctrina/ in cwd). Run `doctrina init` first.");
23
+ }
24
+
25
+ const project = projectName(projectRoot);
26
+ console.log(
27
+ c.bold("Doctrina constitution") +
28
+ c.gray(` — ${project} (standing rules: accepted decisions + non-goals)`),
29
+ );
30
+
31
+ // Principles — the accepted ADRs, by number. Every accepted decision is a
32
+ // binding rule; superseded/withdrawn/proposed ADRs are not yet (or no longer)
33
+ // in force and stay out.
34
+ console.log("");
35
+ console.log(c.bold(" Principles") + c.gray(" (immutable — supersede an ADR to change one)"));
36
+ const adrs = acceptedDecisions(projectRoot);
37
+ if (adrs.length === 0) {
38
+ console.log(` ${c.gray("no accepted ADRs yet — record decisions with `doctrina decision new`")}`);
39
+ } else {
40
+ for (const a of adrs) console.log(` ${c.cyan("ADR " + a.id)} ${a.title}`);
41
+ }
42
+
43
+ // Non-goals — the explicit "what this project will not be", from product.md.
44
+ console.log("");
45
+ console.log(c.bold(" Non-goals") + c.gray(" (.doctrina/product.md)"));
46
+ const nonGoals = productSection(projectRoot, "Non-goals");
47
+ if (nonGoals.length === 0) {
48
+ console.log(` ${c.gray("none declared — add a `## Non-goals` section to product.md")}`);
49
+ } else {
50
+ for (const g of nonGoals) console.log(` ${c.gray("•")} ${g}`);
51
+ }
52
+
53
+ console.log("");
54
+ console.log(
55
+ c.gray(
56
+ ` ${adrs.length} accepted decision${adrs.length === 1 ? "" : "s"} · ` +
57
+ `${nonGoals.length} non-goal${nonGoals.length === 1 ? "" : "s"} · read-only`,
58
+ ),
59
+ );
60
+ return 0;
61
+ }
62
+
63
+ function projectName(projectRoot) {
64
+ try {
65
+ return JSON.parse(read(path.join(projectRoot, ".doctrina", "index.json"))).project
66
+ ?? path.basename(projectRoot);
67
+ } catch {
68
+ return path.basename(projectRoot);
69
+ }
70
+ }
71
+
72
+ // Accepted ADRs (NNNN-slug.md with Status: accepted), oldest first, with the
73
+ // title read from the `# ADR NNNN — <title>` heading.
74
+ function acceptedDecisions(projectRoot) {
75
+ const dir = path.join(projectRoot, ".doctrina", "decisions");
76
+ const out = [];
77
+ if (!isDir(dir)) return out;
78
+ for (const f of readdirSync(dir).sort()) {
79
+ const m = f.match(/^(\d{4})-.*\.md$/);
80
+ if (!m) continue;
81
+ const text = read(path.join(dir, f));
82
+ if ((listHeader(text, "Status") ?? "").toLowerCase() !== "accepted") continue;
83
+ const titleMatch = text.match(/^#\s+ADR\s+\d{4}\s*[—-]\s*(.+)$/m);
84
+ out.push({ id: m[1], title: titleMatch ? titleMatch[1].trim() : f });
85
+ }
86
+ return out;
87
+ }
88
+
89
+ // Bullets under a named `## <section>` of product.md, each accumulated across
90
+ // its continuation lines so a wrapped bullet reads as one rule.
91
+ function productSection(projectRoot, name) {
92
+ const p = path.join(projectRoot, ".doctrina", "product.md");
93
+ if (!isFile(p)) return [];
94
+ const lines = read(p).split(/\r?\n/);
95
+ let inSection = false;
96
+ const out = [];
97
+ let cur = null;
98
+ const flush = () => {
99
+ if (cur) out.push(cur.replace(/\s+/g, " ").trim());
100
+ cur = null;
101
+ };
102
+ for (const line of lines) {
103
+ if (/^##\s+/.test(line)) {
104
+ if (inSection) {
105
+ flush();
106
+ break;
107
+ }
108
+ if (new RegExp(`^##\\s+${name}\\b`, "i").test(line)) inSection = true;
109
+ continue;
110
+ }
111
+ if (!inSection) continue;
112
+ const m = line.match(/^\s*[-*]\s+(.+)$/);
113
+ if (m) {
114
+ flush();
115
+ cur = m[1];
116
+ } else if (cur && line.trim() !== "") {
117
+ cur += " " + line.trim();
118
+ } else if (line.trim() === "") {
119
+ flush();
120
+ }
121
+ }
122
+ flush();
123
+ return out;
124
+ }
125
+
126
+ export const help = `
127
+ Usage: doctrina constitution
128
+
129
+ Print the project's standing rules in one read: the accepted ADRs (the
130
+ immutable decisions that govern how the codebase evolves) and the product's
131
+ declared non-goals. Read-only — it assembles facts the ADRs and product.md
132
+ already own; it never writes.
133
+
134
+ This is the Spec Kit \`constitution.md\` analogue: a single place to see the
135
+ non-negotiables. To change one, supersede the ADR or edit product.md.
136
+ `;
@@ -36,8 +36,14 @@ export async function run(positional, flags) {
36
36
  pushFile("AGENTS.md", "root rules");
37
37
  pushFile(".doctrina/product.md", "product truth");
38
38
 
39
- // 3. The capability spec under work.
39
+ // 3. The capability spec(s) — the current truth. With a named capability,
40
+ // just that one. Without one, EVERY active spec: the default orientation
41
+ // read previously jumped from product.md straight to the ADRs, leaving
42
+ // the single source of truth out of the pack entirely (a review/debug/
43
+ // question read got history but not current truth). Specs come before
44
+ // open changes and ADRs, matching the documented read order.
40
45
  let specMissing = false;
46
+ const specsRoot = path.join(projectRoot, ".doctrina", "specs");
41
47
  if (capability) {
42
48
  const specRel = `.doctrina/specs/${capability}/spec.md`;
43
49
  if (isFile(path.join(projectRoot, specRel))) {
@@ -45,6 +51,11 @@ export async function run(positional, flags) {
45
51
  } else {
46
52
  specMissing = true;
47
53
  }
54
+ } else if (isDir(specsRoot)) {
55
+ for (const cap of readdirSync(specsRoot).sort()) {
56
+ const specRel = `.doctrina/specs/${cap}/spec.md`;
57
+ if (isFile(path.join(projectRoot, specRel))) pushFile(specRel, `spec: ${cap}`);
58
+ }
48
59
  }
49
60
 
50
61
  // 4. Open changes (their proposal, tasks, and deltas).
@@ -117,9 +128,10 @@ export const help = `
117
128
  Usage: doctrina context [<capability>] [--concat]
118
129
 
119
130
  Print the exact context pack for a task, in the documented read
120
- order: AGENTS.md, product.md, the capability spec (when given), open
121
- changes, accepted ADRs. Skills are listed name + description only —
122
- they are on-demand by design. The change archive is excluded.
131
+ order: AGENTS.md, product.md, the capability spec (when given —
132
+ otherwise every active spec, so the current truth is never absent),
133
+ open changes, accepted ADRs. Skills are listed name + description only
134
+ — they are on-demand by design. The change archive is excluded.
123
135
 
124
136
  Flags:
125
137
  --concat Print the file contents (with separators) instead of the
@@ -11,6 +11,13 @@ import { c } from "../lib/colors.js";
11
11
  import { parseFrontmatter } from "./skill.js";
12
12
  import { checkEars, isEarsSpec } from "../lib/ears.js";
13
13
  import { specHeader, listHeader, deriveIndex, indexesMatch, stableStringify } from "../lib/scan.js";
14
+ import { COMMAND_NAMES, referencedCommands } from "../lib/commands.js";
15
+ import { parseAcceptanceCriteria, isVerified } from "../lib/criteria.js";
16
+
17
+ // AGENTS.md is treated as a maintained doctrina-command catalog only once it
18
+ // documents at least this many real commands; below it, the file defers to
19
+ // `doctrina --help` and is never nagged about omissions.
20
+ const CATALOG_THRESHOLD = 8;
14
21
 
15
22
  export async function run(_positional, flags) {
16
23
  const projectRoot = process.cwd();
@@ -25,6 +32,46 @@ export async function run(_positional, flags) {
25
32
  const lines = lineCount(agentsMd);
26
33
  if (lines > 200) errors.push(`AGENTS.md is ${lines} lines (>200, hard limit)`);
27
34
  else if (lines > 150) warnings.push(`AGENTS.md is ${lines} lines (>150 soft limit)`);
35
+
36
+ // 1c. AGENTS.md command-surface drift. AGENTS.md is the hub the agent reads
37
+ // first, so the doctrina commands it documents must match the real CLI.
38
+ // Two drifts are flagged: a reference to a command that does not exist
39
+ // (a typo or a removed command — the agent will try to run it and fail),
40
+ // and a maintained catalog that has fallen behind the CLI (commands the
41
+ // agent never discovers because the hub never names them). validate only
42
+ // checked AGENTS.md *size* before, so the hub could rot while staying
43
+ // green; this closes that gap.
44
+ const agentsText = read(agentsMd);
45
+ const referenced = referencedCommands(agentsText);
46
+ const known = new Set(COMMAND_NAMES);
47
+ // Reverse drift is always flagged — a dangling command reference misleads
48
+ // the agent regardless of how the file is structured.
49
+ for (const cmd of [...referenced].sort()) {
50
+ if (!known.has(cmd)) {
51
+ warnings.push(
52
+ `AGENTS.md references \`doctrina ${cmd}\` which is not a CLI command ` +
53
+ `(typo or removed command — agents reading the hub will try to run it)`,
54
+ );
55
+ }
56
+ }
57
+ // Forward drift (omitted commands) is flagged only when the file presents
58
+ // itself as an exhaustive catalog: it documents many commands AND does not
59
+ // defer to `doctrina --help`. A file that defers is declaring its list
60
+ // illustrative (the shipped template does this), so its omissions are
61
+ // intentional and stay silent.
62
+ const documented = [...referenced].filter((cmd) => known.has(cmd));
63
+ const defersToHelp = /doctrina(?:-cli)?\s+--help/.test(agentsText);
64
+ if (documented.length >= CATALOG_THRESHOLD && !defersToHelp) {
65
+ const missing = COMMAND_NAMES.filter((cmd) => !referenced.has(cmd));
66
+ if (missing.length > 0) {
67
+ warnings.push(
68
+ `AGENTS.md documents the doctrina command surface but omits ` +
69
+ `${missing.length} command${missing.length === 1 ? "" : "s"} ` +
70
+ `(${missing.join(", ")}) — agents reading the hub will not discover ` +
71
+ `them (add them, or defer to \`doctrina --help\` for the full list)`,
72
+ );
73
+ }
74
+ }
28
75
  }
29
76
 
30
77
  // 1b. Nested AGENTS.md files ("nearest wins" hierarchy) obey the same
@@ -306,6 +353,23 @@ export async function run(_positional, flags) {
306
353
  }
307
354
  }
308
355
  }
356
+
357
+ // 8f. Self-certified acceptance criteria (honest gates, ADR 0008). A
358
+ // criterion that asserts [verified] but cites no proof path is a
359
+ // claim with nothing behind it — the dishonest-green case. `coverage`
360
+ // reports every bare criterion; this names the specific combo
361
+ // (claims verified, proves nothing) in the always-run gate. It reads
362
+ // through the shared multi-line parser, so a proof cited on a
363
+ // continuation line still counts (no false positive).
364
+ for (const crit of parseAcceptanceCriteria(text)) {
365
+ if (isVerified(crit) && crit.proofPaths.length === 0) {
366
+ warnings.push(
367
+ `${relPath(projectRoot, specPath)}: acceptance criterion #${crit.n} is marked ` +
368
+ `[verified] but cites no proof path — self-certified (cite the file/test in ` +
369
+ `backticks, or drop the marker; \`doctrina coverage\`)`,
370
+ );
371
+ }
372
+ }
309
373
  }
310
374
  }
311
375
  }
@@ -3,6 +3,8 @@ import process from "node:process";
3
3
  import { readdirSync } from "node:fs";
4
4
  import { exists, isDir, isFile, read } from "../lib/fs-ops.js";
5
5
  import { specHeader, listHeader } from "../lib/scan.js";
6
+ import { parseAcceptanceCriteria, isVerified } from "../lib/criteria.js";
7
+ import * as idx from "../lib/index-json.js";
6
8
  import { c } from "../lib/colors.js";
7
9
  import { suggest } from "../lib/suggest.js";
8
10
 
@@ -66,16 +68,35 @@ export async function run(positional, _flags) {
66
68
  const purpose = sectionParagraph(text, "Purpose");
67
69
  if (purpose) console.log(" " + purpose.replace(/\s+/g, " ").trim());
68
70
 
69
- // 3. The proof (acceptance criteria with cited evidence).
71
+ // 3. The proof (acceptance criteria with cited evidence). Read through the
72
+ // shared multi-line parser so the marks here agree with `doctrina
73
+ // coverage` — a criterion that cites its proof on a continuation line is
74
+ // no longer mis-reported as "no evidence cited". The mark distinguishes
75
+ // the three honest states, including the one that used to read as a false
76
+ // green: a [verified] claim that cites nothing.
70
77
  console.log("");
71
78
  console.log(c.bold(" Proof"));
72
- const criteria = acceptanceWithEvidence(text);
79
+ const criteria = parseAcceptanceCriteria(text);
73
80
  if (criteria.length === 0) {
74
81
  console.log(` ${c.gray("no acceptance criteria declared")}`);
75
82
  } else {
76
83
  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}`);
84
+ const paths = cr.proofPaths.map((p) => `\`${p}\``).join(", ");
85
+ let mark, note;
86
+ if (isVerified(cr) && cr.proofPaths.length > 0) {
87
+ mark = c.green("✓");
88
+ note = c.gray(paths);
89
+ } else if (isVerified(cr)) {
90
+ mark = c.yellow("~");
91
+ note = c.yellow("marked verified but cites no proof");
92
+ } else if (cr.marker !== null) {
93
+ mark = c.yellow("○");
94
+ note = cr.proofPaths.length ? c.gray(paths) : c.gray(`[${cr.marker}]`);
95
+ } else {
96
+ mark = c.gray("·");
97
+ note = cr.proofPaths.length ? c.gray(paths) : c.yellow("no evidence cited");
98
+ }
99
+ console.log(` ${mark} #${cr.n} ${note}`);
79
100
  }
80
101
  }
81
102
 
@@ -89,9 +110,44 @@ export async function run(positional, _flags) {
89
110
  for (const a of adrs) console.log(` ${c.cyan("ADR " + a.id)} ${a.title}`);
90
111
  }
91
112
 
113
+ // 5. History — the archived changes that built this capability. The default
114
+ // context pack excludes changes/archive/ by design, so "what shaped this
115
+ // spec?" otherwise loses the change trail. The index ledger already
116
+ // records each archived change's affected capabilities (specs_affected);
117
+ // surface them here so the provenance chain reaches back to the work that
118
+ // delivered the spec, not only the intent and decisions in front of it.
119
+ console.log("");
120
+ console.log(c.bold(" History"));
121
+ const history = archivedChangesFor(projectRoot, cap);
122
+ if (history.length === 0) {
123
+ console.log(` ${c.gray("no archived change recorded against this capability")}`);
124
+ } else {
125
+ for (const ch of history) {
126
+ console.log(` ${c.cyan(ch.applied ?? "—")} ${ch.title} ${c.gray(`(${ch.op})`)}`);
127
+ }
128
+ }
129
+
92
130
  return 0;
93
131
  }
94
132
 
133
+ // Archived changes whose recorded specs_affected include this capability,
134
+ // oldest first. Read from the index ledger (the same data `index rebuild`
135
+ // derives from each change's spec deltas); absent or unreadable index → none.
136
+ function archivedChangesFor(projectRoot, cap) {
137
+ let index;
138
+ try {
139
+ index = idx.load(projectRoot);
140
+ } catch {
141
+ return [];
142
+ }
143
+ const out = [];
144
+ for (const ch of index.artifacts?.changes_archive ?? []) {
145
+ const hit = (ch.specs_affected ?? []).find((s) => s.capability === cap);
146
+ if (hit) out.push({ applied: ch.applied, title: ch.title, op: hit.operation ?? "MODIFIED" });
147
+ }
148
+ return out.sort((a, b) => String(a.applied ?? "").localeCompare(String(b.applied ?? "")));
149
+ }
150
+
95
151
  // Map of anchor id -> the product.md bullet text that declares it.
96
152
  function productAnchors(projectRoot) {
97
153
  const out = new Map();
@@ -126,30 +182,6 @@ function sectionParagraph(text, name) {
126
182
  return buf.join(" ");
127
183
  }
128
184
 
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
185
  // Accepted ADRs whose body names the capability (word-ish boundary).
154
186
  function decisionsMentioning(projectRoot, cap) {
155
187
  const adrDir = path.join(projectRoot, ".doctrina", "decisions");
@@ -175,7 +207,8 @@ Usage: doctrina why <capability>
175
207
  Explain a capability's provenance by assembling the chain Doctrina already
176
208
  records into one read: the product intent it **Realizes:** ([SC1] anchors),
177
209
  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.
210
+ (with cited evidence), the accepted ADRs that name it, and the archived
211
+ changes that built it (from the index ledger). Read-only.
179
212
 
180
213
  Builds on the intent provenance (ADR 0006/0011) — answer "why was X built,
181
214
  and built this way?" without grepping the tree by hand.
package/src/index.js CHANGED
@@ -31,12 +31,13 @@ import * as close from "./commands/close.js";
31
31
  import * as review from "./commands/review.js";
32
32
  import * as watch from "./commands/watch.js";
33
33
  import * as why from "./commands/why.js";
34
+ import * as constitution from "./commands/constitution.js";
34
35
 
35
36
  const COMMANDS = {
36
37
  init, spec, change, decision, validate, hooks, analyze, clarify,
37
38
  templates, skill, index: indexCmd, next, metrics, context, search,
38
39
  intake, work, coverage, verify, contract, trace,
39
- status, close, review, watch, why,
40
+ status, close, review, watch, why, constitution,
40
41
  };
41
42
 
42
43
  const TOP_HELP = `
@@ -75,6 +76,7 @@ Commands:
75
76
  close Run the whole close sequence for a change in one pass
76
77
  status One-glance project health dashboard
77
78
  why Explain a capability's provenance (intent → proof → ADRs)
79
+ constitution Print the standing rules: accepted ADRs + product non-goals
78
80
  templates list List the templates shipped by the installed CLI
79
81
  templates check Compare the project against the recommended template shape
80
82
  hooks install Install the pre-commit hook
@@ -0,0 +1,46 @@
1
+ // Single canonical list of top-level `doctrina` commands.
2
+ //
3
+ // This is the one place the command surface is named. `src/index.js` builds
4
+ // its dispatch table for these names (an execution-based test asserts every
5
+ // name here resolves to a real handler, so the two never drift), and
6
+ // `doctrina validate` compares it against the commands AGENTS.md documents —
7
+ // the hub the agent reads first — so a stale command catalog or a reference
8
+ // to a command that no longer exists becomes a loud signal instead of silent
9
+ // rot (the "AGENTS.md is the source of truth but nothing keeps it fresh" gap).
10
+ //
11
+ // Keep this sorted by the workflow order used in `--help`, not alphabetically.
12
+ export const COMMAND_NAMES = [
13
+ // bootstrap / day-to-day
14
+ "init", "intake", "work",
15
+ // authoring
16
+ "spec", "change", "contract", "decision", "skill",
17
+ // read / orient
18
+ "context", "search", "status", "next", "why", "constitution",
19
+ // gates
20
+ "analyze", "clarify", "validate", "coverage", "trace", "review", "verify", "close",
21
+ // maintenance
22
+ "templates", "hooks", "index", "watch", "metrics",
23
+ ];
24
+
25
+ // Extract the set of top-level `doctrina <command>` references that appear in
26
+ // CODE context (fenced blocks or inline backtick spans) of a Markdown file —
27
+ // AGENTS.md, typically. Restricting to code context avoids matching prose like
28
+ // "the doctrina framework", which is not a command reference. Both the bare
29
+ // `doctrina <cmd>` and the `npx doctrina-cli <cmd>` forms are recognised.
30
+ export function referencedCommands(markdown) {
31
+ const out = new Set();
32
+ const chunks = [];
33
+ // Pull fenced blocks first and strip them out, so the inline-span scan below
34
+ // cannot mis-pair backticks across a fence boundary (a fence's own backticks
35
+ // would otherwise shift which inline spans pair up).
36
+ const rest = markdown.replace(/```[\s\S]*?```/g, (block) => {
37
+ chunks.push(block);
38
+ return "\n";
39
+ });
40
+ for (const m of rest.matchAll(/`[^`]+`/g)) chunks.push(m[0]);
41
+ const hay = chunks.join("\n");
42
+ for (const m of hay.matchAll(/(?:npx\s+doctrina-cli|doctrina)\s+([a-z][a-z-]+)/g)) {
43
+ out.add(m[1]);
44
+ }
45
+ return out;
46
+ }
@@ -0,0 +1,76 @@
1
+ // Shared parser for the "## Acceptance criteria" section of a spec.
2
+ //
3
+ // A criterion is a numbered item that MAY span several lines (continuation
4
+ // prose), and its evidence is cited as a backtick path span on ANY of those
5
+ // lines. A single-line parser silently misses proof sitting on a continuation
6
+ // line — the bug that made `doctrina why` print "no evidence cited" for a
7
+ // criterion `doctrina coverage` counted as covered. Both commands (and the
8
+ // validate honest-gate) read criteria through this one parser so they can
9
+ // never disagree about what a criterion cites.
10
+ //
11
+ // Returns one row per criterion: { n, marker, body, proofPaths } where marker
12
+ // is the lowercased `[tag]` (e.g. "verified", "unverified") or null, and
13
+ // proofPaths is the de-duplicated list of file-path-looking backtick tokens.
14
+
15
+ export function parseAcceptanceCriteria(text) {
16
+ const lines = text.split(/\r?\n/);
17
+ let inSection = false;
18
+ const rows = [];
19
+ let cur = null;
20
+ const flush = () => {
21
+ if (cur) {
22
+ cur.proofPaths = backtickPaths(cur.body);
23
+ rows.push(cur);
24
+ }
25
+ cur = null;
26
+ };
27
+ for (const line of lines) {
28
+ if (/^##\s+/.test(line)) {
29
+ if (inSection) {
30
+ flush();
31
+ inSection = false;
32
+ }
33
+ if (/^##\s+Acceptance criteria\b/i.test(line)) inSection = true;
34
+ continue;
35
+ }
36
+ if (!inSection) continue;
37
+ const m = line.match(/^\s*(\d+)\.\s+(.*)$/);
38
+ if (m) {
39
+ flush();
40
+ const body = m[2];
41
+ const markMatch = body.match(/^\[([^\]]+)\]/);
42
+ cur = { n: Number(m[1]), marker: markMatch ? markMatch[1].toLowerCase() : null, body };
43
+ } else if (cur) {
44
+ cur.body += (line.trim() === "" ? " " : " " + line.trim());
45
+ }
46
+ }
47
+ flush();
48
+ return rows.filter((r) => r.body.trim().length > 0);
49
+ }
50
+
51
+ // True when a criterion asserts it has been verified (the `[verified...]` tag).
52
+ export function isVerified(row) {
53
+ return row.marker !== null && /^verified/.test(row.marker);
54
+ }
55
+
56
+ function backtickPaths(text) {
57
+ const out = new Set();
58
+ for (const m of text.matchAll(/`([^`]+)`/g)) {
59
+ const token = m[1].trim();
60
+ if (looksLikePath(token)) out.add(token);
61
+ }
62
+ return [...out];
63
+ }
64
+
65
+ // Mirrors the coverage heuristic so the two commands agree: a token is a path
66
+ // if it has a slash or a file extension and is not a URL/placeholder.
67
+ function looksLikePath(s) {
68
+ if (!s) return false;
69
+ if (/^(https?:|mailto:|ftp:|#|@)/i.test(s)) return false;
70
+ if (/[\s<>*?{}]/.test(s)) return false;
71
+ const hasSlash = s.includes("/");
72
+ const hasExt = /\.[a-z0-9]{1,8}$/i.test(s);
73
+ if (!hasSlash && !hasExt) return false;
74
+ if (s.startsWith("-")) return false;
75
+ return true;
76
+ }
@@ -39,6 +39,7 @@ export function blank(projectName, date) {
39
39
  framework_version: cliVersion(),
40
40
  last_updated: date,
41
41
  artifacts: {
42
+ entrypoint: { path: "AGENTS.md" },
42
43
  product: {
43
44
  path: ".doctrina/product.md",
44
45
  status: "active",
package/src/lib/scan.js CHANGED
@@ -192,6 +192,13 @@ export function deriveIndex(projectRoot, current) {
192
192
  });
193
193
  }
194
194
 
195
+ // Entrypoint — the root AGENTS.md the agent reads first (the hub the
196
+ // AGENTS.md-drift gate in `validate` keeps honest). Registering it closes
197
+ // the machine-readable graph in both directions: a tool that enumerates
198
+ // index.json finds the hub itself, not only the artifacts the hub points at.
199
+ // Path-only, so the field never churns the index.
200
+ out.artifacts.entrypoint = { path: "AGENTS.md" };
201
+
195
202
  return out;
196
203
  }
197
204
 
@@ -35,8 +35,9 @@ so prefer it over writing artifacts by hand. Full list: `doctrina --help`.
35
35
 
36
36
  - **Read / orient (any task, first & last):** `doctrina status` (health) ·
37
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>`
38
+ next) · `doctrina why <cap>` (provenance) · `doctrina constitution`
39
+ (standing rules) · `doctrina spec list` · `doctrina decision list` ·
40
+ `doctrina search <query>`
40
41
  - **Start:** `doctrina work "<prompt>"` · `--from-diff` (backfill from
41
42
  existing code) · `--chore` (no-spec infra/docs/build)
42
43
  - **Scaffold (manual creation stays available):** `doctrina spec new <cap>`
@@ -0,0 +1,13 @@
1
+ ---
2
+ description: Load the Doctrina read pack for a task, in read order
3
+ argument-hint: [capability]
4
+ ---
5
+
6
+ Assemble the context for **{{PROJECT_NAME}}** before doing any work.
7
+
8
+ Run `doctrina context $ARGUMENTS --concat` and read the output top to bottom:
9
+ AGENTS.md → product.md → the capability spec(s) → open changes → accepted ADRs.
10
+ With no capability it includes every active spec. It excludes the change
11
+ archive by design — run `doctrina why <capability>` when you need history.
12
+
13
+ Do this for ANY task (review, debug, a question), not only new work.
@@ -0,0 +1,13 @@
1
+ ---
2
+ description: Show and act on the next recommended Doctrina workflow step
3
+ ---
4
+
5
+ Decide the next step for **{{PROJECT_NAME}}** from the framework's own signals.
6
+
7
+ 1. Run `doctrina next` and `doctrina status` to see open work and gate health.
8
+ 2. Do the single highest-priority action it names (resume a change, apply a
9
+ delta, archive, accept an ADR, fix index drift, …).
10
+ 3. Re-run `doctrina next` to confirm the item cleared.
11
+
12
+ Use the read-only commands first; never guess what is pending — let the CLI
13
+ report it.
@@ -0,0 +1,12 @@
1
+ ---
2
+ description: One-glance Doctrina project health and gate status
3
+ ---
4
+
5
+ Report the health of **{{PROJECT_NAME}}**.
6
+
7
+ Run `doctrina status` for the dashboard (index drift, framework stamp,
8
+ coverage %, trace anchors, verify config, artifact counts). For the
9
+ authoritative gates run `doctrina validate` and `doctrina verify`, and
10
+ `doctrina next` for the recommended action.
11
+
12
+ Summarise what is green, what is not, and the single next step.
@@ -0,0 +1,12 @@
1
+ ---
2
+ description: Explain a capability's provenance (intent → proof → ADRs → history)
3
+ argument-hint: <capability>
4
+ ---
5
+
6
+ Explain why a capability of **{{PROJECT_NAME}}** exists and is shaped as it is.
7
+
8
+ Run `doctrina why $ARGUMENTS` and relay the provenance chain it prints: the
9
+ product intent it Realizes, its purpose and status, the acceptance criteria
10
+ that prove it (with cited evidence), the accepted ADRs that decided it, and
11
+ the archived changes that built it. Read-only — answer from the artifacts,
12
+ don't grep the tree by hand.
@@ -0,0 +1,17 @@
1
+ ---
2
+ description: Turn a request into a Doctrina change and drive it through the gates
3
+ argument-hint: <prompt describing the change>
4
+ ---
5
+
6
+ Drive the Doctrina spec-driven workflow for **{{PROJECT_NAME}}**.
7
+
8
+ 1. `doctrina work "$ARGUMENTS"` — scaffolds the change and prints the playbook.
9
+ (No prompt, but you already edited code? Use `doctrina work --from-diff`.)
10
+ 2. Execute the printed playbook in order: read the pack with
11
+ `doctrina context <capability> --concat`, write the spec delta, fill
12
+ `tasks.md`, implement, then `doctrina analyze <id>` → `doctrina change apply <id>`.
13
+ 3. Close it: `doctrina close <id>` (verify → coverage → archive → validate) and
14
+ resolve every `error:` before reporting the work done.
15
+
16
+ Reach for the CLI instead of hand-authoring artifacts. Ask the human only on
17
+ genuine ambiguity.
@@ -0,0 +1,10 @@
1
+ # /doctrina-context — read pack for {{PROJECT_NAME}}
2
+
3
+ Assemble the context before doing any work.
4
+
5
+ Run `doctrina context <capability> --concat` and read top to bottom:
6
+ AGENTS.md → product.md → the capability spec(s) → open changes → accepted ADRs.
7
+ With no capability it includes every active spec. The change archive is
8
+ excluded by design — run `doctrina why <capability>` when you need history.
9
+
10
+ Do this for ANY task (review, debug, a question), not only new work.
@@ -0,0 +1,10 @@
1
+ # /doctrina-next — next step for {{PROJECT_NAME}}
2
+
3
+ Decide the next step from the framework's own signals.
4
+
5
+ 1. Run `doctrina next` and `doctrina status` to see open work and gate health.
6
+ 2. Do the single highest-priority action it names (resume a change, apply a
7
+ delta, archive, accept an ADR, fix index drift, …).
8
+ 3. Re-run `doctrina next` to confirm the item cleared.
9
+
10
+ Use the read-only commands first; never guess what is pending.
@@ -0,0 +1,10 @@
1
+ # /doctrina-status — health of {{PROJECT_NAME}}
2
+
3
+ Report the project's health and gate status.
4
+
5
+ Run `doctrina status` for the dashboard (index drift, framework stamp,
6
+ coverage %, trace anchors, verify config, artifact counts). For the
7
+ authoritative gates run `doctrina validate` and `doctrina verify`, and
8
+ `doctrina next` for the recommended action.
9
+
10
+ Summarise what is green, what is not, and the single next step.
@@ -0,0 +1,9 @@
1
+ # /doctrina-why — provenance for {{PROJECT_NAME}}
2
+
3
+ Explain why a capability exists and is shaped as it is.
4
+
5
+ Run `doctrina why <capability>` and relay the provenance chain it prints: the
6
+ product intent it Realizes, its purpose and status, the acceptance criteria
7
+ that prove it (with cited evidence), the accepted ADRs that decided it, and
8
+ the archived changes that built it. Read-only — answer from the artifacts,
9
+ don't grep the tree by hand.
@@ -0,0 +1,12 @@
1
+ # /doctrina-work — drive a change for {{PROJECT_NAME}}
2
+
3
+ Turn the request into a Doctrina change and drive it through the gates.
4
+
5
+ 1. Run `doctrina work "<the request>"` (or `doctrina work --from-diff` if you
6
+ already edited code) and follow the printed playbook.
7
+ 2. `doctrina context <capability> --concat` → write the spec delta → fill
8
+ `tasks.md` → implement → `doctrina analyze <id>` → `doctrina change apply <id>`.
9
+ 3. `doctrina close <id>` (verify → coverage → archive → validate); resolve
10
+ every `error:` before reporting the work done.
11
+
12
+ Prefer the CLI over hand-authoring artifacts. Ask only on genuine ambiguity.
@@ -4,6 +4,9 @@
4
4
  "framework_version": "{{FRAMEWORK_VERSION}}",
5
5
  "last_updated": "{{DATE}}",
6
6
  "artifacts": {
7
+ "entrypoint": {
8
+ "path": "AGENTS.md"
9
+ },
7
10
  "product": {
8
11
  "path": ".doctrina/product.md",
9
12
  "status": "active",