doctrina-cli 0.15.1 → 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 (93) hide show
  1. package/README.md +27 -72
  2. package/package.json +3 -3
  3. package/src/commands/adapter.js +27 -3
  4. package/src/commands/analyze.js +16 -212
  5. package/src/commands/change.js +144 -267
  6. package/src/commands/ci.js +182 -0
  7. package/src/commands/clarify.js +65 -21
  8. package/src/commands/close.js +349 -45
  9. package/src/commands/completion.js +8 -2
  10. package/src/commands/context.js +284 -111
  11. package/src/commands/contract.js +95 -75
  12. package/src/commands/coverage.js +58 -273
  13. package/src/commands/decision.js +66 -7
  14. package/src/commands/doctor.js +293 -130
  15. package/src/commands/handoff.js +10 -57
  16. package/src/commands/hooks.js +17 -3
  17. package/src/commands/index-rebuild.js +55 -44
  18. package/src/commands/init.js +88 -16
  19. package/src/commands/intake.js +85 -99
  20. package/src/commands/intent.js +14 -1
  21. package/src/commands/metrics.js +65 -88
  22. package/src/commands/next.js +105 -147
  23. package/src/commands/prime.js +23 -91
  24. package/src/commands/report.js +40 -109
  25. package/src/commands/review.js +68 -36
  26. package/src/commands/search.js +18 -4
  27. package/src/commands/show.js +24 -9
  28. package/src/commands/skill.js +23 -39
  29. package/src/commands/spec.js +38 -11
  30. package/src/commands/status.js +69 -171
  31. package/src/commands/templates.js +35 -148
  32. package/src/commands/trace.js +36 -71
  33. package/src/commands/triage.js +4 -82
  34. package/src/commands/upgrade.js +31 -6
  35. package/src/commands/validate.js +44 -877
  36. package/src/commands/verify.js +60 -138
  37. package/src/commands/why.js +2 -2
  38. package/src/commands/work.js +243 -267
  39. package/src/index.js +139 -12
  40. package/src/lib/actions.js +490 -0
  41. package/src/lib/adapters.js +0 -11
  42. package/src/lib/adr-guard.js +19 -0
  43. package/src/lib/agent-changelog.js +133 -0
  44. package/src/lib/analysis.js +264 -0
  45. package/src/lib/args.js +14 -0
  46. package/src/lib/change-ops.js +133 -0
  47. package/src/lib/commands.js +135 -10
  48. package/src/lib/config.js +206 -0
  49. package/src/lib/constitution-model.js +102 -0
  50. package/src/lib/coverage-model.js +431 -0
  51. package/src/lib/criteria.js +23 -0
  52. package/src/lib/doc-model.js +370 -3
  53. package/src/lib/docs-impact.js +270 -27
  54. package/src/lib/ears.js +24 -1
  55. package/src/lib/flag-catalog.js +0 -16
  56. package/src/lib/fs-ops.js +58 -1
  57. package/src/lib/gates.js +231 -29
  58. package/src/lib/git.js +132 -0
  59. package/src/lib/index-json.js +42 -24
  60. package/src/lib/intake-model.js +155 -0
  61. package/src/lib/json-out.js +100 -2
  62. package/src/lib/ledger.js +192 -0
  63. package/src/lib/lexicon.js +228 -0
  64. package/src/lib/metrics-model.js +189 -0
  65. package/src/lib/names.js +42 -0
  66. package/src/lib/playbook.js +97 -0
  67. package/src/lib/project.js +105 -0
  68. package/src/lib/reproducibility.js +137 -0
  69. package/src/lib/runtime.js +191 -5
  70. package/src/lib/scan.js +328 -25
  71. package/src/lib/signoff.js +147 -0
  72. package/src/lib/snapshot.js +231 -0
  73. package/src/lib/spec-ops.js +72 -8
  74. package/src/lib/templates-model.js +289 -0
  75. package/src/lib/trace-model.js +95 -0
  76. package/src/lib/triage-model.js +121 -0
  77. package/src/lib/usage.js +6 -4
  78. package/src/lib/validation-model.js +1381 -0
  79. package/src/lib/version.js +22 -0
  80. package/src/lib/views.js +413 -0
  81. package/src/lib/work-model.js +102 -0
  82. package/templates/AGENTS.md.template +21 -28
  83. package/templates/README.md +35 -0
  84. package/templates/adapters/claude/.claude/commands/doctrina-work.md.template +6 -3
  85. package/templates/adapters/cursor/.cursor/commands/doctrina-work.md.template +4 -3
  86. package/templates/change/proposal.md.template +16 -0
  87. package/templates/change/tasks.md.template +3 -8
  88. package/templates/contract.md.template +2 -2
  89. package/templates/hooks/pre-commit.sample +16 -2
  90. package/templates/playbooks/bootstrap.md.template +47 -0
  91. package/templates/playbooks/chore.md.template +24 -0
  92. package/templates/playbooks/work.md.template +83 -0
  93. package/src/commands/constitution.js +0 -145
package/src/index.js CHANGED
@@ -5,11 +5,11 @@ import { parseArgs } from "./lib/args.js";
5
5
  import { c } from "./lib/colors.js";
6
6
  import { suggest } from "./lib/suggest.js";
7
7
  import { cliVersion } from "./lib/version.js";
8
- import { surfaceHelp, OPERATIONS } from "./lib/commands.js";
8
+ import { surfaceHelp, OPERATIONS, deprecationFor, removalFor } from "./lib/commands.js";
9
9
  import { GLOBAL_FLAGS } from "./lib/flag-catalog.js";
10
10
  import { EXIT, exitCodeHelp } from "./lib/exit-codes.js";
11
- import { recordUsage } from "./lib/usage.js";
12
- import { wantsJson, emitJson, captureOutput, stripAnsi } from "./lib/json-out.js";
11
+ import { recordUsage, operationOf } from "./lib/usage.js";
12
+ import { wantsJson, emitJson, captureOutput, stripAnsi, setDeprecation, deferJson, flushJson } from "./lib/json-out.js";
13
13
 
14
14
  import * as init from "./commands/init.js";
15
15
  import * as spec from "./commands/spec.js";
@@ -37,7 +37,6 @@ import * as close from "./commands/close.js";
37
37
  import * as review from "./commands/review.js";
38
38
  import * as watch from "./commands/watch.js";
39
39
  import * as why from "./commands/why.js";
40
- import * as constitution from "./commands/constitution.js";
41
40
  import * as prime from "./commands/prime.js";
42
41
  import * as handoff from "./commands/handoff.js";
43
42
  import * as show from "./commands/show.js";
@@ -48,20 +47,20 @@ import * as intent from "./commands/intent.js";
48
47
  import * as upgrade from "./commands/upgrade.js";
49
48
  import * as adapter from "./commands/adapter.js";
50
49
  import * as triage from "./commands/triage.js";
50
+ import * as ci from "./commands/ci.js";
51
51
 
52
52
  const COMMANDS = {
53
53
  init, spec, change, decision, validate, hooks, analyze, clarify,
54
54
  templates, skill, index: indexCmd, next, metrics, context, search,
55
55
  intake, work, coverage, verify, contract, trace,
56
- status, close, review, watch, why, constitution,
56
+ status, close, review, watch, why,
57
57
  prime, handoff, show, doctor, report, completion,
58
- intent, upgrade, adapter, triage,
58
+ intent, upgrade, adapter, triage, ci,
59
59
  };
60
60
 
61
61
  const TOP_HELP = `
62
- Usage: doctrina <command> [options]
62
+ Usage: doctrina <command> [options] (\`doctrina <command> --help\` for its flags)
63
63
 
64
- Commands:
65
64
  ${surfaceHelp()}
66
65
 
67
66
  Global flags:
@@ -111,6 +110,17 @@ async function main(argv) {
111
110
 
112
111
  const commandName = positional[0];
113
112
  const command = COMMANDS[commandName];
113
+ const removed = removalFor(positional);
114
+ if (removed) {
115
+ const name = removed === removalFor([commandName]) ? commandName : positional.slice(0, 2).join(" ");
116
+ console.error(c.red("error:") + ` \`doctrina ${name}\` was removed in ${removed.since}`);
117
+ console.error(c.gray("hint: ") + `use \`${removed.use}\` — it prints what the removed command printed`);
118
+ if (wantsJson(flags)) {
119
+ emitJson(operationName(positional), { stderr: [`error: \`doctrina ${name}\` was removed in ${removed.since}`, `hint: use \`${removed.use}\``] },
120
+ { ok: false, exitCode: EXIT.USAGE, args: positional.slice(1) });
121
+ }
122
+ return EXIT.USAGE;
123
+ }
114
124
  if (!command) {
115
125
  console.error(c.red("error:") + ` unknown command "${commandName}"`);
116
126
  const guess = suggest(commandName, Object.keys(COMMANDS));
@@ -127,18 +137,98 @@ async function main(argv) {
127
137
  return 0;
128
138
  }
129
139
 
140
+ // A flag the command does not declare is REFUSED, never ignored.
141
+ //
142
+ // Declaring the flags (C3) fixed the parser swallowing a positional; it
143
+ // did not make anything CHECK the declaration, so an unrecognised flag was
144
+ // simply dropped. On the same tree, `doctrina coverage --strict` exited 1
145
+ // and `doctrina coverage --stricts` exited 0: the gate the operator asked
146
+ // for never ran, and nothing said so. That is the most expensive failure a
147
+ // gate can have, because it is indistinguishable from success — a CI job
148
+ // with a typo in `--strict` stays green forever over a tree the gate would
149
+ // reject (change 0082).
150
+ //
151
+ // Usage error, not gate failure: exit 2 (ADR 0018). Checked after --help,
152
+ // so `doctrina <cmd> --typo --help` still explains the command instead of
153
+ // refusing to.
154
+ if (spec) {
155
+ const declared = new Set([
156
+ ...GLOBAL_FLAGS.boolean, ...GLOBAL_FLAGS.string,
157
+ ...(spec.boolean ?? []), ...(spec.string ?? []),
158
+ ]);
159
+ const undeclared = [...flags.keys()].filter((f) => !declared.has(f));
160
+ if (undeclared.length > 0) {
161
+ const lines = [];
162
+ for (const f of undeclared) {
163
+ lines.push(`error: unknown flag "--${f}" for \`doctrina ${commandName}\``);
164
+ console.error(c.red("error:") + ` unknown flag "--${f}" for \`doctrina ${commandName}\``);
165
+ const guess = suggest(f, [...declared]);
166
+ if (guess) {
167
+ lines.push(`hint: did you mean \`--${guess}\`?`);
168
+ console.error(c.gray("hint: ") + `did you mean \`--${guess}\`?`);
169
+ }
170
+ }
171
+ lines.push(`hint: \`doctrina ${commandName} --help\` lists the flags it accepts`);
172
+ console.error(c.gray("hint: ") + `\`doctrina ${commandName} --help\` lists the flags it accepts`);
173
+ // A consumer that asked for JSON gets JSON, including when the answer
174
+ // is "I refused" (third audit, finding 7). Change 0086 made the
175
+ // envelope tell the truth about the exit code; the flag check runs
176
+ // BEFORE the envelope exists, so a rejected invocation returned exit 2
177
+ // with an empty stdout and the consumer got a parse error instead of
178
+ // `{ok: false, exit_code: 2}`.
179
+ if (wantsJson(flags)) {
180
+ emitJson(operationName(positional), { stderr: lines },
181
+ { ok: false, exitCode: EXIT.USAGE, args: positional.slice(1) });
182
+ }
183
+ return EXIT.USAGE;
184
+ }
185
+ }
186
+
187
+ // A deprecated name keeps working and says so, once, before it runs
188
+ // (change 0049). On stderr, so a piped stdout stays exactly what it was —
189
+ // a warning that corrupts the output it warns about is a breaking change
190
+ // wearing a deprecation's clothes.
191
+ const deprecated = deprecationFor(positional);
192
+ if (deprecated) {
193
+ console.error(c.yellow("deprecated:") + ` this command is superseded — use ${c.cyan(deprecated.use)}`);
194
+ console.error(c.gray(` ${deprecated.why}; the old name still works and will be removed in a later minor.`));
195
+ // And into the envelope, for the consumer that never sees a terminal
196
+ // (change 0061). The prose line above stays for the one that does.
197
+ setDeprecation({ use: deprecated.use, since: deprecated.since, why: deprecated.why });
198
+ }
199
+
130
200
  try {
131
201
  // --json on a command that builds no payload of its own still answers in
132
202
  // JSON: its output is captured into a versioned envelope beside `ok` and
133
203
  // `exit_code`. Branch on those; the lines are for completeness (M7).
134
- if (wantsJson(flags) && !command.jsonNative) {
204
+ // A module with several subcommands is native for some and not others —
205
+ // `contract check` builds a real payload, `contract new` has nothing but
206
+ // its prose — so `jsonNative` may be a predicate over the subcommand's
207
+ // own arguments (change 0068).
208
+ const jsonNative = typeof command.jsonNative === "function"
209
+ ? command.jsonNative(positional.slice(1))
210
+ : command.jsonNative === true;
211
+ if (wantsJson(flags) && !jsonNative) {
135
212
  const { code, stdout, stderr } = await captureOutput(
136
213
  () => command.run(positional.slice(1), flags),
137
214
  );
138
- emitJson([commandName, ...positional.slice(1)].join(" "), {
215
+ emitJson(operationName(positional), {
139
216
  stdout: stdout.map(stripAnsi),
140
217
  stderr: stderr.map(stripAnsi),
141
- }, { ok: code === EXIT.OK, exitCode: code });
218
+ }, { ok: code === EXIT.OK, exitCode: code, args: operationArgs(positional) });
219
+ return code;
220
+ }
221
+ // The native path: hold the payload the command builds, run it, then
222
+ // write the envelope with the code it returned — so `ok` and `exit_code`
223
+ // say what the process says (change 0086).
224
+ if (wantsJson(flags)) {
225
+ deferJson();
226
+ let code = EXIT.OK;
227
+ try {
228
+ code = (await command.run(positional.slice(1), flags)) ?? EXIT.OK;
229
+ } finally {
230
+ flushJson(code);
231
+ }
142
232
  return code;
143
233
  }
144
234
  return await command.run(positional.slice(1), flags);
@@ -157,6 +247,24 @@ async function main(argv) {
157
247
  }
158
248
  }
159
249
 
250
+ // The OPERATION an invocation names, and the arguments it carries — the two
251
+ // halves the envelope's `command` field used to run together (third audit,
252
+ // finding 6). `doctrina why carteira --json` answered `"command": "why
253
+ // carteira"`, so a consumer reading that field got a different shape for
254
+ // every capability, while `next --json` documents `command`/`args` as the
255
+ // contract to branch on. The catalog is what tells a sub-operation
256
+ // (`spec list`) from an argument (`why carteira`) — shape alone cannot,
257
+ // which is the same reason `lib/usage.js` consults it.
258
+ const KNOWN_OPERATIONS = new Set(OPERATIONS.map((o) => o[0]));
259
+
260
+ function operationName(positional) {
261
+ return operationOf(positional, KNOWN_OPERATIONS) ?? positional[0] ?? "";
262
+ }
263
+
264
+ function operationArgs(positional) {
265
+ return positional.slice(operationName(positional).split(" ").length);
266
+ }
267
+
160
268
  main(process.argv.slice(2)).then((code) => {
161
269
  // Record which operation ran, if and only if the operator asked for it
162
270
  // by setting DOCTRINA_USAGE_LOG (M8). Off by default, local file only,
@@ -166,5 +274,24 @@ main(process.argv.slice(2)).then((code) => {
166
274
  code,
167
275
  new Set(OPERATIONS.map((o) => o[0])),
168
276
  );
169
- process.exit(code ?? 0);
277
+ // SET THE CODE; DO NOT CALL process.exit().
278
+ //
279
+ // When stdout is a PIPE — every `doctrina ... | less`, every agent reading
280
+ // this CLI, every `spawnSync` in the suite — Node may not have handed the
281
+ // bytes to the OS yet when this callback runs. `process.exit()` tears the
282
+ // process down immediately and whatever is still buffered is simply lost.
283
+ // Setting `exitCode` lets Node exit on its own once the work is done, and
284
+ // flushing stdout is part of that work.
285
+ //
286
+ // This is not theoretical. Four tests failed on macOS with Node 20.12 and
287
+ // on no other leg of the matrix, all of them `--concat` runs looking for
288
+ // content near the END of a long output. The decisive one printed exactly
289
+ // one section: `product.md` was on disk and in the pack listing, and the
290
+ // concat output stopped before reaching it. Pipe buffer sizes differ per
291
+ // platform, which is precisely why a truncation bug picks one and hides on
292
+ // the others.
293
+ //
294
+ // `--concat` even carries the comment "Keep the pack pipeable". Losing the
295
+ // tail of a pipe is the one way to break that promise.
296
+ process.exitCode = code ?? 0;
170
297
  });