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/commands/status.js
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import * as idx from "../lib/index-json.js";
|
|
7
|
-
import { deriveIndex, indexesMatch, specHeader, listHeader } from "../lib/scan.js";
|
|
8
|
-
import { cliVersion } from "../lib/version.js";
|
|
9
|
-
import { summarize as coverageSummary } from "./coverage.js";
|
|
10
|
-
import { summarize as traceSummary } from "./trace.js";
|
|
11
|
-
import { flagBool } from "../lib/args.js";
|
|
4
|
+
import { exists } from "../lib/fs-ops.js";
|
|
5
|
+
import { flagBool, flagString, parsePositiveInt } from "../lib/args.js";
|
|
12
6
|
import { c } from "../lib/colors.js";
|
|
13
7
|
import { emitJson } from "../lib/json-out.js";
|
|
14
8
|
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
9
|
+
import { collectSnapshot, collectStatus, collectReportWindow } from "../lib/snapshot.js";
|
|
10
|
+
import { renderView, VIEWS } from "../lib/views.js";
|
|
11
|
+
import { draftAgentChangelog, renderDraft } from "../lib/agent-changelog.js";
|
|
12
|
+
import { cliVersion } from "../lib/version.js";
|
|
13
|
+
import { suggest } from "../lib/suggest.js";
|
|
15
14
|
|
|
16
15
|
// One-glance project health (review 2026-06-27 passive-user feature #1): a
|
|
17
16
|
// dashboard that answers "where do things stand?" in a single read, so neither
|
|
18
17
|
// the human nor the agent has to run validate + coverage + trace + next
|
|
19
|
-
// separately. Strictly read-only; never mutates and always exits 0.
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
// separately. Strictly read-only; never mutates and always exits 0.
|
|
19
|
+
//
|
|
20
|
+
// Since change 0037 this is also the HOME of the other read-only views:
|
|
21
|
+
// `--view prime|handoff|report` renders the same snapshot in a different
|
|
22
|
+
// shape. `prime`, `handoff` and `report` remain as their own commands (they
|
|
23
|
+
// are what AGENTS.md tells an agent to run) and delegate straight here, so
|
|
24
|
+
// there is one collector, one set of formatters, and no way for the four to
|
|
25
|
+
// report different numbers (audit finding F7).
|
|
25
26
|
|
|
26
27
|
// Flags this command accepts. Declared HERE, with the command, so
|
|
27
28
|
// adding a command never requires editing the entrypoint — the gap that
|
|
@@ -30,7 +31,10 @@ const CONFIG_REL = ".doctrina/verify.json";
|
|
|
30
31
|
// wrap it in the generic envelope.
|
|
31
32
|
export const jsonNative = true;
|
|
32
33
|
|
|
33
|
-
export const flags = { boolean: ["json"], string: [] };
|
|
34
|
+
export const flags = { boolean: ["json"], string: ["view", "since"] };
|
|
35
|
+
|
|
36
|
+
// The snapshot's views, plus the one draft that is not a view of it.
|
|
37
|
+
const STATUS_VIEWS = [...VIEWS, "agent-changelog"];
|
|
34
38
|
|
|
35
39
|
export async function run(_positional, flags) {
|
|
36
40
|
const projectRoot = process.cwd();
|
|
@@ -38,173 +42,48 @@ export async function run(_positional, flags) {
|
|
|
38
42
|
throw notADoctrinaProject();
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
const
|
|
45
|
+
const view = flagString(flags, "view") ?? "dashboard";
|
|
46
|
+
if (!STATUS_VIEWS.includes(view)) {
|
|
47
|
+
console.error(c.red("error:") + ` unknown view "${view}"`);
|
|
48
|
+
const guess = suggest(view, STATUS_VIEWS);
|
|
49
|
+
console.error(c.gray("hint: ") + (guess ? `did you mean --view ${guess}?` : `available: ${STATUS_VIEWS.join(", ")}`));
|
|
50
|
+
return 2;
|
|
51
|
+
}
|
|
42
52
|
|
|
53
|
+
// --json always answers with the status shape, whatever the view: the
|
|
54
|
+
// envelope is a machine contract and must not change with a formatting flag.
|
|
43
55
|
if (flagBool(flags, "json", false)) {
|
|
44
|
-
emitJson("status",
|
|
56
|
+
emitJson("status", collectStatus(projectRoot));
|
|
45
57
|
return 0;
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
driftLine = c.red("missing/unreadable") + c.gray(" — run `doctrina index rebuild`");
|
|
56
|
-
} else if (s.indexState === "in-sync") {
|
|
57
|
-
driftLine = c.green("in sync");
|
|
58
|
-
} else {
|
|
59
|
-
driftLine = c.yellow("drifted") + c.gray(" — run `doctrina validate --fix`");
|
|
60
|
-
}
|
|
61
|
-
console.log(` ${"index".padEnd(11)} ${driftLine}`);
|
|
62
|
-
|
|
63
|
-
const stampLine = s.stamp === s.cli
|
|
64
|
-
? c.green("current")
|
|
65
|
-
: c.yellow(`${s.stamp ?? "—"} (CLI ${s.cli})`) + c.gray(" — `doctrina index rebuild`");
|
|
66
|
-
console.log(` ${"stamp".padEnd(11)} ${stampLine}`);
|
|
67
|
-
|
|
68
|
-
const cov = s.coverage;
|
|
69
|
-
const covExtra = cov.totalDangling + cov.totalConditional > 0
|
|
70
|
-
? c.yellow(` ${cov.totalDangling} dangling, ${cov.totalConditional} conditional`)
|
|
71
|
-
: "";
|
|
72
|
-
const covColor = cov.pct === 100 ? c.green : cov.pct >= 50 ? c.yellow : c.red;
|
|
73
|
-
console.log(` ${"coverage".padEnd(11)} ${covColor(`${cov.pct}%`)} ${c.gray(`(${cov.totalCovered}/${cov.totalCriteria} criteria)`)}${covExtra}`);
|
|
74
|
-
|
|
75
|
-
const tr = s.trace;
|
|
76
|
-
const trExtra = tr.untraceable + tr.dropped + tr.dangling > 0
|
|
77
|
-
? c.yellow(` ${tr.dropped} dropped, ${tr.untraceable} untraceable`)
|
|
78
|
-
: "";
|
|
79
|
-
const trColor = tr.anchors === 0 ? c.gray : (tr.realized === tr.anchors && tr.untraceable === 0 ? c.green : c.yellow);
|
|
80
|
-
const trText = tr.anchors === 0 ? "no anchors" : `${tr.realized}/${tr.anchors} anchors`;
|
|
81
|
-
console.log(` ${"trace".padEnd(11)} ${trColor(trText)}${trExtra}`);
|
|
82
|
-
|
|
83
|
-
let verifyLine = c.gray("not configured") + c.gray(" — `doctrina verify --init`");
|
|
84
|
-
if (s.verify.invalid) {
|
|
85
|
-
verifyLine = c.red("invalid JSON");
|
|
86
|
-
} else if (s.verify.configured) {
|
|
87
|
-
const n = s.verify.checks;
|
|
88
|
-
verifyLine = c.cyan(`${n} check${n === 1 ? "" : "s"}`) + c.gray(" — run `doctrina verify`");
|
|
89
|
-
}
|
|
90
|
-
console.log(` ${"verify".padEnd(11)} ${verifyLine}`);
|
|
91
|
-
|
|
92
|
-
// --- Work ---
|
|
93
|
-
console.log("");
|
|
94
|
-
console.log(c.bold(" Work"));
|
|
95
|
-
const implBreak = Object.entries(s.specs.impl).map(([k, v]) => `${v} ${k}`).join(", ");
|
|
96
|
-
console.log(` ${"specs".padEnd(11)} ${s.specs.total}${implBreak ? c.gray(` (${implBreak})`) : ""}`);
|
|
97
|
-
console.log(` ${"changes".padEnd(11)} ${s.specs.openChanges} open`);
|
|
98
|
-
const adrNotes = [];
|
|
99
|
-
if (s.decisions.proposed > 0) adrNotes.push(c.yellow(`${s.decisions.proposed} proposed`));
|
|
100
|
-
if (s.decisions.bare > 0) adrNotes.push(c.yellow(`${s.decisions.bare} unproven`));
|
|
101
|
-
console.log(` ${"decisions".padEnd(11)} ${s.decisions.total}${adrNotes.length ? c.gray(" (") + adrNotes.join(c.gray(", ")) + c.gray(")") : ""}`);
|
|
102
|
-
console.log(` ${"skills".padEnd(11)} ${s.skills}`);
|
|
103
|
-
|
|
104
|
-
console.log("");
|
|
105
|
-
console.log(c.gray(" Full gates: `doctrina validate` · `doctrina verify`. Next step: `doctrina next`."));
|
|
106
|
-
return 0;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Pure, render-free status snapshot — the data behind the dashboard, shared
|
|
110
|
-
// with `prime`, `handoff`, `doctor`, and the `--json` output so every consumer
|
|
111
|
-
// reads the same numbers.
|
|
112
|
-
export function collectStatus(projectRoot) {
|
|
113
|
-
let index = null;
|
|
114
|
-
try {
|
|
115
|
-
index = idx.load(projectRoot);
|
|
116
|
-
} catch {
|
|
117
|
-
index = null;
|
|
118
|
-
}
|
|
119
|
-
let indexState = "missing";
|
|
120
|
-
if (index) {
|
|
121
|
-
indexState = indexesMatch(deriveIndex(projectRoot, index), index) ? "in-sync" : "drifted";
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const verifyCfg = path.join(projectRoot, CONFIG_REL);
|
|
125
|
-
let verify = { configured: false, checks: 0, invalid: false };
|
|
126
|
-
if (isFile(verifyCfg)) {
|
|
127
|
-
try {
|
|
128
|
-
const cfg = JSON.parse(read(verifyCfg));
|
|
129
|
-
verify = { configured: true, checks: Array.isArray(cfg?.checks) ? cfg.checks.length : 0, invalid: false };
|
|
130
|
-
} catch {
|
|
131
|
-
verify = { configured: true, checks: 0, invalid: true };
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
project: index?.project ?? path.basename(projectRoot),
|
|
137
|
-
stamp: index?.framework_version ?? null,
|
|
138
|
-
cli: cliVersion(),
|
|
139
|
-
indexState,
|
|
140
|
-
coverage: coverageSummary(projectRoot),
|
|
141
|
-
trace: traceSummary(projectRoot),
|
|
142
|
-
verify,
|
|
143
|
-
specs: countSpecs(projectRoot),
|
|
144
|
-
decisions: countDecisions(projectRoot),
|
|
145
|
-
skills: countSkills(projectRoot),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function countSpecs(projectRoot) {
|
|
150
|
-
const specsDir = path.join(projectRoot, ".doctrina", "specs");
|
|
151
|
-
const impl = {};
|
|
152
|
-
let total = 0;
|
|
153
|
-
if (isDir(specsDir)) {
|
|
154
|
-
for (const cap of readdirSync(specsDir).sort()) {
|
|
155
|
-
const p = path.join(specsDir, cap, "spec.md");
|
|
156
|
-
if (!isFile(p)) continue;
|
|
157
|
-
total += 1;
|
|
158
|
-
const implRaw = specHeader(read(p), "Implementation");
|
|
159
|
-
if (implRaw) {
|
|
160
|
-
const word = implRaw.trim().split(/[\s—-]+/)[0].toLowerCase();
|
|
161
|
-
impl[word] = (impl[word] ?? 0) + 1;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
const changesDir = path.join(projectRoot, ".doctrina", "changes");
|
|
166
|
-
let openChanges = 0;
|
|
167
|
-
if (isDir(changesDir)) {
|
|
168
|
-
for (const e of readdirSync(changesDir)) {
|
|
169
|
-
if (e === "archive" || e.startsWith(".")) continue;
|
|
170
|
-
if (isDir(path.join(changesDir, e))) openChanges += 1;
|
|
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) {
|
|
65
|
+
console.error(c.red("error:") + ` --since expects a positive day count, got "${sinceRaw}"`);
|
|
66
|
+
return 2;
|
|
171
67
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const t = (v ?? "").trim();
|
|
181
|
-
return t === "" || t === "—" || t === "-";
|
|
182
|
-
};
|
|
183
|
-
if (isDir(adrDir)) {
|
|
184
|
-
for (const f of walk(adrDir)) {
|
|
185
|
-
if (!/^\d{4}-.+\.md$/.test(path.basename(f))) continue;
|
|
186
|
-
total += 1;
|
|
187
|
-
const text = read(f);
|
|
188
|
-
const status = (listHeader(text, "Status") ?? "").toLowerCase();
|
|
189
|
-
if (status === "proposed") proposed += 1;
|
|
190
|
-
if (status === "accepted") {
|
|
191
|
-
const evidence = listHeader(text, "Evidence");
|
|
192
|
-
const landed = listHeader(text, "Landed");
|
|
193
|
-
if (evidence !== null && isBare(evidence) && isBare(landed)) bare += 1;
|
|
194
|
-
}
|
|
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;
|
|
195
76
|
}
|
|
77
|
+
options = collectReportWindow(projectRoot, days);
|
|
196
78
|
}
|
|
197
|
-
return { total, proposed, bare };
|
|
198
|
-
}
|
|
199
79
|
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
return walk(skillsDir).filter((f) => f.endsWith(".md")).length;
|
|
80
|
+
const snapshot = collectSnapshot(projectRoot);
|
|
81
|
+
for (const line of renderView(view, snapshot, options)) console.log(line);
|
|
82
|
+
return 0;
|
|
204
83
|
}
|
|
205
84
|
|
|
206
85
|
export const help = `
|
|
207
|
-
Usage: doctrina status [--json]
|
|
86
|
+
Usage: doctrina status [--view <name>] [--since <days>] [--json]
|
|
208
87
|
|
|
209
88
|
Print a one-glance health dashboard for the .doctrina/ project: the gate
|
|
210
89
|
signals (index drift, framework stamp, coverage %, trace anchors, whether
|
|
@@ -212,7 +91,26 @@ verify is configured) and the artifact counts (specs by implementation
|
|
|
212
91
|
state, open changes, decisions, skills). Read-only; always exits 0.
|
|
213
92
|
|
|
214
93
|
Flags:
|
|
215
|
-
--
|
|
94
|
+
--view <name> Render a different shape of the same snapshot:
|
|
95
|
+
dashboard the default one-glance health board
|
|
96
|
+
prime the session primer (\`doctrina prime\`)
|
|
97
|
+
handoff the Markdown resume note (\`doctrina handoff\`)
|
|
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).
|
|
109
|
+
--json Emit the snapshot as JSON (stable shape for agents and CI).
|
|
110
|
+
|
|
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\`.
|
|
216
114
|
|
|
217
115
|
It is a fast summary, not the authoritative gate: run \`doctrina validate\`
|
|
218
116
|
and \`doctrina verify\` for the full structural and build checks, and
|
|
@@ -13,28 +13,16 @@ import { cliVersion } from "../lib/version.js";
|
|
|
13
13
|
import { ARTIFACT_CATEGORIES } from "../lib/index-json.js";
|
|
14
14
|
import { flagBool } from "../lib/args.js";
|
|
15
15
|
import { c } from "../lib/colors.js";
|
|
16
|
+
import { collectFindings, agentsSectionCost, AGENTS_SECTIONS, PRODUCT_SECTIONS, normalizeBlock, hasHeading } from "../lib/templates-model.js";
|
|
17
|
+
|
|
18
|
+
// The findings are collected in lib/templates-model.js, which `doctor` reads
|
|
19
|
+
// too (audit finding F7); this command renders and fixes them.
|
|
20
|
+
export { collectFindings } from "../lib/templates-model.js";
|
|
16
21
|
import { suggest } from "../lib/suggest.js";
|
|
22
|
+
import { ensureDoctrinaProject } from "../lib/project.js";
|
|
17
23
|
|
|
18
24
|
const SUBCOMMANDS = ["list", "check", "update"];
|
|
19
25
|
|
|
20
|
-
// Recommended sections per file kind. Adopters whose files lack these
|
|
21
|
-
// headings get a warning from `templates check`; they are recommendations,
|
|
22
|
-
// not hard requirements (validate handles the hard requirements).
|
|
23
|
-
const AGENTS_SECTIONS = [
|
|
24
|
-
"## Stack",
|
|
25
|
-
"## Commands",
|
|
26
|
-
"## Repository structure",
|
|
27
|
-
"## Conventions and boundaries",
|
|
28
|
-
"## How to read context",
|
|
29
|
-
];
|
|
30
|
-
const PRODUCT_SECTIONS = [
|
|
31
|
-
"## Vision",
|
|
32
|
-
"## Problem",
|
|
33
|
-
"## Target users",
|
|
34
|
-
"## Scope",
|
|
35
|
-
"## Success criteria",
|
|
36
|
-
];
|
|
37
|
-
const INDEX_FIELDS = ["$schema_version", "project", "artifacts"];
|
|
38
26
|
|
|
39
27
|
// Flags this command accepts. Declared HERE, with the command, so
|
|
40
28
|
// adding a command never requires editing the entrypoint — the gap that
|
|
@@ -63,12 +51,12 @@ export async function run(positional, flags) {
|
|
|
63
51
|
function updateTemplates(flags) {
|
|
64
52
|
const writeMode = flagBool(flags, "write", false);
|
|
65
53
|
const projectRoot = process.cwd();
|
|
66
|
-
|
|
67
|
-
console.error(c.red("error:") + " not a Doctrina project (no .doctrina/ in cwd)");
|
|
68
|
-
return 1;
|
|
69
|
-
}
|
|
54
|
+
ensureDoctrinaProject(projectRoot);
|
|
70
55
|
|
|
71
56
|
const plan = [];
|
|
57
|
+
// Updates this run declines to make, each with the reason and the number.
|
|
58
|
+
// Standing down is a RESULT, not a silence (change 0078).
|
|
59
|
+
const held = [];
|
|
72
60
|
|
|
73
61
|
// AGENTS.md command-surface block: the one CLI-OWNED span of the file
|
|
74
62
|
// (marker-delimited, generated from the command catalog). Three cases:
|
|
@@ -165,6 +153,20 @@ function updateTemplates(flags) {
|
|
|
165
153
|
if (!isFile(filePath)) continue;
|
|
166
154
|
const text = read(filePath);
|
|
167
155
|
const missing = sections.filter((h) => !hasHeading(text, h));
|
|
156
|
+
// AGENTS.md has a declared line ceiling, and `analyze` refuses a change
|
|
157
|
+
// that resolves an overflow by raising it — so appending stubs that do
|
|
158
|
+
// not fit would trade one gate's recommendation for another's refusal,
|
|
159
|
+
// silently. The plan says what it would cost and stands down instead
|
|
160
|
+
// (change 0078); the check's remedy already names the cut to make first.
|
|
161
|
+
if (missing.length > 0 && rel === "AGENTS.md") {
|
|
162
|
+
const budget = agentsSectionCost(projectRoot, missing);
|
|
163
|
+
if (!budget.fits) {
|
|
164
|
+
held.push(`AGENTS.md: ${missing.length} stub section(s) not appended — they cost ` +
|
|
165
|
+
`${budget.cost} lines and only ${budget.slack} remain of the declared ceiling; ` +
|
|
166
|
+
`cut ${budget.over} line(s) of prose first`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
168
170
|
if (missing.length > 0) {
|
|
169
171
|
plan.push({
|
|
170
172
|
describe: missing.map((h) => `${rel}: append stub section "${h}"`),
|
|
@@ -172,7 +174,7 @@ function updateTemplates(flags) {
|
|
|
172
174
|
let updated = read(filePath);
|
|
173
175
|
if (!updated.endsWith("\n")) updated += "\n";
|
|
174
176
|
for (const h of missing) {
|
|
175
|
-
updated += `\n${h}\n\n<!-- added by doctrina
|
|
177
|
+
updated += `\n${h}\n\n<!-- added by doctrina upgrade — fill in -->\n`;
|
|
176
178
|
}
|
|
177
179
|
write(filePath, updated, { force: true });
|
|
178
180
|
},
|
|
@@ -215,7 +217,16 @@ function updateTemplates(flags) {
|
|
|
215
217
|
}
|
|
216
218
|
|
|
217
219
|
const steps = plan.flatMap((p) => p.describe);
|
|
220
|
+
for (const reason of held) {
|
|
221
|
+
console.log(c.yellow("held ") + reason);
|
|
222
|
+
}
|
|
218
223
|
if (steps.length === 0) {
|
|
224
|
+
if (held.length > 0) {
|
|
225
|
+
console.log("");
|
|
226
|
+
console.log(c.yellow("hold") + ` ${held.length} update${held.length === 1 ? "" : "s"} not applied — ` +
|
|
227
|
+
"make room first, then re-run");
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
219
230
|
console.log(c.green("ok") + " project already follows the current template shape");
|
|
220
231
|
return 0;
|
|
221
232
|
}
|
|
@@ -274,127 +285,12 @@ function listTemplates() {
|
|
|
274
285
|
|
|
275
286
|
function checkTemplates() {
|
|
276
287
|
const projectRoot = process.cwd();
|
|
277
|
-
|
|
278
|
-
console.error(c.red("error:") + " not a Doctrina project (no .doctrina/ in cwd)");
|
|
279
|
-
console.error(c.gray("hint: ") + "run `doctrina init` first");
|
|
280
|
-
return 1;
|
|
281
|
-
}
|
|
288
|
+
ensureDoctrinaProject(projectRoot);
|
|
282
289
|
const { findings, ok } = collectFindings(projectRoot);
|
|
283
290
|
|
|
284
291
|
return report(findings, ok);
|
|
285
292
|
}
|
|
286
293
|
|
|
287
|
-
// The findings `templates check` reports, as structured records with an
|
|
288
|
-
// executable remedy. Exported so a test can seed each finding, run the
|
|
289
|
-
// remedy it names, and assert the finding clears — a remedy that cannot
|
|
290
|
-
// resolve its own finding is not a remedy (C2).
|
|
291
|
-
export function collectFindings(projectRoot) {
|
|
292
|
-
const findings = [];
|
|
293
|
-
const ok = [];
|
|
294
|
-
|
|
295
|
-
// AGENTS.md sections
|
|
296
|
-
const agentsPath = path.join(projectRoot, "AGENTS.md");
|
|
297
|
-
if (isFile(agentsPath)) {
|
|
298
|
-
const text = read(agentsPath);
|
|
299
|
-
for (const heading of AGENTS_SECTIONS) {
|
|
300
|
-
if (hasHeading(text, heading)) ok.push(`AGENTS.md: ${heading}`);
|
|
301
|
-
else findings.push({ message: `AGENTS.md missing recommended section "${heading}"`, remedy: "doctrina templates update --write" });
|
|
302
|
-
}
|
|
303
|
-
// Command-surface block: present and current vs the installed catalog.
|
|
304
|
-
const block = findSurfaceBlock(text);
|
|
305
|
-
if (!block) {
|
|
306
|
-
findings.push({ message: "AGENTS.md has no doctrina:surface block — agents discover commands through this file", remedy: "doctrina templates update --write" });
|
|
307
|
-
} else if (normalizeBlock(text.slice(block.start, block.end)) !== normalizeBlock(surfaceBlock())) {
|
|
308
|
-
findings.push({ message: "AGENTS.md doctrina:surface block is stale vs the installed CLI", remedy: "doctrina templates update --write" });
|
|
309
|
-
} else {
|
|
310
|
-
ok.push("AGENTS.md: doctrina:surface block current");
|
|
311
|
-
}
|
|
312
|
-
} else {
|
|
313
|
-
findings.push({ message: "AGENTS.md missing at project root", remedy: "doctrina init --force" });
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// product.md sections
|
|
317
|
-
const productPath = path.join(projectRoot, ".doctrina", "product.md");
|
|
318
|
-
if (isFile(productPath)) {
|
|
319
|
-
const text = read(productPath);
|
|
320
|
-
for (const heading of PRODUCT_SECTIONS) {
|
|
321
|
-
if (hasHeading(text, heading)) ok.push(`product.md: ${heading}`);
|
|
322
|
-
else findings.push({ message: `.doctrina/product.md missing recommended section "${heading}"`, remedy: "doctrina templates update --write" });
|
|
323
|
-
}
|
|
324
|
-
} else {
|
|
325
|
-
findings.push({ message: ".doctrina/product.md missing", remedy: "doctrina init --force" });
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Installed agent adapters: a HUB POINTER file must still reference
|
|
329
|
-
// AGENTS.md (that is why one `upgrade --write` refresh of the hub reaches
|
|
330
|
-
// every installed agent). Only files whose template declares the
|
|
331
|
-
// {{AGENTS_MD_PATH}} token are pointers; a slash-command shim reaches the
|
|
332
|
-
// hub through its parent pointer file and is not checked (C2).
|
|
333
|
-
for (const name of listAdapterNames(projectRoot)) {
|
|
334
|
-
const spec = adapterFiles(projectRoot, name);
|
|
335
|
-
if (!spec) continue;
|
|
336
|
-
for (const file of spec.files) {
|
|
337
|
-
const installed = path.join(projectRoot, file.relativePath);
|
|
338
|
-
if (!isFile(installed)) continue; // not installed — nothing to check
|
|
339
|
-
if (!isHubPointer(file)) continue; // command shim, not a pointer
|
|
340
|
-
if (read(installed).includes("AGENTS.md")) {
|
|
341
|
-
ok.push(`adapter ${file.relativePath}: points at AGENTS.md`);
|
|
342
|
-
} else {
|
|
343
|
-
findings.push({
|
|
344
|
-
message: `adapter ${file.relativePath} no longer references AGENTS.md — agents loading it will miss the hub`,
|
|
345
|
-
// Executable and verified: `adapter add --force` rewrites exactly
|
|
346
|
-
// this file from its template. A remedy that cannot resolve the
|
|
347
|
-
// finding it is attached to is not a remedy (C2).
|
|
348
|
-
remedy: `doctrina adapter add ${name} --force`,
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// M2: a command that cannot state its trigger has not earned a place on
|
|
355
|
-
// the surface. The block is an agent's only discovery surface, so both
|
|
356
|
-
// fields are required and the block has a declared size budget.
|
|
357
|
-
for (const name of COMMAND_NAMES) {
|
|
358
|
-
const meta = COMMAND_META[name];
|
|
359
|
-
if (!meta || !meta.purpose || !meta.when) {
|
|
360
|
-
findings.push({
|
|
361
|
-
message: `command "${name}" declares no ${!meta ? "purpose or when" : (!meta.purpose ? "purpose" : "when")} — the surface block cannot say when to reach for it`,
|
|
362
|
-
remedy: null,
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
const surfaceLines = surfaceMarkdown().split("\n").length;
|
|
367
|
-
if (surfaceLines > SURFACE_LINE_BUDGET) {
|
|
368
|
-
findings.push({
|
|
369
|
-
message: `the generated surface block is ${surfaceLines} lines (budget ${SURFACE_LINE_BUDGET}) — cut commands rather than raising the budget`,
|
|
370
|
-
remedy: null,
|
|
371
|
-
});
|
|
372
|
-
} else {
|
|
373
|
-
ok.push(`surface block within budget (${surfaceLines}/${SURFACE_LINE_BUDGET} lines)`);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// index.json schema fields
|
|
377
|
-
const indexPath = path.join(projectRoot, ".doctrina", "index.json");
|
|
378
|
-
if (isFile(indexPath)) {
|
|
379
|
-
try {
|
|
380
|
-
const idx = JSON.parse(read(indexPath));
|
|
381
|
-
for (const field of INDEX_FIELDS) {
|
|
382
|
-
if (idx[field] !== undefined) ok.push(`index.json: ${field}`);
|
|
383
|
-
else findings.push({ message: `.doctrina/index.json missing field "${field}"`, remedy: "doctrina templates update --write" });
|
|
384
|
-
}
|
|
385
|
-
if (idx.$schema_version && idx.$schema_version !== "0.1.0") {
|
|
386
|
-
findings.push({ message: `.doctrina/index.json $schema_version is "${idx.$schema_version}" (expected "0.1.0")`, remedy: null });
|
|
387
|
-
}
|
|
388
|
-
} catch (err) {
|
|
389
|
-
findings.push({ message: `.doctrina/index.json failed to parse: ${err.message}`, remedy: null });
|
|
390
|
-
}
|
|
391
|
-
} else {
|
|
392
|
-
findings.push({ message: ".doctrina/index.json missing", remedy: "doctrina index rebuild" });
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Output
|
|
396
|
-
return { findings, ok };
|
|
397
|
-
}
|
|
398
294
|
|
|
399
295
|
function report(findings, ok) {
|
|
400
296
|
for (const o of ok) console.log(c.green("✓ ") + o);
|
|
@@ -443,9 +339,6 @@ function templateAnchors() {
|
|
|
443
339
|
}
|
|
444
340
|
}
|
|
445
341
|
|
|
446
|
-
function normalizeBlock(s) {
|
|
447
|
-
return s.replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "").trim();
|
|
448
|
-
}
|
|
449
342
|
|
|
450
343
|
// A pre-marker AGENTS.md's hand-written surface section: from the
|
|
451
344
|
// "## Doctrina command surface" heading up to (excluding) the next "## "
|
|
@@ -459,12 +352,6 @@ function findLegacySurfaceSection(text) {
|
|
|
459
352
|
return { start: head.index, end: next ? head.index + head[0].length + next.index : text.length };
|
|
460
353
|
}
|
|
461
354
|
|
|
462
|
-
function hasHeading(text, heading) {
|
|
463
|
-
// Match an exact h2 heading at line start (case-insensitive for the body
|
|
464
|
-
// after "## ", so "## Vision" matches "## VISION" or "## vision" too).
|
|
465
|
-
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
466
|
-
return new RegExp(`^${escaped}\\b`, "im").test(text);
|
|
467
|
-
}
|
|
468
355
|
|
|
469
356
|
export const help = `
|
|
470
357
|
Usage: doctrina templates <subcommand>
|