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.
- package/README.md +27 -72
- package/package.json +3 -3
- package/src/commands/adapter.js +27 -3
- package/src/commands/analyze.js +16 -212
- package/src/commands/change.js +144 -267
- package/src/commands/ci.js +182 -0
- package/src/commands/clarify.js +65 -21
- package/src/commands/close.js +349 -45
- package/src/commands/completion.js +8 -2
- package/src/commands/context.js +284 -111
- package/src/commands/contract.js +95 -75
- package/src/commands/coverage.js +58 -273
- package/src/commands/decision.js +66 -7
- package/src/commands/doctor.js +293 -130
- package/src/commands/handoff.js +10 -57
- package/src/commands/hooks.js +17 -3
- package/src/commands/index-rebuild.js +55 -44
- package/src/commands/init.js +88 -16
- package/src/commands/intake.js +85 -99
- package/src/commands/intent.js +14 -1
- package/src/commands/metrics.js +65 -88
- package/src/commands/next.js +105 -147
- package/src/commands/prime.js +23 -91
- package/src/commands/report.js +40 -109
- package/src/commands/review.js +68 -36
- package/src/commands/search.js +18 -4
- package/src/commands/show.js +24 -9
- package/src/commands/skill.js +23 -39
- package/src/commands/spec.js +38 -11
- package/src/commands/status.js +69 -171
- package/src/commands/templates.js +35 -148
- package/src/commands/trace.js +36 -71
- package/src/commands/triage.js +4 -82
- package/src/commands/upgrade.js +31 -6
- package/src/commands/validate.js +44 -877
- package/src/commands/verify.js +60 -138
- package/src/commands/why.js +2 -2
- package/src/commands/work.js +243 -267
- package/src/index.js +139 -12
- package/src/lib/actions.js +490 -0
- package/src/lib/adapters.js +0 -11
- package/src/lib/adr-guard.js +19 -0
- package/src/lib/agent-changelog.js +133 -0
- package/src/lib/analysis.js +264 -0
- package/src/lib/args.js +14 -0
- package/src/lib/change-ops.js +133 -0
- package/src/lib/commands.js +135 -10
- package/src/lib/config.js +206 -0
- package/src/lib/constitution-model.js +102 -0
- package/src/lib/coverage-model.js +431 -0
- package/src/lib/criteria.js +23 -0
- package/src/lib/doc-model.js +370 -3
- package/src/lib/docs-impact.js +270 -27
- package/src/lib/ears.js +24 -1
- package/src/lib/flag-catalog.js +0 -16
- package/src/lib/fs-ops.js +58 -1
- package/src/lib/gates.js +231 -29
- package/src/lib/git.js +132 -0
- package/src/lib/index-json.js +42 -24
- package/src/lib/intake-model.js +155 -0
- package/src/lib/json-out.js +100 -2
- package/src/lib/ledger.js +192 -0
- package/src/lib/lexicon.js +228 -0
- package/src/lib/metrics-model.js +189 -0
- package/src/lib/names.js +42 -0
- package/src/lib/playbook.js +97 -0
- package/src/lib/project.js +105 -0
- package/src/lib/reproducibility.js +137 -0
- package/src/lib/runtime.js +191 -5
- package/src/lib/scan.js +328 -25
- package/src/lib/signoff.js +147 -0
- package/src/lib/snapshot.js +231 -0
- package/src/lib/spec-ops.js +72 -8
- package/src/lib/templates-model.js +289 -0
- package/src/lib/trace-model.js +95 -0
- package/src/lib/triage-model.js +121 -0
- package/src/lib/usage.js +6 -4
- package/src/lib/validation-model.js +1381 -0
- package/src/lib/version.js +22 -0
- package/src/lib/views.js +413 -0
- package/src/lib/work-model.js +102 -0
- package/templates/AGENTS.md.template +21 -28
- package/templates/README.md +35 -0
- 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 +16 -0
- package/templates/change/tasks.md.template +3 -8
- package/templates/contract.md.template +2 -2
- package/templates/hooks/pre-commit.sample +16 -2
- package/templates/playbooks/bootstrap.md.template +47 -0
- package/templates/playbooks/chore.md.template +24 -0
- package/templates/playbooks/work.md.template +83 -0
- 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,
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
});
|