doctrina-cli 0.12.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.
- package/README.md +1 -1
- package/package.json +3 -2
- package/src/commands/adapter.js +205 -0
- package/src/commands/analyze.js +76 -19
- package/src/commands/change.js +341 -44
- package/src/commands/clarify.js +40 -19
- package/src/commands/close.js +119 -11
- package/src/commands/completion.js +6 -0
- package/src/commands/constitution.js +8 -1
- package/src/commands/context.js +359 -53
- package/src/commands/contract.js +17 -24
- package/src/commands/coverage.js +16 -4
- package/src/commands/decision.js +172 -40
- package/src/commands/doctor.js +38 -7
- package/src/commands/handoff.js +8 -1
- package/src/commands/hooks.js +6 -0
- package/src/commands/index-rebuild.js +8 -1
- package/src/commands/init.js +152 -6
- package/src/commands/intake.js +11 -2
- package/src/commands/intent.js +8 -1
- package/src/commands/metrics.js +80 -7
- package/src/commands/next.js +14 -2
- package/src/commands/prime.js +8 -1
- package/src/commands/report.js +8 -1
- package/src/commands/review.js +8 -1
- package/src/commands/search.js +8 -1
- package/src/commands/show.js +8 -1
- package/src/commands/skill.js +16 -6
- package/src/commands/spec.js +13 -6
- package/src/commands/status.js +14 -2
- package/src/commands/templates.js +277 -40
- package/src/commands/trace.js +15 -3
- package/src/commands/upgrade.js +23 -12
- package/src/commands/validate.js +93 -3
- package/src/commands/verify.js +12 -2
- package/src/commands/watch.js +8 -1
- package/src/commands/why.js +10 -24
- package/src/commands/work.js +69 -7
- package/src/index.js +60 -6
- package/src/lib/adapters.js +140 -0
- package/src/lib/adr-guard.js +47 -0
- package/src/lib/args.js +10 -0
- package/src/lib/clarity.js +1 -0
- package/src/lib/colors.js +1 -0
- package/src/lib/commands.js +314 -3
- package/src/lib/criteria.js +1 -0
- package/src/lib/dates.js +1 -0
- package/src/lib/diff.js +1 -0
- package/src/lib/doc-model.js +287 -0
- package/src/lib/docs-impact.js +167 -0
- package/src/lib/ears.js +1 -0
- package/src/lib/exit-codes.js +103 -0
- package/src/lib/flag-catalog.js +106 -0
- package/src/lib/fs-ops.js +1 -0
- package/src/lib/gates.js +123 -0
- package/src/lib/git.js +97 -0
- package/src/lib/index-json.js +14 -6
- package/src/lib/json-out.js +83 -0
- package/src/lib/prompt.js +18 -1
- package/src/lib/scan.js +81 -33
- package/src/lib/spec-ops.js +228 -3
- package/src/lib/suggest.js +1 -0
- package/src/lib/templates.js +89 -0
- package/src/lib/usage.js +89 -0
- package/src/lib/version.js +1 -0
- package/templates/.gitkeep +0 -0
- package/templates/AGENTS.md.template +71 -75
- package/templates/change/spec-delta.md.template +13 -7
package/src/commands/close.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
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";
|
|
9
|
+
import { printAdrCheckpoint } from "../lib/adr-guard.js";
|
|
10
|
+
import { checkDocsImpact } from "../lib/docs-impact.js";
|
|
11
|
+
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
7
12
|
import * as analyze from "./analyze.js";
|
|
8
13
|
import * as change from "./change.js";
|
|
9
14
|
import * as verify from "./verify.js";
|
|
10
15
|
import * as coverage from "./coverage.js";
|
|
11
16
|
import * as trace from "./trace.js";
|
|
12
17
|
import * as validate from "./validate.js";
|
|
18
|
+
import * as skill from "./skill.js";
|
|
13
19
|
|
|
14
20
|
// One-command close (review 2026-06-27 passive-user feature #2). The work
|
|
15
21
|
// playbook lists the closing sequence — analyze → apply → verify → coverage →
|
|
@@ -19,19 +25,48 @@ import * as validate from "./validate.js";
|
|
|
19
25
|
// rerun, so the agent makes one call and the human approves once. It is a
|
|
20
26
|
// driver over the existing commands; it adds no new checks of its own.
|
|
21
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
|
+
|
|
22
33
|
export async function run(positional, flags) {
|
|
23
|
-
|
|
24
|
-
if (!id) {
|
|
34
|
+
if (positional.length === 0) {
|
|
25
35
|
console.error(c.red("error:") + " close requires a change <id> (e.g. doctrina close 0001-add-login)");
|
|
26
36
|
return 2;
|
|
27
37
|
}
|
|
28
38
|
const projectRoot = process.cwd();
|
|
29
39
|
if (!exists(path.join(projectRoot, ".doctrina"))) {
|
|
30
|
-
throw
|
|
40
|
+
throw notADoctrinaProject();
|
|
31
41
|
}
|
|
32
42
|
|
|
43
|
+
// Batch close (operator review 2026-07-19 §4.5): each id closes
|
|
44
|
+
// independently, worst exit code wins, so a 9-change implementation lot is
|
|
45
|
+
// one command, not nine.
|
|
46
|
+
let worst = 0;
|
|
47
|
+
for (const id of positional) {
|
|
48
|
+
if (positional.length > 1) {
|
|
49
|
+
console.log("");
|
|
50
|
+
console.log(c.bold(`════ close ${id}`));
|
|
51
|
+
}
|
|
52
|
+
worst = Math.max(worst, await closeOne(projectRoot, id, flags));
|
|
53
|
+
}
|
|
54
|
+
if (positional.length > 1) {
|
|
55
|
+
console.log("");
|
|
56
|
+
console.log(worst === 0
|
|
57
|
+
? c.green("ok") + ` all ${positional.length} changes closed`
|
|
58
|
+
: c.red("fail") + " at least one close stopped — see above");
|
|
59
|
+
}
|
|
60
|
+
return worst;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function closeOne(projectRoot, id, flags) {
|
|
33
64
|
const force = flagBool(flags, "force", false);
|
|
34
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;
|
|
35
70
|
|
|
36
71
|
// Scope the coverage gate to the capabilities THIS change touches (its
|
|
37
72
|
// deltas), so one deliberately deferred spec elsewhere in the tree cannot
|
|
@@ -39,6 +74,7 @@ export async function run(positional, flags) {
|
|
|
39
74
|
// item 4). A change with no deltas (chore / metadata-only) falls back to
|
|
40
75
|
// the whole-tree gate — there is no narrower honest scope for it.
|
|
41
76
|
const touched = touchedCapabilities(projectRoot, id);
|
|
77
|
+
/** @type {FlagMap} */
|
|
42
78
|
const coverageFlags = new Map([["strict", true]]);
|
|
43
79
|
let coverageRerun = "doctrina coverage --strict";
|
|
44
80
|
if (touched.length > 0) {
|
|
@@ -52,6 +88,20 @@ export async function run(positional, flags) {
|
|
|
52
88
|
const verifyConfigured = isFile(path.join(projectRoot, ".doctrina", "verify.json"));
|
|
53
89
|
const steps = [
|
|
54
90
|
{ label: "analyze", rerun: `doctrina analyze ${id}`, run: () => analyze.run([id], new Map()) },
|
|
91
|
+
// ADR checkpoint (operator review §4.6): the playbook's "record an ADR"
|
|
92
|
+
// step was skippable in silence. Advisory — it names the accepted ADRs
|
|
93
|
+
// whose text cites the touched capabilities and the amend commands, but
|
|
94
|
+
// an ADR merely mentioning a capability is normal, so it never blocks.
|
|
95
|
+
{
|
|
96
|
+
label: "ADR checkpoint (advisory)",
|
|
97
|
+
rerun: "doctrina decision list",
|
|
98
|
+
run: async () => {
|
|
99
|
+
if (printAdrCheckpoint(projectRoot, touched, { c }) === 0) {
|
|
100
|
+
console.log(c.green("ok") + " no accepted ADR cites the touched capabilities");
|
|
101
|
+
}
|
|
102
|
+
return 0;
|
|
103
|
+
},
|
|
104
|
+
},
|
|
55
105
|
{ label: "apply", rerun: `doctrina change apply ${id}`, run: () => change.run(["apply", id], new Map()) },
|
|
56
106
|
verifyConfigured
|
|
57
107
|
? { label: "verify", rerun: "doctrina verify", run: () => verify.run([], new Map()) }
|
|
@@ -64,11 +114,35 @@ export async function run(positional, flags) {
|
|
|
64
114
|
// trace is advisory (provenance is a warning, not a hard gate): report it,
|
|
65
115
|
// never let it block the close.
|
|
66
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
|
+
},
|
|
67
141
|
{ label: "archive", rerun: `doctrina change archive ${id}${force ? " --force" : ""}`, run: () => change.run(["archive", id], archiveFlags) },
|
|
68
142
|
{ label: "validate", rerun: "doctrina validate", run: () => validate.run([], new Map()) },
|
|
69
143
|
];
|
|
70
144
|
|
|
71
|
-
console.log(c.bold(`Closing change ${id}`) + c.gray(" — analyze → 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"));
|
|
72
146
|
|
|
73
147
|
for (let i = 0; i < steps.length; i++) {
|
|
74
148
|
const step = steps[i];
|
|
@@ -86,6 +160,13 @@ export async function run(positional, flags) {
|
|
|
86
160
|
console.error(c.red("error:") + ` ${err.message}`);
|
|
87
161
|
}
|
|
88
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
|
+
}
|
|
89
170
|
console.log("");
|
|
90
171
|
console.log(c.red(`✗ close stopped at "${step.label}"`) + c.gray(` (step ${i + 1}/${steps.length})`));
|
|
91
172
|
console.log(c.gray("Fix it, then rerun the step or the whole close:"));
|
|
@@ -95,6 +176,26 @@ export async function run(positional, flags) {
|
|
|
95
176
|
}
|
|
96
177
|
}
|
|
97
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
|
+
|
|
189
|
+
// Capture the lesson while it is fresh (operator review §4.7): surface the
|
|
190
|
+
// fix-shaped lessons not yet written as skills, right where the operator
|
|
191
|
+
// just proved they finish what they start. Advisory — a listing, never a
|
|
192
|
+
// gate, and a suggest failure never un-closes the change.
|
|
193
|
+
console.log("");
|
|
194
|
+
console.log(c.gray("──── skills (advisory)"));
|
|
195
|
+
try {
|
|
196
|
+
await skill.run(["suggest"], new Map());
|
|
197
|
+
} catch { /* advisory only */ }
|
|
198
|
+
|
|
98
199
|
console.log("");
|
|
99
200
|
console.log(c.green(`✓ change ${id} closed`) + c.gray(" — verified, archived, and validated."));
|
|
100
201
|
console.log(c.gray("Next: ") + c.cyan("doctrina next"));
|
|
@@ -115,21 +216,28 @@ function touchedCapabilities(projectRoot, id) {
|
|
|
115
216
|
}
|
|
116
217
|
|
|
117
218
|
export const help = `
|
|
118
|
-
Usage: doctrina close <id
|
|
219
|
+
Usage: doctrina close <id...> [--force]
|
|
119
220
|
|
|
120
221
|
Run the whole closing sequence for a change in one pass, stopping at the
|
|
121
222
|
first failure with the exact command to rerun:
|
|
122
223
|
|
|
123
|
-
analyze → change apply → verify → coverage
|
|
124
|
-
change's touched capabilities) → trace → change
|
|
224
|
+
analyze → ADR checkpoint (advisory) → change apply → verify → coverage
|
|
225
|
+
--strict (scoped to the change's touched capabilities) → trace → change
|
|
226
|
+
archive → validate → skill suggest (advisory)
|
|
125
227
|
|
|
126
228
|
The coverage gate is scoped to the capabilities the change's deltas touch,
|
|
127
229
|
so a deliberately deferred spec elsewhere cannot block an unrelated close;
|
|
128
230
|
a change with no deltas gates on the whole tree. verify is skipped (with a
|
|
129
|
-
note) when no .doctrina/verify.json is declared; trace
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
231
|
+
note) when no .doctrina/verify.json is declared; trace, the ADR checkpoint
|
|
232
|
+
(accepted ADRs citing the touched capabilities — amend via \`decision
|
|
233
|
+
supersede\`, not silence), and the closing skill-suggest listing are
|
|
234
|
+
advisory (reports, never blockers). This is a driver over the existing
|
|
235
|
+
commands so the agent makes one call instead of nine and the human
|
|
236
|
+
approves once.
|
|
237
|
+
|
|
238
|
+
Multiple ids close in sequence, each independently; the exit code is the
|
|
239
|
+
worst per-id result. Preview what close would refuse: \`doctrina change
|
|
240
|
+
check <id>\`.
|
|
133
241
|
|
|
134
242
|
Options:
|
|
135
243
|
--force Pass through to \`change archive\` (archive even if verification
|
|
@@ -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
|
|
29
|
+
throw notADoctrinaProject();
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
const project = projectName(projectRoot);
|