doctrina-cli 0.9.0 → 0.11.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 +1 -1
- package/src/commands/completion.js +116 -0
- package/src/commands/constitution.js +138 -0
- package/src/commands/context.js +118 -22
- package/src/commands/coverage.js +41 -16
- package/src/commands/doctor.js +135 -0
- package/src/commands/handoff.js +94 -0
- package/src/commands/init.js +12 -1
- package/src/commands/next.js +32 -16
- package/src/commands/prime.js +118 -0
- package/src/commands/report.js +142 -0
- package/src/commands/show.js +173 -0
- package/src/commands/status.js +70 -40
- package/src/commands/trace.js +15 -2
- package/src/commands/validate.js +97 -1
- package/src/commands/why.js +132 -36
- package/src/index.js +12 -41
- package/src/lib/commands.js +115 -0
- package/src/lib/criteria.js +76 -0
- package/src/lib/index-json.js +1 -0
- package/src/lib/scan.js +7 -0
- package/templates/AGENTS.md.template +3 -2
- package/templates/README.md +7 -1
- package/templates/adapters/aider/CONVENTIONS.md.template +4 -3
- package/templates/adapters/claude/.claude/commands/doctrina-context.md.template +13 -0
- package/templates/adapters/claude/.claude/commands/doctrina-next.md.template +13 -0
- package/templates/adapters/claude/.claude/commands/doctrina-status.md.template +12 -0
- package/templates/adapters/claude/.claude/commands/doctrina-why.md.template +12 -0
- package/templates/adapters/claude/.claude/commands/doctrina-work.md.template +17 -0
- package/templates/adapters/cursor/.cursor/commands/doctrina-context.md.template +10 -0
- package/templates/adapters/cursor/.cursor/commands/doctrina-next.md.template +10 -0
- package/templates/adapters/cursor/.cursor/commands/doctrina-status.md.template +10 -0
- package/templates/adapters/cursor/.cursor/commands/doctrina-why.md.template +9 -0
- package/templates/adapters/cursor/.cursor/commands/doctrina-work.md.template +12 -0
- package/templates/doctrina/index.json.template +3 -0
package/src/commands/why.js
CHANGED
|
@@ -3,6 +3,8 @@ import process from "node:process";
|
|
|
3
3
|
import { readdirSync } from "node:fs";
|
|
4
4
|
import { exists, isDir, isFile, read } from "../lib/fs-ops.js";
|
|
5
5
|
import { specHeader, listHeader } from "../lib/scan.js";
|
|
6
|
+
import { parseAcceptanceCriteria, isVerified } from "../lib/criteria.js";
|
|
7
|
+
import * as idx from "../lib/index-json.js";
|
|
6
8
|
import { c } from "../lib/colors.js";
|
|
7
9
|
import { suggest } from "../lib/suggest.js";
|
|
8
10
|
|
|
@@ -24,10 +26,17 @@ export async function run(positional, _flags) {
|
|
|
24
26
|
const known = isDir(specsDir) ? readdirSync(specsDir).filter((e) => isFile(path.join(specsDir, e, "spec.md"))) : [];
|
|
25
27
|
|
|
26
28
|
if (!cap) {
|
|
27
|
-
console.error(c.red("error:") + " why requires a <capability>");
|
|
29
|
+
console.error(c.red("error:") + " why requires a <capability> (or an intent anchor like SC1)");
|
|
28
30
|
if (known.length) console.error(c.gray("known: ") + known.sort().join(", "));
|
|
29
31
|
return 2;
|
|
30
32
|
}
|
|
33
|
+
|
|
34
|
+
// Reverse mode: `doctrina why SC1` walks the chain the other way — from a
|
|
35
|
+
// product intent anchor to the capabilities that realize it and the work
|
|
36
|
+
// that delivered them. Forward mode asks "why does this capability exist?";
|
|
37
|
+
// reverse mode asks "who delivers this promise?".
|
|
38
|
+
if (/^[A-Z]+\d+$/.test(cap)) return whyAnchor(projectRoot, cap, specsDir, known);
|
|
39
|
+
|
|
31
40
|
const specPath = path.join(specsDir, cap, "spec.md");
|
|
32
41
|
if (!isFile(specPath)) {
|
|
33
42
|
console.error(c.red("error:") + ` no spec for capability "${cap}"`);
|
|
@@ -66,16 +75,35 @@ export async function run(positional, _flags) {
|
|
|
66
75
|
const purpose = sectionParagraph(text, "Purpose");
|
|
67
76
|
if (purpose) console.log(" " + purpose.replace(/\s+/g, " ").trim());
|
|
68
77
|
|
|
69
|
-
// 3. The proof (acceptance criteria with cited evidence).
|
|
78
|
+
// 3. The proof (acceptance criteria with cited evidence). Read through the
|
|
79
|
+
// shared multi-line parser so the marks here agree with `doctrina
|
|
80
|
+
// coverage` — a criterion that cites its proof on a continuation line is
|
|
81
|
+
// no longer mis-reported as "no evidence cited". The mark distinguishes
|
|
82
|
+
// the three honest states, including the one that used to read as a false
|
|
83
|
+
// green: a [verified] claim that cites nothing.
|
|
70
84
|
console.log("");
|
|
71
85
|
console.log(c.bold(" Proof"));
|
|
72
|
-
const criteria =
|
|
86
|
+
const criteria = parseAcceptanceCriteria(text);
|
|
73
87
|
if (criteria.length === 0) {
|
|
74
88
|
console.log(` ${c.gray("no acceptance criteria declared")}`);
|
|
75
89
|
} else {
|
|
76
90
|
for (const cr of criteria) {
|
|
77
|
-
const
|
|
78
|
-
|
|
91
|
+
const paths = cr.proofPaths.map((p) => `\`${p}\``).join(", ");
|
|
92
|
+
let mark, note;
|
|
93
|
+
if (isVerified(cr) && cr.proofPaths.length > 0) {
|
|
94
|
+
mark = c.green("✓");
|
|
95
|
+
note = c.gray(paths);
|
|
96
|
+
} else if (isVerified(cr)) {
|
|
97
|
+
mark = c.yellow("~");
|
|
98
|
+
note = c.yellow("marked verified but cites no proof");
|
|
99
|
+
} else if (cr.marker !== null) {
|
|
100
|
+
mark = c.yellow("○");
|
|
101
|
+
note = cr.proofPaths.length ? c.gray(paths) : c.gray(`[${cr.marker}]`);
|
|
102
|
+
} else {
|
|
103
|
+
mark = c.gray("·");
|
|
104
|
+
note = cr.proofPaths.length ? c.gray(paths) : c.yellow("no evidence cited");
|
|
105
|
+
}
|
|
106
|
+
console.log(` ${mark} #${cr.n} ${note}`);
|
|
79
107
|
}
|
|
80
108
|
}
|
|
81
109
|
|
|
@@ -89,9 +117,96 @@ export async function run(positional, _flags) {
|
|
|
89
117
|
for (const a of adrs) console.log(` ${c.cyan("ADR " + a.id)} ${a.title}`);
|
|
90
118
|
}
|
|
91
119
|
|
|
120
|
+
// 5. History — the archived changes that built this capability. The default
|
|
121
|
+
// context pack excludes changes/archive/ by design, so "what shaped this
|
|
122
|
+
// spec?" otherwise loses the change trail. The index ledger already
|
|
123
|
+
// records each archived change's affected capabilities (specs_affected);
|
|
124
|
+
// surface them here so the provenance chain reaches back to the work that
|
|
125
|
+
// delivered the spec, not only the intent and decisions in front of it.
|
|
126
|
+
console.log("");
|
|
127
|
+
console.log(c.bold(" History"));
|
|
128
|
+
const history = archivedChangesFor(projectRoot, cap);
|
|
129
|
+
if (history.length === 0) {
|
|
130
|
+
console.log(` ${c.gray("no archived change recorded against this capability")}`);
|
|
131
|
+
} else {
|
|
132
|
+
for (const ch of history) {
|
|
133
|
+
console.log(` ${c.cyan(ch.applied ?? "—")} ${ch.title} ${c.gray(`(${ch.op})`)}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
92
137
|
return 0;
|
|
93
138
|
}
|
|
94
139
|
|
|
140
|
+
// Reverse provenance: one intent anchor → the capabilities that realize it,
|
|
141
|
+
// each with its proof state, plus the archived changes that built them.
|
|
142
|
+
function whyAnchor(projectRoot, id, specsDir, known) {
|
|
143
|
+
const anchors = productAnchors(projectRoot);
|
|
144
|
+
if (!anchors.has(id)) {
|
|
145
|
+
console.error(c.red("error:") + ` no anchor [${id}] in .doctrina/product.md`);
|
|
146
|
+
if (anchors.size) console.error(c.gray("known: ") + [...anchors.keys()].join(", "));
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(c.bold(`Why ${id}`) + c.gray(" — reverse provenance (intent → capabilities → proof → history)"));
|
|
151
|
+
console.log("");
|
|
152
|
+
console.log(c.bold(" Intent"));
|
|
153
|
+
console.log(` ${c.cyan(id)} ${anchors.get(id)}`);
|
|
154
|
+
|
|
155
|
+
const realizing = [];
|
|
156
|
+
for (const capName of known.sort()) {
|
|
157
|
+
const text = read(path.join(specsDir, capName, "spec.md"));
|
|
158
|
+
const realizesRaw = specHeader(text, "Realizes");
|
|
159
|
+
const ids = realizesRaw ? (realizesRaw.match(/[A-Z]+\d+/g) ?? []) : [];
|
|
160
|
+
if (ids.includes(id)) realizing.push({ cap: capName, text });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log("");
|
|
164
|
+
console.log(c.bold(" Realized by"));
|
|
165
|
+
if (realizing.length === 0) {
|
|
166
|
+
console.log(` ${c.red("nothing")} ${c.gray("— dropped intent: no spec declares Realizes: " + id + " (doctrina trace)")}`);
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
for (const s of realizing) {
|
|
170
|
+
const status = specHeader(s.text, "Status") ?? "—";
|
|
171
|
+
const impl = specHeader(s.text, "Implementation") ?? "—";
|
|
172
|
+
const criteria = parseAcceptanceCriteria(s.text);
|
|
173
|
+
const proven = criteria.filter((cr) => cr.proofPaths.length > 0).length;
|
|
174
|
+
console.log(
|
|
175
|
+
` ${c.cyan(s.cap.padEnd(14))} ${c.gray(`status: ${status} · implementation: ${impl} · proof: ${proven}/${criteria.length} criteria cite evidence`)}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log("");
|
|
180
|
+
console.log(c.bold(" History"));
|
|
181
|
+
let any = false;
|
|
182
|
+
for (const s of realizing) {
|
|
183
|
+
for (const ch of archivedChangesFor(projectRoot, s.cap)) {
|
|
184
|
+
console.log(` ${c.cyan(ch.applied ?? "—")} ${ch.title} ${c.gray(`(${s.cap}, ${ch.op})`)}`);
|
|
185
|
+
any = true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (!any) console.log(` ${c.gray("no archived change recorded against the realizing capabilities")}`);
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Archived changes whose recorded specs_affected include this capability,
|
|
193
|
+
// oldest first. Read from the index ledger (the same data `index rebuild`
|
|
194
|
+
// derives from each change's spec deltas); absent or unreadable index → none.
|
|
195
|
+
function archivedChangesFor(projectRoot, cap) {
|
|
196
|
+
let index;
|
|
197
|
+
try {
|
|
198
|
+
index = idx.load(projectRoot);
|
|
199
|
+
} catch {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
const out = [];
|
|
203
|
+
for (const ch of index.artifacts?.changes_archive ?? []) {
|
|
204
|
+
const hit = (ch.specs_affected ?? []).find((s) => s.capability === cap);
|
|
205
|
+
if (hit) out.push({ applied: ch.applied, title: ch.title, op: hit.operation ?? "MODIFIED" });
|
|
206
|
+
}
|
|
207
|
+
return out.sort((a, b) => String(a.applied ?? "").localeCompare(String(b.applied ?? "")));
|
|
208
|
+
}
|
|
209
|
+
|
|
95
210
|
// Map of anchor id -> the product.md bullet text that declares it.
|
|
96
211
|
function productAnchors(projectRoot) {
|
|
97
212
|
const out = new Map();
|
|
@@ -126,30 +241,6 @@ function sectionParagraph(text, name) {
|
|
|
126
241
|
return buf.join(" ");
|
|
127
242
|
}
|
|
128
243
|
|
|
129
|
-
// Numbered acceptance criteria with their [mark] and any backtick proof paths.
|
|
130
|
-
function acceptanceWithEvidence(text) {
|
|
131
|
-
const lines = text.split(/\r?\n/);
|
|
132
|
-
let inSection = false;
|
|
133
|
-
const out = [];
|
|
134
|
-
for (const line of lines) {
|
|
135
|
-
if (/^##\s+/.test(line)) {
|
|
136
|
-
inSection = /^##\s+Acceptance criteria\b/i.test(line);
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
if (!inSection) continue;
|
|
140
|
-
const m = line.match(/^\s*(\d+)\.\s+(.*)$/);
|
|
141
|
-
if (!m) continue;
|
|
142
|
-
const body = m[2];
|
|
143
|
-
const markMatch = body.match(/^\[([^\]]+)\]/);
|
|
144
|
-
const mark = markMatch
|
|
145
|
-
? (/^verified/i.test(markMatch[1]) ? c.green("✓") : c.yellow("○"))
|
|
146
|
-
: c.gray("·");
|
|
147
|
-
const proof = [...body.matchAll(/`([^`]+)`/g)].map((x) => x[1]).filter((s) => s.includes("/") || /\.[a-z0-9]{1,8}$/i.test(s));
|
|
148
|
-
out.push({ n: m[1], mark, proof });
|
|
149
|
-
}
|
|
150
|
-
return out;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
244
|
// Accepted ADRs whose body names the capability (word-ish boundary).
|
|
154
245
|
function decisionsMentioning(projectRoot, cap) {
|
|
155
246
|
const adrDir = path.join(projectRoot, ".doctrina", "decisions");
|
|
@@ -170,13 +261,18 @@ function decisionsMentioning(projectRoot, cap) {
|
|
|
170
261
|
}
|
|
171
262
|
|
|
172
263
|
export const help = `
|
|
173
|
-
Usage: doctrina why <capability>
|
|
264
|
+
Usage: doctrina why <capability | anchor>
|
|
265
|
+
|
|
266
|
+
Explain provenance in either direction, read-only:
|
|
174
267
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
268
|
+
doctrina why billing forward — the product intent the capability
|
|
269
|
+
Realizes:, its purpose/status, the acceptance
|
|
270
|
+
criteria that prove it, the accepted ADRs that
|
|
271
|
+
name it, and the archived changes that built it.
|
|
272
|
+
doctrina why SC1 reverse — the intent anchor's product.md text,
|
|
273
|
+
the capabilities that realize it (with proof
|
|
274
|
+
state), and the archived changes behind them.
|
|
179
275
|
|
|
180
|
-
Builds on the intent provenance (ADR 0006/0011) — answer "why was X built
|
|
181
|
-
|
|
276
|
+
Builds on the intent provenance (ADR 0006/0011) — answer "why was X built"
|
|
277
|
+
or "who delivers this promise?" without grepping the tree by hand.
|
|
182
278
|
`;
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { parseArgs } from "./lib/args.js";
|
|
|
4
4
|
import { c } from "./lib/colors.js";
|
|
5
5
|
import { suggest } from "./lib/suggest.js";
|
|
6
6
|
import { cliVersion } from "./lib/version.js";
|
|
7
|
+
import { surfaceHelp } from "./lib/commands.js";
|
|
7
8
|
|
|
8
9
|
import * as init from "./commands/init.js";
|
|
9
10
|
import * as spec from "./commands/spec.js";
|
|
@@ -31,57 +32,27 @@ import * as close from "./commands/close.js";
|
|
|
31
32
|
import * as review from "./commands/review.js";
|
|
32
33
|
import * as watch from "./commands/watch.js";
|
|
33
34
|
import * as why from "./commands/why.js";
|
|
35
|
+
import * as constitution from "./commands/constitution.js";
|
|
36
|
+
import * as prime from "./commands/prime.js";
|
|
37
|
+
import * as handoff from "./commands/handoff.js";
|
|
38
|
+
import * as show from "./commands/show.js";
|
|
39
|
+
import * as doctor from "./commands/doctor.js";
|
|
40
|
+
import * as report from "./commands/report.js";
|
|
41
|
+
import * as completion from "./commands/completion.js";
|
|
34
42
|
|
|
35
43
|
const COMMANDS = {
|
|
36
44
|
init, spec, change, decision, validate, hooks, analyze, clarify,
|
|
37
45
|
templates, skill, index: indexCmd, next, metrics, context, search,
|
|
38
46
|
intake, work, coverage, verify, contract, trace,
|
|
39
|
-
status, close, review, watch, why,
|
|
47
|
+
status, close, review, watch, why, constitution,
|
|
48
|
+
prime, handoff, show, doctor, report, completion,
|
|
40
49
|
};
|
|
41
50
|
|
|
42
51
|
const TOP_HELP = `
|
|
43
52
|
Usage: doctrina <command> [options]
|
|
44
53
|
|
|
45
54
|
Commands:
|
|
46
|
-
|
|
47
|
-
intake Store the full project description; print the bootstrap playbook
|
|
48
|
-
work Brief prompt -> scaffolded change + guided work playbook
|
|
49
|
-
spec new Create a new capability spec (--bug for bug-shape)
|
|
50
|
-
spec list List specs with version, status, and size
|
|
51
|
-
change new Open a change proposal
|
|
52
|
-
change apply Apply spec deltas (ADDED/REMOVED auto, MODIFIED manual)
|
|
53
|
-
change archive Archive an applied change
|
|
54
|
-
change diff Preview spec deltas (line diff for MODIFIED)
|
|
55
|
-
contract new Own the integration surface (ports, env, interfaces)
|
|
56
|
-
contract check Verify port collisions, env drift, referenced specs
|
|
57
|
-
decision new Create the next sequentially numbered ADR
|
|
58
|
-
decision accept Flip a proposed ADR to accepted
|
|
59
|
-
decision land Record that an accepted ADR is now implemented (non-mutating)
|
|
60
|
-
decision supersede Create a new ADR that supersedes an existing one
|
|
61
|
-
decision list List ADRs with status, date, and title
|
|
62
|
-
skill new Scaffold an on-demand procedural memory skill
|
|
63
|
-
skill list List skills with their descriptions
|
|
64
|
-
skill sync Mirror skill frontmatter descriptions into index.json
|
|
65
|
-
skill suggest Surface fix-shaped lessons worth a skill (--write scaffolds)
|
|
66
|
-
analyze Inspect a change folder before applying
|
|
67
|
-
clarify Smell-test a Markdown file for ambiguity (--all for the tree)
|
|
68
|
-
context Print the context pack for a task in read order
|
|
69
|
-
search Search the artifact tree, grouped by category
|
|
70
|
-
validate Run schema and structural checks
|
|
71
|
-
coverage Report acceptance criteria with linked evidence (--strict gates)
|
|
72
|
-
trace Report intent provenance: product intent → specs (--strict gates)
|
|
73
|
-
review Conformance review of your changes vs specs/ADRs/contracts
|
|
74
|
-
verify Run project-declared typecheck/test/build checks (the real gate)
|
|
75
|
-
close Run the whole close sequence for a change in one pass
|
|
76
|
-
status One-glance project health dashboard
|
|
77
|
-
why Explain a capability's provenance (intent → proof → ADRs)
|
|
78
|
-
templates list List the templates shipped by the installed CLI
|
|
79
|
-
templates check Compare the project against the recommended template shape
|
|
80
|
-
hooks install Install the pre-commit hook
|
|
81
|
-
index rebuild Regenerate index.json from the artifacts on disk
|
|
82
|
-
next Print the recommended next workflow actions
|
|
83
|
-
watch Re-run validate --fix + next on every change (--once for one pass)
|
|
84
|
-
metrics Local git-derived adoption metrics (no network)
|
|
55
|
+
${surfaceHelp()}
|
|
85
56
|
|
|
86
57
|
Global flags:
|
|
87
58
|
--help, -h Show this message (or per-command help if after a command)
|
|
@@ -90,7 +61,7 @@ Global flags:
|
|
|
90
61
|
|
|
91
62
|
async function main(argv) {
|
|
92
63
|
const { positional, flags } = parseArgs(argv, {
|
|
93
|
-
boolean: ["help", "h", "version", "v", "force", "non-interactive", "check", "save", "bug", "write", "all", "concat", "archive", "strict", "list", "init", "fix", "once", "clean"],
|
|
64
|
+
boolean: ["help", "h", "version", "v", "force", "non-interactive", "check", "save", "bug", "write", "all", "concat", "archive", "strict", "list", "init", "fix", "once", "clean", "json"],
|
|
94
65
|
});
|
|
95
66
|
|
|
96
67
|
if (flags.get("version") || flags.get("v")) {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Single canonical list of top-level `doctrina` commands.
|
|
2
|
+
//
|
|
3
|
+
// This is the one place the command surface is named. `src/index.js` builds
|
|
4
|
+
// its dispatch table for these names (an execution-based test asserts every
|
|
5
|
+
// name here resolves to a real handler, so the two never drift), and
|
|
6
|
+
// `doctrina validate` compares it against the commands AGENTS.md documents —
|
|
7
|
+
// the hub the agent reads first — so a stale command catalog or a reference
|
|
8
|
+
// to a command that no longer exists becomes a loud signal instead of silent
|
|
9
|
+
// rot (the "AGENTS.md is the source of truth but nothing keeps it fresh" gap).
|
|
10
|
+
//
|
|
11
|
+
// Keep this sorted by the workflow order used in `--help`, not alphabetically.
|
|
12
|
+
export const COMMAND_NAMES = [
|
|
13
|
+
// bootstrap / day-to-day
|
|
14
|
+
"init", "intake", "work",
|
|
15
|
+
// authoring
|
|
16
|
+
"spec", "change", "contract", "decision", "skill",
|
|
17
|
+
// read / orient
|
|
18
|
+
"prime", "context", "show", "search", "status", "next", "why", "handoff", "constitution",
|
|
19
|
+
// gates
|
|
20
|
+
"analyze", "clarify", "validate", "coverage", "trace", "review", "verify", "close", "doctor",
|
|
21
|
+
// maintenance
|
|
22
|
+
"templates", "hooks", "index", "watch", "metrics", "report", "completion",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// The full operation surface — every `doctrina <command> [<subcommand>]` a
|
|
26
|
+
// user can invoke, each with the one-line summary `--help` prints. This is
|
|
27
|
+
// the single source the top-level help is GENERATED from, so an operation
|
|
28
|
+
// that exists in a dispatch switch but not here is now a loud test failure
|
|
29
|
+
// instead of a hidden feature (`spec set` / `change abandon` shipped and
|
|
30
|
+
// stayed invisible to `--help`, AGENTS.md, and the CLI reference — the exact
|
|
31
|
+
// drift the AGENTS.md gate could not see because its catalog stops at the
|
|
32
|
+
// top-level names). Tests assert: every op resolves to a real handler, the
|
|
33
|
+
// top-level words equal COMMAND_NAMES, and the cli-reference docs cover
|
|
34
|
+
// every op.
|
|
35
|
+
export const OPERATIONS = [
|
|
36
|
+
["init", "Scaffold AGENTS.md and .doctrina/ in the current directory"],
|
|
37
|
+
["intake", "Store the full project description; print the bootstrap playbook"],
|
|
38
|
+
["work", "Brief prompt -> scaffolded change + guided work playbook"],
|
|
39
|
+
["spec new", "Create a new capability spec (--bug for bug-shape)"],
|
|
40
|
+
["spec list", "List specs with version, status, and size"],
|
|
41
|
+
["spec set", "Edit spec headers / a criterion mark and resync the index"],
|
|
42
|
+
["change new", "Open a change proposal"],
|
|
43
|
+
["change apply", "Apply spec deltas (ADDED/REMOVED auto, MODIFIED manual)"],
|
|
44
|
+
["change archive", "Archive an applied change"],
|
|
45
|
+
["change diff", "Preview spec deltas (line diff for MODIFIED)"],
|
|
46
|
+
["change abandon", "Discard an open change cleanly (recorded in the ledger)"],
|
|
47
|
+
["contract new", "Own the integration surface (ports, env, interfaces)"],
|
|
48
|
+
["contract list", "List contracts with status and last-updated date"],
|
|
49
|
+
["contract check", "Verify port collisions, env drift, referenced specs"],
|
|
50
|
+
["decision new", "Create the next sequentially numbered ADR"],
|
|
51
|
+
["decision accept", "Flip a proposed ADR to accepted"],
|
|
52
|
+
["decision land", "Record that an accepted ADR is now implemented (non-mutating)"],
|
|
53
|
+
["decision supersede", "Create a new ADR that supersedes an existing one"],
|
|
54
|
+
["decision list", "List ADRs with status, date, and title"],
|
|
55
|
+
["skill new", "Scaffold an on-demand procedural memory skill"],
|
|
56
|
+
["skill list", "List skills with their descriptions"],
|
|
57
|
+
["skill sync", "Mirror skill frontmatter descriptions into index.json"],
|
|
58
|
+
["skill suggest", "Surface fix-shaped lessons worth a skill (--write scaffolds)"],
|
|
59
|
+
["analyze", "Inspect a change folder before applying"],
|
|
60
|
+
["clarify", "Smell-test a Markdown file for ambiguity (--all for the tree)"],
|
|
61
|
+
["prime", "Session primer: gates, rules, open work, next steps in one read"],
|
|
62
|
+
["context", "Print the context pack for a task in read order"],
|
|
63
|
+
["show", "Point-read a requirement/criterion/ADR (cli-R12, cli-C3, 0007)"],
|
|
64
|
+
["search", "Search the artifact tree, grouped by category"],
|
|
65
|
+
["validate", "Run schema and structural checks"],
|
|
66
|
+
["coverage", "Report acceptance criteria with linked evidence (--strict gates)"],
|
|
67
|
+
["trace", "Report intent provenance: product intent → specs (--strict gates)"],
|
|
68
|
+
["review", "Conformance review of your changes vs specs/ADRs/contracts"],
|
|
69
|
+
["verify", "Run project-declared typecheck/test/build checks (the real gate)"],
|
|
70
|
+
["close", "Run the whole close sequence for a change in one pass"],
|
|
71
|
+
["doctor", "Aggregate diagnostic with per-finding remediation (a driver)"],
|
|
72
|
+
["status", "One-glance project health dashboard"],
|
|
73
|
+
["why", "Explain provenance: a capability's chain, or an anchor's (SC1)"],
|
|
74
|
+
["handoff", "Markdown handoff note: open work, task state, resume command"],
|
|
75
|
+
["constitution", "Print the standing rules: accepted ADRs + product non-goals"],
|
|
76
|
+
["templates list", "List the templates shipped by the installed CLI"],
|
|
77
|
+
["templates check", "Compare the project against the recommended template shape"],
|
|
78
|
+
["templates update", "Additive fixer for check findings (preview; --write applies)"],
|
|
79
|
+
["hooks install", "Install the pre-commit hook"],
|
|
80
|
+
["index rebuild", "Regenerate index.json from the artifacts on disk"],
|
|
81
|
+
["next", "Print the recommended next workflow actions"],
|
|
82
|
+
["watch", "Re-run validate --fix + next on every change (--once for one pass)"],
|
|
83
|
+
["metrics", "Local git-derived adoption metrics (no network)"],
|
|
84
|
+
["report", "Markdown digest for a period: changes, gates, git summary"],
|
|
85
|
+
["completion", "Print bash/zsh/pwsh completions (generated from the catalog)"],
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// The Commands block of `doctrina --help`, generated from OPERATIONS so the
|
|
89
|
+
// printed surface can never omit an operation the catalog knows about.
|
|
90
|
+
export function surfaceHelp() {
|
|
91
|
+
return OPERATIONS.map(([op, summary]) => ` ${op.padEnd(21)}${summary}`).join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Extract the set of top-level `doctrina <command>` references that appear in
|
|
95
|
+
// CODE context (fenced blocks or inline backtick spans) of a Markdown file —
|
|
96
|
+
// AGENTS.md, typically. Restricting to code context avoids matching prose like
|
|
97
|
+
// "the doctrina framework", which is not a command reference. Both the bare
|
|
98
|
+
// `doctrina <cmd>` and the `npx doctrina-cli <cmd>` forms are recognised.
|
|
99
|
+
export function referencedCommands(markdown) {
|
|
100
|
+
const out = new Set();
|
|
101
|
+
const chunks = [];
|
|
102
|
+
// Pull fenced blocks first and strip them out, so the inline-span scan below
|
|
103
|
+
// cannot mis-pair backticks across a fence boundary (a fence's own backticks
|
|
104
|
+
// would otherwise shift which inline spans pair up).
|
|
105
|
+
const rest = markdown.replace(/```[\s\S]*?```/g, (block) => {
|
|
106
|
+
chunks.push(block);
|
|
107
|
+
return "\n";
|
|
108
|
+
});
|
|
109
|
+
for (const m of rest.matchAll(/`[^`]+`/g)) chunks.push(m[0]);
|
|
110
|
+
const hay = chunks.join("\n");
|
|
111
|
+
for (const m of hay.matchAll(/(?:npx\s+doctrina-cli|doctrina)\s+([a-z][a-z-]+)/g)) {
|
|
112
|
+
out.add(m[1]);
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Shared parser for the "## Acceptance criteria" section of a spec.
|
|
2
|
+
//
|
|
3
|
+
// A criterion is a numbered item that MAY span several lines (continuation
|
|
4
|
+
// prose), and its evidence is cited as a backtick path span on ANY of those
|
|
5
|
+
// lines. A single-line parser silently misses proof sitting on a continuation
|
|
6
|
+
// line — the bug that made `doctrina why` print "no evidence cited" for a
|
|
7
|
+
// criterion `doctrina coverage` counted as covered. Both commands (and the
|
|
8
|
+
// validate honest-gate) read criteria through this one parser so they can
|
|
9
|
+
// never disagree about what a criterion cites.
|
|
10
|
+
//
|
|
11
|
+
// Returns one row per criterion: { n, marker, body, proofPaths } where marker
|
|
12
|
+
// is the lowercased `[tag]` (e.g. "verified", "unverified") or null, and
|
|
13
|
+
// proofPaths is the de-duplicated list of file-path-looking backtick tokens.
|
|
14
|
+
|
|
15
|
+
export function parseAcceptanceCriteria(text) {
|
|
16
|
+
const lines = text.split(/\r?\n/);
|
|
17
|
+
let inSection = false;
|
|
18
|
+
const rows = [];
|
|
19
|
+
let cur = null;
|
|
20
|
+
const flush = () => {
|
|
21
|
+
if (cur) {
|
|
22
|
+
cur.proofPaths = backtickPaths(cur.body);
|
|
23
|
+
rows.push(cur);
|
|
24
|
+
}
|
|
25
|
+
cur = null;
|
|
26
|
+
};
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
if (/^##\s+/.test(line)) {
|
|
29
|
+
if (inSection) {
|
|
30
|
+
flush();
|
|
31
|
+
inSection = false;
|
|
32
|
+
}
|
|
33
|
+
if (/^##\s+Acceptance criteria\b/i.test(line)) inSection = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (!inSection) continue;
|
|
37
|
+
const m = line.match(/^\s*(\d+)\.\s+(.*)$/);
|
|
38
|
+
if (m) {
|
|
39
|
+
flush();
|
|
40
|
+
const body = m[2];
|
|
41
|
+
const markMatch = body.match(/^\[([^\]]+)\]/);
|
|
42
|
+
cur = { n: Number(m[1]), marker: markMatch ? markMatch[1].toLowerCase() : null, body };
|
|
43
|
+
} else if (cur) {
|
|
44
|
+
cur.body += (line.trim() === "" ? " " : " " + line.trim());
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
flush();
|
|
48
|
+
return rows.filter((r) => r.body.trim().length > 0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// True when a criterion asserts it has been verified (the `[verified...]` tag).
|
|
52
|
+
export function isVerified(row) {
|
|
53
|
+
return row.marker !== null && /^verified/.test(row.marker);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function backtickPaths(text) {
|
|
57
|
+
const out = new Set();
|
|
58
|
+
for (const m of text.matchAll(/`([^`]+)`/g)) {
|
|
59
|
+
const token = m[1].trim();
|
|
60
|
+
if (looksLikePath(token)) out.add(token);
|
|
61
|
+
}
|
|
62
|
+
return [...out];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Mirrors the coverage heuristic so the two commands agree: a token is a path
|
|
66
|
+
// if it has a slash or a file extension and is not a URL/placeholder.
|
|
67
|
+
function looksLikePath(s) {
|
|
68
|
+
if (!s) return false;
|
|
69
|
+
if (/^(https?:|mailto:|ftp:|#|@)/i.test(s)) return false;
|
|
70
|
+
if (/[\s<>*?{}]/.test(s)) return false;
|
|
71
|
+
const hasSlash = s.includes("/");
|
|
72
|
+
const hasExt = /\.[a-z0-9]{1,8}$/i.test(s);
|
|
73
|
+
if (!hasSlash && !hasExt) return false;
|
|
74
|
+
if (s.startsWith("-")) return false;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
package/src/lib/index-json.js
CHANGED
package/src/lib/scan.js
CHANGED
|
@@ -192,6 +192,13 @@ export function deriveIndex(projectRoot, current) {
|
|
|
192
192
|
});
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
// Entrypoint — the root AGENTS.md the agent reads first (the hub the
|
|
196
|
+
// AGENTS.md-drift gate in `validate` keeps honest). Registering it closes
|
|
197
|
+
// the machine-readable graph in both directions: a tool that enumerates
|
|
198
|
+
// index.json finds the hub itself, not only the artifacts the hub points at.
|
|
199
|
+
// Path-only, so the field never churns the index.
|
|
200
|
+
out.artifacts.entrypoint = { path: "AGENTS.md" };
|
|
201
|
+
|
|
195
202
|
return out;
|
|
196
203
|
}
|
|
197
204
|
|
|
@@ -35,8 +35,9 @@ so prefer it over writing artifacts by hand. Full list: `doctrina --help`.
|
|
|
35
35
|
|
|
36
36
|
- **Read / orient (any task, first & last):** `doctrina status` (health) ·
|
|
37
37
|
`doctrina context [<cap>] --concat` (read pack) · `doctrina next` (what's
|
|
38
|
-
next) · `doctrina why <cap>` (provenance) · `doctrina
|
|
39
|
-
`doctrina
|
|
38
|
+
next) · `doctrina why <cap>` (provenance) · `doctrina constitution`
|
|
39
|
+
(standing rules) · `doctrina spec list` · `doctrina decision list` ·
|
|
40
|
+
`doctrina search <query>`
|
|
40
41
|
- **Start:** `doctrina work "<prompt>"` · `--from-diff` (backfill from
|
|
41
42
|
existing code) · `--chore` (no-spec infra/docs/build)
|
|
42
43
|
- **Scaffold (manual creation stays available):** `doctrina spec new <cap>`
|
package/templates/README.md
CHANGED
|
@@ -21,6 +21,9 @@ replace. No conditionals, loops, or includes.
|
|
|
21
21
|
| `{{DECISION_NUMBER}}` | Zero-padded ADR number (e.g. `0007`) | next available |
|
|
22
22
|
| `{{DECISION_TITLE}}` | ADR title | required, no default |
|
|
23
23
|
| `{{DECISION_SLUG}}` | kebab-case slug derived from `{{DECISION_TITLE}}` | derived |
|
|
24
|
+
| `{{SKILL_NAME}}` | Slug for a new on-demand skill | required, no default |
|
|
25
|
+
| `{{CONTRACT_NAME}}` | Slug for a new integration contract | required, no default |
|
|
26
|
+
| `{{FRAMEWORK_VERSION}}` | Version of the CLI that scaffolded/stamped the tree | running CLI version |
|
|
24
27
|
| `{{AGENTS_MD_PATH}}` | Path to root `AGENTS.md` from the adapter location | per adapter |
|
|
25
28
|
|
|
26
29
|
CLI authors and template authors share this vocabulary. Adding a token
|
|
@@ -49,13 +52,16 @@ change/
|
|
|
49
52
|
design.md.template
|
|
50
53
|
spec-delta.md.template
|
|
51
54
|
decision.md.template one ADR (Nygard/MADR)
|
|
55
|
+
contract.md.template one integration contract (ports/env/interfaces)
|
|
52
56
|
hooks/
|
|
53
57
|
pre-commit.sample installed by `doctrina hooks install`
|
|
54
58
|
watch.sample on-save validation wrapper (manual wiring)
|
|
55
59
|
adapters/
|
|
56
60
|
claude/CLAUDE.md.template
|
|
61
|
+
claude/.claude/commands/doctrina-*.md.template native slash commands (core loop)
|
|
57
62
|
codex/README.md
|
|
58
63
|
cursor/.cursor/rules/00-doctrina.mdc.template
|
|
64
|
+
cursor/.cursor/commands/doctrina-*.md.template native slash commands (core loop)
|
|
59
65
|
copilot/.github/copilot-instructions.md.template
|
|
60
66
|
gemini/GEMINI.md.template
|
|
61
67
|
aider/CONVENTIONS.md.template
|
|
@@ -76,7 +82,7 @@ adapters/
|
|
|
76
82
|
| `cursor` | `.cursor/rules/00-doctrina.mdc` with `alwaysApply: true` | Cursor's `.mdc` rules live in a dedicated directory; the file is a thin pointer. |
|
|
77
83
|
| `copilot` | `.github/copilot-instructions.md` | GitHub Copilot's repository-level custom instructions path. |
|
|
78
84
|
| `gemini` | `GEMINI.md` at project root | Gemini CLI's native context file. |
|
|
79
|
-
| `aider` | `CONVENTIONS.md` at project root |
|
|
85
|
+
| `aider` | `CONVENTIONS.md` at project root | Read-only context once wired in (`aider --read CONVENTIONS.md`, or `read:` in `.aider.conf.yml`). |
|
|
80
86
|
| `windsurf` | `.windsurfrules` at project root | Windsurf's rules file convention. |
|
|
81
87
|
| `continue` | `.continue/rules/00-doctrina.md` | Continue.dev's rules directory. |
|
|
82
88
|
| `amp` / `devin` / `factory` / `jules` | nothing | AGENTS.md-native (same posture as `codex`); the directory marks the support contract and gives future per-agent affordances a home. |
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# CONVENTIONS.md — {{PROJECT_NAME}}
|
|
2
2
|
|
|
3
|
-
Aider reads this file as read-only context
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Aider reads this file as read-only context once wired in: launch
|
|
4
|
+
with `aider --read CONVENTIONS.md`, or add `read: CONVENTIONS.md`
|
|
5
|
+
to `.aider.conf.yml` so it loads on every run. This project routes
|
|
6
|
+
all operational rules through the open AGENTS.md standard.
|
|
6
7
|
|
|
7
8
|
See `{{AGENTS_MD_PATH}}` for the canonical operational source of
|
|
8
9
|
truth. The Doctrina framework artifacts (specs, decisions,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Load the Doctrina read pack for a task, in read order
|
|
3
|
+
argument-hint: [capability]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Assemble the context for **{{PROJECT_NAME}}** before doing any work.
|
|
7
|
+
|
|
8
|
+
Run `doctrina context $ARGUMENTS --concat` and read the output top to bottom:
|
|
9
|
+
AGENTS.md → product.md → the capability spec(s) → open changes → accepted ADRs.
|
|
10
|
+
With no capability it includes every active spec. It excludes the change
|
|
11
|
+
archive by design — run `doctrina why <capability>` when you need history.
|
|
12
|
+
|
|
13
|
+
Do this for ANY task (review, debug, a question), not only new work.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Show and act on the next recommended Doctrina workflow step
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Decide the next step for **{{PROJECT_NAME}}** from the framework's own signals.
|
|
6
|
+
|
|
7
|
+
1. Run `doctrina next` and `doctrina status` to see open work and gate health.
|
|
8
|
+
2. Do the single highest-priority action it names (resume a change, apply a
|
|
9
|
+
delta, archive, accept an ADR, fix index drift, …).
|
|
10
|
+
3. Re-run `doctrina next` to confirm the item cleared.
|
|
11
|
+
|
|
12
|
+
Use the read-only commands first; never guess what is pending — let the CLI
|
|
13
|
+
report it.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: One-glance Doctrina project health and gate status
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Report the health of **{{PROJECT_NAME}}**.
|
|
6
|
+
|
|
7
|
+
Run `doctrina status` for the dashboard (index drift, framework stamp,
|
|
8
|
+
coverage %, trace anchors, verify config, artifact counts). For the
|
|
9
|
+
authoritative gates run `doctrina validate` and `doctrina verify`, and
|
|
10
|
+
`doctrina next` for the recommended action.
|
|
11
|
+
|
|
12
|
+
Summarise what is green, what is not, and the single next step.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Explain a capability's provenance (intent → proof → ADRs → history)
|
|
3
|
+
argument-hint: <capability>
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Explain why a capability of **{{PROJECT_NAME}}** exists and is shaped as it is.
|
|
7
|
+
|
|
8
|
+
Run `doctrina why $ARGUMENTS` and relay the provenance chain it prints: the
|
|
9
|
+
product intent it Realizes, its purpose and status, the acceptance criteria
|
|
10
|
+
that prove it (with cited evidence), the accepted ADRs that decided it, and
|
|
11
|
+
the archived changes that built it. Read-only — answer from the artifacts,
|
|
12
|
+
don't grep the tree by hand.
|