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.
- package/README.md +27 -72
- package/package.json +3 -3
- package/src/commands/adapter.js +3 -2
- package/src/commands/analyze.js +8 -0
- package/src/commands/change.js +28 -77
- package/src/commands/clarify.js +8 -1
- package/src/commands/close.js +65 -10
- package/src/commands/completion.js +8 -2
- package/src/commands/context.js +6 -4
- package/src/commands/decision.js +12 -0
- package/src/commands/doctor.js +1 -1
- package/src/commands/hooks.js +6 -1
- package/src/commands/index-rebuild.js +52 -2
- package/src/commands/init.js +19 -4
- package/src/commands/intake.js +55 -7
- package/src/commands/intent.js +14 -1
- package/src/commands/metrics.js +13 -5
- package/src/commands/next.js +1 -1
- package/src/commands/prime.js +4 -5
- package/src/commands/report.js +9 -20
- package/src/commands/skill.js +8 -15
- package/src/commands/spec.js +13 -7
- package/src/commands/status.js +39 -20
- package/src/commands/templates.js +4 -10
- package/src/commands/upgrade.js +31 -6
- package/src/commands/validate.js +32 -8
- package/src/commands/work.js +10 -5
- package/src/index.js +34 -6
- package/src/lib/actions.js +30 -24
- package/src/lib/args.js +14 -0
- package/src/lib/change-ops.js +3 -1
- package/src/lib/commands.js +88 -15
- package/src/lib/coverage-model.js +40 -1
- package/src/lib/doc-model.js +47 -6
- package/src/lib/docs-impact.js +74 -0
- package/src/lib/ears.js +24 -1
- package/src/lib/gates.js +53 -9
- package/src/lib/index-json.js +40 -5
- package/src/lib/intake-model.js +71 -2
- package/src/lib/lexicon.js +38 -1
- package/src/lib/metrics-model.js +30 -0
- package/src/lib/project.js +49 -1
- package/src/lib/scan.js +126 -14
- package/src/lib/snapshot.js +23 -1
- package/src/lib/spec-ops.js +24 -5
- package/src/lib/templates-model.js +35 -8
- package/src/lib/validation-model.js +116 -34
- package/src/lib/views.js +4 -1
- package/src/lib/work-model.js +25 -0
- package/templates/AGENTS.md.template +3 -4
- package/templates/adapters/claude/.claude/commands/doctrina-work.md.template +6 -3
- package/templates/adapters/cursor/.cursor/commands/doctrina-work.md.template +4 -3
- package/templates/change/proposal.md.template +15 -0
- package/templates/change/tasks.md.template +3 -8
- package/templates/contract.md.template +2 -2
- package/templates/hooks/pre-commit.sample +10 -0
- package/templates/playbooks/bootstrap.md.template +6 -2
- package/templates/playbooks/chore.md.template +9 -4
- package/templates/playbooks/work.md.template +10 -3
- package/src/commands/constitution.js +0 -68
package/src/commands/status.js
CHANGED
|
@@ -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 {
|
|
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 (!
|
|
46
|
+
if (!STATUS_VIEWS.includes(view)) {
|
|
43
47
|
console.error(c.red("error:") + ` unknown view "${view}"`);
|
|
44
|
-
const guess = suggest(view,
|
|
45
|
-
console.error(c.gray("hint: ") + (guess ? `did you mean --view ${guess}?` : `available: ${
|
|
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
|
-
|
|
57
|
-
if (view === "report") {
|
|
58
|
-
const sinceRaw = flagString(flags, "since")
|
|
59
|
-
const days =
|
|
60
|
-
if (
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
different numbers; \`prime
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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);
|
package/src/commands/upgrade.js
CHANGED
|
@@ -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
|
|
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.
|
|
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),
|
|
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
|
|
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
|
`;
|
package/src/commands/validate.js
CHANGED
|
@@ -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
|
|
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((
|
|
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
|
|
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
|
-
--
|
|
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
|
`;
|
package/src/commands/work.js
CHANGED
|
@@ -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
|
|
519
|
-
|
|
520
|
-
|
|
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,
|
|
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(
|
|
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
|
});
|
package/src/lib/actions.js
CHANGED
|
@@ -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: "
|
|
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
|
|
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;
|
package/src/lib/change-ops.js
CHANGED
|
@@ -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
|
|
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`));
|