doctrina-cli 0.15.1 → 0.16.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/adapter.js +24 -1
- package/src/commands/analyze.js +8 -212
- package/src/commands/change.js +143 -217
- package/src/commands/ci.js +182 -0
- package/src/commands/clarify.js +58 -21
- package/src/commands/close.js +291 -42
- package/src/commands/constitution.js +14 -91
- package/src/commands/context.js +278 -107
- package/src/commands/contract.js +95 -75
- package/src/commands/coverage.js +58 -273
- package/src/commands/decision.js +54 -7
- package/src/commands/doctor.js +292 -129
- package/src/commands/handoff.js +10 -57
- package/src/commands/hooks.js +11 -2
- package/src/commands/index-rebuild.js +9 -48
- package/src/commands/init.js +70 -13
- package/src/commands/intake.js +30 -92
- package/src/commands/metrics.js +53 -84
- package/src/commands/next.js +105 -147
- package/src/commands/prime.js +24 -91
- package/src/commands/report.js +50 -108
- 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 +16 -25
- package/src/commands/spec.js +26 -5
- package/src/commands/status.js +51 -172
- package/src/commands/templates.js +31 -138
- package/src/commands/trace.js +36 -71
- package/src/commands/triage.js +4 -82
- package/src/commands/validate.js +14 -871
- package/src/commands/verify.js +60 -138
- package/src/commands/why.js +2 -2
- package/src/commands/work.js +234 -263
- package/src/index.js +106 -7
- package/src/lib/actions.js +484 -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/change-ops.js +131 -0
- package/src/lib/commands.js +56 -4
- package/src/lib/config.js +206 -0
- package/src/lib/constitution-model.js +102 -0
- package/src/lib/coverage-model.js +392 -0
- package/src/lib/criteria.js +23 -0
- package/src/lib/doc-model.js +326 -0
- package/src/lib/docs-impact.js +196 -27
- package/src/lib/flag-catalog.js +0 -16
- package/src/lib/fs-ops.js +58 -1
- package/src/lib/gates.js +181 -23
- package/src/lib/git.js +132 -0
- package/src/lib/index-json.js +2 -19
- package/src/lib/intake-model.js +86 -0
- package/src/lib/json-out.js +100 -2
- package/src/lib/ledger.js +192 -0
- package/src/lib/lexicon.js +191 -0
- package/src/lib/metrics-model.js +159 -0
- package/src/lib/names.js +42 -0
- package/src/lib/playbook.js +97 -0
- package/src/lib/project.js +57 -0
- package/src/lib/reproducibility.js +137 -0
- package/src/lib/runtime.js +191 -5
- package/src/lib/scan.js +204 -13
- package/src/lib/signoff.js +147 -0
- package/src/lib/snapshot.js +209 -0
- package/src/lib/spec-ops.js +50 -5
- package/src/lib/templates-model.js +262 -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 +1299 -0
- package/src/lib/version.js +22 -0
- package/src/lib/views.js +410 -0
- package/src/lib/work-model.js +77 -0
- package/templates/AGENTS.md.template +20 -26
- package/templates/README.md +35 -0
- package/templates/change/proposal.md.template +1 -0
- package/templates/hooks/pre-commit.sample +6 -2
- package/templates/playbooks/bootstrap.md.template +43 -0
- package/templates/playbooks/chore.md.template +19 -0
- package/templates/playbooks/work.md.template +76 -0
package/src/lib/version.js
CHANGED
|
@@ -11,6 +11,28 @@ import { readFileSync } from "node:fs";
|
|
|
11
11
|
|
|
12
12
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
|
|
14
|
+
// Numeric semver comparison ("0.15.10" sorts above "0.15.9"); anything
|
|
15
|
+
// that is not X.Y.Z sorts lowest, so a garbage stamp is always migrated.
|
|
16
|
+
export function compareVersions(a, b) {
|
|
17
|
+
const parse = (v) => {
|
|
18
|
+
const m = String(v ?? "").match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
19
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
20
|
+
};
|
|
21
|
+
const pa = parse(a); const pb = parse(b);
|
|
22
|
+
if (!pa && !pb) return 0;
|
|
23
|
+
if (!pa) return -1;
|
|
24
|
+
if (!pb) return 1;
|
|
25
|
+
for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1;
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The stamp to WRITE: the newer of what the index holds and what runs
|
|
30
|
+
// (change 0116). An older CLI rebuilding a newer tree keeps the newer
|
|
31
|
+
// stamp; a newer CLI migrates an older one forward. Never rewound.
|
|
32
|
+
export function newestVersion(current, running) {
|
|
33
|
+
return compareVersions(current, running) > 0 ? String(current) : String(running);
|
|
34
|
+
}
|
|
35
|
+
|
|
14
36
|
export function cliVersion() {
|
|
15
37
|
// src/lib/version.js -> packages/doctrina-cli/package.json
|
|
16
38
|
const pkgPath = path.resolve(here, "..", "..", "package.json");
|
package/src/lib/views.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { c } from "./colors.js";
|
|
3
|
+
import { today } from "./dates.js";
|
|
4
|
+
import { churnByCapability } from "./ledger.js";
|
|
5
|
+
|
|
6
|
+
// Executed proof and signed proof are different claims (change 0039). One
|
|
7
|
+
// phrasing, used by every view, so none of them can quietly add the two
|
|
8
|
+
// together and call the total green.
|
|
9
|
+
function signoffNote(verify, { verbose = false } = {}) {
|
|
10
|
+
const s = verify.signoffs;
|
|
11
|
+
if (!s || s.manual === 0) return "";
|
|
12
|
+
const stale = s.expired + s.unverifiable + s.pending;
|
|
13
|
+
if (stale === 0) return verbose ? ` (${s.manual} signed, all current)` : ` · ${s.manual} signed`;
|
|
14
|
+
const parts = [];
|
|
15
|
+
if (s.expired) parts.push(`${s.expired} expired`);
|
|
16
|
+
if (s.unverifiable) parts.push(`${s.unverifiable} unverifiable`);
|
|
17
|
+
if (s.pending) parts.push(`${s.pending} unsigned`);
|
|
18
|
+
return ` (${s.manual} signed, ${parts.join(", ")})`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The four VIEWS of one snapshot (audit finding F7).
|
|
22
|
+
//
|
|
23
|
+
// `status`, `prime`, `handoff` and `report` differ only in shape: a
|
|
24
|
+
// dashboard, a session primer, a Markdown resume note, a period digest. They
|
|
25
|
+
// were four commands, each re-collecting the tree and reaching into the
|
|
26
|
+
// others' modules for the parts it did not collect itself.
|
|
27
|
+
//
|
|
28
|
+
// Each function below takes the snapshot lib/snapshot.js already produced and
|
|
29
|
+
// returns lines. They are pure: no reads, no writes, no process exits — which
|
|
30
|
+
// is what makes "the four views agree" a property of the code rather than a
|
|
31
|
+
// promise, and what lets `doctrina status --view <name>` render any of them.
|
|
32
|
+
|
|
33
|
+
export const VIEWS = ["dashboard", "prime", "handoff", "report", "rules"];
|
|
34
|
+
|
|
35
|
+
// Count the recorded lanes, keeping "unknown" as its own row rather than
|
|
36
|
+
// dropping it: a mix that silently omits the changes it could not classify
|
|
37
|
+
// reports a cleaner project than the one that exists.
|
|
38
|
+
function laneMix(records) {
|
|
39
|
+
const counts = new Map();
|
|
40
|
+
let overridden = 0;
|
|
41
|
+
let total = 0;
|
|
42
|
+
for (const raw of records) {
|
|
43
|
+
total += 1;
|
|
44
|
+
if (!raw) {
|
|
45
|
+
counts.set("unknown", (counts.get("unknown") ?? 0) + 1);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const lane = String(raw).split(/[\s(]/)[0] || "unknown";
|
|
49
|
+
counts.set(lane, (counts.get(lane) ?? 0) + 1);
|
|
50
|
+
if (/opened as|opened anyway/.test(raw)) overridden += 1;
|
|
51
|
+
}
|
|
52
|
+
return { total, overridden, rows: [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Render one named view. Unknown names are a caller error, not a fallback. */
|
|
56
|
+
// One rendering of the coverage ratio, shared by every view (change 0037
|
|
57
|
+
// unified the collector; this is the same argument for the label). A pct of
|
|
58
|
+
// null means no criterion was declared — there is nothing to be a ratio OF,
|
|
59
|
+
// so the views say so instead of printing a score. `trace` has always said
|
|
60
|
+
// "no anchors declared" for its own empty case; coverage now matches it.
|
|
61
|
+
export function coverageLabel(cov) {
|
|
62
|
+
if (cov.pct === null) return `no criteria declared (0/0)`;
|
|
63
|
+
return `${cov.pct}% (${cov.totalCovered}/${cov.totalCriteria} criteria)`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function renderView(name, snapshot, options = {}) {
|
|
67
|
+
switch (name) {
|
|
68
|
+
case "dashboard": return dashboard(snapshot);
|
|
69
|
+
case "prime": return prime(snapshot);
|
|
70
|
+
case "handoff": return handoff(snapshot);
|
|
71
|
+
case "report": return report(snapshot, options);
|
|
72
|
+
case "rules": return rules(snapshot);
|
|
73
|
+
default: throw new Error(`unknown view "${name}" (declared: ${VIEWS.join(", ")})`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --------------------------------------------------------------- dashboard
|
|
78
|
+
|
|
79
|
+
export function dashboard(s) {
|
|
80
|
+
const out = [];
|
|
81
|
+
out.push(c.bold("Doctrina status") + c.gray(` — ${s.project} (framework ${s.stamp ?? "—"} / CLI ${s.cli})`));
|
|
82
|
+
out.push("");
|
|
83
|
+
|
|
84
|
+
out.push(c.bold(" Gates"));
|
|
85
|
+
let driftLine;
|
|
86
|
+
if (s.indexState === "missing") {
|
|
87
|
+
driftLine = c.red("missing/unreadable") + c.gray(" — run `doctrina index rebuild`");
|
|
88
|
+
} else if (s.indexState === "in-sync") {
|
|
89
|
+
driftLine = c.green("in sync");
|
|
90
|
+
} else {
|
|
91
|
+
driftLine = c.yellow("drifted") + c.gray(" — run `doctrina validate --fix`");
|
|
92
|
+
}
|
|
93
|
+
out.push(` ${"index".padEnd(11)} ${driftLine}`);
|
|
94
|
+
|
|
95
|
+
const stampLine = s.stamp === s.cli
|
|
96
|
+
? c.green("current")
|
|
97
|
+
: c.yellow(`${s.stamp ?? "—"} (CLI ${s.cli})`) + c.gray(" — `doctrina index rebuild`");
|
|
98
|
+
out.push(` ${"stamp".padEnd(11)} ${stampLine}`);
|
|
99
|
+
|
|
100
|
+
const cov = s.coverage;
|
|
101
|
+
const covExtra = cov.totalDangling + cov.totalConditional > 0
|
|
102
|
+
? c.yellow(` ${cov.totalDangling} dangling, ${cov.totalConditional} conditional`)
|
|
103
|
+
: "";
|
|
104
|
+
if (cov.pct === null) {
|
|
105
|
+
out.push(` ${"coverage".padEnd(11)} ${c.yellow("no criteria declared")} ${c.gray("(0/0)")}`);
|
|
106
|
+
} else {
|
|
107
|
+
const covColor = cov.pct === 100 ? c.green : cov.pct >= 50 ? c.yellow : c.red;
|
|
108
|
+
out.push(` ${"coverage".padEnd(11)} ${covColor(`${cov.pct}%`)} ${c.gray(`(${cov.totalCovered}/${cov.totalCriteria} criteria)`)}${covExtra}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const tr = s.trace;
|
|
112
|
+
const trExtra = tr.untraceable + tr.dropped + tr.dangling > 0
|
|
113
|
+
? c.yellow(` ${tr.dropped} dropped, ${tr.untraceable} untraceable`)
|
|
114
|
+
: "";
|
|
115
|
+
const trColor = tr.anchors === 0 ? c.gray : (tr.realized === tr.anchors && tr.untraceable === 0 ? c.green : c.yellow);
|
|
116
|
+
const trText = tr.anchors === 0 ? "no anchors" : `${tr.realized}/${tr.anchors} anchors`;
|
|
117
|
+
out.push(` ${"trace".padEnd(11)} ${trColor(trText)}${trExtra}`);
|
|
118
|
+
|
|
119
|
+
let verifyLine = c.gray("not configured") + c.gray(" — `doctrina verify --init`");
|
|
120
|
+
if (s.verify.invalid) {
|
|
121
|
+
verifyLine = c.red("invalid JSON");
|
|
122
|
+
} else if (s.verify.configured) {
|
|
123
|
+
const n = s.verify.checks;
|
|
124
|
+
const stale = s.verify.signoffs
|
|
125
|
+
? s.verify.signoffs.expired + s.verify.signoffs.unverifiable + s.verify.signoffs.pending
|
|
126
|
+
: 0;
|
|
127
|
+
verifyLine = c.cyan(`${n} check${n === 1 ? "" : "s"}`) +
|
|
128
|
+
(stale > 0 ? c.yellow(signoffNote(s.verify)) : c.gray(signoffNote(s.verify))) +
|
|
129
|
+
c.gray(" — run `doctrina verify`");
|
|
130
|
+
}
|
|
131
|
+
out.push(` ${"verify".padEnd(11)} ${verifyLine}`);
|
|
132
|
+
|
|
133
|
+
out.push("");
|
|
134
|
+
out.push(c.bold(" Work"));
|
|
135
|
+
const implBreak = Object.entries(s.specs.impl).map(([k, v]) => `${v} ${k}`).join(", ");
|
|
136
|
+
out.push(` ${"specs".padEnd(11)} ${s.specs.total}${implBreak ? c.gray(` (${implBreak})`) : ""}`);
|
|
137
|
+
out.push(` ${"changes".padEnd(11)} ${s.specs.openChanges} open`);
|
|
138
|
+
const adrNotes = [];
|
|
139
|
+
if (s.decisions.proposed > 0) adrNotes.push(c.yellow(`${s.decisions.proposed} proposed`));
|
|
140
|
+
if (s.decisions.bare > 0) adrNotes.push(c.yellow(`${s.decisions.bare} unproven`));
|
|
141
|
+
out.push(` ${"decisions".padEnd(11)} ${s.decisions.total}${adrNotes.length ? c.gray(" (") + adrNotes.join(c.gray(", ")) + c.gray(")") : ""}`);
|
|
142
|
+
out.push(` ${"skills".padEnd(11)} ${s.skills}`);
|
|
143
|
+
|
|
144
|
+
out.push("");
|
|
145
|
+
out.push(c.gray(" Full gates: `doctrina validate` · `doctrina verify`. Next step: `doctrina next`."));
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ------------------------------------------------------------------- prime
|
|
150
|
+
|
|
151
|
+
export function prime(s) {
|
|
152
|
+
const out = [];
|
|
153
|
+
out.push(c.bold("Doctrina prime") + c.gray(` — ${s.project} (framework ${s.stamp ?? "—"} / CLI ${s.cli})`));
|
|
154
|
+
out.push("");
|
|
155
|
+
|
|
156
|
+
const cov = s.coverage.pct === null
|
|
157
|
+
? "no criteria declared"
|
|
158
|
+
: `${s.coverage.pct}% (${s.coverage.totalCovered}/${s.coverage.totalCriteria})`;
|
|
159
|
+
const tr = s.trace.anchors === 0 ? "no anchors" : `${s.trace.realized}/${s.trace.anchors}`;
|
|
160
|
+
const verify = s.verify.configured
|
|
161
|
+
? `${s.verify.checks} verify checks${signoffNote(s.verify)}`
|
|
162
|
+
: "verify not configured";
|
|
163
|
+
out.push(c.bold("Gates ") + `index ${s.indexState} · coverage ${cov} · trace ${tr} · ${verify}`);
|
|
164
|
+
const implBreak = Object.entries(s.specs.impl).map(([k, v]) => `${v} ${k}`).join(", ");
|
|
165
|
+
out.push(
|
|
166
|
+
c.bold("Work ") +
|
|
167
|
+
`${s.specs.total} specs${implBreak ? ` (${implBreak})` : ""} · ${s.specs.openChanges} open change${s.specs.openChanges === 1 ? "" : "s"} · ` +
|
|
168
|
+
`${s.decisions.total} decisions · ${s.skills} skills`,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
out.push("");
|
|
172
|
+
out.push(c.bold("Rules") + c.gray(` (${s.adrs.length} accepted ADRs — \`doctrina prime --rules\` for detail)`));
|
|
173
|
+
for (const a of s.adrs) out.push(` ${c.cyan(a.id)} ${a.title}`);
|
|
174
|
+
if (s.nonGoals.length > 0) {
|
|
175
|
+
out.push(c.gray(` + ${s.nonGoals.length} non-goal${s.nonGoals.length === 1 ? "" : "s"} declared in product.md`));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
out.push("");
|
|
179
|
+
out.push(c.bold("Open work"));
|
|
180
|
+
if (s.openChanges.length === 0) {
|
|
181
|
+
out.push(c.gray(" none — `doctrina work \"<prompt>\"` opens the next change"));
|
|
182
|
+
} else {
|
|
183
|
+
for (const ch of s.openChanges) {
|
|
184
|
+
const tasks = ch.tasksTotal > 0 ? ` · tasks ${ch.tasksDone}/${ch.tasksTotal}` : "";
|
|
185
|
+
out.push(` ${c.cyan(ch.id)} ${ch.title ?? ""}${c.gray(` (${ch.status}${tasks})`)}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
out.push("");
|
|
190
|
+
out.push(c.bold("Next"));
|
|
191
|
+
if (s.actions.length === 0) {
|
|
192
|
+
out.push(c.gray(" nothing pending — pick up new work"));
|
|
193
|
+
} else {
|
|
194
|
+
s.actions.slice(0, 5).forEach((a, i) => out.push(` ${i + 1}. ${a.text}`));
|
|
195
|
+
if (s.actions.length > 5) out.push(c.gray(` … ${s.actions.length - 5} more — \`doctrina next\``));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
out.push("");
|
|
199
|
+
out.push(c.gray("Read deeper: `doctrina context [<cap>] --concat` · `doctrina why <cap>` · `doctrina show <ref>`"));
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ----------------------------------------------------------------- handoff
|
|
204
|
+
|
|
205
|
+
export function handoff(s) {
|
|
206
|
+
const out = [];
|
|
207
|
+
out.push(`# Doctrina handoff — ${s.project} (${today()})`);
|
|
208
|
+
out.push("");
|
|
209
|
+
|
|
210
|
+
out.push("## Where things stand");
|
|
211
|
+
out.push("");
|
|
212
|
+
out.push(`- index: ${s.indexState} · framework stamp: ${s.stamp ?? "—"} (CLI ${s.cli})`);
|
|
213
|
+
out.push(`- coverage: ${coverageLabel(s.coverage)}` +
|
|
214
|
+
(s.coverage.totalDangling ? `, ${s.coverage.totalDangling} dangling` : "") +
|
|
215
|
+
(s.coverage.totalConditional ? `, ${s.coverage.totalConditional} conditional` : ""));
|
|
216
|
+
const tr = s.trace.anchors === 0 ? "no anchors declared" : `${s.trace.realized}/${s.trace.anchors} anchors realized`;
|
|
217
|
+
out.push(`- trace: ${tr}` + (s.trace.untraceable ? ` (${s.trace.untraceable} untraceable)` : ""));
|
|
218
|
+
out.push(`- verify: ${s.verify.configured ? `${s.verify.checks} checks declared${signoffNote(s.verify, { verbose: true })} — run \`doctrina verify\`` : "not configured"}`);
|
|
219
|
+
|
|
220
|
+
out.push("");
|
|
221
|
+
if (s.openChanges.length === 0) {
|
|
222
|
+
out.push("## Open work");
|
|
223
|
+
out.push("");
|
|
224
|
+
out.push("- none — the tree is at rest; start with `doctrina work \"<prompt>\"`");
|
|
225
|
+
} else {
|
|
226
|
+
for (const ch of s.openChanges) {
|
|
227
|
+
out.push(`## Open change \`${ch.id}\`${ch.title ? ` — ${ch.title}` : ""}`);
|
|
228
|
+
out.push("");
|
|
229
|
+
out.push(`- proposal status: ${ch.status}`);
|
|
230
|
+
if (ch.tasksTotal > 0) {
|
|
231
|
+
out.push(`- tasks: ${ch.tasksDone}/${ch.tasksTotal} checked`);
|
|
232
|
+
for (const t of ch.unchecked.slice(0, 8)) out.push(` - [ ] ${t}`);
|
|
233
|
+
if (ch.unchecked.length > 8) out.push(` - … ${ch.unchecked.length - 8} more in tasks.md`);
|
|
234
|
+
} else {
|
|
235
|
+
out.push("- tasks: no tasks.md checklist found");
|
|
236
|
+
}
|
|
237
|
+
out.push(`- resume with: \`doctrina work --resume ${ch.id}\` · close with: \`doctrina close ${ch.id}\``);
|
|
238
|
+
out.push("");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
out.push("## Next actions");
|
|
243
|
+
out.push("");
|
|
244
|
+
if (s.actions.length === 0) {
|
|
245
|
+
out.push("1. nothing pending — `doctrina next` will confirm; pick up new work");
|
|
246
|
+
} else {
|
|
247
|
+
s.actions.forEach((a, i) => out.push(`${i + 1}. ${a.text}`));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
out.push("");
|
|
251
|
+
out.push("*Generated read-only from the tree — regenerate anytime with `doctrina handoff`.*");
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ------------------------------------------------------------------- rules
|
|
256
|
+
//
|
|
257
|
+
// The project's standing rules, in full: the accepted ADRs and the product's
|
|
258
|
+
// declared non-goals. Assembled, never owned — to change a principle you
|
|
259
|
+
// supersede its ADR, to change a non-goal you edit product.md.
|
|
260
|
+
//
|
|
261
|
+
// One rendering, two callers (change 0049). `prime --rules` prints it, and
|
|
262
|
+
// `doctrina constitution` — deprecated, kept working — prints the same lines,
|
|
263
|
+
// so the merge is demonstrable rather than asserted. `prime` on its own still
|
|
264
|
+
// shows the ADR titles and a non-goal COUNT: the primer is a fixed-size read,
|
|
265
|
+
// and the full list is what `--rules` is for.
|
|
266
|
+
export function rules(s) {
|
|
267
|
+
const out = [];
|
|
268
|
+
out.push(c.bold("Standing rules") + c.gray(` — ${s.project} (accepted decisions + non-goals)`));
|
|
269
|
+
out.push("");
|
|
270
|
+
out.push(c.bold(" Principles") + c.gray(" (immutable — supersede an ADR to change one)"));
|
|
271
|
+
if (s.adrs.length === 0) {
|
|
272
|
+
out.push(` ${c.gray("no accepted ADRs yet — record decisions with `doctrina decision new`")}`);
|
|
273
|
+
} else {
|
|
274
|
+
for (const a of s.adrs) out.push(` ${c.cyan("ADR " + a.id)} ${a.title}`);
|
|
275
|
+
}
|
|
276
|
+
out.push("");
|
|
277
|
+
out.push(c.bold(" Non-goals") + c.gray(" (.doctrina/product.md)"));
|
|
278
|
+
if (s.nonGoals.length === 0) {
|
|
279
|
+
// Name the act that is actually missing (rule C2): telling an author to
|
|
280
|
+
// add a section they already wrote is a remedy that resolves nothing.
|
|
281
|
+
out.push(` ${c.gray(s.hasNonGoalsSection
|
|
282
|
+
? "none declared — the `## Non-goals` section of product.md is empty"
|
|
283
|
+
: "none declared — add a `## Non-goals` section to product.md")}`);
|
|
284
|
+
} else {
|
|
285
|
+
for (const g of s.nonGoals) out.push(` ${c.gray("•")} ${g}`);
|
|
286
|
+
}
|
|
287
|
+
out.push("");
|
|
288
|
+
out.push(c.gray(` ${s.adrs.length} accepted decision${s.adrs.length === 1 ? "" : "s"} · ` +
|
|
289
|
+
`${s.nonGoals.length} non-goal${s.nonGoals.length === 1 ? "" : "s"} · read-only`));
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ------------------------------------------------------------------ report
|
|
294
|
+
|
|
295
|
+
function asPct(x) {
|
|
296
|
+
return typeof x === "number" ? `${(x * 100).toFixed(1)}%` : "—";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @param {object} s the snapshot
|
|
301
|
+
* @param {{ days?: number, cutoffIso?: string, git?: any, gitState?: any, metrics?: any }} [options]
|
|
302
|
+
*/
|
|
303
|
+
export function report(s, { days = 7, cutoffIso = "", git = null, gitState = null, metrics = null } = {}) {
|
|
304
|
+
const out = [];
|
|
305
|
+
out.push(`# Doctrina report — ${s.project} (${cutoffIso} → ${today()})`);
|
|
306
|
+
out.push("");
|
|
307
|
+
|
|
308
|
+
out.push("## Gates");
|
|
309
|
+
out.push("");
|
|
310
|
+
out.push(`- index: ${s.indexState} · framework stamp: ${s.stamp ?? "—"} (CLI ${s.cli})`);
|
|
311
|
+
out.push(`- coverage: ${coverageLabel(s.coverage)}` +
|
|
312
|
+
(s.coverage.totalDangling ? ` — ${s.coverage.totalDangling} dangling` : "") +
|
|
313
|
+
(s.coverage.totalConditional ? ` — ${s.coverage.totalConditional} conditional` : ""));
|
|
314
|
+
out.push(`- trace: ${s.trace.anchors === 0 ? "no anchors declared" : `${s.trace.realized}/${s.trace.anchors} anchors realized`}`);
|
|
315
|
+
out.push(`- verify: ${s.verify.configured ? `${s.verify.checks} checks declared${signoffNote(s.verify, { verbose: true })}` : "not configured"}`);
|
|
316
|
+
|
|
317
|
+
out.push("");
|
|
318
|
+
out.push("## Changes");
|
|
319
|
+
out.push("");
|
|
320
|
+
const archived = s.archive
|
|
321
|
+
.filter((ch) => typeof ch.applied === "string" && ch.applied >= cutoffIso)
|
|
322
|
+
.sort((a, b) => String(a.applied).localeCompare(String(b.applied)));
|
|
323
|
+
if (archived.length === 0) {
|
|
324
|
+
out.push(`- archived in period: none`);
|
|
325
|
+
} else {
|
|
326
|
+
out.push(`- archived in period: ${archived.length}`);
|
|
327
|
+
for (const ch of archived) out.push(` - ${ch.applied} — \`${ch.id}\` ${ch.title}`);
|
|
328
|
+
}
|
|
329
|
+
if (s.openChanges.length === 0) {
|
|
330
|
+
out.push("- open now: none");
|
|
331
|
+
} else {
|
|
332
|
+
out.push(`- open now: ${s.openChanges.length}`);
|
|
333
|
+
for (const ch of s.openChanges) {
|
|
334
|
+
const tasks = ch.tasksTotal > 0 ? ` (tasks ${ch.tasksDone}/${ch.tasksTotal})` : "";
|
|
335
|
+
out.push(` - \`${ch.id}\`${ch.title ? ` ${ch.title}` : ""}${tasks}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// What KIND of work the period held (change 0042). The classifier's verdict
|
|
340
|
+
// used to be computed, printed and thrown away, so no report could answer
|
|
341
|
+
// this — and the classifier had no set of right and wrong answers to be
|
|
342
|
+
// calibrated against. A change with no recorded lane is counted as unknown,
|
|
343
|
+
// never folded into a lane it might not belong to.
|
|
344
|
+
const lanes = laneMix([...archived.map((ch) => ch.lane ?? null), ...s.openLanes]);
|
|
345
|
+
if (lanes.total > 0) {
|
|
346
|
+
out.push("");
|
|
347
|
+
out.push("## Lanes");
|
|
348
|
+
out.push("");
|
|
349
|
+
for (const [lane, n] of lanes.rows) out.push(`- ${lane}: ${n}`);
|
|
350
|
+
if (lanes.overridden > 0) {
|
|
351
|
+
out.push(`- of which the operator opened a different lane than read: ${lanes.overridden}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// WHERE the work landed, from the ledger rather than from git (change
|
|
356
|
+
// 0046). git counts files; the ledger counts capabilities, which is the
|
|
357
|
+
// unit the specs, the gates and this report are all written in. The number
|
|
358
|
+
// is reported, never judged: a capability that moves often may be badly
|
|
359
|
+
// drawn or may simply be where the work is, and nothing here can tell those
|
|
360
|
+
// apart (ADR 0005).
|
|
361
|
+
const churn = churnByCapability(s.ledger ?? [], { since: cutoffIso });
|
|
362
|
+
if (churn.length > 0) {
|
|
363
|
+
out.push("");
|
|
364
|
+
out.push("## Capability churn (in period)");
|
|
365
|
+
out.push("");
|
|
366
|
+
for (const row of churn) {
|
|
367
|
+
out.push(`- ${row.capability}: ${row.changes} change${row.changes === 1 ? "" : "s"} (last ${row.last})`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
out.push("");
|
|
372
|
+
out.push("## Artifacts");
|
|
373
|
+
out.push("");
|
|
374
|
+
out.push(`- specs: ${s.specs.total} · decisions: ${s.decisions.total}` +
|
|
375
|
+
(s.decisions.proposed ? ` (${s.decisions.proposed} proposed)` : "") +
|
|
376
|
+
` · skills: ${s.skills}`);
|
|
377
|
+
|
|
378
|
+
out.push("");
|
|
379
|
+
out.push("## Git (local, last " + days + " days)");
|
|
380
|
+
out.push("");
|
|
381
|
+
if (!git) {
|
|
382
|
+
// Name the ACTUAL condition, from the one git door (change 0040), not a
|
|
383
|
+
// list of causes that might apply. In a freshly `git init`ed repository
|
|
384
|
+
// this said "not a repository, or git not installed" — two causes, both
|
|
385
|
+
// false, sending the reader to look for an installation problem that was
|
|
386
|
+
// not there, while `metrics` read the same state and got it right
|
|
387
|
+
// (change 0080).
|
|
388
|
+
const why = gitState?.reason;
|
|
389
|
+
out.push(`- no git history available${why ? ` — ${why}` : ""}`);
|
|
390
|
+
} else {
|
|
391
|
+
out.push(`- commits: ${git.commits} (${git.fixes} fix-shaped, ${git.commits ? Math.round((git.fixes / git.commits) * 100) : 0}%)`);
|
|
392
|
+
if (git.churn.length > 0) {
|
|
393
|
+
out.push(`- top-churn files:`);
|
|
394
|
+
for (const [file, n] of git.churn) out.push(` - ${file} (${n} touches)`);
|
|
395
|
+
}
|
|
396
|
+
// The rework proxies for the same window, from the same snapshot
|
|
397
|
+
// `doctrina metrics` renders (change 0050) — reported, never judged:
|
|
398
|
+
// both rates move with team size and release cadence, and iterative
|
|
399
|
+
// work trips the re-edit proxy exactly like rework does.
|
|
400
|
+
if (metrics) {
|
|
401
|
+
out.push(`- reverts: ${metrics.reverts} (${asPct(metrics.revert_rate)} of commits)`);
|
|
402
|
+
out.push(`- ${metrics.reedit_window_days}-day re-edit rate: ${asPct(metrics.reedit_rate)}` +
|
|
403
|
+
" (touches a file edited in the prior window — a proxy for rework, not a verdict)");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
out.push("");
|
|
408
|
+
out.push(`*Generated read-only by \`doctrina report --since ${days}\`; deeper numbers: \`doctrina metrics\`.*`);
|
|
409
|
+
return out;
|
|
410
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Ranking capabilities — by a set of changed files, or by a prompt.
|
|
3
|
+
//
|
|
4
|
+
// `review` needs the diff ranker to map a diff onto the specs it touches, and
|
|
5
|
+
// reached into `commands/work.js` for it (audit finding F7). Both rankers are
|
|
6
|
+
// pure functions over the spec tree, and `work` now ACTS on the prompt one
|
|
7
|
+
// (it scaffolds the winner's delta), so neither belongs in a command module:
|
|
8
|
+
// ADR 0025 — command modules render, shared logic lives in lib/.
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { readdirSync } from "node:fs";
|
|
11
|
+
import { isDir, isFile, read } from "./fs-ops.js";
|
|
12
|
+
import { score, terms } from "./lexicon.js";
|
|
13
|
+
import { parseSourceGlobs } from "./scan.js";
|
|
14
|
+
import { globToRegExp } from "./runtime.js";
|
|
15
|
+
// Rank capabilities by the working tree, not the prompt (review F10): a changed
|
|
16
|
+
// file scores its capability when the file sits under a path segment named for
|
|
17
|
+
// it, or when the spec cites the file as evidence. A deterministic overlap
|
|
18
|
+
// hint for the agent — never a decision (ADR 0005).
|
|
19
|
+
export function rankCapabilitiesByDiff(projectRoot, files, { limit = 3 } = {}) {
|
|
20
|
+
const specsDir = path.join(projectRoot, ".doctrina", "specs");
|
|
21
|
+
if (!files || files.length === 0 || !isDir(specsDir)) return [];
|
|
22
|
+
const norm = files.map((f) => f.replace(/\\/g, "/"));
|
|
23
|
+
|
|
24
|
+
const ranked = [];
|
|
25
|
+
for (const cap of readdirSync(specsDir).sort()) {
|
|
26
|
+
const specPath = path.join(specsDir, cap, "spec.md");
|
|
27
|
+
if (!isFile(specPath)) continue;
|
|
28
|
+
const specText = read(specPath);
|
|
29
|
+
// A DECLARED glob outranks every inference: the spec said this code is
|
|
30
|
+
// its own, and a declaration is not a guess (change 0077).
|
|
31
|
+
const declared = parseSourceGlobs(specText).map((g) => globToRegExp(g));
|
|
32
|
+
let score = 0;
|
|
33
|
+
for (const f of norm) {
|
|
34
|
+
if (declared.some((re) => re.test(f))) score += 10; // the spec claims it
|
|
35
|
+
if (f.split("/").includes(cap)) score += 3; // src/<cap>/... etc.
|
|
36
|
+
if (specText.includes(f)) score += 5; // file cited in the spec
|
|
37
|
+
else if (specText.includes(path.basename(f))) score += 2; // filename cited
|
|
38
|
+
}
|
|
39
|
+
if (score > 0) ranked.push({ id: cap, score, path: `.doctrina/specs/${cap}/spec.md` });
|
|
40
|
+
}
|
|
41
|
+
// `limit` keeps the work-playbook hint short (top 3); review passes
|
|
42
|
+
// Infinity — truncating there is how it missed 5 of 8 touched capabilities
|
|
43
|
+
// in the 0.11.0 field session.
|
|
44
|
+
return ranked.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id)).slice(0, limit);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Deterministic term overlap: fold prompt and spec text with the SHARED
|
|
48
|
+
// lexicon, then score. This is a hint for the agent, never a decision
|
|
49
|
+
// (ADR 0005).
|
|
50
|
+
//
|
|
51
|
+
// Before change 0040 this carried its own stop list and its own fold, while
|
|
52
|
+
// `context --for` carried different ones — and the work playbook tells the
|
|
53
|
+
// agent to run `context` immediately after `work`. Two rankers, in sequence,
|
|
54
|
+
// on the same prompt, free to disagree about which capability it is about.
|
|
55
|
+
// Both now read lib/lexicon.js, and the score below is that module's
|
|
56
|
+
// PROJECTION of the relevance tuple, not a second calculation.
|
|
57
|
+
export function rankCapabilities(projectRoot, prompt) {
|
|
58
|
+
const specsDir = path.join(projectRoot, ".doctrina", "specs");
|
|
59
|
+
const queryTerms = terms(prompt);
|
|
60
|
+
if (queryTerms.length === 0 || !isDir(specsDir)) return [];
|
|
61
|
+
|
|
62
|
+
const ranked = [];
|
|
63
|
+
for (const cap of readdirSync(specsDir).sort()) {
|
|
64
|
+
const specPath = path.join(specsDir, cap, "spec.md");
|
|
65
|
+
if (!isFile(specPath)) continue;
|
|
66
|
+
// The capability name is the emphasis: a term in the name says far more
|
|
67
|
+
// about what the prompt is about than the same term buried in the body.
|
|
68
|
+
const points = score(read(specPath), queryTerms, cap.split("-").join(" "));
|
|
69
|
+
if (points > 0) ranked.push({ id: cap, score: points, path: `.doctrina/specs/${cap}/spec.md` });
|
|
70
|
+
}
|
|
71
|
+
const sorted = ranked.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
72
|
+
// The margin over the NEXT capability — computed before the top-3 cut, so
|
|
73
|
+
// the winner's margin is a real one — is what lets a caller tell a lead
|
|
74
|
+
// apart from a coin toss (change 0044). The last capability's runner-up is
|
|
75
|
+
// 0, the same convention `triage` uses for a lane nothing scored against.
|
|
76
|
+
return sorted.slice(0, 3).map((e, i) => ({ ...e, margin: e.score - (sorted[i + 1]?.score ?? 0) }));
|
|
77
|
+
}
|
|
@@ -1,34 +1,28 @@
|
|
|
1
1
|
# AGENTS.md — {{PROJECT_NAME}}
|
|
2
2
|
|
|
3
3
|
Operational source of truth for AI coding agents working in this
|
|
4
|
-
repository. Follows the open AGENTS.md standard
|
|
4
|
+
repository. Follows the open AGENTS.md standard; the Doctrina framework
|
|
5
|
+
artifacts live under `.doctrina/`.
|
|
5
6
|
|
|
6
7
|
## What this repo is
|
|
7
8
|
|
|
8
9
|
{{PROJECT_DESCRIPTION}}
|
|
9
10
|
|
|
10
|
-
This repository uses the Doctrina framework for spec-driven, AGENTS.md
|
|
11
|
-
native multi-agent development. The full framework artifacts live under
|
|
12
|
-
`.doctrina/`.
|
|
13
|
-
|
|
14
11
|
## Working from intent (you drive; the human stays passive)
|
|
15
12
|
|
|
16
13
|
You — the AI agent — run the Doctrina commands. The human gives a brief
|
|
17
14
|
prompt and approves; do not make them run the CLI or hand-author artifacts.
|
|
18
15
|
|
|
19
|
-
- **Bootstrap.**
|
|
20
|
-
are not written
|
|
21
|
-
playbook in one pass (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
source of truth — never edit `intake.md` to change requirements.
|
|
16
|
+
- **Bootstrap.** No capability in `.doctrina/specs/`, or an intake still
|
|
17
|
+
`Status: pending`? The specs are not written: run `doctrina intake --text
|
|
18
|
+
"<description>"` and execute the printed playbook in one pass (existing
|
|
19
|
+
codebase: `doctrina work --from-diff`). After conversion the specs are the
|
|
20
|
+
only source of truth — never edit `intake.md` to change requirements.
|
|
25
21
|
- **Day-to-day.** Start every session with `doctrina prime` (gates, rules,
|
|
26
|
-
open work in one read). Turn any request into a change with `doctrina
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Before compaction or handover: `doctrina handoff`. Ask the human only on
|
|
31
|
-
genuine ambiguity.
|
|
22
|
+
open work in one read). Turn any request into a change with `doctrina work
|
|
23
|
+
"<prompt>"` and follow the printed playbook. Preview a close with `doctrina
|
|
24
|
+
change check <id>`; close with `doctrina close <id>`. Unsure what's next?
|
|
25
|
+
`doctrina next`. Before compaction or handover: `doctrina handoff`.
|
|
32
26
|
|
|
33
27
|
<!-- doctrina:surface:begin — CLI-owned block, generated from the installed command catalog. Refreshed by `doctrina upgrade --write`; edits inside are overwritten. -->
|
|
34
28
|
## Doctrina command surface (generated — reach for these, don't hand-author)
|
|
@@ -40,7 +34,7 @@ canonical templates and syncs `index.json`. Flags: `doctrina <cmd> --help`.
|
|
|
40
34
|
- `doctrina intake` — store the intent and print the bootstrap playbook. *When:* you have a full project description and no specs yet.
|
|
41
35
|
- `doctrina adapter list|add|remove` — install/remove agent adapters (additive). *When:* adding or removing an agent's pointer files.
|
|
42
36
|
**Orient**
|
|
43
|
-
- `doctrina prime (session start)` — gates,
|
|
37
|
+
- `doctrina prime (session start)` — gates, open work and next steps in one read (--rules for the full standing rules). *When:* at the START of every session.
|
|
44
38
|
- `doctrina context [<cap>] --for "<task>" --concat` — the read pack, assembled to fit a token budget. *When:* before working on any task, to load the right files.
|
|
45
39
|
- `doctrina show` — point-read a single artifact by reference. *When:* you need one requirement, criterion, or ADR, not a file.
|
|
46
40
|
- `doctrina search` — search the artifact tree, grouped by category. *When:* you do not know which artifact mentions a term.
|
|
@@ -48,12 +42,11 @@ canonical templates and syncs `index.json`. Flags: `doctrina <cmd> --help`.
|
|
|
48
42
|
- `doctrina next` — the recommended next workflow action. *When:* you finished something and do not know what follows.
|
|
49
43
|
- `doctrina why` — provenance: intent, proof, ADRs, and history. *When:* you need to justify or trace a capability's existence.
|
|
50
44
|
- `doctrina handoff (before compaction/handover)` — a resume note: open work, task state, next command. *When:* BEFORE compaction or handing over to another session.
|
|
51
|
-
- `doctrina constitution` — accepted ADRs and product non-goals. *When:* you need the standing rules before deciding something.
|
|
52
45
|
**Change**
|
|
53
46
|
- `doctrina triage` — classify the lane (product/runtime/chore) and check the declared runtime surface. *When:* a request arrives — BEFORE scaffolding, especially if it smells like an incident.
|
|
54
47
|
- `doctrina work "<prompt>" (--capability · --chore · --from-diff · --quiet)` — scaffold a change and print the playbook to execute. *When:* a request arrives that changes behaviour (triage says PRODUCT).
|
|
55
48
|
- `doctrina spec new|list|set` — create, list, and edit capability specs. *When:* a capability needs creating or its headers advancing.
|
|
56
|
-
- `doctrina change new|apply|archive|check|tick|
|
|
49
|
+
- `doctrina change new|apply|archive|check|tick|abandon` — new / apply / archive / check (--verbose) / tick / abandon. *When:* driving a change through its lifecycle by hand.
|
|
57
50
|
- `doctrina contract new|list|check` — own and verify the integration surface. *When:* the change touches ports, env vars, or public endpoints.
|
|
58
51
|
- `doctrina decision new|accept|land|supersede|list|scope` — record, accept, land, scope, and supersede ADRs. *When:* the change decides something a later session must not relitigate.
|
|
59
52
|
- `doctrina skill new|list|sync|suggest` — capture on-demand procedural memory. *When:* a lesson is worth not relearning.
|
|
@@ -68,7 +61,7 @@ canonical templates and syncs `index.json`. Flags: `doctrina <cmd> --help`.
|
|
|
68
61
|
- `doctrina verify` — the project's declared typecheck/test/build checks. *When:* the real build gate must run.
|
|
69
62
|
- `doctrina close <id...>` — the whole closing sequence in one attested pass. *When:* a change is implemented and ready to finish.
|
|
70
63
|
- `doctrina doctor` — aggregate diagnostic with per-finding remedies. *When:* something looks wrong and you do not know which gate to ask.
|
|
71
|
-
**Maintain** (triggers: `doctrina <command> --help`) — `doctrina templates list|check|update` · `doctrina hooks install` · `doctrina index rebuild` · `doctrina watch` · `doctrina metrics` · `doctrina report` · `doctrina completion` · `doctrina upgrade --write`
|
|
64
|
+
**Maintain** (triggers: `doctrina <command> --help`) — `doctrina templates list|check|update` · `doctrina hooks install` · `doctrina index rebuild` · `doctrina watch` · `doctrina metrics` · `doctrina report` · `doctrina completion` · `doctrina upgrade --write` · `doctrina ci --emit github`
|
|
72
65
|
<!-- doctrina:surface:end -->
|
|
73
66
|
|
|
74
67
|
## Stack and tooling
|
|
@@ -121,12 +114,13 @@ Finish by running `doctrina validate` and resolving every `error:`.
|
|
|
121
114
|
|
|
122
115
|
`doctrina context [<capability>] --concat` assembles the read pack in one
|
|
123
116
|
call: this `AGENTS.md` → `product.md` → the capability spec → open changes →
|
|
124
|
-
accepted ADRs. Run it for ANY task
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
`.doctrina/skills/`; load a body only when its trigger fires.
|
|
117
|
+
accepted ADRs. Run it for ANY task; it skips `changes/archive/`. Skills are
|
|
118
|
+
on demand: read the `description:` / `when:` frontmatter in
|
|
119
|
+
`.doctrina/skills/` and load a body only when its trigger fires.
|
|
128
120
|
|
|
129
|
-
Keep this file under 150 lines.
|
|
121
|
+
Keep this file under 150 lines. The scaffold already spends most of them
|
|
122
|
+
and the generated block above grows with the CLI — `doctrina doctor`
|
|
123
|
+
reports what is left. Density beats prose.
|
|
130
124
|
|
|
131
125
|
## Definition of done
|
|
132
126
|
|
package/templates/README.md
CHANGED
|
@@ -29,10 +29,45 @@ replace. No conditionals, loops, or includes.
|
|
|
29
29
|
CLI authors and template authors share this vocabulary. Adding a token
|
|
30
30
|
requires extending the templates spec at `.doctrina/specs/templates/spec.md`.
|
|
31
31
|
|
|
32
|
+
### Playbook tokens
|
|
33
|
+
|
|
34
|
+
The playbooks under `playbooks/` carry the variable parts of the procedure
|
|
35
|
+
the agent executes. Each is PRE-RENDERED by the CLI into a plain string, so
|
|
36
|
+
the template stays a document rather than a language with conditionals and
|
|
37
|
+
loops in it.
|
|
38
|
+
|
|
39
|
+
| Token | Meaning | Default if not supplied |
|
|
40
|
+
|-------|---------|-------------------------|
|
|
41
|
+
| `{{TITLE}}` | "Work playbook" or "Backfill playbook" | required in `work` |
|
|
42
|
+
| `{{PROMPT}}` | The prompt the change was opened from, verbatim | required |
|
|
43
|
+
| `{{FROM_DIFF_NOTE}}` | The code-first note, on `--from-diff` | empty |
|
|
44
|
+
| `{{THIN_WARNING}}` | The thin-prompt clarification block | empty |
|
|
45
|
+
| `{{CAPABILITY_BLOCK}}` | The pinned capability, the ranked hints, or "no match" | required in `work` |
|
|
46
|
+
| `{{DIFF_MATCHES}}` | Capabilities the working tree touched | empty |
|
|
47
|
+
| `{{STEP3_INTRO}}` | The delta step's opening, pinned or not | required in `work` |
|
|
48
|
+
| `{{FROM_DIFF_DELTA_NOTE}}` | The backfill caveat on writing the delta | empty |
|
|
49
|
+
| `{{MISSING_SPECS_WARNING}}` | Warning that `.doctrina/specs/` is absent | empty |
|
|
50
|
+
|
|
51
|
+
A line holding NOTHING but one token whose value is empty is removed rather
|
|
52
|
+
than left blank, which is what lets the optional blocks above be plain tokens.
|
|
53
|
+
|
|
54
|
+
Colour is marked inline and expanded before substitution, so a token's value
|
|
55
|
+
can never inject it:
|
|
56
|
+
|
|
57
|
+
| Markup | Renders as |
|
|
58
|
+
|--------|-----------|
|
|
59
|
+
| `[[c]]...[[/c]]` | cyan — a command to run |
|
|
60
|
+
| `[[g]]...[[/g]]` | gray — commentary |
|
|
61
|
+
| `[[b]]...[[/b]]` | bold — a heading |
|
|
62
|
+
| `[[y]]...[[/y]]` | yellow — a warning |
|
|
63
|
+
|
|
32
64
|
## Inventory (v0)
|
|
33
65
|
|
|
34
66
|
```
|
|
35
67
|
README.md this file
|
|
68
|
+
playbooks/work.md.template the work / backfill playbook
|
|
69
|
+
playbooks/chore.md.template the spec-less chore playbook
|
|
70
|
+
playbooks/bootstrap.md.template the intake -> specs playbook
|
|
36
71
|
AGENTS.md.template root rules for the target repo
|
|
37
72
|
doctrina/ the .doctrina/ skeleton
|
|
38
73
|
product.md.template
|