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
@@ -2,13 +2,14 @@
2
2
  import path from "node:path";
3
3
  import process from "node:process";
4
4
  import { exists } from "../lib/fs-ops.js";
5
- import { flagBool, flagString } from "../lib/args.js";
5
+ import { flagBool, flagString, parsePositiveInt } from "../lib/args.js";
6
6
  import { c } from "../lib/colors.js";
7
7
  import { emitJson } from "../lib/json-out.js";
8
8
  import { notADoctrinaProject } from "../lib/exit-codes.js";
9
- import { collectSnapshot, collectStatus } from "../lib/snapshot.js";
9
+ import { collectSnapshot, collectStatus, collectReportWindow } from "../lib/snapshot.js";
10
10
  import { renderView, VIEWS } from "../lib/views.js";
11
- import { gitWindow, historyState, windowCutoff } from "../lib/git.js";
11
+ import { draftAgentChangelog, renderDraft } from "../lib/agent-changelog.js";
12
+ import { cliVersion } from "../lib/version.js";
12
13
  import { suggest } from "../lib/suggest.js";
13
14
 
14
15
  // One-glance project health (review 2026-06-27 passive-user feature #1): a
@@ -32,6 +33,9 @@ export const jsonNative = true;
32
33
 
33
34
  export const flags = { boolean: ["json"], string: ["view", "since"] };
34
35
 
36
+ // The snapshot's views, plus the one draft that is not a view of it.
37
+ const STATUS_VIEWS = [...VIEWS, "agent-changelog"];
38
+
35
39
  export async function run(_positional, flags) {
36
40
  const projectRoot = process.cwd();
37
41
  if (!exists(path.join(projectRoot, ".doctrina"))) {
@@ -39,10 +43,10 @@ export async function run(_positional, flags) {
39
43
  }
40
44
 
41
45
  const view = flagString(flags, "view") ?? "dashboard";
42
- if (!VIEWS.includes(view)) {
46
+ if (!STATUS_VIEWS.includes(view)) {
43
47
  console.error(c.red("error:") + ` unknown view "${view}"`);
44
- const guess = suggest(view, VIEWS);
45
- console.error(c.gray("hint: ") + (guess ? `did you mean --view ${guess}?` : `available: ${VIEWS.join(", ")}`));
48
+ const guess = suggest(view, STATUS_VIEWS);
49
+ console.error(c.gray("hint: ") + (guess ? `did you mean --view ${guess}?` : `available: ${STATUS_VIEWS.join(", ")}`));
46
50
  return 2;
47
51
  }
48
52
 
@@ -53,18 +57,24 @@ export async function run(_positional, flags) {
53
57
  return 0;
54
58
  }
55
59
 
56
- const options = {};
57
- if (view === "report") {
58
- const sinceRaw = flagString(flags, "since") ?? "7";
59
- const days = Number.parseInt(sinceRaw, 10);
60
- if (!Number.isFinite(days) || days <= 0) {
60
+ let options = {};
61
+ if (view === "report" || view === "agent-changelog") {
62
+ const sinceRaw = flagString(flags, "since");
63
+ const days = parsePositiveInt(sinceRaw ?? "7");
64
+ if (days === null) {
61
65
  console.error(c.red("error:") + ` --since expects a positive day count, got "${sinceRaw}"`);
62
66
  return 2;
63
67
  }
64
- options.days = days;
65
- options.cutoffIso = windowCutoff(days);
66
- options.git = gitWindow(projectRoot, days);
67
- options.gitState = historyState(projectRoot);
68
+ // A DRAFT of the block `upgrade --write` writes into AGENTS.md, proposed
69
+ // from what the archived changes said they touched. Not a view of the
70
+ // snapshot, so it renders here; authorship stays human (ADR 0005). With
71
+ // no --since the window is "since the last tag".
72
+ if (view === "agent-changelog") {
73
+ const draft = draftAgentChangelog(projectRoot, { days: sinceRaw === undefined ? null : days });
74
+ for (const line of renderDraft(draft, cliVersion())) console.log(line);
75
+ return 0;
76
+ }
77
+ options = collectReportWindow(projectRoot, days);
68
78
  }
69
79
 
70
80
  const snapshot = collectSnapshot(projectRoot);
@@ -85,13 +95,22 @@ Flags:
85
95
  dashboard the default one-glance health board
86
96
  prime the session primer (\`doctrina prime\`)
87
97
  handoff the Markdown resume note (\`doctrina handoff\`)
88
- report the period digest (\`doctrina report\`)
89
- --since <days> With --view report: the window (default 7).
98
+ report the period digest: gates, archived changes,
99
+ open work, and local git numbers
100
+ rules the standing rules (\`doctrina prime --rules\`)
101
+ agent-changelog
102
+ a DRAFT of the AGENTS.md "What changed" block:
103
+ one bullet per archived change that touched a
104
+ documented surface, newest first, at most five.
105
+ It proposes; a person cuts and rewrites.
106
+ --since <days> With --view report: the window (default 7). With
107
+ --view agent-changelog: the window (default: since the
108
+ last tag).
90
109
  --json Emit the snapshot as JSON (stable shape for agents and CI).
91
110
 
92
- All four views read ONE collection of the tree, so they can never report
93
- different numbers; \`prime\`, \`handoff\` and \`report\` are the same views
94
- under their own names.
111
+ The views read ONE collection of the tree, so they can never report
112
+ different numbers; \`prime\` and \`handoff\` are the same views under their
113
+ own names. \`doctrina report\` is deprecated: it is \`--view report\`.
95
114
 
96
115
  It is a fast summary, not the authoritative gate: run \`doctrina validate\`
97
116
  and \`doctrina verify\` for the full structural and build checks, and
@@ -19,6 +19,7 @@ import { collectFindings, agentsSectionCost, AGENTS_SECTIONS, PRODUCT_SECTIONS,
19
19
  // too (audit finding F7); this command renders and fixes them.
20
20
  export { collectFindings } from "../lib/templates-model.js";
21
21
  import { suggest } from "../lib/suggest.js";
22
+ import { ensureDoctrinaProject } from "../lib/project.js";
22
23
 
23
24
  const SUBCOMMANDS = ["list", "check", "update"];
24
25
 
@@ -50,10 +51,7 @@ export async function run(positional, flags) {
50
51
  function updateTemplates(flags) {
51
52
  const writeMode = flagBool(flags, "write", false);
52
53
  const projectRoot = process.cwd();
53
- if (!isDir(path.join(projectRoot, ".doctrina"))) {
54
- console.error(c.red("error:") + " not a Doctrina project (no .doctrina/ in cwd)");
55
- return 1;
56
- }
54
+ ensureDoctrinaProject(projectRoot);
57
55
 
58
56
  const plan = [];
59
57
  // Updates this run declines to make, each with the reason and the number.
@@ -176,7 +174,7 @@ function updateTemplates(flags) {
176
174
  let updated = read(filePath);
177
175
  if (!updated.endsWith("\n")) updated += "\n";
178
176
  for (const h of missing) {
179
- updated += `\n${h}\n\n<!-- added by doctrina templates update — fill in -->\n`;
177
+ updated += `\n${h}\n\n<!-- added by doctrina upgrade — fill in -->\n`;
180
178
  }
181
179
  write(filePath, updated, { force: true });
182
180
  },
@@ -287,11 +285,7 @@ function listTemplates() {
287
285
 
288
286
  function checkTemplates() {
289
287
  const projectRoot = process.cwd();
290
- if (!isDir(path.join(projectRoot, ".doctrina"))) {
291
- console.error(c.red("error:") + " not a Doctrina project (no .doctrina/ in cwd)");
292
- console.error(c.gray("hint: ") + "run `doctrina init` first");
293
- return 1;
294
- }
288
+ ensureDoctrinaProject(projectRoot);
295
289
  const { findings, ok } = collectFindings(projectRoot);
296
290
 
297
291
  return report(findings, ok);
@@ -9,6 +9,7 @@ import { flagBool } from "../lib/args.js";
9
9
  import { c } from "../lib/colors.js";
10
10
  import { today } from "../lib/dates.js";
11
11
  import { notADoctrinaProject } from "../lib/exit-codes.js";
12
+ import { collectFindings } from "../lib/templates-model.js";
12
13
  import * as templates from "./templates.js";
13
14
  import * as validate from "./validate.js";
14
15
 
@@ -56,11 +57,23 @@ export async function run(_positional, flags) {
56
57
  // 2. Additive template updates (missing recommended sections in AGENTS.md /
57
58
  // product.md, missing index.json fields). Reuses `templates update`
58
59
  // verbatim: preview without --write, apply with it.
59
- console.log(c.gray("──── 1/3 scaffold shape (templates update)"));
60
+ console.log(c.gray("──── 1/3 scaffold shape"));
60
61
  const tplFlags = writeMode ? new Map([["write", true]]) : new Map();
61
62
  const tplCode = await templates.run(["update"], tplFlags);
62
63
  if (tplCode !== 0 && !writeMode) pending += 1; // preview exits 1 when updates are pending
63
64
 
65
+ // What the additive update cannot fix — a drifted adapter, a broken
66
+ // playbook, an index schema it does not know — used to be visible only to
67
+ // `templates check`. Printed here, each with its own remedy, so the one
68
+ // command that brings a project up to date also says what is left for a
69
+ // person (change 0178: the check and the update became this step).
70
+ const manual = collectFindings(projectRoot).findings
71
+ .filter((f) => !/upgrade --write|index rebuild/.test(f.remedy ?? ""));
72
+ for (const f of manual) {
73
+ console.log(c.yellow("✗ ") + f.message);
74
+ console.log(c.gray(" fix: ") + (f.remedy ? c.cyan(f.remedy) : c.gray("manual repair — no command can fix this")));
75
+ }
76
+
64
77
  // 3. Index: rebuild from the tree, which also migrates the framework stamp.
65
78
  console.log("");
66
79
  console.log(c.gray("──── 2/3 index + framework stamp"));
@@ -101,14 +114,22 @@ export async function run(_positional, flags) {
101
114
  const vCode = await validate.run([], vFlags);
102
115
 
103
116
  console.log("");
117
+ if (manual.length > 0) {
118
+ console.log(c.yellow("manual") + ` ${manual.length} finding${manual.length === 1 ? "" : "s"} the upgrade cannot repair — see step 1`);
119
+ }
104
120
  if (!writeMode) {
105
- if (pending > 0) {
121
+ if (pending > 0 || manual.length > 0) {
122
+ if (pending === 0) return 1;
106
123
  console.log(c.yellow("pending") + ` upgrade steps found — apply them: ${c.cyan("doctrina upgrade --write")}`);
107
124
  return 1;
108
125
  }
109
126
  console.log(c.green("ok") + " nothing to upgrade — the project matches the installed CLI");
110
127
  return vCode;
111
128
  }
129
+ if (vCode === 0 && manual.length > 0) {
130
+ console.log(c.yellow(`upgraded to ${running} with manual repairs left`) + " — each finding in step 1 names its fix.");
131
+ return 1;
132
+ }
112
133
  if (vCode === 0) {
113
134
  console.log(c.green(`✓ project upgraded to ${running}`) + c.gray(" — review any warnings above; new commands: `doctrina --help`."));
114
135
  console.log(c.gray("AGENTS.md's doctrina:surface block was regenerated from the installed catalog, so agents reading the hub now discover the current commands."));
@@ -125,16 +146,20 @@ Bring an existing project up to the installed CLI after an npm update.
125
146
  The project keeps the scaffold of the version that init-ed it; this is
126
147
  the one command that closes the gap, orchestrating the existing pieces:
127
148
 
128
- 1. templates update — regenerate the AGENTS.md doctrina:surface block
149
+ 1. scaffold shape — regenerate the AGENTS.md doctrina:surface block
129
150
  from the installed command catalog (the block is
130
151
  CLI-owned, ADR 0015 — this is how agents reading
131
- the hub discover commands added since init), and
152
+ the hub discover commands added since init),
132
153
  append missing recommended sections / index.json
133
- fields (additive-only outside the block)
154
+ fields (additive-only outside the block), and
155
+ list what no command can repair (a drifted
156
+ adapter, a broken playbook), each with its fix
134
157
  2. index rebuild — regenerate index.json from the tree and migrate
135
158
  the framework_version stamp to the running CLI
136
159
  3. validate (--fix) — surface anything the upgrade cannot fix
137
160
  (hand-authored drift, new validate checks)
138
161
 
139
- Preview by default (exits 1 when steps are pending); --write applies.
162
+ Preview by default (exits 1 when steps are pending or a manual repair is
163
+ left); --write applies. The preview is what \`templates check\` reported
164
+ and --write what \`templates update --write\` did — both deprecated.
140
165
  `;
@@ -20,7 +20,7 @@ import { collectValidation } from "../lib/validation-model.js";
20
20
  // wrap it in the generic envelope.
21
21
  export const jsonNative = true;
22
22
 
23
- export const flags = { boolean: ["fix", "json", "runtime"], string: [] };
23
+ export const flags = { boolean: ["fix", "json", "runtime", "strict"], string: [] };
24
24
 
25
25
  export async function run(_positional, flags) {
26
26
  const projectRoot = process.cwd();
@@ -34,10 +34,26 @@ export async function run(_positional, flags) {
34
34
  runtime: flagBool(flags, "runtime", false),
35
35
  });
36
36
 
37
+ // --strict makes a warning count against the exit code, matching what
38
+ // `--strict` already means on `coverage` and `trace`: fail on any gap.
39
+ //
40
+ // The default has to stay lenient — a warning is advice, and advice that
41
+ // blocks a commit stops being read. But that leniency left a gate that
42
+ // COULD NOT REPROVE: the CI step that validates the shipped examples runs
43
+ // this command, both examples had drifted to warnings, and the step passed
44
+ // green through every run. The EARS defect the retrofit example exists to
45
+ // teach against was reintroduced and sat there, twice, because nothing
46
+ // downstream of `validate` could say no.
47
+ //
48
+ // So the choice belongs to the caller. A human at a terminal wants the
49
+ // advice; a gate wants a verdict.
50
+ const strict = flagBool(flags, "strict", false);
51
+ const failed = errors.length > 0 || (strict && warnings.length > 0);
52
+
37
53
  // Output
38
54
  if (flagBool(flags, "json", false)) {
39
- emitJson("validate", { errors, warnings });
40
- return errors.length === 0 ? 0 : 1;
55
+ emitJson("validate", { errors, warnings, strict });
56
+ return failed ? 1 : 0;
41
57
  }
42
58
  // The repairs print first and in the order they were made: --fix changes
43
59
  // the tree, and what it changed must be visible before what it then found.
@@ -50,17 +66,21 @@ export async function run(_positional, flags) {
50
66
  console.log(c.green("ok") + " all validation checks passed");
51
67
  } else {
52
68
  const summary = `${errors.length} error${errors.length === 1 ? "" : "s"}, ${warnings.length} warning${warnings.length === 1 ? "" : "s"}`;
53
- console.log((errors.length === 0 ? c.green("ok") : c.red("fail")) + " " + summary);
69
+ console.log((failed ? c.red("fail") : c.green("ok")) + " " + summary);
70
+ // Say WHY a run with no errors is failing, or the exit code reads as a bug.
71
+ if (strict && errors.length === 0 && warnings.length > 0) {
72
+ console.log(c.gray(" --strict: warnings count against the exit code"));
73
+ }
54
74
  }
55
- return errors.length === 0 ? 0 : 1;
75
+ return failed ? 1 : 0;
56
76
  }
57
77
 
58
78
  export const help = `
59
- Usage: doctrina validate [--fix] [--runtime] [--json]
79
+ Usage: doctrina validate [--fix] [--runtime] [--strict] [--json]
60
80
 
61
81
  Run schema, artifact-existence, and structural checks against the
62
82
  .doctrina/ tree in the current working directory. Exits 0 if no errors
63
- (warnings allowed), 1 otherwise.
83
+ (warnings allowed), 1 otherwise. With --strict, a warning fails it too.
64
84
 
65
85
  Two structural checks worth naming:
66
86
 
@@ -84,5 +104,9 @@ Flags:
84
104
  selectors checked against the workflows and code that are
85
105
  supposed to honour them (the same checks as \`contract check\`).
86
106
  Opt-in because it reads files outside .doctrina/.
87
- --json Emit { ok, errors, warnings } as JSON (stable shape for agents/CI).
107
+ --strict Treat warnings as failures, the way \`coverage --strict\` and
108
+ \`trace --strict\` already do. The default stays lenient because
109
+ a warning is advice; a GATE wants a verdict, so anything that
110
+ must be able to reprove passes this.
111
+ --json Emit { ok, errors, warnings, strict } as JSON (stable shape for agents/CI).
88
112
  `;
@@ -15,7 +15,7 @@ import { printPlaybookTemplate } from "../lib/playbook.js";
15
15
  import { changedFiles } from "../lib/git.js";
16
16
  import { slugFromPrompt, fold, CONFIDENT_MARGIN } from "../lib/lexicon.js";
17
17
  import { rankCapabilities, rankCapabilitiesByDiff } from "../lib/work-model.js";
18
- import { isChangeId } from "../lib/project.js";
18
+ import { isChangeId, refuseChangeRef } from "../lib/project.js";
19
19
  export { rankCapabilities, rankCapabilitiesByDiff } from "../lib/work-model.js";
20
20
 
21
21
  // `work` is the second half of the no-ceremony path (ADR 0005): a brief
@@ -29,7 +29,7 @@ export { rankCapabilities, rankCapabilitiesByDiff } from "../lib/work-model.js";
29
29
  // Flags this command accepts. Declared HERE, with the command, so
30
30
  // adding a command never requires editing the entrypoint — the gap that
31
31
  // let six flags ship undeclared and silently swallow a positional (C3).
32
- export const flags = { boolean: ["json", "chore", "force", "from-diff", "no-spec", "quiet"], string: ["capability", "id", "resume", "title"] };
32
+ export const flags = { boolean: ["json", "chore", "design", "force", "from-diff", "no-spec", "quiet"], string: ["capability", "id", "resume", "title"] };
33
33
 
34
34
  export async function run(positional, flags) {
35
35
  const prompt = positional.join(" ").trim();
@@ -387,6 +387,8 @@ function resumeChange(projectRoot, resumeId) {
387
387
  if (open.length) console.error(c.gray("open: ") + open.map((ch) => ch.id).join(", "));
388
388
  return 2;
389
389
  }
390
+ const refused = refuseChangeRef(resumeId);
391
+ if (refused !== null) return refused;
390
392
  const entry = open.find((ch) => ch.id === resumeId);
391
393
  if (!entry && !isDir(path.join(projectRoot, ".doctrina", "changes", resumeId))) {
392
394
  console.error(c.red("error:") + ` no open change "${resumeId}"`);
@@ -515,9 +517,10 @@ change id (NNNN-<slug>) from the prompt, opens the change folder,
515
517
  records the prompt under the proposal's "## Why", ranks existing specs
516
518
  by term overlap as a capability hint, flags a thin/under-specified prompt
517
519
  so you clarify before writing deltas (advisory), and prints the steps:
518
- context → spec delta → tasks → implement → analyze → apply → verify
519
- (verify + coverage) → archive → validate. No natural-language
520
- interpretation happens in the CLI.
520
+ context, spec delta, tasks, implement, then \`doctrina change check <id>\`
521
+ to preview the close and \`doctrina close <id>\` to finish, which runs the
522
+ whole closing sequence in one pass. No natural-language interpretation
523
+ happens in the CLI.
521
524
 
522
525
  The change's spec delta is scaffolded with **Operation:** prefilled
523
526
  whenever the CLI can name the capability: from --capability, or from the
@@ -542,6 +545,8 @@ Options:
542
545
  prompt needed) and print a code-first playbook (F8)
543
546
  --chore, --no-spec Open a spec-less chore change (infra/docs/build) with a
544
547
  playbook that skips the spec-delta steps (F9)
548
+ --design Also scaffold design.md, for a change with non-trivial
549
+ choices (opt-in, as with \`change new --design\`)
545
550
  --force Open a new change even when one is open, overwrite an
546
551
  existing change folder, and proceed past the lane hold
547
552
  below
package/src/index.js CHANGED
@@ -5,7 +5,7 @@ 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, deprecationFor } 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
11
  import { recordUsage, operationOf } from "./lib/usage.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";
@@ -54,15 +53,14 @@ const COMMANDS = {
54
53
  init, spec, change, decision, validate, hooks, analyze, clarify,
55
54
  templates, skill, index: indexCmd, next, metrics, context, search,
56
55
  intake, work, coverage, verify, contract, trace,
57
- status, close, review, watch, why, constitution,
56
+ status, close, review, watch, why,
58
57
  prime, handoff, show, doctor, report, completion,
59
58
  intent, upgrade, adapter, triage, ci,
60
59
  };
61
60
 
62
61
  const TOP_HELP = `
63
- Usage: doctrina <command> [options]
62
+ Usage: doctrina <command> [options] (\`doctrina <command> --help\` for its flags)
64
63
 
65
- Commands:
66
64
  ${surfaceHelp()}
67
65
 
68
66
  Global flags:
@@ -112,6 +110,17 @@ async function main(argv) {
112
110
 
113
111
  const commandName = positional[0];
114
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
+ }
115
124
  if (!command) {
116
125
  console.error(c.red("error:") + ` unknown command "${commandName}"`);
117
126
  const guess = suggest(commandName, Object.keys(COMMANDS));
@@ -265,5 +274,24 @@ main(process.argv.slice(2)).then((code) => {
265
274
  code,
266
275
  new Set(OPERATIONS.map((o) => o[0])),
267
276
  );
268
- 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;
269
297
  });
@@ -155,6 +155,34 @@ export function computeActions(projectRoot, gates = {}) {
155
155
  }));
156
156
  }
157
157
 
158
+ // Index drift comes right after the intake (change 0193). It used to be
159
+ // surfaced LAST as "silent rot" — but `validate` reports it as an error,
160
+ // the close's index-drift step blocks on it, the fix is one runnable
161
+ // command, and the actions below read the very index that drifted. Ranked
162
+ // fifth, under advisory nudges, it told the agent to polish product.md
163
+ // before running the one command that unblocked everything.
164
+ try {
165
+ const current = idx.load(projectRoot);
166
+ if (!indexesMatch(deriveIndex(projectRoot, current), current)) {
167
+ actions.push(action({
168
+ id: "index-drift",
169
+ command: "index rebuild",
170
+ gate: "index",
171
+ runnable: true,
172
+ why: "index.json has drifted from the tree",
173
+ }));
174
+ }
175
+ } catch {
176
+ actions.push(action({
177
+ id: "index-unreadable",
178
+ command: "index rebuild",
179
+ gate: "index",
180
+ runnable: true,
181
+ why: "index.json is missing or unreadable",
182
+ }));
183
+ }
184
+
185
+
158
186
  // Open changes drive the loop: finish what is started before opening more.
159
187
  const changesDir = path.join(projectRoot, ".doctrina", "changes");
160
188
  if (isDir(changesDir)) {
@@ -200,12 +228,12 @@ export function computeActions(projectRoot, gates = {}) {
200
228
  if (deltas.length > 0) {
201
229
  actions.push(action({
202
230
  id: "change-apply-ready",
203
- command: "analyze",
231
+ command: "change check",
204
232
  args: [id],
205
233
  gate: "structure",
206
234
  runnable: true,
207
235
  why: `tasks done, ${deltas.length} delta${deltas.length === 1 ? "" : "s"} ready`,
208
- text: `doctrina analyze ${id}, then doctrina change apply ${id} — ` +
236
+ text: `doctrina change check ${id}, then doctrina close ${id} — ` +
209
237
  `tasks done, ${deltas.length} delta${deltas.length === 1 ? "" : "s"} ready`,
210
238
  }));
211
239
  } else {
@@ -377,28 +405,6 @@ export function computeActions(projectRoot, gates = {}) {
377
405
  }));
378
406
  }
379
407
 
380
- // Index drift is silent rot; surface it last.
381
- try {
382
- const current = idx.load(projectRoot);
383
- if (!indexesMatch(deriveIndex(projectRoot, current), current)) {
384
- actions.push(action({
385
- id: "index-drift",
386
- command: "index rebuild",
387
- gate: "index",
388
- runnable: true,
389
- why: "index.json has drifted from the tree",
390
- }));
391
- }
392
- } catch {
393
- actions.push(action({
394
- id: "index-unreadable",
395
- command: "index rebuild",
396
- gate: "index",
397
- runnable: true,
398
- why: "index.json is missing or unreadable",
399
- }));
400
- }
401
-
402
408
  return actions;
403
409
  }
404
410
 
package/src/lib/args.js CHANGED
@@ -74,9 +74,23 @@ export function parseArgs(argv, opts = {}) {
74
74
  function isNextTokenFlag(token) {
75
75
  if (token === undefined) return true;
76
76
  if (typeof token !== "string") return true;
77
+ // `--budget -5` is a value, not a flag named "5": no flag name starts with
78
+ // a digit, and reading it as one answered "unknown flag --5, did you mean
79
+ // --h?" instead of letting the command say the number is out of range.
80
+ if (/^-\d/.test(token)) return false;
77
81
  return token.startsWith("-");
78
82
  }
79
83
 
84
+ // A count read off a flag: digits only, above zero, or null. `parseInt`
85
+ // reads "7x" as 7 and "1.5" as 1, so a typo became a different request that
86
+ // ran without a word.
87
+ export function parsePositiveInt(raw) {
88
+ const s = String(raw ?? "").trim();
89
+ if (!/^\d+$/.test(s)) return null;
90
+ const n = Number(s);
91
+ return Number.isSafeInteger(n) && n > 0 ? n : null;
92
+ }
93
+
80
94
  export function flagString(flags, name, fallback = undefined) {
81
95
  const v = flags.get(name);
82
96
  if (v === undefined || v === true) return fallback;
@@ -96,8 +96,10 @@ export function changeNew(args, flags) {
96
96
 
97
97
  console.log("");
98
98
  if (chore) {
99
+ // A chore is still a change: it closes through the one definition of
100
+ // done, not a hand-run apply and archive (change 0180).
99
101
  console.log(c.bold("Chore opened.") + " No spec deltas expected — implement, check the tasks, then " +
100
- c.cyan(`doctrina change apply ${id}`) + " and " + c.cyan(`doctrina change archive ${id}`) + ".");
102
+ c.cyan(`doctrina close ${id}`) + ".");
101
103
  } else {
102
104
  console.log(c.bold("Change opened.") + " Add spec deltas under " +
103
105
  c.cyan(`.doctrina/changes/${id}/specs/<capability>/delta.md`));