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
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.18.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|delta
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,43 +72,29 @@ 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
+ `remove-requirement` / `remove-criterion`),
77
+ applies it mechanically to the target spec — all
104
78
  ops or none — and regenerates the index (ADR 0007). Without an ops
105
79
  block it prints a manual-merge pointer and does not write: auto-merging
106
80
  arbitrary prose is the kind of implicit decision Doctrina avoids.
107
81
 
108
82
  On any spec write, the index is rebuilt from the tree so it never drifts
109
- from the applied spec. `doctrina change abandon <id>` is the inverse of
83
+ from the applied spec. `doctrina change delta <id> <capability>` writes the
84
+ ops lines for you (`--requirement`, `--criterion`, `--bump`), dry-run
85
+ against the spec before anything is written. `doctrina change abandon <id>` is the inverse of
110
86
  `change new`: it deletes an open change and its index entry and ledgers
111
- the abandonment.
87
+ the abandonment. `doctrina close <id>` runs apply together with verify,
88
+ coverage, archive and validate in one attested pass — the definition of
89
+ done.
112
90
 
113
91
  ## `validate` checks
114
92
 
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).
93
+ Structure, schema, EARS grammar, index drift, size caps, links, skills,
94
+ ADR anchoring and more — the full, numbered list with the reason for each
95
+ check is in the
96
+ [CLI reference](https://github.com/GCarin1/Doctrina/blob/main/docs/en/cli-reference.md#doctrina-validate).
97
+ A second copy here fell behind it once; there is now one list.
140
98
 
141
99
  Exit code: 0 if no errors (warnings allowed), 1 otherwise. `--fix`
142
100
  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.18.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,16 +17,16 @@ 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
- import { changeNew } from "../lib/change-ops.js";
22
+ import { changeNew, addDeltaOps } 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", "delta"];
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
28
28
  // let six flags ship undeclared and silently swallow a positional (C3).
29
- export const flags = { boolean: ["json", "all", "chore", "design", "force", "no-spec", "verbose"], string: ["reason"] };
29
+ export const flags = { boolean: ["json", "all", "chore", "design", "force", "no-spec", "verbose"], string: ["reason", "requirement", "criterion", "bump", "op"] };
30
30
 
31
31
  export async function run(positional, flags) {
32
32
  const sub = positional[0];
@@ -42,10 +42,10 @@ 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);
47
+ case "delta":
48
+ return changeDelta(positional.slice(1), flags);
49
49
  default:
50
50
  console.error(c.red("error:") + ` unknown change subcommand "${sub ?? ""}"`);
51
51
  const guess = suggest(sub, SUBCOMMANDS);
@@ -100,6 +100,8 @@ async function forEachId(ids, name, one) {
100
100
  }
101
101
  let worst = 0;
102
102
  for (const id of ids) {
103
+ const refused = refuseChangeRef(id);
104
+ if (refused !== null) { worst = Math.max(worst, refused); continue; }
103
105
  if (ids.length > 1) {
104
106
  console.log("");
105
107
  console.log(c.bold(`──── change ${name} ${id}`));
@@ -108,6 +110,14 @@ async function forEachId(ids, name, one) {
108
110
  try {
109
111
  code = await one(id);
110
112
  } catch (err) {
113
+ // A TYPED error is a condition of the RUN, not a verdict on this id:
114
+ // "not a Doctrina project" is equally true of every id, and the class
115
+ // it carries is the whole point of the contract. Flattening it to 1
116
+ // here made `change apply|archive|check` answer "fix your work and
117
+ // retry" where `change new`, one switch arm away, answered "run the
118
+ // setup command first" — same condition, same sentence, two codes.
119
+ // The entrypoint owns the mapping and prints the remedy; let it.
120
+ if (err && typeof err.exitCode === "number") throw err;
111
121
  console.error(c.red("error:") + ` ${err.message}`);
112
122
  code = 1;
113
123
  }
@@ -308,7 +318,7 @@ async function changeCheck(id, { verbose = false } = {}) {
308
318
  let failures = 0;
309
319
 
310
320
  // 1. Structural analysis — same checks analyze runs before an apply.
311
- console.log(c.gray("──── 1/3 structure (analyze)"));
321
+ console.log(c.gray("──── 1/3 structure"));
312
322
  const analyze = await import("./analyze.js");
313
323
  if ((await analyze.run([id], new Map())) !== 0) failures += 1;
314
324
 
@@ -348,7 +358,7 @@ async function changeCheck(id, { verbose = false } = {}) {
348
358
  if (deltaFiles.length === 0) console.log(c.gray("- no spec deltas"));
349
359
  if (opsFindings > 0) failures += 1;
350
360
 
351
- // --verbose: the same per-delta preview `change diff` prints. The dry-run
361
+ // --verbose: the per-delta preview the removed `change diff` printed. The dry-run
352
362
  // above says whether the ops WOULD apply; this says what the file would
353
363
  // look like afterwards, which is the question the separate command existed
354
364
  // to answer (change 0049).
@@ -390,12 +400,57 @@ async function changeCheck(id, { verbose = false } = {}) {
390
400
  // `tick <id> --all` checks everything (tasks.md + the proposal's
391
401
  // ## Verification). Ticking is a claim of completion — the honest gate is
392
402
  // still archive/verify; this only removes the mechanical friction.
403
+ // `change delta <id> <capability> [ops]` — write ops lines into the change's
404
+ // MODIFIED delta through the grammar `apply` reads (change 0205). Rendering
405
+ // only; lib/change-ops.js decides.
406
+ function changeDelta(args, flags) {
407
+ const [id, capability, ...rest] = args;
408
+ if (!id || !capability) {
409
+ console.error(c.red("error:") + " change delta requires <id> <capability> and at least one operation");
410
+ console.error(c.gray("hint: ") + "doctrina change delta <id> <capability> --requirement \"event: When <trigger>, the system shall <action>.\" --criterion \"[unverified] <signal> — verified by `<test>`\" --bump minor");
411
+ return EXIT.USAGE;
412
+ }
413
+ const refused = refuseChangeRef(id);
414
+ if (refused !== null) return refused;
415
+ const projectRoot = process.cwd();
416
+ ensureDoctrinaProject(projectRoot);
417
+ if (!isDir(path.join(projectRoot, ".doctrina", "changes", id))) {
418
+ console.error(c.red("error:") + ` change "${id}" not found at .doctrina/changes/${id}`);
419
+ return EXIT.USAGE;
420
+ }
421
+ // In the order a reader expects them: requirement, criterion, any raw op,
422
+ // and the bump last.
423
+ const lines = [];
424
+ const requirement = flagString(flags, "requirement");
425
+ const criterion = flagString(flags, "criterion");
426
+ const raw = flagString(flags, "op") ?? (rest.length > 0 ? rest.join(" ") : undefined);
427
+ const bump = flagString(flags, "bump");
428
+ if (requirement) lines.push(`append-requirement ${requirement}`);
429
+ if (criterion) lines.push(`append-criterion ${criterion}`);
430
+ if (raw) lines.push(raw);
431
+ if (bump) lines.push(`bump-version ${bump}`);
432
+
433
+ const r = addDeltaOps(projectRoot, id, capability, lines);
434
+ if (r.code !== EXIT.OK) {
435
+ console.error(c.red("error:") + ` ${r.error}`);
436
+ for (const d of r.details ?? []) console.error(` - ${d}`);
437
+ if (r.hint) console.error(c.gray("hint: ") + r.hint);
438
+ return r.code;
439
+ }
440
+ console.log(c.green(r.created ? "created" : "updated") + ` ${r.deltaRel}` +
441
+ c.gray(` — ${lines.length} op${lines.length === 1 ? "" : "s"} added; the block now holds ${r.block.length} (${r.block.join(", ")}), and applies cleanly`));
442
+ console.log(c.gray("Preview the merge: ") + c.cyan(`doctrina change check ${id} --verbose`));
443
+ return EXIT.OK;
444
+ }
445
+
393
446
  function changeTick(args, flags) {
394
447
  const id = args[0];
395
448
  if (!id) {
396
449
  console.error(c.red("error:") + " change tick requires <id> [ordinals... | --all]");
397
450
  return 2;
398
451
  }
452
+ const refused = refuseChangeRef(id);
453
+ if (refused !== null) return refused;
399
454
  const projectRoot = process.cwd();
400
455
  ensureDoctrinaProject(projectRoot);
401
456
  const changeDir = path.join(projectRoot, ".doctrina", "changes", id);
@@ -522,7 +577,7 @@ function changeArchive(args, flags) {
522
577
 
523
578
  // Verification gate. "Done" is a claim until it is checked. Archiving is
524
579
  // the act of declaring a change finished, so it must refuse while any
525
- // task (including closing steps) or any declared verification item is
580
+ // task or any declared verification item is
526
581
  // still unchecked. This is the difference between "boxes marked" and
527
582
  // "verification passed" the framework was faulted for collapsing.
528
583
  // Preconditions from the shared map (C6): structure AND verification.
@@ -594,14 +649,10 @@ function changeArchive(args, flags) {
594
649
  console.log(c.green("ledger") + ` +1 line in ${relPath(projectRoot, ledgerFile)}`);
595
650
 
596
651
  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
- });
652
+ idx.moveChangeToArchive(index, id, archivedChangeEntry(
653
+ exists(proposal) ? read(proposal) : "",
654
+ { id, archiveName, applied: date, specsAffected, title },
655
+ ));
605
656
  idx.touch(index, date);
606
657
  idx.save(projectRoot, index);
607
658
  console.log(c.green("indexed") + " change archived");
@@ -618,6 +669,8 @@ async function changeAbandon(args, flags) {
618
669
  console.error(c.red("error:") + " change abandon requires <id>");
619
670
  return 2;
620
671
  }
672
+ const refused = refuseChangeRef(id);
673
+ if (refused !== null) return refused;
621
674
  const projectRoot = process.cwd();
622
675
  ensureDoctrinaProject(projectRoot);
623
676
 
@@ -675,9 +728,9 @@ async function changeAbandon(args, flags) {
675
728
  // ADDED reports the body it would write, REMOVED the spec it would delete,
676
729
  // MODIFIED a line diff against the current spec.
677
730
  //
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
731
+ // `change check --verbose` prints this after its ops dry-run. It was also
732
+ // the whole of `change diff`, until that alias was retired (changes 0049,
733
+ // 0175) — the one renderer is what made the merge honest. Returns the number of
681
734
  // deltas it could not read.
682
735
  export function printDeltaPreview(projectRoot, changeDir, deltaFiles) {
683
736
  let errors = 0;
@@ -725,33 +778,6 @@ export function printDeltaPreview(projectRoot, changeDir, deltaFiles) {
725
778
  return errors;
726
779
  }
727
780
 
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
781
  function extractDeltaBody(text) {
756
782
  // The delta separates headers from the spec body with a `---` line.
757
783
  const idxSep = text.indexOf("\n---\n");
@@ -760,34 +786,6 @@ function extractDeltaBody(text) {
760
786
  }
761
787
 
762
788
 
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
789
  export const help = `
792
790
  Usage: doctrina change <subcommand> [args]
793
791
 
@@ -805,7 +803,7 @@ Subcommands:
805
803
  MODIFIED without one prints a manual-merge pointer.
806
804
  On any spec write the index is rebuilt from the tree.
807
805
  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
806
+ check <id...> Pre-close dry-run, read-only: the structural
809
807
  checks + every ops block executed in memory against
810
808
  its target + the archive gate preview + an advisory
811
809
  list of accepted ADRs citing the touched capabilities.
@@ -815,8 +813,13 @@ Subcommands:
815
813
  or every one with --all. No args = list only.
816
814
  abandon <id> Delete an open change folder and its index entry, and
817
815
  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.
816
+ delta <id> <capability> [--requirement "<section>: <text>"] [--criterion "<text>"]
817
+ [--bump major|minor|patch] [--op "<op line>" | <op line>]
818
+ Write ops into the change's MODIFIED delta for the
819
+ capability — created from the template when absent —
820
+ parsed by the grammar apply reads and dry-run against
821
+ the spec first: nothing is written unless the whole
822
+ block applies. A second --bump replaces the first.
820
823
 
821
824
  apply / archive / check accept multiple ids (batch close of a backlog); the
822
825
  exit code is the worst per-id result.
@@ -829,9 +832,12 @@ A MODIFIED delta may carry a fenced operations block that \`apply\` executes:
829
832
  append-criterion [unverified] new signal — verified by \`test/x.test.ts\`
830
833
  append-requirement event: When <trigger>, the system shall <action>.
831
834
  replace-requirement ubiquitous 2: The system shall <action>.
835
+ remove-requirement state 1: superseded by event 3
836
+ remove-criterion 4: proven by criterion 2 since 0.9
832
837
  \`\`\`
833
838
  append-* ops resolve numbering/position at apply time, so concurrent open
834
- changes appending to the same spec cannot collide.
839
+ changes appending to the same spec cannot collide. \`change delta\` writes
840
+ these lines for you, one call per addition.
835
841
 
836
842
  Options:
837
843
  --force Overwrite existing files (where applicable)
@@ -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
  {