doctrina-cli 0.16.0 → 0.17.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 (60) hide show
  1. package/README.md +27 -72
  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 +28 -77
  6. package/src/commands/clarify.js +8 -1
  7. package/src/commands/close.js +65 -10
  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 +1 -1
  19. package/src/commands/prime.js +4 -5
  20. package/src/commands/report.js +9 -20
  21. package/src/commands/skill.js +8 -15
  22. package/src/commands/spec.js +13 -7
  23. package/src/commands/status.js +39 -20
  24. package/src/commands/templates.js +4 -10
  25. package/src/commands/upgrade.js +31 -6
  26. package/src/commands/validate.js +32 -8
  27. package/src/commands/work.js +10 -5
  28. package/src/index.js +34 -6
  29. package/src/lib/actions.js +30 -24
  30. package/src/lib/args.js +14 -0
  31. package/src/lib/change-ops.js +3 -1
  32. package/src/lib/commands.js +88 -15
  33. package/src/lib/coverage-model.js +40 -1
  34. package/src/lib/doc-model.js +47 -6
  35. package/src/lib/docs-impact.js +74 -0
  36. package/src/lib/ears.js +24 -1
  37. package/src/lib/gates.js +53 -9
  38. package/src/lib/index-json.js +40 -5
  39. package/src/lib/intake-model.js +71 -2
  40. package/src/lib/lexicon.js +38 -1
  41. package/src/lib/metrics-model.js +30 -0
  42. package/src/lib/project.js +49 -1
  43. package/src/lib/scan.js +126 -14
  44. package/src/lib/snapshot.js +23 -1
  45. package/src/lib/spec-ops.js +24 -5
  46. package/src/lib/templates-model.js +35 -8
  47. package/src/lib/validation-model.js +116 -34
  48. package/src/lib/views.js +4 -1
  49. package/src/lib/work-model.js +25 -0
  50. package/templates/AGENTS.md.template +3 -4
  51. package/templates/adapters/claude/.claude/commands/doctrina-work.md.template +6 -3
  52. package/templates/adapters/cursor/.cursor/commands/doctrina-work.md.template +4 -3
  53. package/templates/change/proposal.md.template +15 -0
  54. package/templates/change/tasks.md.template +3 -8
  55. package/templates/contract.md.template +2 -2
  56. package/templates/hooks/pre-commit.sample +10 -0
  57. package/templates/playbooks/bootstrap.md.template +6 -2
  58. package/templates/playbooks/chore.md.template +9 -4
  59. package/templates/playbooks/work.md.template +10 -3
  60. package/src/commands/constitution.js +0 -68
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.16.0 — current release. Zero runtime dependencies
6
+ **Status:** v0.17.0 — current release. Zero runtime dependencies
7
7
  (Node.js standard library only).
8
8
 
9
9
  ## Install
@@ -18,59 +18,31 @@ Requires Node.js 20.12 or later.
18
18
 
19
19
  ## Commands
20
20
 
21
+ Start with `doctrina init` in your repository, then tell your AI agent to
22
+ read `AGENTS.md` and run `doctrina next` — it drives the rest. The
23
+ surface, by the moment you reach for it:
24
+
21
25
  ```
22
- doctrina init scaffold AGENTS.md + .doctrina/ in cwd
23
- doctrina intake [<file>] store the full description + bootstrap playbook (clarification gate)
24
- doctrina work "<prompt>" brief prompt -> scaffolded change + work playbook (clarification gate)
25
- doctrina work --resume <id> reprint an open change's playbook (no new change)
26
- doctrina work --from-diff backfill a spec from working-tree changes (code-first)
27
- doctrina work "<prompt>" --chore spec-less chore (infra/docs/build): no spec-delta steps
28
- doctrina spec new <capability> create a capability spec
29
- doctrina spec list list specs with version, status, size
30
- doctrina spec set <cap> [--bump ...] edit spec headers/criterion + resync index (no lockstep)
31
- doctrina change new <id> "<title>" open a change proposal
32
- doctrina change apply <id> apply spec deltas (ADDED/REMOVED auto; MODIFIED ops block applies)
33
- doctrina change archive <id> archive an applied change
34
- doctrina change abandon <id> delete an open change + its index entry (ledgered)
35
- doctrina change diff <id> preview spec deltas (line diff for MODIFIED)
36
- doctrina contract new <name> own the integration surface (ports, env, interfaces)
37
- doctrina contract check verify port collisions, env drift, referenced specs
38
- doctrina decision new "<title>" create the next sequential ADR
39
- doctrina decision accept <num> flip a proposed ADR to accepted
40
- doctrina decision land <num> [path...] stamp an accepted ADR as implemented (non-mutating Landed:)
41
- doctrina decision supersede <num> "<t>" supersede an existing ADR
42
- doctrina decision list list ADRs with status, date, title
43
- doctrina skill new <name> scaffold an on-demand procedural-memory skill
44
- doctrina skill list list skills with descriptions (read-only)
45
- doctrina skill sync mirror skill frontmatter descriptions into index.json
46
- doctrina analyze <change-id> inspect a change folder before applying
47
- doctrina clarify <path> smell-test a Markdown file for ambiguity (--all: tree)
48
- doctrina context [<capability>] print the context pack in read order (--concat)
49
- doctrina search <term> [...] search artifacts, grouped by category (ranked)
50
- doctrina validate schema + artifact-existence + index-drift checks (--fix rebuilds)
51
- doctrina coverage acceptance criteria with linked evidence (--strict gates; skips = conditional)
52
- doctrina trace intent provenance: product intent -> specs (--strict gates)
53
- doctrina verify run project typecheck/test/build checks (the real gate)
54
- doctrina verify --clean reproducibility lint: clean-checkout footguns (dist/codegen)
55
- doctrina templates list enumerate shipped templates
56
- doctrina templates check compare project against recommended template shape
57
- doctrina templates update additive fixer for check findings (preview; --write applies)
58
- doctrina hooks install install pre-commit hook
59
- doctrina index rebuild regenerate index.json from the files (--check for CI)
60
- doctrina next print the recommended next workflow actions
61
- doctrina metrics local git-derived adoption metrics (no network)
62
- doctrina --help | -h print the command surface
63
- doctrina --version | -v print the package version
26
+ Bootstrap init · intake · adapter list|add|remove
27
+ Orient prime · next · status · context · show · search · why · handoff
28
+ Change triage · work · spec new|list|set · change new|apply|archive|check|tick|abandon
29
+ contract new|list|check · decision new|accept|land|supersede|list|scope
30
+ skill new|list|suggest · intent add|list
31
+ Gate clarify · validate · coverage · trace · review · verify · close · doctor
32
+ Maintain upgrade · index rebuild · hooks install · watch · metrics · completion · ci · templates list
64
33
  ```
65
34
 
66
- Per-command help is available with `doctrina <command> --help`.
35
+ `doctrina --help` prints the same grouping with one line per command, and
36
+ `doctrina <command> --help` the flags of each. Every command, flag and exit
37
+ code is in the
38
+ [CLI reference](https://github.com/GCarin1/Doctrina/blob/main/docs/en/cli-reference.md).
67
39
 
68
40
  ## `init` flags
69
41
 
70
42
  | Flag | Purpose |
71
43
  |------|---------|
72
44
  | `--project-name <name>` | Override the project name (defaults to cwd basename) |
73
- | `--project-description <text>` | One-sentence description (prompted if omitted) |
45
+ | `--project-description <text>` | One-sentence description, with no intake. Without it on a terminal, `init` asks you to describe the project once and keeps the answer as the intake |
74
46
  | `--agent <name>` | Install the adapter for one of the twelve supported agents (`claude`, `codex`, `cursor`, `copilot`, `gemini`, `aider`, `windsurf`, `continue`, `amp`, `devin`, `factory`, `jules`) or `all` |
75
47
  | `--date <YYYY-MM-DD>` | Override the system date in artifacts |
76
48
  | `--force` | Overwrite existing files |
@@ -100,7 +72,8 @@ For each spec delta found under `.doctrina/changes/<id>/specs/`:
100
72
  - **REMOVED:** deletes the target spec.
101
73
  - **MODIFIED:** when the delta carries a fenced ` ```ops ` block
102
74
  (`set-header` / `bump-version` / `set-criterion` / `replace-criterion`
103
- / `append-criterion`), applies it mechanically to the target spec — all
75
+ / `append-criterion` / `append-requirement` / `replace-requirement`),
76
+ applies it mechanically to the target spec — all
104
77
  ops or none — and regenerates the index (ADR 0007). Without an ops
105
78
  block it prints a manual-merge pointer and does not write: auto-merging
106
79
  arbitrary prose is the kind of implicit decision Doctrina avoids.
@@ -108,35 +81,17 @@ For each spec delta found under `.doctrina/changes/<id>/specs/`:
108
81
  On any spec write, the index is rebuilt from the tree so it never drifts
109
82
  from the applied spec. `doctrina change abandon <id>` is the inverse of
110
83
  `change new`: it deletes an open change and its index entry and ledgers
111
- the abandonment.
84
+ the abandonment. `doctrina close <id>` runs apply together with verify,
85
+ coverage, archive and validate in one attested pass — the definition of
86
+ done.
112
87
 
113
88
  ## `validate` checks
114
89
 
115
- 1. `AGENTS.md` exists at root and is ≤ 200 lines (warns at > 150).
116
- 2. `.doctrina/product.md` exists.
117
- 3. `.doctrina/index.json` parses and matches the v0.1 shape.
118
- 4. Every artifact referenced in `index.json` exists.
119
- 5. Every ADR has a parseable `Status:` header.
120
- 6. Every adapter template under `.doctrina/templates/adapters/` is ≤ 30 lines.
121
- 7. Every open change has a `proposal.md`.
122
- 8. Capability specs are ≤ 400 lines (warns above).
123
- 9. ADRs are ≤ 300 lines (warns above).
124
- 10. Spec/ADR files on disk are present in `index.json` (orphan warn).
125
- 11. Markdown links inside specs/ADRs resolve to real files (stale ref warn).
126
- 12. Skills carry the required frontmatter triple (`name`, `description`, `when`).
127
- 13. Skills are ≤ 200 lines (warns at > 150).
128
- 14. Each skill's `name:` matches its filename slug.
129
- 15. Indexed artifact metadata (spec/decision/change/contract) matches its
130
- file — a mismatch is an **error** so a green `validate` can't hide the
131
- drift `index rebuild --check` would catch (G5; `validate --fix` rebuilds).
132
- 16. Each skill's frontmatter description matches `index.json` (drift warn; `skill sync` fixes).
133
- 17. EARS grammar shape per section in specs declaring `## Requirements (EARS)` (warn).
134
- 18. Nested `AGENTS.md` files obey the root size caps (warn > 150, error > 200).
135
- 19. No two ADR files share the same `NNNN` number (merge-collision error).
136
- 20. `index.json` records a `framework_version` matching the running CLI
137
- (stamp-divergence warn; `index rebuild` migrates it).
138
- 21. Accepted ADRs anchor to reality via `Evidence:` and/or `Landed:`; cited
139
- paths must exist on disk (drift warn).
90
+ Structure, schema, EARS grammar, index drift, size caps, links, skills,
91
+ ADR anchoring and more — the full, numbered list with the reason for each
92
+ check is in the
93
+ [CLI reference](https://github.com/GCarin1/Doctrina/blob/main/docs/en/cli-reference.md#doctrina-validate).
94
+ A second copy here fell behind it once; there is now one list.
140
95
 
141
96
  Exit code: 0 if no errors (warnings allowed), 1 otherwise. `--fix`
142
97
  regenerates `index.json` from the tree before validating.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doctrina-cli",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Doctrina CLI — spec-driven, AGENTS.md-native framework for multi-agent AI development.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "type": "module",
17
17
  "bin": {
18
- "doctrina": "./src/index.js"
18
+ "doctrina": "src/index.js"
19
19
  },
20
20
  "scripts": {
21
21
  "test": "node --test",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",
35
- "url": "https://github.com/gcarin1/doctrina.git",
35
+ "url": "git+https://github.com/GCarin1/Doctrina.git",
36
36
  "directory": "packages/doctrina-cli"
37
37
  },
38
38
  "publishConfig": {
@@ -10,6 +10,7 @@ import { today } from "../lib/dates.js";
10
10
  import { c } from "../lib/colors.js";
11
11
  import { suggest } from "../lib/suggest.js";
12
12
  import { notADoctrinaProject } from "../lib/exit-codes.js";
13
+ import { projectName } from "../lib/project.js";
13
14
 
14
15
  // `doctrina adapter` — add, remove, and inventory agent adapters (audit
15
16
  // item C1).
@@ -96,7 +97,7 @@ function adapterAdd(projectRoot, name, cmdFlags) {
96
97
 
97
98
  const force = flagBool(cmdFlags, "force", false);
98
99
  const tokens = {
99
- PROJECT_NAME: path.basename(projectRoot),
100
+ PROJECT_NAME: projectName(projectRoot),
100
101
  PROJECT_DESCRIPTION: "",
101
102
  DATE: today(),
102
103
  };
@@ -145,7 +146,7 @@ function adapterRemove(projectRoot, name, cmdFlags) {
145
146
  // Only delete files this adapter created AND that still match what it
146
147
  // would write. A file the user edited is theirs now; --force overrides.
147
148
  const force = flagBool(cmdFlags, "force", false);
148
- const tokens = { PROJECT_NAME: path.basename(projectRoot), PROJECT_DESCRIPTION: "", DATE: today() };
149
+ const tokens = { PROJECT_NAME: projectName(projectRoot), PROJECT_DESCRIPTION: "", DATE: today() };
149
150
  let removed = 0;
150
151
  let kept = 0;
151
152
  // Directories a removed file lived in, innermost first. Only these are
@@ -5,6 +5,7 @@ import { isDir, relPath } from "../lib/fs-ops.js";
5
5
  import { c } from "../lib/colors.js";
6
6
  import { collectAnalysis } from "../lib/analysis.js";
7
7
  import { EXIT } from "../lib/exit-codes.js";
8
+ import { ensureDoctrinaProject, refuseChangeRef } from "../lib/project.js";
8
9
 
9
10
  // The findings themselves live in lib/analysis.js, where the gate map reads
10
11
  // them; this command is their renderer (audit finding F7).
@@ -23,6 +24,13 @@ export async function run(positional, _flags) {
23
24
  }
24
25
 
25
26
  const projectRoot = process.cwd();
27
+ // The project first, then the reference. Asked the other way round, a
28
+ // directory that is not a project at all answered `change "x" not found`
29
+ // — which points at creating a change somewhere there is nothing to
30
+ // create it in.
31
+ ensureDoctrinaProject(projectRoot);
32
+ const refused = refuseChangeRef(id);
33
+ if (refused !== null) return refused;
26
34
  const changeDir = path.join(projectRoot, ".doctrina", "changes", id);
27
35
  if (!isDir(changeDir)) {
28
36
  console.error(c.red("error:") + ` change "${id}" not found at ${relPath(projectRoot, changeDir)}`);
@@ -6,7 +6,7 @@ import { exists, isDir, isFile, lineCount, mkdirp, move, read, relPath, remove,
6
6
  import { diffLines, formatUnified } from "../lib/diff.js";
7
7
  import { locateTemplatesDir, loadTemplateTree, materialiseEntry } from "../lib/templates.js";
8
8
  import * as idx from "../lib/index-json.js";
9
- import { deriveIndex } from "../lib/scan.js";
9
+ import { archivedChangeEntry, deriveIndex } from "../lib/scan.js";
10
10
  import { extractOps, applyOps } from "../lib/spec-ops.js";
11
11
  import { printAdrCheckpoint } from "../lib/adr-guard.js";
12
12
  import { GATES, TRANSITIONS, checkTransition, recordForcedGap } from "../lib/gates.js";
@@ -17,11 +17,11 @@ import { appendLedgerLine, archivedLine, abandonedLine } from "../lib/ledger.js"
17
17
  import { suggest } from "../lib/suggest.js";
18
18
  import { confirm, isInteractive } from "../lib/prompt.js";
19
19
  import { EXIT } from "../lib/exit-codes.js";
20
- import { notADoctrinaProject } from "../lib/exit-codes.js";
20
+ import { ensureDoctrinaProject, refuseChangeRef } from "../lib/project.js";
21
21
  import { parseOperation, parseCapabilityFromDelta, isUntouchedScaffold } from "../lib/doc-model.js";
22
22
  import { changeNew } from "../lib/change-ops.js";
23
23
 
24
- const SUBCOMMANDS = ["new", "apply", "archive", "check", "tick", "diff", "abandon"];
24
+ const SUBCOMMANDS = ["new", "apply", "archive", "check", "tick", "abandon"];
25
25
 
26
26
  // Flags this command accepts. Declared HERE, with the command, so
27
27
  // adding a command never requires editing the entrypoint — the gap that
@@ -42,8 +42,6 @@ export async function run(positional, flags) {
42
42
  changeCheck(id, { verbose: flagBool(flags, "verbose", false) }));
43
43
  case "tick":
44
44
  return changeTick(positional.slice(1), flags);
45
- case "diff":
46
- return changeDiff(positional.slice(1), flags);
47
45
  case "abandon":
48
46
  return await changeAbandon(positional.slice(1), flags);
49
47
  default:
@@ -100,6 +98,8 @@ async function forEachId(ids, name, one) {
100
98
  }
101
99
  let worst = 0;
102
100
  for (const id of ids) {
101
+ const refused = refuseChangeRef(id);
102
+ if (refused !== null) { worst = Math.max(worst, refused); continue; }
103
103
  if (ids.length > 1) {
104
104
  console.log("");
105
105
  console.log(c.bold(`──── change ${name} ${id}`));
@@ -108,6 +108,14 @@ async function forEachId(ids, name, one) {
108
108
  try {
109
109
  code = await one(id);
110
110
  } catch (err) {
111
+ // A TYPED error is a condition of the RUN, not a verdict on this id:
112
+ // "not a Doctrina project" is equally true of every id, and the class
113
+ // it carries is the whole point of the contract. Flattening it to 1
114
+ // here made `change apply|archive|check` answer "fix your work and
115
+ // retry" where `change new`, one switch arm away, answered "run the
116
+ // setup command first" — same condition, same sentence, two codes.
117
+ // The entrypoint owns the mapping and prints the remedy; let it.
118
+ if (err && typeof err.exitCode === "number") throw err;
111
119
  console.error(c.red("error:") + ` ${err.message}`);
112
120
  code = 1;
113
121
  }
@@ -308,7 +316,7 @@ async function changeCheck(id, { verbose = false } = {}) {
308
316
  let failures = 0;
309
317
 
310
318
  // 1. Structural analysis — same checks analyze runs before an apply.
311
- console.log(c.gray("──── 1/3 structure (analyze)"));
319
+ console.log(c.gray("──── 1/3 structure"));
312
320
  const analyze = await import("./analyze.js");
313
321
  if ((await analyze.run([id], new Map())) !== 0) failures += 1;
314
322
 
@@ -348,7 +356,7 @@ async function changeCheck(id, { verbose = false } = {}) {
348
356
  if (deltaFiles.length === 0) console.log(c.gray("- no spec deltas"));
349
357
  if (opsFindings > 0) failures += 1;
350
358
 
351
- // --verbose: the same per-delta preview `change diff` prints. The dry-run
359
+ // --verbose: the per-delta preview the removed `change diff` printed. The dry-run
352
360
  // above says whether the ops WOULD apply; this says what the file would
353
361
  // look like afterwards, which is the question the separate command existed
354
362
  // to answer (change 0049).
@@ -396,6 +404,8 @@ function changeTick(args, flags) {
396
404
  console.error(c.red("error:") + " change tick requires <id> [ordinals... | --all]");
397
405
  return 2;
398
406
  }
407
+ const refused = refuseChangeRef(id);
408
+ if (refused !== null) return refused;
399
409
  const projectRoot = process.cwd();
400
410
  ensureDoctrinaProject(projectRoot);
401
411
  const changeDir = path.join(projectRoot, ".doctrina", "changes", id);
@@ -522,7 +532,7 @@ function changeArchive(args, flags) {
522
532
 
523
533
  // Verification gate. "Done" is a claim until it is checked. Archiving is
524
534
  // the act of declaring a change finished, so it must refuse while any
525
- // task (including closing steps) or any declared verification item is
535
+ // task or any declared verification item is
526
536
  // still unchecked. This is the difference between "boxes marked" and
527
537
  // "verification passed" the framework was faulted for collapsing.
528
538
  // Preconditions from the shared map (C6): structure AND verification.
@@ -594,14 +604,10 @@ function changeArchive(args, flags) {
594
604
  console.log(c.green("ledger") + ` +1 line in ${relPath(projectRoot, ledgerFile)}`);
595
605
 
596
606
  const index = idx.load(projectRoot);
597
- idx.moveChangeToArchive(index, id, {
598
- id,
599
- title,
600
- path: `.doctrina/changes/archive/${archiveName}`,
601
- status: "applied",
602
- applied: date,
603
- specs_affected: specsAffected,
604
- });
607
+ idx.moveChangeToArchive(index, id, archivedChangeEntry(
608
+ exists(proposal) ? read(proposal) : "",
609
+ { id, archiveName, applied: date, specsAffected, title },
610
+ ));
605
611
  idx.touch(index, date);
606
612
  idx.save(projectRoot, index);
607
613
  console.log(c.green("indexed") + " change archived");
@@ -618,6 +624,8 @@ async function changeAbandon(args, flags) {
618
624
  console.error(c.red("error:") + " change abandon requires <id>");
619
625
  return 2;
620
626
  }
627
+ const refused = refuseChangeRef(id);
628
+ if (refused !== null) return refused;
621
629
  const projectRoot = process.cwd();
622
630
  ensureDoctrinaProject(projectRoot);
623
631
 
@@ -675,9 +683,9 @@ async function changeAbandon(args, flags) {
675
683
  // ADDED reports the body it would write, REMOVED the spec it would delete,
676
684
  // MODIFIED a line diff against the current spec.
677
685
  //
678
- // One renderer, two callers (change 0049): `change diff` is this and nothing
679
- // else, and `change check --verbose` prints it after its ops dry-run — which
680
- // is what makes the merge honest rather than a claim. Returns the number of
686
+ // `change check --verbose` prints this after its ops dry-run. It was also
687
+ // the whole of `change diff`, until that alias was retired (changes 0049,
688
+ // 0175) — the one renderer is what made the merge honest. Returns the number of
681
689
  // deltas it could not read.
682
690
  export function printDeltaPreview(projectRoot, changeDir, deltaFiles) {
683
691
  let errors = 0;
@@ -725,33 +733,6 @@ export function printDeltaPreview(projectRoot, changeDir, deltaFiles) {
725
733
  return errors;
726
734
  }
727
735
 
728
- function changeDiff(args, _flags) {
729
- const id = args[0];
730
- if (!id) {
731
- console.error(c.red("error:") + " change diff requires <id>");
732
- return 2;
733
- }
734
- const projectRoot = process.cwd();
735
- ensureDoctrinaProject(projectRoot);
736
-
737
- const changeDir = path.join(projectRoot, ".doctrina", "changes", id);
738
- if (!isDir(changeDir)) {
739
- console.error(c.red("error:") + ` change "${id}" not found at ${relPath(projectRoot, changeDir)}`);
740
- return EXIT.USAGE;
741
- }
742
-
743
- const deltaFiles = walk(path.join(changeDir, "specs")).filter((p) => p.endsWith("delta.md"));
744
- if (deltaFiles.length === 0) {
745
- console.log(c.gray("no spec deltas in this change; nothing to diff"));
746
- return 0;
747
- }
748
-
749
- const errors = printDeltaPreview(projectRoot, changeDir, deltaFiles);
750
- console.log("");
751
- return errors > 0 ? 1 : 0;
752
- }
753
-
754
-
755
736
  function extractDeltaBody(text) {
756
737
  // The delta separates headers from the spec body with a `---` line.
757
738
  const idxSep = text.indexOf("\n---\n");
@@ -760,34 +741,6 @@ function extractDeltaBody(text) {
760
741
  }
761
742
 
762
743
 
763
- // Reasons a change is not finished enough to archive. Counts unchecked
764
- // GitHub-style checkboxes (`- [ ]`) in tasks.md (every task, including the
765
- // closing steps) and in the proposal's "## Verification" section. Returns
766
- // a list of human-readable blocker strings; empty means clear to archive.
767
- function collectArchiveBlockers(changeDir) {
768
- const blockers = [];
769
- const countUnchecked = (s) => (s.match(/^\s*-\s*\[ \]/gm) ?? []).length;
770
-
771
- const tasksPath = path.join(changeDir, "tasks.md");
772
- if (exists(tasksPath)) {
773
- const n = countUnchecked(read(tasksPath));
774
- if (n > 0) blockers.push(`${n} unchecked task${n === 1 ? "" : "s"} in tasks.md (closing steps count)`);
775
- }
776
-
777
- const proposalPath = path.join(changeDir, "proposal.md");
778
- if (exists(proposalPath)) {
779
- const n = countUnchecked(getSection(read(proposalPath), "Verification"));
780
- if (n > 0) blockers.push(`${n} unmet verification item${n === 1 ? "" : "s"} in proposal.md (## Verification)`);
781
- }
782
- return blockers;
783
- }
784
-
785
- function ensureDoctrinaProject(projectRoot) {
786
- if (!exists(path.join(projectRoot, ".doctrina"))) {
787
- throw notADoctrinaProject();
788
- }
789
- }
790
-
791
744
  export const help = `
792
745
  Usage: doctrina change <subcommand> [args]
793
746
 
@@ -805,7 +758,7 @@ Subcommands:
805
758
  MODIFIED without one prints a manual-merge pointer.
806
759
  On any spec write the index is rebuilt from the tree.
807
760
  archive <id...> Move the change to .doctrina/changes/archive/YYYY-MM-DD-<id>/
808
- check <id...> Pre-close dry-run, read-only: analyze's structural
761
+ check <id...> Pre-close dry-run, read-only: the structural
809
762
  checks + every ops block executed in memory against
810
763
  its target + the archive gate preview + an advisory
811
764
  list of accepted ADRs citing the touched capabilities.
@@ -815,8 +768,6 @@ Subcommands:
815
768
  or every one with --all. No args = list only.
816
769
  abandon <id> Delete an open change folder and its index entry, and
817
770
  record the abandonment in the ledger ([--reason "..."]).
818
- diff <id> Preview every spec delta: line diff for MODIFIED,
819
- summary for ADDED/REMOVED. Read-only.
820
771
 
821
772
  apply / archive / check accept multiple ids (batch close of a backlog); the
822
773
  exit code is the worst per-id result.
@@ -37,7 +37,14 @@ const RULES_EN = [
37
37
  // hid the one real finding among them ("name some of them", which said
38
38
  // nothing about how many). A gate that cries wolf teaches people to
39
39
  // ignore it.
40
- re: /(?<!\bhow\s+)\b(many|few|some|several)\b(?!\s+\d)/gi,
40
+ //
41
+ // The quality adjectives sit here for the same reason the quantifiers do:
42
+ // "robust" names no threshold, so it cannot be met or missed. They were
43
+ // in the Portuguese lexicon and nowhere in this one, so the SAME sentence
44
+ // translated came back with three smells in one language and none in the
45
+ // other — and this project keeps its own pages in both. A smell test that
46
+ // answers differently per language is not testing the writing.
47
+ re: /(?<!\bhow\s+)\b(many|few|some|several|adequate|robust|scalable)\b(?!\s+\d)/gi,
41
48
  hint: "quantify (use a number or a precise scope)",
42
49
  },
43
50
  {
@@ -7,18 +7,20 @@ import { flagBool } from "../lib/args.js";
7
7
  import { c } from "../lib/colors.js";
8
8
  import { parseCapabilityFromDelta } from "../lib/doc-model.js";
9
9
  import { printAdrCheckpoint, acceptedDecisionCount } from "../lib/adr-guard.js";
10
- import { docsRemedy, checkDocsImpact } from "../lib/docs-impact.js";
10
+ import { docsRemedy, checkDocsImpact, checkChangelogImpact } from "../lib/docs-impact.js";
11
11
  import { collectRuntimeFindings } from "../lib/runtime.js";
12
12
  import { derivedImplementations, implementationMismatch, summarize } from "../lib/coverage-model.js";
13
13
  import { specHeader, dependentsOf } from "../lib/scan.js";
14
- import { sequence, stepRerun } from "../lib/gates.js";
14
+ import { sequence, sequenceLabels, stepRerun } from "../lib/gates.js";
15
15
  import { EXIT, notADoctrinaProject } from "../lib/exit-codes.js";
16
+ import { refuseChangeRef } from "../lib/project.js";
16
17
  import * as analyze from "./analyze.js";
17
18
  import * as change from "./change.js";
18
19
  import * as verify from "./verify.js";
19
20
  import * as coverage from "./coverage.js";
20
21
  import * as trace from "./trace.js";
21
22
  import * as validate from "./validate.js";
23
+ import * as indexRebuild from "./index-rebuild.js";
22
24
  import { appendLedgerLine, docsGapLine, ledgerPath as ledgerFile } from "../lib/ledger.js";
23
25
  import * as review from "./review.js";
24
26
  import * as skill from "./skill.js";
@@ -67,6 +69,8 @@ export async function run(positional, flags) {
67
69
  }
68
70
 
69
71
  async function closeOne(projectRoot, id, flags) {
72
+ const refused = refuseChangeRef(id);
73
+ if (refused !== null) return refused;
70
74
  // A change id that does not resolve is the USAGE class, before any step
71
75
  // is sequenced (change 0114) — every sibling that takes a change id
72
76
  // answers 2; `close` answered 1 through the analyze step it ran first.
@@ -257,10 +261,24 @@ async function closeOne(projectRoot, id, flags) {
257
261
  // escape hatch archive offers, and records the gap in the ledger.
258
262
  docs: {
259
263
  run: async () => {
260
- const r = checkDocsImpact(projectRoot, path.join(projectRoot, ".doctrina", "changes", id));
264
+ const changeDir = path.join(projectRoot, ".doctrina", "changes", id);
265
+ const r = checkDocsImpact(projectRoot, changeDir);
261
266
  if (r.ok) {
262
267
  console.log(c.green("ok") + ` ${r.reason}`);
263
- return 0;
268
+ // Documentation says how it works; the changelog says that it
269
+ // changed. Asking only the first is how thirteen changes closed in
270
+ // one session with an empty `## [Unreleased]` behind them.
271
+ const log = checkChangelogImpact(projectRoot, changeDir);
272
+ if (log.ok) {
273
+ console.log(c.green("ok") + ` ${log.reason}`);
274
+ return 0;
275
+ }
276
+ console.error(c.red("error:") + ` this change ${log.reason}:`);
277
+ for (const s of log.signals) console.error(` - ${s}`);
278
+ console.error(c.gray("hint: ") + "add an entry under the changelog's unreleased heading" +
279
+ ", or pass --force to close anyway (records the gap)");
280
+ docsGap = log;
281
+ return 1;
264
282
  }
265
283
  console.error(c.red("error:") + ` this change ${r.reason}:`);
266
284
  for (const s of r.signals) console.error(` - ${s}`);
@@ -268,8 +286,21 @@ async function closeOne(projectRoot, id, flags) {
268
286
  // Doctrina's own repository (change 0058): an adopting project with
269
287
  // no `docs/` was being told to write English AND Portuguese and to
270
288
  // read a skill it does not have.
271
- console.error(c.gray("hint: ") + docsRemedy(projectRoot) +
272
- ", or pass --force to close anyway (records the gap)");
289
+ console.error(c.gray("hint: ") + docsRemedy(projectRoot) + ".");
290
+ // THE SECOND ANSWER, NAMED WHERE IT IS NEEDED.
291
+ //
292
+ // The gate reads names, and a name is all it can see: a change that
293
+ // explains an effect, or a Scope boundaries line saying what it does
294
+ // NOT touch, reads exactly like one that alters the command. When the
295
+ // only remedy on offer was `--force`, an author looking at a false
296
+ // positive took the worst of the two exits — the ledger fills with
297
+ // gaps that were never gaps, and a real one stops standing out.
298
+ //
299
+ // The declaration exists for precisely this (change 0139) and was
300
+ // named nowhere the author would be looking.
301
+ console.error(c.gray(" ") + "if the names above are only MENTIONED, say so on the record: " +
302
+ "add `- **Documented surface:** n/a — <why>` to the proposal.");
303
+ console.error(c.gray(" ") + "or pass --force to close anyway (records the gap).");
273
304
  docsGap = r;
274
305
  return 1;
275
306
  },
@@ -280,6 +311,10 @@ async function closeOne(projectRoot, id, flags) {
280
311
  run: () => change.run(["archive", id], archiveFlags),
281
312
  },
282
313
 
314
+ // In-process, like every other runner here: the close starts no subprocess
315
+ // of its own binary, and a criterion holds it to that.
316
+ "index-drift": { run: () => indexRebuild.run(["rebuild"], new Map([["check", true]])) },
317
+
283
318
  validate: { run: () => validate.run([], new Map()) },
284
319
  };
285
320
 
@@ -459,15 +494,35 @@ function touchedCapabilities(projectRoot, id) {
459
494
  return [...caps].sort();
460
495
  }
461
496
 
497
+ // The sequence is DECLARED once, in lib/gates.js, and rendered here. It used
498
+ // to be typed out in this string, which is how the help came to name ten
499
+ // steps of thirteen — review, implementation, docs and the index-drift check
500
+ // were added over time and the paragraph was not.
501
+ const SEQUENCE = wrapArrows(sequenceLabels("close"), 70, " ");
502
+
503
+ function wrapArrows(labels, width, indent) {
504
+ const lines = [];
505
+ let line = "";
506
+ for (const label of labels) {
507
+ const piece = line === "" ? label : `${line} → ${label}`;
508
+ if (piece.length + indent.length > width && line !== "") {
509
+ lines.push(indent + line + " →");
510
+ line = label;
511
+ } else line = piece;
512
+ }
513
+ if (line !== "") lines.push(indent + line);
514
+ return lines.join("\n");
515
+ }
516
+
462
517
  export const help = `
463
518
  Usage: doctrina close <id...> [--force]
464
519
 
465
520
  Run the whole closing sequence for a change in one pass, stopping at the
466
521
  first failure with the exact command to rerun:
467
522
 
468
- analyze → ADR checkpoint (advisory) → change apply → runtime → verify →
469
- coverage --strict (scoped to the change's touched capabilities) → trace →
470
- change archive → validate → skill suggest (advisory)
523
+ ${SEQUENCE}
524
+
525
+ then a closing skill-suggest listing (advisory).
471
526
 
472
527
  The coverage gate is scoped to the capabilities the change's deltas touch,
473
528
  so a deliberately deferred spec elsewhere cannot block an unrelated close;
@@ -481,7 +536,7 @@ trace, the ADR checkpoint
481
536
  (accepted ADRs citing the touched capabilities — amend via \`decision
482
537
  supersede\`, not silence), and the closing skill-suggest listing are
483
538
  advisory (reports, never blockers). This is a driver over the existing
484
- commands so the agent makes one call instead of nine and the human
539
+ commands so the agent makes one call instead of thirteen and the human
485
540
  approves once.
486
541
 
487
542
  Multiple ids close in sequence, each independently; the exit code is the
@@ -1,15 +1,21 @@
1
1
  // @ts-check
2
- import { OPERATIONS, COMMAND_NAMES } from "../lib/commands.js";
2
+ import { DEPRECATED, OPERATIONS } from "../lib/commands.js";
3
3
  import { c } from "../lib/colors.js";
4
4
 
5
5
  // Shell completions, generated from the same OPERATIONS catalog that feeds
6
6
  // `--help` and the docs drift tests — so the completion script can never
7
7
  // know a different surface than the CLI ships. Static output by design:
8
8
  // paste it into your shell config once; regenerate after upgrading.
9
+ //
10
+ // Deprecated operations are left out (change 0188): they still run, with a
11
+ // notice, for a script that already types them — but tab completion is how
12
+ // a person discovers the surface, and it should offer the replacement only.
13
+ const LIVE = OPERATIONS.filter(([op]) => !DEPRECATED[op]);
14
+ const COMMAND_NAMES = [...new Set(LIVE.map(([op]) => op.split(" ")[0]))];
9
15
 
10
16
  function subcommandMap() {
11
17
  const map = new Map();
12
- for (const [op] of OPERATIONS) {
18
+ for (const [op] of LIVE) {
13
19
  const [cmd, sub] = op.split(" ");
14
20
  if (!sub) continue;
15
21
  if (!map.has(cmd)) map.set(cmd, []);
@@ -7,7 +7,7 @@ import { listHeader, parseDependsOn, parseAdrScope, adrSummary, knownCapabilitie
7
7
  import { suggest } from "../lib/suggest.js";
8
8
  import { checklistProgress, getTitle, getSectionParagraph } from "../lib/doc-model.js";
9
9
  import { parseFrontmatter } from "../lib/doc-model.js";
10
- import { flagBool, flagString, flagGivenWithoutValue } from "../lib/args.js";
10
+ import { flagBool, flagString, flagGivenWithoutValue, parsePositiveInt } from "../lib/args.js";
11
11
  import { c } from "../lib/colors.js";
12
12
  import { loadConfig, DEFAULTS } from "../lib/config.js";
13
13
  import { GIT_STATE, historyState, changedFiles } from "../lib/git.js";
@@ -466,16 +466,18 @@ function capabilitiesInPlay(projectRoot) {
466
466
  return [...caps];
467
467
  }
468
468
 
469
+ // Line endings are not content: a Windows checkout stores every line with a
470
+ // CR, which added ~2% to the same pack, so the budget gate — run on every OS
471
+ // in CI — could pass on Linux and fail on Windows for the same tree.
469
472
  function estimateTokens(text) {
470
- return Math.round(text.length / TOKEN_DIVISOR);
473
+ return Math.round(text.replace(/\r\n/g, "\n").length / TOKEN_DIVISOR);
471
474
  }
472
475
 
473
476
  // Flag beats project config beats the built-in default. Returns null for a
474
477
  // malformed --budget so the caller can report a usage error.
475
478
  function resolveBudget(projectRoot, budgetRaw) {
476
479
  if (budgetRaw !== undefined) {
477
- const n = Number.parseInt(budgetRaw, 10);
478
- return Number.isFinite(n) && n > 0 ? n : null;
480
+ return parsePositiveInt(budgetRaw);
479
481
  }
480
482
  // One configuration reader (lib/config.js, change 0047): `.doctrina/
481
483
  // config.json` is the declared home and index.json's `config` block is