doctrina-cli 0.13.0 → 0.14.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 (66) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -2
  3. package/src/commands/adapter.js +205 -0
  4. package/src/commands/analyze.js +66 -19
  5. package/src/commands/change.js +100 -42
  6. package/src/commands/clarify.js +8 -1
  7. package/src/commands/close.js +57 -2
  8. package/src/commands/completion.js +6 -0
  9. package/src/commands/constitution.js +8 -1
  10. package/src/commands/context.js +359 -53
  11. package/src/commands/contract.js +17 -24
  12. package/src/commands/coverage.js +16 -4
  13. package/src/commands/decision.js +172 -40
  14. package/src/commands/doctor.js +38 -7
  15. package/src/commands/handoff.js +8 -1
  16. package/src/commands/hooks.js +6 -0
  17. package/src/commands/index-rebuild.js +8 -1
  18. package/src/commands/init.js +139 -6
  19. package/src/commands/intake.js +11 -2
  20. package/src/commands/intent.js +8 -1
  21. package/src/commands/metrics.js +80 -7
  22. package/src/commands/next.js +14 -2
  23. package/src/commands/prime.js +8 -1
  24. package/src/commands/report.js +8 -1
  25. package/src/commands/review.js +8 -1
  26. package/src/commands/search.js +8 -1
  27. package/src/commands/show.js +8 -1
  28. package/src/commands/skill.js +16 -6
  29. package/src/commands/spec.js +13 -6
  30. package/src/commands/status.js +14 -2
  31. package/src/commands/templates.js +193 -59
  32. package/src/commands/trace.js +15 -3
  33. package/src/commands/upgrade.js +8 -1
  34. package/src/commands/validate.js +58 -2
  35. package/src/commands/verify.js +12 -2
  36. package/src/commands/watch.js +8 -1
  37. package/src/commands/why.js +10 -24
  38. package/src/commands/work.js +8 -1
  39. package/src/index.js +60 -6
  40. package/src/lib/adapters.js +140 -0
  41. package/src/lib/adr-guard.js +1 -0
  42. package/src/lib/args.js +10 -0
  43. package/src/lib/clarity.js +1 -0
  44. package/src/lib/colors.js +1 -0
  45. package/src/lib/commands.js +239 -26
  46. package/src/lib/criteria.js +1 -0
  47. package/src/lib/dates.js +1 -0
  48. package/src/lib/diff.js +1 -0
  49. package/src/lib/doc-model.js +287 -0
  50. package/src/lib/docs-impact.js +167 -0
  51. package/src/lib/ears.js +1 -0
  52. package/src/lib/exit-codes.js +103 -0
  53. package/src/lib/flag-catalog.js +106 -0
  54. package/src/lib/fs-ops.js +1 -0
  55. package/src/lib/gates.js +123 -0
  56. package/src/lib/git.js +97 -0
  57. package/src/lib/index-json.js +14 -6
  58. package/src/lib/json-out.js +83 -0
  59. package/src/lib/prompt.js +18 -1
  60. package/src/lib/scan.js +81 -33
  61. package/src/lib/spec-ops.js +94 -9
  62. package/src/lib/suggest.js +1 -0
  63. package/src/lib/templates.js +89 -0
  64. package/src/lib/usage.js +89 -0
  65. package/src/lib/version.js +1 -0
  66. package/templates/AGENTS.md.template +59 -53
@@ -1,10 +1,14 @@
1
+ // @ts-check
1
2
  import path from "node:path";
2
3
  import process from "node:process";
4
+ import { appendFileSync } from "node:fs";
3
5
  import { exists, isFile, read, walk } from "../lib/fs-ops.js";
4
6
  import { flagBool } from "../lib/args.js";
5
7
  import { c } from "../lib/colors.js";
6
8
  import { parseCapabilityFromDelta } from "./change.js";
7
9
  import { printAdrCheckpoint } from "../lib/adr-guard.js";
10
+ import { checkDocsImpact } from "../lib/docs-impact.js";
11
+ import { notADoctrinaProject } from "../lib/exit-codes.js";
8
12
  import * as analyze from "./analyze.js";
9
13
  import * as change from "./change.js";
10
14
  import * as verify from "./verify.js";
@@ -21,6 +25,11 @@ import * as skill from "./skill.js";
21
25
  // rerun, so the agent makes one call and the human approves once. It is a
22
26
  // driver over the existing commands; it adds no new checks of its own.
23
27
 
28
+ // Flags this command accepts. Declared HERE, with the command, so
29
+ // adding a command never requires editing the entrypoint — the gap that
30
+ // let six flags ship undeclared and silently swallow a positional (C3).
31
+ export const flags = { boolean: ["json", "force"], string: [] };
32
+
24
33
  export async function run(positional, flags) {
25
34
  if (positional.length === 0) {
26
35
  console.error(c.red("error:") + " close requires a change <id> (e.g. doctrina close 0001-add-login)");
@@ -28,7 +37,7 @@ export async function run(positional, flags) {
28
37
  }
29
38
  const projectRoot = process.cwd();
30
39
  if (!exists(path.join(projectRoot, ".doctrina"))) {
31
- throw new Error("not a Doctrina project (no .doctrina/ in cwd). Run `doctrina init` first.");
40
+ throw notADoctrinaProject();
32
41
  }
33
42
 
34
43
  // Batch close (operator review 2026-07-19 §4.5): each id closes
@@ -54,6 +63,10 @@ export async function run(positional, flags) {
54
63
  async function closeOne(projectRoot, id, flags) {
55
64
  const force = flagBool(flags, "force", false);
56
65
  const archiveFlags = force ? new Map([["force", true]]) : new Map();
66
+ // Set by the docs gate when it fails, so a --force close can record the
67
+ // gap in the ledger after the archive lands.
68
+ /** @type {{ signals: string[] } | null} */
69
+ let docsGap = null;
57
70
 
58
71
  // Scope the coverage gate to the capabilities THIS change touches (its
59
72
  // deltas), so one deliberately deferred spec elsewhere in the tree cannot
@@ -61,6 +74,7 @@ async function closeOne(projectRoot, id, flags) {
61
74
  // item 4). A change with no deltas (chore / metadata-only) falls back to
62
75
  // the whole-tree gate — there is no narrower honest scope for it.
63
76
  const touched = touchedCapabilities(projectRoot, id);
77
+ /** @type {FlagMap} */
64
78
  const coverageFlags = new Map([["strict", true]]);
65
79
  let coverageRerun = "doctrina coverage --strict";
66
80
  if (touched.length > 0) {
@@ -100,11 +114,35 @@ async function closeOne(projectRoot, id, flags) {
100
114
  // trace is advisory (provenance is a warning, not a hard gate): report it,
101
115
  // never let it block the close.
102
116
  { label: "trace", rerun: "doctrina trace", run: async () => { await trace.run([], new Map()); return 0; } },
117
+ // Docs ship inside the change (D2): a change that alters a documented
118
+ // surface — a command, a flag, an exit code — closes only with the
119
+ // documentation that describes it. A blocking gate, because a docs
120
+ // phase scheduled after the work never happens; --force is the same
121
+ // escape hatch archive offers, and records the gap in the ledger.
122
+ {
123
+ label: "docs",
124
+ forceable: true,
125
+ rerun: "edit docs/ (EN + PT), then rerun",
126
+ run: async () => {
127
+ const r = checkDocsImpact(projectRoot, path.join(projectRoot, ".doctrina", "changes", id));
128
+ if (r.ok) {
129
+ console.log(c.green("ok") + ` ${r.reason}`);
130
+ return 0;
131
+ }
132
+ console.error(c.red("error:") + ` this change ${r.reason}:`);
133
+ for (const s of r.signals) console.error(` - ${s}`);
134
+ console.error(c.gray("hint: ") +
135
+ "document it in docs/en/ AND docs/pt/ (the `keep-docs-en-pt-parity` skill), " +
136
+ "or pass --force to close anyway (records the gap)");
137
+ docsGap = r;
138
+ return 1;
139
+ },
140
+ },
103
141
  { label: "archive", rerun: `doctrina change archive ${id}${force ? " --force" : ""}`, run: () => change.run(["archive", id], archiveFlags) },
104
142
  { label: "validate", rerun: "doctrina validate", run: () => validate.run([], new Map()) },
105
143
  ];
106
144
 
107
- console.log(c.bold(`Closing change ${id}`) + c.gray(" — analyze → ADR checkpoint → apply → verify → coverage → trace → archive → validate"));
145
+ console.log(c.bold(`Closing change ${id}`) + c.gray(" — analyze → ADR checkpoint → apply → verify → coverage → trace → docs → archive → validate"));
108
146
 
109
147
  for (let i = 0; i < steps.length; i++) {
110
148
  const step = steps[i];
@@ -122,6 +160,13 @@ async function closeOne(projectRoot, id, flags) {
122
160
  console.error(c.red("error:") + ` ${err.message}`);
123
161
  }
124
162
  if (code !== 0) {
163
+ // A forceable gate under --force warns and continues, matching how
164
+ // `change archive --force` handles incomplete verification: the gap
165
+ // is recorded, not hidden.
166
+ if (code !== 0 && step.forceable && force) {
167
+ console.log(c.yellow("warn:") + ` proceeding past "${step.label}" (--force) — the gap is recorded in the ledger`);
168
+ continue;
169
+ }
125
170
  console.log("");
126
171
  console.log(c.red(`✗ close stopped at "${step.label}"`) + c.gray(` (step ${i + 1}/${steps.length})`));
127
172
  console.log(c.gray("Fix it, then rerun the step or the whole close:"));
@@ -131,6 +176,16 @@ async function closeOne(projectRoot, id, flags) {
131
176
  }
132
177
  }
133
178
 
179
+ // Record a forced docs gap in the ledger, so history shows the change
180
+ // shipped without its documentation rather than showing nothing.
181
+ if (docsGap) {
182
+ const ledgerPath = path.join(projectRoot, ".doctrina", "changes", "archive", "LEDGER.md");
183
+ if (isFile(ledgerPath)) {
184
+ appendFileSync(ledgerPath, ` - docs gap: ${id} closed with --force; ${docsGap.signals.join("; ")} documented nowhere\n`);
185
+ console.log(c.yellow("ledger") + " recorded the docs gap");
186
+ }
187
+ }
188
+
134
189
  // Capture the lesson while it is fresh (operator review §4.7): surface the
135
190
  // fix-shaped lessons not yet written as skills, right where the operator
136
191
  // just proved they finish what they start. Advisory — a listing, never a
@@ -1,3 +1,4 @@
1
+ // @ts-check
1
2
  import { OPERATIONS, COMMAND_NAMES } from "../lib/commands.js";
2
3
  import { c } from "../lib/colors.js";
3
4
 
@@ -90,6 +91,11 @@ Register-ArgumentCompleter -Native -CommandName doctrina -ScriptBlock {
90
91
  `;
91
92
  }
92
93
 
94
+ // Flags this command accepts. Declared HERE, with the command, so
95
+ // adding a command never requires editing the entrypoint — the gap that
96
+ // let six flags ship undeclared and silently swallow a positional (C3).
97
+ export const flags = { boolean: ["json"], string: [] };
98
+
93
99
  export async function run(positional, _flags) {
94
100
  const shell = positional[0];
95
101
  const generators = { bash: bashScript, zsh: zshScript, pwsh: pwshScript, powershell: pwshScript };
@@ -1,9 +1,11 @@
1
+ // @ts-check
1
2
  import path from "node:path";
2
3
  import process from "node:process";
3
4
  import { readdirSync } from "node:fs";
4
5
  import { exists, isDir, isFile, read } from "../lib/fs-ops.js";
5
6
  import { listHeader } from "../lib/scan.js";
6
7
  import { c } from "../lib/colors.js";
8
+ import { notADoctrinaProject } from "../lib/exit-codes.js";
7
9
 
8
10
  // The project's standing rules in one read — Spec Kit parity for its
9
11
  // `constitution.md`, but ASSEMBLED, not a new home for facts. Doctrina's
@@ -16,10 +18,15 @@ import { c } from "../lib/colors.js";
16
18
  // To change a principle, supersede its ADR; to change a non-goal, edit
17
19
  // product.md. The command never writes — it has nothing of its own to own.
18
20
 
21
+ // Flags this command accepts. Declared HERE, with the command, so
22
+ // adding a command never requires editing the entrypoint — the gap that
23
+ // let six flags ship undeclared and silently swallow a positional (C3).
24
+ export const flags = { boolean: ["json"], string: [] };
25
+
19
26
  export async function run(_positional, _flags) {
20
27
  const projectRoot = process.cwd();
21
28
  if (!exists(path.join(projectRoot, ".doctrina"))) {
22
- throw new Error("not a Doctrina project (no .doctrina/ in cwd). Run `doctrina init` first.");
29
+ throw notADoctrinaProject();
23
30
  }
24
31
 
25
32
  const project = projectName(projectRoot);