doctrina-cli 0.16.0 → 0.18.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.
Files changed (71) hide show
  1. package/README.md +31 -73
  2. package/package.json +3 -3
  3. package/src/commands/adapter.js +3 -2
  4. package/src/commands/analyze.js +8 -0
  5. package/src/commands/change.js +86 -80
  6. package/src/commands/clarify.js +8 -1
  7. package/src/commands/close.js +136 -34
  8. package/src/commands/completion.js +8 -2
  9. package/src/commands/context.js +6 -4
  10. package/src/commands/decision.js +12 -0
  11. package/src/commands/doctor.js +1 -1
  12. package/src/commands/hooks.js +6 -1
  13. package/src/commands/index-rebuild.js +52 -2
  14. package/src/commands/init.js +19 -4
  15. package/src/commands/intake.js +55 -7
  16. package/src/commands/intent.js +14 -1
  17. package/src/commands/metrics.js +13 -5
  18. package/src/commands/next.js +2 -1
  19. package/src/commands/prime.js +4 -5
  20. package/src/commands/report.js +9 -20
  21. package/src/commands/review.js +47 -8
  22. package/src/commands/skill.js +8 -139
  23. package/src/commands/spec.js +13 -7
  24. package/src/commands/status.js +39 -20
  25. package/src/commands/templates.js +4 -10
  26. package/src/commands/upgrade.js +31 -6
  27. package/src/commands/validate.js +32 -8
  28. package/src/commands/verify.js +59 -6
  29. package/src/commands/work.js +63 -8
  30. package/src/index.js +34 -6
  31. package/src/lib/actions.js +38 -28
  32. package/src/lib/args.js +14 -0
  33. package/src/lib/change-ops.js +95 -1
  34. package/src/lib/commands.js +96 -15
  35. package/src/lib/coverage-model.js +40 -1
  36. package/src/lib/doc-model.js +47 -6
  37. package/src/lib/docs-impact.js +74 -0
  38. package/src/lib/ears.js +24 -1
  39. package/src/lib/gates.js +53 -9
  40. package/src/lib/git.js +59 -2
  41. package/src/lib/index-json.js +40 -5
  42. package/src/lib/intake-model.js +71 -2
  43. package/src/lib/lexicon.js +38 -1
  44. package/src/lib/metrics-model.js +30 -0
  45. package/src/lib/playbook.js +1 -1
  46. package/src/lib/project.js +49 -1
  47. package/src/lib/runtime.js +6 -0
  48. package/src/lib/scan.js +139 -14
  49. package/src/lib/signoff.js +24 -0
  50. package/src/lib/skill-candidates.js +162 -0
  51. package/src/lib/snapshot.js +61 -2
  52. package/src/lib/spec-ops.js +141 -6
  53. package/src/lib/templates-model.js +35 -8
  54. package/src/lib/validation-model.js +117 -35
  55. package/src/lib/verify-cache.js +76 -0
  56. package/src/lib/views.js +10 -2
  57. package/src/lib/work-model.js +57 -12
  58. package/templates/AGENTS.md.template +3 -4
  59. package/templates/README.md +2 -0
  60. package/templates/adapters/claude/.claude/commands/doctrina-work.md.template +6 -3
  61. package/templates/adapters/cursor/.cursor/commands/doctrina-work.md.template +4 -3
  62. package/templates/change/proposal.md.template +15 -0
  63. package/templates/change/spec-delta.md.template +2 -0
  64. package/templates/change/tasks.md.template +3 -8
  65. package/templates/contract.md.template +2 -2
  66. package/templates/hooks/pre-commit.sample +10 -0
  67. package/templates/playbooks/bootstrap.md.template +6 -2
  68. package/templates/playbooks/chore.md.template +9 -4
  69. package/templates/playbooks/consolidate.md.template +28 -0
  70. package/templates/playbooks/work.md.template +13 -3
  71. package/src/commands/constitution.js +0 -68
@@ -0,0 +1,76 @@
1
+ // @ts-check
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { git, GIT_STATE, worktreeFingerprint } from "./git.js";
6
+ import { cliVersion } from "./version.js";
7
+
8
+ // A green verify, remembered for the exact code it ran on (change 0203).
9
+ //
10
+ // A field review of 0.17 put the fixed cost of a small change at the close
11
+ // running the whole suite — 2.5 to 4 minutes — and an agent typically runs
12
+ // `doctrina verify` itself right before `doctrina close`, so the same suite
13
+ // ran twice on the same code. In a batch close it ran once per id, on a tree
14
+ // whose only difference between ids was the artifacts the previous close
15
+ // archived.
16
+ //
17
+ // The fingerprint is the CODE: the working tree (tracked, untracked, edits,
18
+ // .gitignore respected) minus `.doctrina/`, plus the verify.json that
19
+ // defines the checks and the CLI version that runs them. `.doctrina/` is
20
+ // left out on purpose — the close's apply and archive move it between the
21
+ // verify a person ran and the one the close would run — and the artifact
22
+ // tree is proven by the close's own steps (coverage, trace, index drift,
23
+ // validate), which always run.
24
+ //
25
+ // The record lives in the git directory, never in the tree: it is a fact
26
+ // about this checkout, not an artifact to commit or review.
27
+
28
+ const RECORD = "doctrina-verify-green.json";
29
+
30
+ /**
31
+ * @param {string} projectRoot
32
+ * @param {string} configText the verify.json the checks come from
33
+ * @returns {string | null} null when git cannot fingerprint the tree
34
+ */
35
+ export function verifyFingerprint(projectRoot, configText) {
36
+ const tree = worktreeFingerprint(projectRoot, { exclude: [".doctrina"] });
37
+ if (tree === null) return null;
38
+ return createHash("sha256").update(`${tree}\n${configText}\n${cliVersion()}`).digest("hex");
39
+ }
40
+
41
+ function recordPath(projectRoot) {
42
+ const dir = git(projectRoot, ["rev-parse", "--absolute-git-dir"]);
43
+ return dir.state === GIT_STATE.OK ? path.join(dir.stdout.trim(), RECORD) : null;
44
+ }
45
+
46
+ /**
47
+ * Remember that every check passed on this fingerprint.
48
+ * @param {string} projectRoot
49
+ * @param {string} fingerprint
50
+ * @param {string[]} checks
51
+ */
52
+ export function recordGreen(projectRoot, fingerprint, checks) {
53
+ const file = recordPath(projectRoot);
54
+ if (!file) return;
55
+ try {
56
+ mkdirSync(path.dirname(file), { recursive: true });
57
+ writeFileSync(file, JSON.stringify({ fingerprint, checks, at: new Date().toISOString() }, null, 2) + "\n");
58
+ } catch { /* a cache that cannot be written is a cache miss, never an error */ }
59
+ }
60
+
61
+ /**
62
+ * The last green run, when it covered exactly this fingerprint.
63
+ * @param {string} projectRoot
64
+ * @param {string} fingerprint
65
+ * @returns {{ fingerprint: string, checks: string[], at: string } | null}
66
+ */
67
+ export function reusableGreen(projectRoot, fingerprint) {
68
+ const file = recordPath(projectRoot);
69
+ if (!file) return null;
70
+ try {
71
+ const rec = JSON.parse(readFileSync(file, "utf8"));
72
+ return rec && rec.fingerprint === fingerprint ? rec : null;
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
package/src/lib/views.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { c } from "./colors.js";
3
3
  import { today } from "./dates.js";
4
4
  import { churnByCapability } from "./ledger.js";
5
+ import { STALE_CHANGE_DAYS } from "./snapshot.js";
5
6
 
6
7
  // Executed proof and signed proof are different claims (change 0039). One
7
8
  // phrasing, used by every view, so none of them can quietly add the two
@@ -182,7 +183,11 @@ export function prime(s) {
182
183
  } else {
183
184
  for (const ch of s.openChanges) {
184
185
  const tasks = ch.tasksTotal > 0 ? ` · tasks ${ch.tasksDone}/${ch.tasksTotal}` : "";
185
- out.push(` ${c.cyan(ch.id)} ${ch.title ?? ""}${c.gray(` (${ch.status}${tasks})`)}`);
186
+ // The age, once it is news (change 0201): a change opened today and
187
+ // one forgotten for three weeks used to read identically.
188
+ const age = ch.ageDays !== null && ch.ageDays !== undefined && ch.ageDays >= STALE_CHANGE_DAYS
189
+ ? ` · open ${ch.ageDays}d` : "";
190
+ out.push(` ${c.cyan(ch.id)} ${ch.title ?? ""}${c.gray(` (${ch.status}${tasks}${age})`)}`);
186
191
  }
187
192
  }
188
193
 
@@ -222,6 +227,9 @@ export function handoff(s) {
222
227
  out.push("## Open work");
223
228
  out.push("");
224
229
  out.push("- none — the tree is at rest; start with `doctrina work \"<prompt>\"`");
230
+ // The note is pasted into another session, so the section has to END like
231
+ // the loop below ends: a heading glued to a list item is not a heading.
232
+ out.push("");
225
233
  } else {
226
234
  for (const ch of s.openChanges) {
227
235
  out.push(`## Open change \`${ch.id}\`${ch.title ? ` — ${ch.title}` : ""}`);
@@ -405,6 +413,6 @@ export function report(s, { days = 7, cutoffIso = "", git = null, gitState = nul
405
413
  }
406
414
 
407
415
  out.push("");
408
- out.push(`*Generated read-only by \`doctrina report --since ${days}\`; deeper numbers: \`doctrina metrics\`.*`);
416
+ out.push(`*Generated read-only by \`doctrina status --view report --since ${days}\`; deeper numbers: \`doctrina metrics\`.*`);
409
417
  return out;
410
418
  }
@@ -21,29 +21,74 @@ export function rankCapabilitiesByDiff(projectRoot, files, { limit = 3 } = {}) {
21
21
  if (!files || files.length === 0 || !isDir(specsDir)) return [];
22
22
  const norm = files.map((f) => f.replace(/\\/g, "/"));
23
23
 
24
- const ranked = [];
24
+ const specs = [];
25
25
  for (const cap of readdirSync(specsDir).sort()) {
26
26
  const specPath = path.join(specsDir, cap, "spec.md");
27
27
  if (!isFile(specPath)) continue;
28
- const specText = read(specPath);
29
- // A DECLARED glob outranks every inference: the spec said this code is
30
- // its own, and a declaration is not a guess (change 0077).
31
- const declared = parseSourceGlobs(specText).map((g) => globToRegExp(g));
32
- let score = 0;
33
- for (const f of norm) {
34
- if (declared.some((re) => re.test(f))) score += 10; // the spec claims it
35
- if (f.split("/").includes(cap)) score += 3; // src/<cap>/... etc.
36
- if (specText.includes(f)) score += 5; // file cited in the spec
37
- else if (specText.includes(path.basename(f))) score += 2; // filename cited
28
+ const text = read(specPath);
29
+ specs.push({ cap, text, declared: parseSourceGlobs(text).map((g) => globToRegExp(g)) });
30
+ }
31
+
32
+ // Per file, the strongest answer the tree holds wins, and a weaker one is
33
+ // never added on top of it (change 0200, ADR 0027):
34
+ // 1. the specs whose `**Source:**` claims the file — a declaration;
35
+ // 2. else the specs citing the exact path (a proof or a pointer);
36
+ // 3. else the path/basename inferences, and only from specs that declare
37
+ // NO Source — a spec that stated what it owns has said all it owns.
38
+ // The old sum added every signal from every spec, so a file claimed by
39
+ // `health` still "belonged" to `cloud` because cloud's spec mentioned an
40
+ // `index.ts`, and every close warned about a spec the change never touched.
41
+ const scores = new Map();
42
+ const add = (cap, n) => scores.set(cap, (scores.get(cap) ?? 0) + n);
43
+ for (const f of norm) {
44
+ const claimed = specs.filter((s) => s.declared.some((re) => re.test(f)));
45
+ if (claimed.length > 0) {
46
+ for (const s of claimed) add(s.cap, 10);
47
+ continue;
48
+ }
49
+ const citing = specs.filter((s) => s.text.includes(f));
50
+ if (citing.length > 0) {
51
+ for (const s of citing) add(s.cap, 5);
52
+ continue;
53
+ }
54
+ for (const s of specs) {
55
+ if (s.declared.length > 0) continue;
56
+ if (f.split("/").includes(s.cap)) add(s.cap, 3); // src/<cap>/... etc.
57
+ if (s.text.includes(path.basename(f))) add(s.cap, 2); // filename cited
38
58
  }
39
- if (score > 0) ranked.push({ id: cap, score, path: `.doctrina/specs/${cap}/spec.md` });
40
59
  }
60
+ const ranked = [...scores].map(([id, score]) => ({ id, score, path: `.doctrina/specs/${id}/spec.md` }));
41
61
  // `limit` keeps the work-playbook hint short (top 3); review passes
42
62
  // Infinity — truncating there is how it missed 5 of 8 touched capabilities
43
63
  // in the 0.11.0 field session.
44
64
  return ranked.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id)).slice(0, limit);
45
65
  }
46
66
 
67
+ /**
68
+ * The capabilities whose `Source:` header CLAIMS a file — ADR 0027's question
69
+ * asked directly, with no inference in the answer.
70
+ *
71
+ * `rankCapabilitiesByDiff` deliberately falls back to inferences so a project
72
+ * that has not declared anything still gets a hint. That makes it the wrong
73
+ * function to enforce the declaration with: a file no spec names still comes
74
+ * back with an owner, scored 5 instead of 10, and a test asking "does this
75
+ * have an owner" cannot tell the two apart. `lib/names.js` sat undeclared
76
+ * behind exactly that.
77
+ */
78
+ export function declaredOwners(projectRoot, file) {
79
+ const specsDir = path.join(projectRoot, ".doctrina", "specs");
80
+ if (!isDir(specsDir)) return [];
81
+ const norm = String(file).replace(/\\/g, "/");
82
+ const owners = [];
83
+ for (const cap of readdirSync(specsDir).sort()) {
84
+ const specPath = path.join(specsDir, cap, "spec.md");
85
+ if (!isFile(specPath)) continue;
86
+ const globs = parseSourceGlobs(read(specPath)).map((g) => globToRegExp(g));
87
+ if (globs.some((re) => re.test(norm))) owners.push(cap);
88
+ }
89
+ return owners;
90
+ }
91
+
47
92
  // Deterministic term overlap: fold prompt and spec text with the SHARED
48
93
  // lexicon, then score. This is a hint for the agent, never a decision
49
94
  // (ADR 0005).
@@ -46,13 +46,12 @@ canonical templates and syncs `index.json`. Flags: `doctrina <cmd> --help`.
46
46
  - `doctrina triage` — classify the lane (product/runtime/chore) and check the declared runtime surface. *When:* a request arrives — BEFORE scaffolding, especially if it smells like an incident.
47
47
  - `doctrina work "<prompt>" (--capability · --chore · --from-diff · --quiet)` — scaffold a change and print the playbook to execute. *When:* a request arrives that changes behaviour (triage says PRODUCT).
48
48
  - `doctrina spec new|list|set` — create, list, and edit capability specs. *When:* a capability needs creating or its headers advancing.
49
- - `doctrina change new|apply|archive|check|tick|abandon` — new / apply / archive / check (--verbose) / tick / abandon. *When:* driving a change through its lifecycle by hand.
49
+ - `doctrina change new|apply|archive|check|tick|abandon|delta` — drive a change by hand — check --verbose previews every delta. *When:* driving a change through its lifecycle by hand.
50
50
  - `doctrina contract new|list|check` — own and verify the integration surface. *When:* the change touches ports, env vars, or public endpoints.
51
51
  - `doctrina decision new|accept|land|supersede|list|scope` — record, accept, land, scope, and supersede ADRs. *When:* the change decides something a later session must not relitigate.
52
- - `doctrina skill new|list|sync|suggest` — capture on-demand procedural memory. *When:* a lesson is worth not relearning.
52
+ - `doctrina skill new|list|suggest` — capture on-demand procedural memory. *When:* a lesson is worth not relearning.
53
53
  - `doctrina intent add|list` — append and list product intent anchors. *When:* new product intent appears after the intake.
54
54
  **Gate**
55
- - `doctrina analyze` — structural pre-flight on a change folder. *When:* before applying a change.
56
55
  - `doctrina clarify --all (--lang pt|en)` — smell-test Markdown for ambiguity. *When:* before applying, or before opening a PR.
57
56
  - `doctrina validate (--fix)` — schema, structure, EARS, and index drift. *When:* after any artifact edit, and before considering work done.
58
57
  - `doctrina coverage --strict` — acceptance criteria against cited evidence. *When:* before claiming a capability is proven.
@@ -61,7 +60,7 @@ canonical templates and syncs `index.json`. Flags: `doctrina <cmd> --help`.
61
60
  - `doctrina verify` — the project's declared typecheck/test/build checks. *When:* the real build gate must run.
62
61
  - `doctrina close <id...>` — the whole closing sequence in one attested pass. *When:* a change is implemented and ready to finish.
63
62
  - `doctrina doctor` — aggregate diagnostic with per-finding remedies. *When:* something looks wrong and you do not know which gate to ask.
64
- **Maintain** (triggers: `doctrina <command> --help`) — `doctrina templates list|check|update` · `doctrina hooks install` · `doctrina index rebuild` · `doctrina watch` · `doctrina metrics` · `doctrina report` · `doctrina completion` · `doctrina upgrade --write` · `doctrina ci --emit github`
63
+ **Maintain** (triggers: `doctrina <command> --help`) — `doctrina templates list` · `doctrina hooks install` · `doctrina index rebuild` · `doctrina watch` · `doctrina metrics` · `doctrina completion` · `doctrina upgrade --write` · `doctrina ci --emit github`
65
64
  <!-- doctrina:surface:end -->
66
65
 
67
66
  ## Stack and tooling
@@ -45,6 +45,7 @@ loops in it.
45
45
  | `{{CAPABILITY_BLOCK}}` | The pinned capability, the ranked hints, or "no match" | required in `work` |
46
46
  | `{{DIFF_MATCHES}}` | Capabilities the working tree touched | empty |
47
47
  | `{{STEP3_INTRO}}` | The delta step's opening, pinned or not | required in `work` |
48
+ | `{{SHAPE}}` | The spec's measured size — lines, requirements per EARS subsection, criteria | required in `consolidate` |
48
49
  | `{{FROM_DIFF_DELTA_NOTE}}` | The backfill caveat on writing the delta | empty |
49
50
  | `{{MISSING_SPECS_WARNING}}` | Warning that `.doctrina/specs/` is absent | empty |
50
51
 
@@ -68,6 +69,7 @@ README.md this file
68
69
  playbooks/work.md.template the work / backfill playbook
69
70
  playbooks/chore.md.template the spec-less chore playbook
70
71
  playbooks/bootstrap.md.template the intake -> specs playbook
72
+ playbooks/consolidate.md.template the spec consolidation playbook
71
73
  AGENTS.md.template root rules for the target repo
72
74
  doctrina/ the .doctrina/ skeleton
73
75
  product.md.template
@@ -9,9 +9,12 @@ Drive the Doctrina spec-driven workflow for **{{PROJECT_NAME}}**.
9
9
  (No prompt, but you already edited code? Use `doctrina work --from-diff`.)
10
10
  2. Execute the printed playbook in order: read the pack with
11
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.
12
+ `tasks.md`, implement. `doctrina change check <id>` previews everything
13
+ the close would refuse.
14
+ 3. Close it: `doctrina close <id>` — one attested pass over every gate. The
15
+ sequence is declared in one place and printed by `doctrina close --help`;
16
+ read it there rather than from a copy. Resolve every `error:` before
17
+ reporting the work done.
15
18
 
16
19
  Reach for the CLI instead of hand-authoring artifacts. Ask the human only on
17
20
  genuine ambiguity.
@@ -5,8 +5,9 @@ Turn the request into a Doctrina change and drive it through the gates.
5
5
  1. Run `doctrina work "<the request>"` (or `doctrina work --from-diff` if you
6
6
  already edited code) and follow the printed playbook.
7
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.
8
+ `tasks.md` → implement → `doctrina change check <id>` (the close's preview).
9
+ 3. `doctrina close <id>` — one attested pass over every gate; the sequence is
10
+ declared in one place and printed by `doctrina close --help`, so read it
11
+ there and not from a copy. Resolve every `error:` before reporting done.
11
12
 
12
13
  Prefer the CLI over hand-authoring artifacts. Ask only on genuine ambiguity.
@@ -6,6 +6,21 @@
6
6
  - **Lane:**
7
7
  - **Affects specs:**
8
8
 
9
+ <!--
10
+ Optional, and usually absent. The closing docs gate reads COMMAND and FLAG
11
+ names out of the prose below and asks for documentation when it finds any.
12
+ It cannot tell a change from a mention: explaining an effect, or writing a
13
+ Scope boundaries line about what this deliberately does NOT touch, names
14
+ things just as loudly as changing them would.
15
+
16
+ When that happens, say so on the record instead of forcing the close:
17
+
18
+ - **Documented surface:** n/a — names two commands to explain an effect; alters neither
19
+
20
+ `none` reads the same as `n/a`, and a BARE one silences nothing — the
21
+ reason is the declaration.
22
+ -->
23
+
9
24
  ## Why
10
25
 
11
26
  <!-- The user-visible or technical reason this change exists. -->
@@ -24,6 +24,8 @@ typical delta applies mechanically end to end; only free-prose rewrites
24
24
  ```
25
25
 
26
26
  Requirement sections: ubiquitous | event | state | unwanted | optional.
27
+ To take one out: `remove-requirement <section> <n>` / `remove-criterion <n>`
28
+ (the criteria after it renumber; write removals highest number first).
27
29
  append-* ops resolve numbering/position at APPLY time, so several open
28
30
  changes appending to the same spec never collide on numbers — order of
29
31
  application decides.
@@ -2,16 +2,11 @@
2
2
 
3
3
  <!--
4
4
  Each task is a single checkable item. Keep tasks small (under a few hours
5
- of work). The change is done when every box is checked, including the
6
- three closing steps at the bottom.
5
+ of work). The change is done when every box is checked and
6
+ `doctrina close {{CHANGE_ID}}` succeeds — the close applies the deltas,
7
+ archives the change and updates the index, so those are not boxes here.
7
8
  -->
8
9
 
9
10
  - [ ]
10
11
  - [ ]
11
12
  - [ ]
12
-
13
- ## Closing steps
14
-
15
- - [ ] Apply the change: merge each delta into the corresponding spec.
16
- - [ ] Archive the change folder to `.doctrina/changes/archive/{{DATE}}-{{CHANGE_ID}}/`.
17
- - [ ] Update `.doctrina/index.json` with new or modified artifacts.
@@ -107,8 +107,8 @@ Example (Behave): features/**/*.feature with pattern @([a-z0-9_-]+)
107
107
  <!--
108
108
  Ceilings that are a CONTRACT, not a preference: token limits, payload
109
109
  caps, timeouts. Direction distinguishes what goes IN from what comes
110
- OUT, because they fail differently — and `doctrina analyze` refuses a
111
- change that "fixes" an overflow by raising an OUTPUT ceiling, which
110
+ OUT, because they fail differently — and `doctrina change check` and the
111
+ close refuse a change that "fixes" an overflow by raising an OUTPUT ceiling, which
112
112
  buys headroom by truncating what mattered rather than sending less.
113
113
  -->
114
114
 
@@ -26,3 +26,13 @@ $DOCTRINA validate --fix || exit 1
26
26
  if [ -f .doctrina/index.json ]; then
27
27
  git add .doctrina/index.json
28
28
  fi
29
+
30
+ # ...and then check that the index this commit carries describes THIS COMMIT.
31
+ #
32
+ # Everything above reads the working tree. index.json is derived from the
33
+ # whole tree, so it cannot describe a subset of one: stage part of
34
+ # .doctrina/ and the index that just got staged names artifacts the commit
35
+ # does not contain. On your disk they are all still there, so `validate`,
36
+ # `index rebuild --check` and the close all pass — and the checkout in CI
37
+ # fails with "index.json references missing artifact".
38
+ $DOCTRINA index rebuild --check --staged || exit 1
@@ -35,8 +35,12 @@ Execute in order, in a single linear pass:
35
35
  If something in the intake is genuinely ambiguous, ask the user
36
36
  before assuming.
37
37
 
38
- 7. Mark the intake consumed: in .doctrina/intake.md flip
39
- "- **Status:** pending" to "- **Status:** converted".
38
+ 7. Mark the intake consumed — through the CLI, not by hand:
39
+ [[c]]doctrina intake --converted[[/c]]
40
+ [[g]] `next` branches on that header and `validate` refuses any other[[/g]]
41
+ [[g]] value, because every word but the two declared ones used to read[[/g]]
42
+ [[g]] as pending in silence. It refuses while `validate` reports an[[/g]]
43
+ [[g]] error — the step-6 gates are what "done" means.[[/g]]
40
44
 
41
45
  8. [[c]]doctrina next[[/c]] — follow the recommendation. Implementation then
42
46
  flows through [[c]]doctrina work "<brief prompt>"[[/c]], one change at a time.
@@ -6,14 +6,19 @@ Execute in order, in a single linear pass:
6
6
 
7
7
  1. Replace the placeholder tasks in .doctrina/changes/{{CHANGE_ID}}/tasks.md
8
8
  with small, checkable steps; implement them, checking each box.
9
- [[g]] (Do it before implementing — analyze/close refuse leftover scaffold[[/g]]
9
+ [[g]] (Do it before implementing — the close refuses leftover scaffold[[/g]]
10
10
  [[g]] placeholders, and `change tick` will not tick an empty box.)[[/g]]
11
- 2. Prove it (archive refuses unchecked boxes):
11
+ 2. Prove it (the close refuses unchecked boxes):
12
12
  [[c]]doctrina verify[[/c]] — the project's typecheck/test/build gate
13
13
  [[c]]doctrina verify --clean[[/c]] — clean-checkout reproducibility lint
14
14
  Then check the proposal's ## Verification boxes and every task.
15
15
  3. This is a chore: no spec changes. If you find yourself needing a
16
16
  spec delta, it is not a chore — reopen with [[c]]doctrina work "<prompt>"[[/c]].
17
- 4. [[c]]doctrina change apply {{CHANGE_ID}}[[/c]] (zero deltas → flips to applied).
18
- 5. [[c]]doctrina change archive {{CHANGE_ID}}[[/c]], then [[c]]doctrina validate[[/c]].
17
+ 4. Close in one attested pass — the same close as any change; a chore
18
+ simply has no delta to apply:
19
+ [[c]]doctrina close {{CHANGE_ID}}[[/c]]
20
+ [[g]] It stops at the first failure with the exact rerun command, and the[[/g]]
21
+ [[g]] sequence is printed by the close itself. A chore that alters a[[/g]]
22
+ [[g]] documented surface still owes the docs and a changelog entry — or[[/g]]
23
+ [[g]] `- **Documented surface:** n/a — <why>` in the proposal.[[/g]]
19
24
  [[g]] If this fix taught a lesson worth not relearning, capture it: [[/g]][[c]]doctrina skill new <slug>[[/c]][[g]].[[/g]]
@@ -0,0 +1,28 @@
1
+ [[b]]Consolidation playbook — change {{CHANGE_ID}}[[/b]][[g]] — agent-executed (ADR 0005); the spec shrinks, the behaviour does not change.[[/g]]
2
+
3
+ Capability: [[c]]{{CAPABILITY}}[[/c]] — {{SHAPE}}
4
+
5
+ Execute in order, in a single linear pass:
6
+
7
+ 1. Read the spec and its history:
8
+ [[c]]doctrina context {{CAPABILITY}} --concat[[/c]]
9
+ [[c]]doctrina why {{CAPABILITY}}[[/c]]
10
+ 2. Find what the spec says more than once: a requirement a later one
11
+ superseded, two requirements stating the same rule, a criterion proven
12
+ by another one, a bullet describing behaviour that no longer exists.
13
+ You decide; the CLI only measures. Keep every rule the code still
14
+ honours — consolidating removes words, never guarantees.
15
+ 3. Write the delta with the ops (the scaffold is at
16
+ .doctrina/changes/{{CHANGE_ID}}/specs/{{CAPABILITY}}/delta.md):
17
+ [[c]]doctrina change delta {{CHANGE_ID}} {{CAPABILITY}} --op "remove-requirement <section> <n>: <why>"[[/c]]
18
+ [[c]]doctrina change delta {{CHANGE_ID}} {{CAPABILITY}} --op "replace-requirement <section> <n>: <merged text>"[[/c]]
19
+ [[c]]doctrina change delta {{CHANGE_ID}} {{CAPABILITY}} --op "remove-criterion <n>: <why>"[[/c]]
20
+ [[c]]doctrina change delta {{CHANGE_ID}} {{CAPABILITY}} --bump minor[[/c]]
21
+ [[g]] Ops run in order and a removal renumbers what follows it: write the[[/g]]
22
+ [[g]] removals of one section highest number first. Every kept criterion[[/g]]
23
+ [[g]] must still cite proof that exists — coverage gates the close.[[/g]]
24
+ [[g]] Preview the merged spec: [[/g]][[c]]doctrina change check {{CHANGE_ID}} --verbose[[/c]]
25
+ 4. Fill the proposal's What (what was merged or removed, and why each) and
26
+ replace the placeholder tasks; no code changes.
27
+ 5. Close in one attested pass:
28
+ [[c]]doctrina close {{CHANGE_ID}}[[/c]]
@@ -40,12 +40,15 @@ Execute in order, in a single linear pass:
40
40
  [[g]] append-* ops number/position at APPLY time, so concurrent changes[[/g]]
41
41
  [[g]] appending to the same spec never collide. Only free-prose rewrites[[/g]]
42
42
  [[g]] (Purpose, Maturity, ...) stay a by-hand merge.[[/g]]
43
+ [[g]] Or let the CLI write the lines and dry-run them against the spec:[[/g]]
44
+ [[g]] doctrina change delta {{CHANGE_ID}} <capability> --requirement "event: ..." \[[/g]]
45
+ [[g]] --criterion "[unverified] ... — verified by `...`" --bump minor[[/g]]
43
46
  {{FROM_DIFF_DELTA_NOTE}}
44
47
 
45
48
  4. Replace the placeholder tasks in .doctrina/changes/{{CHANGE_ID}}/tasks.md
46
49
  with small, checkable implementation tasks (a few hours each, max),
47
50
  and record the change's What/Scope in its proposal.md. Do this BEFORE
48
- implementing — analyze and close refuse a change whose scaffold
51
+ implementing — `change check` and close refuse a change whose scaffold
49
52
  placeholders were never replaced, and `change tick` will not tick them.
50
53
 
51
54
  5. Implement task by task, checking each box as it lands. Advance the
@@ -67,9 +70,16 @@ Execute in order, in a single linear pass:
67
70
  [[g]] It self-reviews on the way through: the close runs `doctrina review`[[/g]]
68
71
  [[g]] before the apply, where its findings can still change what gets[[/g]]
69
72
  [[g]] written. Advisory — it reports and the close continues.[[/g]]
70
- [[g]] (equivalent, step by step: [[/g]][[g]]analyze → change apply → verify → coverage → trace → change archive → validate)[[/g]]
73
+ [[g]] The sequence is declared in one place and printed by the close itself,[[/g]]
74
+ [[g]] so read it there rather than from a copy that goes stale.[[/g]]
71
75
  Before it: check the proposal's ## Verification boxes and every task,
72
- closing steps included, and bump Implementation to verified.
76
+ and bump Implementation to verified.
77
+ Two gates ask for WRITING, and both refuse late if you leave them:
78
+ a change that alters a documented surface needs the documentation to
79
+ move with it, AND an entry in the project's changelog — one says how it
80
+ works, the other says that it changed. If the command and flag names in
81
+ your prose are only MENTIONED, declare it instead of forcing:
82
+ [[c]]- **Documented surface:** n/a — <why>[[/c]] in the proposal.
73
83
 
74
84
  8. [[c]]doctrina next[[/c]] for the follow-up.
75
85
  [[g]] If this change taught a reusable lesson (a fix you'd hate to relearn,[[/g]]
@@ -1,68 +0,0 @@
1
- // @ts-check
2
- import path from "node:path";
3
- import process from "node:process";
4
- import { readdirSync } from "node:fs";
5
- import { exists, isDir, isFile, read } from "../lib/fs-ops.js";
6
- import { listHeader } from "../lib/scan.js";
7
- import { c } from "../lib/colors.js";
8
- import { notADoctrinaProject } from "../lib/exit-codes.js";
9
- import { acceptedDecisions, productSection } from "../lib/constitution-model.js";
10
- import { collectSnapshot } from "../lib/snapshot.js";
11
- import { renderView } from "../lib/views.js";
12
-
13
- // Read by the project snapshot as well; see lib/constitution-model.js.
14
- export { acceptedDecisions, productSection } from "../lib/constitution-model.js";
15
-
16
- // The project's standing rules in one read — Spec Kit parity for its
17
- // `constitution.md`, but ASSEMBLED, not a new home for facts. Doctrina's
18
- // constitution is its accepted ADRs (the immutable decisions that govern how
19
- // the codebase evolves) plus the product's declared non-goals. Both already
20
- // own those facts elsewhere; this is a read-only digest that gathers them so a
21
- // human or agent (especially one migrating from Spec Kit, which expects a
22
- // single constitution) can see "the rules" without reading the whole tree.
23
- //
24
- // To change a principle, supersede its ADR; to change a non-goal, edit
25
- // product.md. The command never writes — it has nothing of its own to own.
26
-
27
- // Flags this command accepts. Declared HERE, with the command, so
28
- // adding a command never requires editing the entrypoint — the gap that
29
- // let six flags ship undeclared and silently swallow a positional (C3).
30
- export const flags = { boolean: ["json"], string: [] };
31
-
32
- export async function run(_positional, _flags) {
33
- const projectRoot = process.cwd();
34
- if (!exists(path.join(projectRoot, ".doctrina"))) {
35
- throw notADoctrinaProject();
36
- }
37
- // Deprecated (change 0049): the standing rules are a VIEW of the shared
38
- // collection, and `prime --rules` renders it. This command prints the same
39
- // lines — not a similar set, the same ones — so the deprecation is a
40
- // rename with a warning rather than a loss.
41
- for (const line of renderView("rules", collectSnapshot(projectRoot))) console.log(line);
42
- return 0;
43
- }
44
-
45
- function projectName(projectRoot) {
46
- try {
47
- return JSON.parse(read(path.join(projectRoot, ".doctrina", "index.json"))).project
48
- ?? path.basename(projectRoot);
49
- } catch {
50
- return path.basename(projectRoot);
51
- }
52
- }
53
-
54
-
55
- export const help = `
56
- Usage: doctrina constitution
57
-
58
- Print the project's standing rules in one read: the accepted ADRs (the
59
- immutable decisions that govern how the codebase evolves) and the product's
60
- declared non-goals. Read-only — it assembles facts the ADRs and product.md
61
- already own; it never writes.
62
-
63
- This is the Spec Kit \`constitution.md\` analogue: a single place to see the
64
- non-negotiables. To change one, supersede the ADR or edit product.md.
65
-
66
- DEPRECATED: use \`doctrina prime --rules\`, which prints exactly these lines.
67
- This name keeps working and will be removed in a future minor.
68
- `;