doctrina-cli 0.13.0 → 0.15.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 +120 -17
- package/src/commands/change.js +100 -42
- package/src/commands/clarify.js +8 -1
- package/src/commands/close.js +57 -2
- package/src/commands/completion.js +6 -0
- package/src/commands/constitution.js +8 -1
- package/src/commands/context.js +391 -58
- package/src/commands/contract.js +76 -43
- package/src/commands/coverage.js +126 -12
- package/src/commands/decision.js +172 -40
- package/src/commands/doctor.js +92 -11
- 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 +139 -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 +38 -3
- 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 +201 -8
- package/src/commands/spec.js +13 -6
- package/src/commands/status.js +14 -2
- package/src/commands/templates.js +193 -59
- package/src/commands/trace.js +15 -3
- package/src/commands/triage.js +248 -0
- package/src/commands/upgrade.js +8 -1
- package/src/commands/validate.js +148 -5
- package/src/commands/verify.js +126 -5
- package/src/commands/watch.js +8 -1
- package/src/commands/why.js +10 -24
- package/src/commands/work.js +44 -3
- package/src/index.js +61 -6
- package/src/lib/adapters.js +140 -0
- package/src/lib/adr-guard.js +1 -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 +252 -27
- 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/pipeline.js +161 -0
- package/src/lib/prompt.js +18 -1
- package/src/lib/runtime.js +683 -0
- package/src/lib/scan.js +81 -33
- package/src/lib/spec-ops.js +94 -9
- 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/AGENTS.md.template +59 -53
- package/templates/contract.md.template +74 -3
- package/templates/spec.md.template +22 -0
package/src/commands/skill.js
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
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 { spawnSync } from "node:child_process";
|
|
5
6
|
import { exists, isDir, isFile, mkdirp, read, relPath, walk, write } from "../lib/fs-ops.js";
|
|
6
|
-
import { locateTemplatesDir, substitute } from "../lib/templates.js";
|
|
7
|
+
import { readTemplate, locateTemplatesDir, substitute } from "../lib/templates.js";
|
|
7
8
|
import * as idx from "../lib/index-json.js";
|
|
8
9
|
import { today } from "../lib/dates.js";
|
|
9
|
-
import { flagBool, flagString } from "../lib/args.js";
|
|
10
|
+
import { flagBool, flagString, flagGivenWithoutValue } from "../lib/args.js";
|
|
10
11
|
import { c } from "../lib/colors.js";
|
|
11
12
|
import { suggest } from "../lib/suggest.js";
|
|
13
|
+
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
12
14
|
|
|
13
15
|
const SUBCOMMANDS = ["new", "list", "sync", "suggest"];
|
|
14
16
|
|
|
17
|
+
// Flags this command accepts. Declared HERE, with the command, so
|
|
18
|
+
// adding a command never requires editing the entrypoint — the gap that
|
|
19
|
+
// let six flags ship undeclared and silently swallow a positional (C3).
|
|
20
|
+
export const flags = { boolean: ["json", "force", "write"], string: ["since", "from-error"] };
|
|
21
|
+
|
|
15
22
|
export async function run(positional, flags) {
|
|
16
23
|
const sub = positional[0];
|
|
17
24
|
if (!SUBCOMMANDS.includes(sub)) {
|
|
@@ -54,12 +61,187 @@ const FIX_SHAPED_SUBJECT =
|
|
|
54
61
|
const GIT_LOG_LIMIT = 200;
|
|
55
62
|
const MAX_SHOWN = 20;
|
|
56
63
|
|
|
64
|
+
// A skill drafted from the ERROR that taught the lesson (change 0029).
|
|
65
|
+
//
|
|
66
|
+
// `suggest` used to fire only after the fact — a skill was born when the
|
|
67
|
+
// agent remembered to write one, which is to say after the incident had
|
|
68
|
+
// already cost a session. The moment the lesson actually exists is the
|
|
69
|
+
// moment the error is on screen, and that text carries exactly what a
|
|
70
|
+
// trigger needs: the paths, the identifiers, the message itself.
|
|
71
|
+
//
|
|
72
|
+
// The draft is a SEED, not a skill. It fills the trigger — the field a
|
|
73
|
+
// human is least likely to write in a matchable form — and leaves the
|
|
74
|
+
// procedure to the person who just solved it.
|
|
75
|
+
const ERROR_STOP = new Set([
|
|
76
|
+
"the", "and", "for", "with", "that", "this", "from", "have", "has", "was",
|
|
77
|
+
"were", "not", "but", "you", "your", "are", "its", "it's", "can", "could",
|
|
78
|
+
"would", "should", "will", "when", "then", "than", "there", "their", "been",
|
|
79
|
+
"error", "warning", "failed", "failure", "exception", "traceback", "at",
|
|
80
|
+
"line", "file", "in", "on", "of", "to", "is", "a", "an", "no", "if",
|
|
81
|
+
// Stack-trace furniture and modal verbs: present in almost every error,
|
|
82
|
+
// distinctive in none, and they crowd out the words that identify one.
|
|
83
|
+
"most", "recent", "call", "calls", "last", "raise", "raised", "throw",
|
|
84
|
+
"thrown", "must", "one", "none", "self", "return", "returns", "def",
|
|
85
|
+
"module", "stack", "trace", "during", "handling", "above", "occurred",
|
|
86
|
+
"caused", "unexpected", "invalid", "expected", "got", "value", "values",
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
export function draftFromError(text) {
|
|
90
|
+
const raw = String(text ?? "").trim();
|
|
91
|
+
if (raw === "") return null;
|
|
92
|
+
|
|
93
|
+
// Paths and file references: the most matchable thing an error carries.
|
|
94
|
+
const paths = [...new Set(
|
|
95
|
+
(raw.match(/[\w.@-]+(?:[/\\][\w.@-]+)+(?:\.\w{1,6})?|[\w-]+\.(?:py|js|mjs|cjs|ts|tsx|yml|yaml|json|feature|go|rb|java|rs|sh|toml|ini|env)\b/g) ?? [])
|
|
96
|
+
.map((p) => p.replace(/[.,;:)\]]+$/, "")),
|
|
97
|
+
)].slice(0, 4);
|
|
98
|
+
|
|
99
|
+
// ALL_CAPS identifiers: env vars, error codes, constants.
|
|
100
|
+
const identifiers = [...new Set(raw.match(/\b[A-Z][A-Z0-9_]{2,}\b/g) ?? [])].slice(0, 4);
|
|
101
|
+
|
|
102
|
+
// Quoted fragments are usually the operative part of the message — but a
|
|
103
|
+
// traceback quotes its own filenames, and repeating a path already
|
|
104
|
+
// captured above buys nothing.
|
|
105
|
+
const quoted = [...new Set(
|
|
106
|
+
[...raw.matchAll(/["'`]([^"'`\n]{4,60})["'`]/g)].map((m) => m[1].trim()),
|
|
107
|
+
)].filter((q) => !paths.includes(q)).slice(0, 2);
|
|
108
|
+
|
|
109
|
+
// Distinctive words, most frequent first — the fallback when an error
|
|
110
|
+
// carries no structure at all.
|
|
111
|
+
const counts = new Map();
|
|
112
|
+
for (const w of raw.toLowerCase().match(/[a-z][a-z0-9_-]{3,}/g) ?? []) {
|
|
113
|
+
if (ERROR_STOP.has(w)) continue;
|
|
114
|
+
counts.set(w, (counts.get(w) ?? 0) + 1);
|
|
115
|
+
}
|
|
116
|
+
const keywords = [...counts.entries()]
|
|
117
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
118
|
+
.map(([w]) => w)
|
|
119
|
+
.slice(0, 6);
|
|
120
|
+
|
|
121
|
+
// The trigger, built to be MATCHED: concrete tokens first, prose second.
|
|
122
|
+
const anchors = [...quoted, ...identifiers, ...keywords.slice(0, 3)].slice(0, 4);
|
|
123
|
+
const wherePart = paths.length > 0 ? `, or work touches ${paths.slice(0, 2).map((p) => `\`${p}\``).join(" / ")}` : "";
|
|
124
|
+
const when = anchors.length > 0
|
|
125
|
+
? `The task hits ${anchors.map((a) => `"${a}"`).join(" / ")}${wherePart}.`
|
|
126
|
+
: `The task reproduces: ${firstLine(raw).slice(0, 90)}.`;
|
|
127
|
+
|
|
128
|
+
// The slug seeds from identifiers first, then keywords — deduped on the
|
|
129
|
+
// NORMALISED token, so AXE_SEVERITY and the "axe_severity" the same error
|
|
130
|
+
// also mentions do not both land in the name.
|
|
131
|
+
const seedTokens = [];
|
|
132
|
+
const seenToken = new Set();
|
|
133
|
+
for (const token of [...identifiers.map((i) => i.toLowerCase()), ...keywords]) {
|
|
134
|
+
const norm = token.replace(/[^a-z0-9]/g, "");
|
|
135
|
+
if (norm === "" || seenToken.has(norm)) continue;
|
|
136
|
+
seenToken.add(norm);
|
|
137
|
+
seedTokens.push(token);
|
|
138
|
+
if (seedTokens.length === 4) break;
|
|
139
|
+
}
|
|
140
|
+
const slug = skillSlug(seedTokens.join("-") || firstLine(raw)) || "captured-error";
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
slug,
|
|
144
|
+
when,
|
|
145
|
+
keywords,
|
|
146
|
+
paths,
|
|
147
|
+
identifiers,
|
|
148
|
+
excerpt: raw.split(/\r?\n/).slice(0, 12).join("\n"),
|
|
149
|
+
source: "error",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function firstLine(text) {
|
|
154
|
+
return String(text).split(/\r?\n/).find((l) => l.trim() !== "")?.trim() ?? "";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Preview or scaffold one error-seeded draft.
|
|
158
|
+
function writeErrorDraft(projectRoot, draft, doWrite) {
|
|
159
|
+
const skillsDir = path.join(projectRoot, ".doctrina", "skills");
|
|
160
|
+
const target = path.join(skillsDir, `${draft.slug}.md`);
|
|
161
|
+
|
|
162
|
+
if (!doWrite) {
|
|
163
|
+
console.log(c.bold("Skill draft") + c.gray(" — seeded from the error you just hit:"));
|
|
164
|
+
console.log("");
|
|
165
|
+
console.log(` ${c.cyan("slug")} ${draft.slug}`);
|
|
166
|
+
console.log(` ${c.cyan("when")} ${draft.when}`);
|
|
167
|
+
if (draft.paths.length > 0) console.log(` ${c.cyan("paths")} ${draft.paths.join(", ")}`);
|
|
168
|
+
console.log("");
|
|
169
|
+
if (exists(target)) {
|
|
170
|
+
console.log(c.yellow("note: ") + `${relPath(projectRoot, target)} already exists — writing would skip it`);
|
|
171
|
+
}
|
|
172
|
+
console.log(c.gray("Scaffold it: ") + c.cyan("doctrina skill suggest --from-error <text|file> --write"));
|
|
173
|
+
console.log(c.gray("The trigger is drafted; the procedure is yours to write while you still remember it."));
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (exists(target)) {
|
|
178
|
+
console.log(c.yellow("skip ") + ` ${draft.slug} (already exists)`);
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const tpl = read(path.join(locateTemplatesDir(), "skill.md.template"));
|
|
183
|
+
let body = substitute(tpl, { SKILL_NAME: draft.slug });
|
|
184
|
+
// Fill the trigger — the field a human is least likely to write in a form
|
|
185
|
+
// anything can match, and the one `context` ranks on.
|
|
186
|
+
body = body.replace(
|
|
187
|
+
/^when: .*$/m,
|
|
188
|
+
`when: ${draft.when.replace(/\r?\n/g, " ")}`,
|
|
189
|
+
);
|
|
190
|
+
body += `\n<!-- Seeded from an error. The failure to capture:\n\n${draft.excerpt}\n-->\n`;
|
|
191
|
+
|
|
192
|
+
mkdirp(skillsDir);
|
|
193
|
+
write(target, body, { force: false });
|
|
194
|
+
|
|
195
|
+
const index = idx.load(projectRoot);
|
|
196
|
+
const date = today();
|
|
197
|
+
idx.addSkill(index, {
|
|
198
|
+
id: draft.slug,
|
|
199
|
+
path: `.doctrina/skills/${draft.slug}.md`,
|
|
200
|
+
description: parseFrontmatter(body, "description") ?? "<edit me — one-sentence summary>",
|
|
201
|
+
last_updated: date,
|
|
202
|
+
});
|
|
203
|
+
idx.touch(index, date);
|
|
204
|
+
idx.save(projectRoot, index);
|
|
205
|
+
|
|
206
|
+
console.log(c.green("created") + ` ${relPath(projectRoot, target)}`);
|
|
207
|
+
console.log(c.gray("The trigger is drafted from the error. Write the procedure while you still remember it,"));
|
|
208
|
+
console.log(c.gray("then fill `description:` — `doctrina validate` warns on a trigger nothing can match."));
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
57
212
|
function skillSuggest(args, flags) {
|
|
58
213
|
const projectRoot = process.cwd();
|
|
59
214
|
ensureDoctrinaProject(projectRoot);
|
|
60
215
|
const doWrite = flagBool(flags, "write", false);
|
|
61
216
|
const since = flagString(flags, "since");
|
|
62
217
|
|
|
218
|
+
// --from-error: draft from the failure that is on screen RIGHT NOW,
|
|
219
|
+
// rather than waiting for the lesson to be remembered later. When given,
|
|
220
|
+
// it is the only source — this is a specific request, not a survey.
|
|
221
|
+
// A value-taking flag written without a value is a usage error, not a
|
|
222
|
+
// silent fall-through to the survey — `--from-error` with nothing after
|
|
223
|
+
// it would otherwise run the ordinary scan and look like it worked.
|
|
224
|
+
if (flagGivenWithoutValue(flags, "from-error")) {
|
|
225
|
+
console.error(c.red("error:") + " --from-error needs the error text, or a path to a file holding it");
|
|
226
|
+
return 2;
|
|
227
|
+
}
|
|
228
|
+
const fromError = flagString(flags, "from-error");
|
|
229
|
+
if (fromError !== undefined) {
|
|
230
|
+
if (fromError === "" || fromError === null) {
|
|
231
|
+
console.error(c.red("error:") + " --from-error needs the error text, or a path to a file holding it");
|
|
232
|
+
return 2;
|
|
233
|
+
}
|
|
234
|
+
// A path is read; anything else is taken as the text itself.
|
|
235
|
+
const asPath = path.resolve(projectRoot, fromError);
|
|
236
|
+
const text = isFile(asPath) ? read(asPath) : fromError;
|
|
237
|
+
const draft = draftFromError(text);
|
|
238
|
+
if (!draft) {
|
|
239
|
+
console.error(c.red("error:") + " --from-error was given nothing to draft from");
|
|
240
|
+
return 2;
|
|
241
|
+
}
|
|
242
|
+
return writeErrorDraft(projectRoot, draft, doWrite);
|
|
243
|
+
}
|
|
244
|
+
|
|
63
245
|
const skillsDir = path.join(projectRoot, ".doctrina", "skills");
|
|
64
246
|
const existingFiles = isDir(skillsDir) ? walk(skillsDir).filter((f) => f.endsWith(".md")) : [];
|
|
65
247
|
const existing = new Set(existingFiles.map((f) => path.basename(f, ".md")));
|
|
@@ -81,6 +263,7 @@ function skillSuggest(args, flags) {
|
|
|
81
263
|
|
|
82
264
|
// Two deterministic sources, deduped by slug. `seen` guards against a lesson
|
|
83
265
|
// appearing twice (e.g. a fix committed and later archived as a change).
|
|
266
|
+
/** @type {SkillCandidate[]} */
|
|
84
267
|
const candidates = [];
|
|
85
268
|
const seen = new Set();
|
|
86
269
|
|
|
@@ -93,6 +276,7 @@ function skillSuggest(args, flags) {
|
|
|
93
276
|
if (!FIX_SHAPED.test(id)) continue;
|
|
94
277
|
const slug = skillSlug(id);
|
|
95
278
|
if (seen.has(slug)) continue;
|
|
279
|
+
/** @type {SkillCandidate} */
|
|
96
280
|
const cand = { id, slug, source: "change", from: name };
|
|
97
281
|
if (captured(cand)) continue; // captured (any slug) or cited by an existing skill
|
|
98
282
|
const proposal = path.join(archiveDir, name, "proposal.md");
|
|
@@ -198,6 +382,7 @@ function gitFixCommits(projectRoot, { since, limit }) {
|
|
|
198
382
|
const r = spawnSync("git", args, { cwd: projectRoot, encoding: "utf8" });
|
|
199
383
|
if (r.error || r.status !== 0 || !r.stdout) return [];
|
|
200
384
|
|
|
385
|
+
/** @type {SkillCandidate[]} */
|
|
201
386
|
const out = [];
|
|
202
387
|
const seen = new Set();
|
|
203
388
|
for (const record of r.stdout.split("\x1e")) {
|
|
@@ -280,11 +465,11 @@ function skillNew(args, flags) {
|
|
|
280
465
|
return 1;
|
|
281
466
|
}
|
|
282
467
|
|
|
283
|
-
const
|
|
284
|
-
const
|
|
285
|
-
const body = substitute(read(tplPath), { SKILL_NAME: name });
|
|
468
|
+
const tpl = readTemplate(projectRoot, "skill.md.template");
|
|
469
|
+
const body = substitute(tpl.body, { SKILL_NAME: name });
|
|
286
470
|
write(targetPath, body, { force });
|
|
287
|
-
console.log(c.green("created") + ` ${relPath(projectRoot, targetPath)}`
|
|
471
|
+
console.log(c.green("created") + ` ${relPath(projectRoot, targetPath)}` +
|
|
472
|
+
(tpl.source === "project" ? c.gray(" (project template)") : ""));
|
|
288
473
|
|
|
289
474
|
const date = today();
|
|
290
475
|
const index = idx.load(projectRoot);
|
|
@@ -395,7 +580,7 @@ function parseFrontmatter(text, key) {
|
|
|
395
580
|
|
|
396
581
|
function ensureDoctrinaProject(projectRoot) {
|
|
397
582
|
if (!exists(path.join(projectRoot, ".doctrina"))) {
|
|
398
|
-
throw
|
|
583
|
+
throw notADoctrinaProject();
|
|
399
584
|
}
|
|
400
585
|
}
|
|
401
586
|
|
|
@@ -413,7 +598,7 @@ Subcommands:
|
|
|
413
598
|
sync Copy each skill's frontmatter description into
|
|
414
599
|
.doctrina/index.json so the index never drifts
|
|
415
600
|
from the frontmatter (single source of truth).
|
|
416
|
-
suggest [--write] [--since <ref>]
|
|
601
|
+
suggest [--write] [--since <ref>] [--from-error <text|file>]
|
|
417
602
|
Surface fix-shaped lessons not yet captured as skills,
|
|
418
603
|
from two sources: archived change proposals and the git
|
|
419
604
|
commit history (ADR 0013). --write scaffolds a stub per
|
|
@@ -421,6 +606,14 @@ Subcommands:
|
|
|
421
606
|
--since <ref> scans commits in <ref>..HEAD instead of the
|
|
422
607
|
last 200 (e.g. --since v0.7.0).
|
|
423
608
|
|
|
609
|
+
--from-error <text|file> drafts ONE skill from the failure
|
|
610
|
+
on screen right now, instead of waiting for the lesson to
|
|
611
|
+
be remembered later. It fills the \`when:\` trigger from the
|
|
612
|
+
error's own paths, identifiers and message — the field a
|
|
613
|
+
human is least likely to write in a form anything can
|
|
614
|
+
match, and the one \`context\` ranks on. The procedure stays
|
|
615
|
+
yours to write. Add --write to scaffold it.
|
|
616
|
+
|
|
424
617
|
Skills are written by humans, not generated. See docs/en/skills.md
|
|
425
618
|
for the design rationale and the distinction from specs / AGENTS.md /
|
|
426
619
|
the rejected memory/ folder.
|
package/src/commands/spec.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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, lineCount, read, relPath, write } from "../lib/fs-ops.js";
|
|
5
|
-
import { locateTemplatesDir, substitute } from "../lib/templates.js";
|
|
6
|
+
import { readTemplate, locateTemplatesDir, substitute } from "../lib/templates.js";
|
|
6
7
|
import { specHeader, deriveIndex } from "../lib/scan.js";
|
|
7
8
|
import { setHeader, bumpVersion, setCriterionMark } from "../lib/spec-ops.js";
|
|
8
9
|
import * as idx from "../lib/index-json.js";
|
|
@@ -10,9 +11,15 @@ import { today } from "../lib/dates.js";
|
|
|
10
11
|
import { flagBool, flagString } from "../lib/args.js";
|
|
11
12
|
import { c } from "../lib/colors.js";
|
|
12
13
|
import { suggest } from "../lib/suggest.js";
|
|
14
|
+
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
13
15
|
|
|
14
16
|
const SUBCOMMANDS = ["new", "list", "set"];
|
|
15
17
|
|
|
18
|
+
// Flags this command accepts. Declared HERE, with the command, so
|
|
19
|
+
// adding a command never requires editing the entrypoint — the gap that
|
|
20
|
+
// let six flags ship undeclared and silently swallow a positional (C3).
|
|
21
|
+
export const flags = { boolean: ["json", "bug", "force"], string: ["bump", "criterion", "implementation", "status", "version"] };
|
|
22
|
+
|
|
16
23
|
export async function run(positional, flags) {
|
|
17
24
|
const sub = positional[0];
|
|
18
25
|
if (!SUBCOMMANDS.includes(sub)) {
|
|
@@ -44,13 +51,13 @@ export async function run(positional, flags) {
|
|
|
44
51
|
return 1;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
|
-
const templatesDir = locateTemplatesDir();
|
|
48
54
|
const tplName = bug ? "spec-bug.md.template" : "spec.md.template";
|
|
49
|
-
const
|
|
55
|
+
const tpl = readTemplate(projectRoot, tplName);
|
|
50
56
|
const date = today();
|
|
51
|
-
const body = substitute(
|
|
57
|
+
const body = substitute(tpl.body, { CAPABILITY: capability, DATE: date });
|
|
52
58
|
write(targetPath, body, { force });
|
|
53
|
-
console.log(c.green("created") + ` ${relPath(projectRoot, targetPath)}${bug ? " (bug-spec shape)" : ""}`
|
|
59
|
+
console.log(c.green("created") + ` ${relPath(projectRoot, targetPath)}${bug ? " (bug-spec shape)" : ""}` +
|
|
60
|
+
(tpl.source === "project" ? c.gray(" (project template)") : ""));
|
|
54
61
|
|
|
55
62
|
const index = idx.load(projectRoot);
|
|
56
63
|
// Two axes: a fresh capability scaffold is a draft document for a
|
|
@@ -197,7 +204,7 @@ function specSet(args, flags) {
|
|
|
197
204
|
|
|
198
205
|
function ensureDoctrinaProject(projectRoot) {
|
|
199
206
|
if (!exists(path.join(projectRoot, ".doctrina"))) {
|
|
200
|
-
throw
|
|
207
|
+
throw notADoctrinaProject();
|
|
201
208
|
}
|
|
202
209
|
}
|
|
203
210
|
|
package/src/commands/status.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import process from "node:process";
|
|
3
4
|
import { readdirSync } from "node:fs";
|
|
@@ -9,6 +10,8 @@ import { summarize as coverageSummary } from "./coverage.js";
|
|
|
9
10
|
import { summarize as traceSummary } from "./trace.js";
|
|
10
11
|
import { flagBool } from "../lib/args.js";
|
|
11
12
|
import { c } from "../lib/colors.js";
|
|
13
|
+
import { emitJson } from "../lib/json-out.js";
|
|
14
|
+
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
12
15
|
|
|
13
16
|
// One-glance project health (review 2026-06-27 passive-user feature #1): a
|
|
14
17
|
// dashboard that answers "where do things stand?" in a single read, so neither
|
|
@@ -20,16 +23,25 @@ import { c } from "../lib/colors.js";
|
|
|
20
23
|
|
|
21
24
|
const CONFIG_REL = ".doctrina/verify.json";
|
|
22
25
|
|
|
26
|
+
// Flags this command accepts. Declared HERE, with the command, so
|
|
27
|
+
// adding a command never requires editing the entrypoint — the gap that
|
|
28
|
+
// let six flags ship undeclared and silently swallow a positional (C3).
|
|
29
|
+
// This command builds its own JSON payload; the entrypoint must not
|
|
30
|
+
// wrap it in the generic envelope.
|
|
31
|
+
export const jsonNative = true;
|
|
32
|
+
|
|
33
|
+
export const flags = { boolean: ["json"], string: [] };
|
|
34
|
+
|
|
23
35
|
export async function run(_positional, flags) {
|
|
24
36
|
const projectRoot = process.cwd();
|
|
25
37
|
if (!exists(path.join(projectRoot, ".doctrina"))) {
|
|
26
|
-
throw
|
|
38
|
+
throw notADoctrinaProject();
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
const s = collectStatus(projectRoot);
|
|
30
42
|
|
|
31
43
|
if (flagBool(flags, "json", false)) {
|
|
32
|
-
|
|
44
|
+
emitJson("status", s);
|
|
33
45
|
return 0;
|
|
34
46
|
}
|
|
35
47
|
|