doctrina-cli 0.18.0 → 0.21.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 +3 -2
- package/package.json +1 -1
- package/src/commands/adapter.js +84 -0
- package/src/commands/analyze.js +14 -3
- package/src/commands/change.js +164 -17
- package/src/commands/clarify.js +1 -204
- package/src/commands/close.js +18 -1
- package/src/commands/context.js +81 -9
- package/src/commands/contract.js +1 -0
- package/src/commands/delivery.js +356 -0
- package/src/commands/doctor.js +21 -1
- package/src/commands/handoff.js +11 -2
- package/src/commands/index-rebuild.js +23 -0
- package/src/commands/init.js +28 -1
- package/src/commands/intake.js +60 -3
- package/src/commands/metrics.js +43 -3
- package/src/commands/next.js +41 -6
- package/src/commands/prime.js +125 -7
- package/src/commands/review.js +39 -3
- package/src/commands/skill.js +2 -1
- package/src/commands/status.js +28 -0
- package/src/commands/upgrade.js +33 -0
- package/src/commands/validate.js +18 -0
- package/src/commands/verify.js +32 -4
- package/src/commands/work.js +126 -7
- package/src/commands/workspace.js +419 -0
- package/src/index.js +100 -66
- package/src/lib/actions.js +44 -4
- package/src/lib/adapters.js +143 -1
- package/src/lib/args.js +33 -1
- package/src/lib/clarify-scan.js +221 -0
- package/src/lib/code-inventory.js +770 -0
- package/src/lib/commands.js +52 -5
- package/src/lib/context-slice.js +149 -0
- package/src/lib/delivery.js +286 -0
- package/src/lib/docs-impact.js +36 -1
- package/src/lib/fs-ops.js +23 -0
- package/src/lib/intake-model.js +1 -1
- package/src/lib/json-out.js +9 -1
- package/src/lib/metrics-model.js +23 -1
- package/src/lib/playbook.js +1 -1
- package/src/lib/shell.js +45 -0
- package/src/lib/skill-export.js +95 -0
- package/src/lib/snapshot.js +43 -2
- package/src/lib/task-card.js +195 -0
- package/src/lib/views.js +52 -3
- package/src/lib/work-model.js +66 -2
- package/src/lib/workspace-agents.js +306 -0
- package/src/lib/workspace-guard.js +65 -0
- package/src/lib/workspace-locate.js +141 -0
- package/src/lib/workspace-tools.js +160 -0
- package/src/lib/workspace-view.js +391 -0
- package/src/lib/workspace.js +323 -0
- package/templates/AGENTS.md.template +17 -14
- package/templates/README.md +11 -1
- package/templates/adapters/claude/.claude/settings.json.template +18 -0
- package/templates/adapters/cursor/.cursor/hooks.json.template +8 -0
- package/templates/adapters/cursor/.cursor/rules/00-doctrina.mdc.template +14 -9
- package/templates/adapters/cursor/.cursor/{commands/doctrina-context.md.template → skills/doctrina-context/SKILL.md.template} +6 -0
- package/templates/adapters/cursor/.cursor/{commands/doctrina-next.md.template → skills/doctrina-next/SKILL.md.template} +6 -0
- package/templates/adapters/cursor/.cursor/{commands/doctrina-status.md.template → skills/doctrina-status/SKILL.md.template} +6 -0
- package/templates/adapters/cursor/.cursor/{commands/doctrina-why.md.template → skills/doctrina-why/SKILL.md.template} +6 -0
- package/templates/adapters/cursor/.cursor/{commands/doctrina-work.md.template → skills/doctrina-work/SKILL.md.template} +6 -0
- package/templates/delivery.md.template +39 -0
- package/templates/playbooks/absorb.md.template +63 -0
- package/templates/playbooks/bootstrap.md.template +18 -6
- package/templates/playbooks/delivery.md.template +44 -0
- package/templates/playbooks/work.md.template +1 -1
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import {
|
|
3
|
+
existsSync, lstatSync, linkSync, mkdirSync, readFileSync, readdirSync,
|
|
4
|
+
realpathSync, rmdirSync, statSync, symlinkSync, unlinkSync, copyFileSync,
|
|
5
|
+
} from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { isDir, isFile, read, toPosix, write } from "./fs-ops.js";
|
|
8
|
+
import { listHeader } from "./scan.js";
|
|
9
|
+
import { openChanges } from "./snapshot.js";
|
|
10
|
+
import { git as runGit, GIT_STATE } from "./git.js";
|
|
11
|
+
import { cdThen } from "./shell.js";
|
|
12
|
+
|
|
13
|
+
// A WORKSPACE holds the context of several repositories in one place (ADR
|
|
14
|
+
// 0029): which projects exist, what each one is, how they depend on each
|
|
15
|
+
// other, and — for a repository that cannot carry `.doctrina/` — the
|
|
16
|
+
// artifacts themselves. It is a plain directory the human keeps as a private
|
|
17
|
+
// git repository, so the context of a repository nobody may commit to is
|
|
18
|
+
// still versioned, by its owner, somewhere.
|
|
19
|
+
//
|
|
20
|
+
// The CLI never moves the code. A SHADOW project's artifacts live under
|
|
21
|
+
// `<workspace>/projects/<name>/` and are LINKED into the repository, then
|
|
22
|
+
// hidden from that repository's git through `.git/info/exclude` — a file git
|
|
23
|
+
// reads and never commits. Every existing command keeps working unchanged in
|
|
24
|
+
// the repository, because from its working directory `.doctrina/` is simply
|
|
25
|
+
// there.
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
WORKSPACE_FILE, WORKSPACE_SCHEMA, PROJECT_MODES, PROJECT_NAME, EXCLUDE_BEGIN, EXCLUDE_END,
|
|
29
|
+
blankWorkspace, isWorkspaceDir, readWorkspace, writeWorkspace, doctrinaHome, pointerPath, writePointer,
|
|
30
|
+
findWorkspace, projectAt, shadowDirOf, artifactRootOf,
|
|
31
|
+
} from "./workspace-locate.js";
|
|
32
|
+
import {
|
|
33
|
+
EXCLUDE_BEGIN, EXCLUDE_END, isWorkspaceDir, readWorkspace, writeWorkspace, findWorkspace, projectAt,
|
|
34
|
+
shadowDirOf, artifactRootOf, WORKSPACE_FILE, doctrinaHome, pointerPath, blankWorkspace, writePointer, PROJECT_NAME, PROJECT_MODES, WORKSPACE_SCHEMA, safeReal,
|
|
35
|
+
} from "./workspace-locate.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Where a project stands. `missing` when its repository path is gone;
|
|
39
|
+
* `pending` while it has no spec or its intake is unconverted — the
|
|
40
|
+
* project still has to be absorbed; `ready` otherwise.
|
|
41
|
+
*/
|
|
42
|
+
const INTAKE_PENDING = "intake not converted";
|
|
43
|
+
// A shadow's folder in the workspace is gone (change 0235): `attach` restores it.
|
|
44
|
+
const SHADOW_GONE = "its shadow folder in the workspace is gone";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The command that moves a pending project on (second audit, change 0233).
|
|
48
|
+
* `--from-code` starts an absorb and refuses once the inventory is stored —
|
|
49
|
+
* every surface still named it for a project whose absorb was under way. A
|
|
50
|
+
* stored, pending intake reprints its playbook with a bare `doctrina intake`.
|
|
51
|
+
* @param {{ path: string, name?: string }} project
|
|
52
|
+
* @param {string} reason projectState's reason
|
|
53
|
+
*/
|
|
54
|
+
export function absorbCommand(project, reason) {
|
|
55
|
+
if (reason === SHADOW_GONE) return `doctrina workspace attach ${project.name ?? "<name>"}`;
|
|
56
|
+
return reason === INTAKE_PENDING
|
|
57
|
+
? cdThen(project.path, "doctrina intake")
|
|
58
|
+
: cdThen(project.path, "doctrina intake --from-code");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function projectState(wsDir, project) {
|
|
62
|
+
if (!isDir(project.path)) return { state: "missing", reason: `path not found: ${project.path}`, open: [] };
|
|
63
|
+
const root = artifactRootOf(wsDir, project);
|
|
64
|
+
const doctrina = path.join(root, ".doctrina");
|
|
65
|
+
if (project.mode === "shadow" && !isDir(doctrina)) return { state: "pending", reason: SHADOW_GONE, open: [] };
|
|
66
|
+
if (!isDir(doctrina)) return { state: "pending", reason: "no .doctrina/ yet", open: [] };
|
|
67
|
+
const intake = path.join(doctrina, "intake.md");
|
|
68
|
+
if (isFile(intake) && /pending/i.test(listHeader(read(intake), "Status") ?? "")) {
|
|
69
|
+
return { state: "pending", reason: INTAKE_PENDING, open: openChanges(root) };
|
|
70
|
+
}
|
|
71
|
+
const specs = path.join(doctrina, "specs");
|
|
72
|
+
const count = isDir(specs) ? readdirSync(specs).filter((d) => isFile(path.join(specs, d, "spec.md"))).length : 0;
|
|
73
|
+
if (count === 0) return { state: "pending", reason: "no capability spec yet", open: openChanges(root) };
|
|
74
|
+
return { state: "ready", reason: `${count} spec${count === 1 ? "" : "s"}`, open: openChanges(root) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Links
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
function shadowFiles(shadowDir) {
|
|
82
|
+
const out = [];
|
|
83
|
+
const stack = [""];
|
|
84
|
+
while (stack.length) {
|
|
85
|
+
const rel = stack.pop();
|
|
86
|
+
for (const entry of readdirSync(path.join(shadowDir, rel))) {
|
|
87
|
+
const r = rel ? path.join(rel, entry) : entry;
|
|
88
|
+
if (r === ".git" || r === ".doctrina") continue;
|
|
89
|
+
if (isDir(path.join(shadowDir, r))) stack.push(r);
|
|
90
|
+
else out.push(r);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out.sort();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** True when `at` already resolves to `target` (a symlink, a junction, or a hard link). */
|
|
97
|
+
export function linksTo(at, target) {
|
|
98
|
+
let st;
|
|
99
|
+
try {
|
|
100
|
+
st = lstatSync(at);
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
if (st.isSymbolicLink()) return safeReal(at) === safeReal(target);
|
|
105
|
+
try {
|
|
106
|
+
const t = statSync(target);
|
|
107
|
+
return !st.isDirectory() && st.ino === t.ino && st.dev === t.dev && st.ino !== 0;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* What attaching a shadow project would do, entry by entry. `.doctrina/` is
|
|
115
|
+
* linked as one directory — every command creates files inside it. Every
|
|
116
|
+
* other file the shadow holds (AGENTS.md, the agent's rule and hooks) is
|
|
117
|
+
* linked one by one, so a repository that already has a `.cursor/` keeps it.
|
|
118
|
+
* A path the repository already owns is a conflict, left untouched.
|
|
119
|
+
* @returns {{ rel: string, kind: "dir"|"file", action: "link"|"ok"|"conflict" }[]}
|
|
120
|
+
*/
|
|
121
|
+
export function linkPlan(shadowDir, repoDir) {
|
|
122
|
+
const entries = [{ rel: ".doctrina", kind: /** @type {const} */ ("dir") },
|
|
123
|
+
...shadowFiles(shadowDir).map((rel) => ({ rel, kind: /** @type {const} */ ("file") }))];
|
|
124
|
+
return entries.map(({ rel, kind }) => {
|
|
125
|
+
const at = path.join(repoDir, rel);
|
|
126
|
+
const target = path.join(shadowDir, rel);
|
|
127
|
+
let action = /** @type {"link"|"ok"|"conflict"} */ ("link");
|
|
128
|
+
if (linksTo(at, target)) action = "ok";
|
|
129
|
+
else if (existsOrLink(at)) action = "conflict";
|
|
130
|
+
return { rel: toPosix(rel), kind, action };
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function existsOrLink(p) {
|
|
135
|
+
try {
|
|
136
|
+
lstatSync(p);
|
|
137
|
+
return true;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create the links a plan calls for. A directory becomes a symlink (a
|
|
145
|
+
* junction on Windows, which needs no privilege); a file becomes a symlink,
|
|
146
|
+
* else a hard link, else — last resort — a copy, which `attach` reports
|
|
147
|
+
* because a copy does not follow the shadow.
|
|
148
|
+
* @returns {{ rel: string, how: string }[]}
|
|
149
|
+
*/
|
|
150
|
+
export function applyLinks(plan, shadowDir, repoDir) {
|
|
151
|
+
const done = [];
|
|
152
|
+
for (const entry of plan) {
|
|
153
|
+
if (entry.action !== "link") continue;
|
|
154
|
+
const at = path.join(repoDir, entry.rel);
|
|
155
|
+
const target = path.join(shadowDir, entry.rel);
|
|
156
|
+
mkdirSync(path.dirname(at), { recursive: true });
|
|
157
|
+
if (entry.kind === "dir") {
|
|
158
|
+
symlinkSync(target, at, "junction");
|
|
159
|
+
done.push({ rel: entry.rel, how: "linked" });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
symlinkSync(target, at, "file");
|
|
164
|
+
done.push({ rel: entry.rel, how: "linked" });
|
|
165
|
+
} catch {
|
|
166
|
+
try {
|
|
167
|
+
linkSync(target, at);
|
|
168
|
+
done.push({ rel: entry.rel, how: "hard-linked" });
|
|
169
|
+
} catch {
|
|
170
|
+
copyFileSync(target, at);
|
|
171
|
+
done.push({ rel: entry.rel, how: "copied" });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return done;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The shadow folder a repository's `.doctrina/` links to, or null when the
|
|
180
|
+
* repository owns its `.doctrina/` (change 0238).
|
|
181
|
+
* @param {string} projectRoot
|
|
182
|
+
*/
|
|
183
|
+
export function shadowRootOf(projectRoot) {
|
|
184
|
+
const link = path.join(projectRoot, ".doctrina");
|
|
185
|
+
try {
|
|
186
|
+
if (!lstatSync(link).isSymbolicLink()) return null;
|
|
187
|
+
return path.dirname(realpathSync(link));
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Re-link a shadowed repository after its shadow gained or lost files
|
|
195
|
+
* (change 0238): a link for each new file, the links to removed files taken
|
|
196
|
+
* out, and the exclude block rewritten. A command that writes an adapter
|
|
197
|
+
* file for a shadowed repository writes it in the shadow and calls this —
|
|
198
|
+
* written in the repository it was a stray, untracked file.
|
|
199
|
+
* @param {string} projectRoot
|
|
200
|
+
* @param {Array<{ rel: string, content: string }>} gone the files the shadow lost, with what they held
|
|
201
|
+
* @returns {{ linked: string[], unlinked: string[] } | null} null for a repository that is not a shadow
|
|
202
|
+
*/
|
|
203
|
+
export function relinkShadow(projectRoot, gone = []) {
|
|
204
|
+
const shadow = shadowRootOf(projectRoot);
|
|
205
|
+
if (!shadow) return null;
|
|
206
|
+
const unlinked = [];
|
|
207
|
+
for (const { rel, content } of gone) {
|
|
208
|
+
const at = path.join(projectRoot, rel);
|
|
209
|
+
let st;
|
|
210
|
+
try {
|
|
211
|
+
st = lstatSync(at);
|
|
212
|
+
} catch {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
// A symlink to the removed file, or the hard link or copy a platform
|
|
216
|
+
// without symlinks got instead — still holding what the shadow held.
|
|
217
|
+
if (st.isSymbolicLink() || (st.isFile() && readFileSync(at, "utf8").replace(/\r\n/g, "\n").trim() === content.replace(/\r\n/g, "\n").trim())) {
|
|
218
|
+
unlinkSync(at);
|
|
219
|
+
unlinked.push(toPosix(rel));
|
|
220
|
+
pruneEmpty(path.dirname(at), projectRoot);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const plan = linkPlan(shadow, projectRoot);
|
|
224
|
+
const linked = applyLinks(plan, shadow, projectRoot).map((d) => d.rel);
|
|
225
|
+
writeExclude(projectRoot, excludeEntries(plan, projectRoot));
|
|
226
|
+
return { linked, unlinked };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Remove every link the shadow put in the repository — the link, never what
|
|
231
|
+
* it points at: `fs-ops.remove` follows a directory link and would empty
|
|
232
|
+
* the shadow, which is why this module does its own unlinking.
|
|
233
|
+
* @returns {string[]} the paths removed
|
|
234
|
+
*/
|
|
235
|
+
export function removeLinks(shadowDir, repoDir) {
|
|
236
|
+
const removed = [];
|
|
237
|
+
for (const entry of linkPlan(shadowDir, repoDir)) {
|
|
238
|
+
if (entry.action !== "ok") continue;
|
|
239
|
+
const at = path.join(repoDir, entry.rel);
|
|
240
|
+
try {
|
|
241
|
+
unlinkSync(at);
|
|
242
|
+
} catch {
|
|
243
|
+
rmdirSync(at);
|
|
244
|
+
}
|
|
245
|
+
removed.push(entry.rel);
|
|
246
|
+
pruneEmpty(path.dirname(at), repoDir);
|
|
247
|
+
}
|
|
248
|
+
return removed;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function pruneEmpty(dir, stopAt) {
|
|
252
|
+
let cur = dir;
|
|
253
|
+
while (cur.startsWith(stopAt + path.sep)) {
|
|
254
|
+
try {
|
|
255
|
+
if (readdirSync(cur).length > 0) return;
|
|
256
|
+
rmdirSync(cur);
|
|
257
|
+
} catch {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
cur = path.dirname(cur);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// The repository's local exclude file
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
function git(repoDir, args) {
|
|
269
|
+
const r = runGit(repoDir, args);
|
|
270
|
+
return r.state === GIT_STATE.OK ? r.stdout : null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** `git init` a new workspace. True when it worked. */
|
|
274
|
+
export function gitInit(dir) {
|
|
275
|
+
return runGit(dir, ["init", "-q"]).state === GIT_STATE.OK;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** `.git/info/exclude` for this repository (worktrees included), or null outside git. */
|
|
279
|
+
export function excludePathOf(repoDir) {
|
|
280
|
+
const out = git(repoDir, ["rev-parse", "--git-path", "info/exclude"]);
|
|
281
|
+
if (out === null) return null;
|
|
282
|
+
return path.resolve(repoDir, out.trim());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** True when the repository tracks anything under `rel`. */
|
|
286
|
+
export function tracksAnything(repoDir, rel) {
|
|
287
|
+
const out = git(repoDir, ["ls-files", "--", rel]);
|
|
288
|
+
return out !== null && out.trim() !== "";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** The patterns that keep a shadow invisible to the repository's git. */
|
|
292
|
+
export function excludeEntries(plan, repoDir) {
|
|
293
|
+
// No trailing slash on a linked directory: git sees a symlink as a file,
|
|
294
|
+
// and `/.doctrina/` would match only a real directory.
|
|
295
|
+
const out = plan.filter((e) => e.action !== "conflict").map((e) => "/" + e.rel);
|
|
296
|
+
// `index rebuild` exports skills into the repository's own folders; they
|
|
297
|
+
// belong to the shadow too, unless the repository already keeps skills.
|
|
298
|
+
for (const dir of [".agents/skills", ".claude/skills"]) {
|
|
299
|
+
if (!tracksAnything(repoDir, dir)) out.push(`/${dir}/`);
|
|
300
|
+
}
|
|
301
|
+
return [...new Set(out)];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Replace the managed block of an exclude file's text. */
|
|
305
|
+
export function withExcludeBlock(text, entries) {
|
|
306
|
+
const lines = String(text ?? "").replace(/\r\n/g, "\n").split("\n");
|
|
307
|
+
const b = lines.indexOf(EXCLUDE_BEGIN);
|
|
308
|
+
const e = lines.indexOf(EXCLUDE_END);
|
|
309
|
+
const kept = b >= 0 && e > b ? [...lines.slice(0, b), ...lines.slice(e + 1)] : lines;
|
|
310
|
+
while (kept.length > 0 && kept[kept.length - 1] === "") kept.pop();
|
|
311
|
+
if (entries.length === 0) return kept.length ? kept.join("\n") + "\n" : "";
|
|
312
|
+
return [...kept, ...(kept.length ? [""] : []), EXCLUDE_BEGIN, ...entries, EXCLUDE_END, ""].join("\n");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Write (or with no entries, clear) the managed block. Returns the file, or null outside git. */
|
|
316
|
+
export function writeExclude(repoDir, entries) {
|
|
317
|
+
const p = excludePathOf(repoDir);
|
|
318
|
+
if (!p) return null;
|
|
319
|
+
const before = existsSync(p) ? readFileSync(p, "utf8") : "";
|
|
320
|
+
mkdirSync(path.dirname(p), { recursive: true });
|
|
321
|
+
write(p, withExcludeBlock(before, entries), { force: true });
|
|
322
|
+
return p;
|
|
323
|
+
}
|
|
@@ -16,13 +16,17 @@ prompt and approves; do not make them run the CLI or hand-author artifacts.
|
|
|
16
16
|
- **Bootstrap.** No capability in `.doctrina/specs/`, or an intake still
|
|
17
17
|
`Status: pending`? The specs are not written: run `doctrina intake --text
|
|
18
18
|
"<description>"` and execute the printed playbook in one pass (existing
|
|
19
|
-
|
|
19
|
+
code, no specs: `doctrina intake --from-code`). After conversion the specs are the
|
|
20
20
|
only source of truth — never edit `intake.md` to change requirements.
|
|
21
|
-
- **Day-to-day.**
|
|
22
|
-
|
|
23
|
-
"<
|
|
24
|
-
change
|
|
25
|
-
|
|
21
|
+
- **Day-to-day.** `doctrina prime` opens the session (gates, rules, open
|
|
22
|
+
work). A request that changes behaviour is a change opened BEFORE code is
|
|
23
|
+
edited: `doctrina work "<request>"` — it classifies the lane itself; one
|
|
24
|
+
change per activity the human asked for. Name the change id in your first
|
|
25
|
+
sentence to the human. Read with `doctrina context <cap> --for "<task>"
|
|
26
|
+
--concat` — `prime` is state, not the spec. Project skills are listed
|
|
27
|
+
where your agent finds skills (`.agents/skills/`, `.claude/skills/`).
|
|
28
|
+
Close with `doctrina close <id>` and repeat the archive path it prints.
|
|
29
|
+
Unsure what's next? `doctrina next`. Before handover: `doctrina handoff`.
|
|
26
30
|
|
|
27
31
|
<!-- doctrina:surface:begin — CLI-owned block, generated from the installed command catalog. Refreshed by `doctrina upgrade --write`; edits inside are overwritten. -->
|
|
28
32
|
## Doctrina command surface (generated — reach for these, don't hand-author)
|
|
@@ -43,10 +47,10 @@ canonical templates and syncs `index.json`. Flags: `doctrina <cmd> --help`.
|
|
|
43
47
|
- `doctrina why` — provenance: intent, proof, ADRs, and history. *When:* you need to justify or trace a capability's existence.
|
|
44
48
|
- `doctrina handoff (before compaction/handover)` — a resume note: open work, task state, next command. *When:* BEFORE compaction or handing over to another session.
|
|
45
49
|
**Change**
|
|
46
|
-
- `doctrina triage` — classify the lane (product/runtime/chore) and check the declared runtime surface. *When:*
|
|
47
|
-
- `doctrina work "<prompt>" (--capability · --chore · --from-diff · --quiet)` — scaffold a change and print the playbook to execute. *When:* a request
|
|
50
|
+
- `doctrina triage` — classify the lane (product/runtime/chore) and check the declared runtime surface. *When:* diagnosing an incident or a runtime symptom — `work` classifies every other request itself.
|
|
51
|
+
- `doctrina work "<prompt>" (--capability · --chore · --from-diff · --quiet)` — scaffold a change and print the playbook to execute. *When:* a request changes behaviour — BEFORE editing code; it classifies the lane itself.
|
|
48
52
|
- `doctrina spec new|list|set` — create, list, and edit capability specs. *When:* a capability needs creating or its headers advancing.
|
|
49
|
-
- `doctrina change new|apply|archive|check|tick|abandon|delta` — drive a change by hand — check --verbose previews every delta. *When:* driving a change through its lifecycle by hand.
|
|
53
|
+
- `doctrina change new|apply|archive|check|tick|abandon|delta|card` — drive a change by hand — check --verbose previews every delta. *When:* driving a change through its lifecycle by hand.
|
|
50
54
|
- `doctrina contract new|list|check` — own and verify the integration surface. *When:* the change touches ports, env vars, or public endpoints.
|
|
51
55
|
- `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.
|
|
52
56
|
- `doctrina skill new|list|suggest` — capture on-demand procedural memory. *When:* a lesson is worth not relearning.
|
|
@@ -60,6 +64,7 @@ canonical templates and syncs `index.json`. Flags: `doctrina <cmd> --help`.
|
|
|
60
64
|
- `doctrina verify` — the project's declared typecheck/test/build checks. *When:* the real build gate must run.
|
|
61
65
|
- `doctrina close <id...>` — the whole closing sequence in one attested pass. *When:* a change is implemented and ready to finish.
|
|
62
66
|
- `doctrina doctor` — aggregate diagnostic with per-finding remedies. *When:* something looks wrong and you do not know which gate to ask.
|
|
67
|
+
**Orchestrate** (triggers: `doctrina <command> --help`) — `doctrina workspace init|add|attach|detach|list|tool|guard` · `doctrina delivery new|route|list|status|tick|check|close`
|
|
63
68
|
**Maintain** (triggers: `doctrina <command> --help`) — `doctrina templates list` · `doctrina hooks install` · `doctrina index rebuild` · `doctrina watch` · `doctrina metrics` · `doctrina completion` · `doctrina upgrade --write` · `doctrina ci --emit github`
|
|
64
69
|
<!-- doctrina:surface:end -->
|
|
65
70
|
|
|
@@ -111,11 +116,9 @@ Finish by running `doctrina validate` and resolving every `error:`.
|
|
|
111
116
|
|
|
112
117
|
## How to read context efficiently
|
|
113
118
|
|
|
114
|
-
`doctrina context
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
on demand: read the `description:` / `when:` frontmatter in
|
|
118
|
-
`.doctrina/skills/` and load a body only when its trigger fires.
|
|
119
|
+
`doctrina context <cap> --for "<task>" --concat` is the read pack for ANY
|
|
120
|
+
task: `product.md`, the spec, open changes and accepted ADRs, cut to what
|
|
121
|
+
the task is about. `doctrina show <ref>` reads one requirement or ADR.
|
|
119
122
|
|
|
120
123
|
Keep this file under 150 lines. The scaffold already spends most of them
|
|
121
124
|
and the generated block above grows with the CLI — `doctrina doctor`
|
package/templates/README.md
CHANGED
|
@@ -25,6 +25,11 @@ replace. No conditionals, loops, or includes.
|
|
|
25
25
|
| `{{CONTRACT_NAME}}` | Slug for a new integration contract | required, no default |
|
|
26
26
|
| `{{FRAMEWORK_VERSION}}` | Version of the CLI that scaffolded/stamped the tree | running CLI version |
|
|
27
27
|
| `{{AGENTS_MD_PATH}}` | Path to root `AGENTS.md` from the adapter location | per adapter |
|
|
28
|
+
| `{{DELIVERY_ID}}` | Delivery folder identifier (`NNNN-slug`) | required in `delivery` |
|
|
29
|
+
| `{{DELIVERY_TITLE}}` | The delivery's title | required in `delivery` |
|
|
30
|
+
| `{{DELIVERY_TITLE_ARG}}` | The delivery's title quoted as one shell argument, for a command line | optional in the `delivery` playbook |
|
|
31
|
+
| `{{DELIVERY_REF}}` | The tracker item it comes from | `—` |
|
|
32
|
+
| `{{DELIVERY_PROJECTS}}` | The routed projects, dependencies first | `—` |
|
|
28
33
|
|
|
29
34
|
CLI authors and template authors share this vocabulary. Adding a token
|
|
30
35
|
requires extending the templates spec at `.doctrina/specs/templates/spec.md`.
|
|
@@ -45,9 +50,14 @@ loops in it.
|
|
|
45
50
|
| `{{CAPABILITY_BLOCK}}` | The pinned capability, the ranked hints, or "no match" | required in `work` |
|
|
46
51
|
| `{{DIFF_MATCHES}}` | Capabilities the working tree touched | empty |
|
|
47
52
|
| `{{STEP3_INTRO}}` | The delta step's opening, pinned or not | required in `work` |
|
|
53
|
+
| `{{FOR}}` | The task, as the read pack's `--for` query (quotes stripped, 80 chars at most) | required in `work` |
|
|
48
54
|
| `{{SHAPE}}` | The spec's measured size — lines, requirements per EARS subsection, criteria | required in `consolidate` |
|
|
49
55
|
| `{{FROM_DIFF_DELTA_NOTE}}` | The backfill caveat on writing the delta | empty |
|
|
50
56
|
| `{{MISSING_SPECS_WARNING}}` | Warning that `.doctrina/specs/` is absent | empty |
|
|
57
|
+
| `{{WORKSPACE_STEP}}` | The closing step naming the workspace a project belongs to | empty, in `absorb` |
|
|
58
|
+
| `{{DELIVERY_FILE}}` | The delivery file, relative to the workspace | required in `delivery` |
|
|
59
|
+
| `{{ROUTING}}` | The routed order, or the projects ranked against the title | required in `delivery` |
|
|
60
|
+
| `{{TOOLS}}` | The tools the workspace declares, with their access | empty, in `delivery` |
|
|
51
61
|
|
|
52
62
|
A line holding NOTHING but one token whose value is empty is removed rather
|
|
53
63
|
than left blank, which is what lets the optional blocks above be plain tokens.
|
|
@@ -98,7 +108,7 @@ adapters/
|
|
|
98
108
|
claude/.claude/commands/doctrina-*.md.template native slash commands (core loop)
|
|
99
109
|
codex/README.md
|
|
100
110
|
cursor/.cursor/rules/00-doctrina.mdc.template
|
|
101
|
-
cursor/.cursor/
|
|
111
|
+
cursor/.cursor/skills/doctrina-*/SKILL.md.template the core loop as skills (explicit `/` invocation)
|
|
102
112
|
copilot/.github/copilot-instructions.md.template
|
|
103
113
|
gemini/GEMINI.md.template
|
|
104
114
|
aider/CONVENTIONS.md.template
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{ "type": "command", "command": "doctrina prime --hook claude 2>/dev/null || true" }
|
|
7
|
+
]
|
|
8
|
+
}
|
|
9
|
+
],
|
|
10
|
+
"UserPromptSubmit": [
|
|
11
|
+
{
|
|
12
|
+
"hooks": [
|
|
13
|
+
{ "type": "command", "command": "doctrina prime --pulse 2>/dev/null || true" }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Doctrina
|
|
2
|
+
description: Doctrina — the change cycle, every turn; the full rules are in AGENTS.md
|
|
3
3
|
alwaysApply: true
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Doctrina
|
|
6
|
+
# Doctrina
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
The canonical rules and command surface are in `{{AGENTS_MD_PATH}}` — read it
|
|
9
|
+
first. The change cycle applies to every request, whether or not it names
|
|
10
|
+
Doctrina:
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
- A request that changes behaviour is a change, opened BEFORE code is
|
|
13
|
+
edited: `doctrina work "<request>"`. One change per activity asked for.
|
|
14
|
+
- The first sentence to the human names the change id.
|
|
15
|
+
- Read with `doctrina context <cap> --for "<task>" --concat`; `doctrina
|
|
16
|
+
prime` is the project's state, not the spec.
|
|
17
|
+
- Project skills are in `.agents/skills/` (generated from `.doctrina/skills/`).
|
|
18
|
+
- Finish with `doctrina close <id>` and repeat the archive path it prints.
|
|
14
19
|
|
|
15
|
-
Do not
|
|
16
|
-
|
|
20
|
+
Do not add rules here: this file carries only the cycle, because Cursor's
|
|
21
|
+
per-prompt hook cannot; everything else belongs in AGENTS.md.
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: doctrina-work
|
|
3
|
+
description: Turn a request into a Doctrina change and drive it through the gates
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
1
7
|
# /doctrina-work — drive a change for {{PROJECT_NAME}}
|
|
2
8
|
|
|
3
9
|
Turn the request into a Doctrina change and drive it through the gates.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Delivery {{DELIVERY_ID}} — {{DELIVERY_TITLE}}
|
|
2
|
+
|
|
3
|
+
- **Status:** open
|
|
4
|
+
- **Date:** {{DATE}}
|
|
5
|
+
- **Ref:** {{DELIVERY_REF}}
|
|
6
|
+
- **Projects:** {{DELIVERY_PROJECTS}}
|
|
7
|
+
|
|
8
|
+
<!-- One delivery = one item of work that crosses repositories (a story, a
|
|
9
|
+
screen, a fix). Each project it touches gets ONE change, opened inside
|
|
10
|
+
that repository with `doctrina work "<title>" --delivery {{DELIVERY_ID}}`.
|
|
11
|
+
This file holds what no single repository owns: the story, the sources,
|
|
12
|
+
the plan across projects, the release and what was watched after it. -->
|
|
13
|
+
|
|
14
|
+
## Story
|
|
15
|
+
|
|
16
|
+
<!-- The tracker item, verbatim: title, description, acceptance criteria.
|
|
17
|
+
Read it through the tracker tool; never paraphrase a criterion. -->
|
|
18
|
+
|
|
19
|
+
## Sources
|
|
20
|
+
|
|
21
|
+
<!-- One link per line: documentation pages, design frames, dashboards,
|
|
22
|
+
queries — each with what it was read for. -->
|
|
23
|
+
|
|
24
|
+
## Plan
|
|
25
|
+
|
|
26
|
+
<!-- Per project, in dependency order: what changes and why there. -->
|
|
27
|
+
|
|
28
|
+
## Release
|
|
29
|
+
|
|
30
|
+
<!-- How each project reaches its environment, and the proof it did. Every
|
|
31
|
+
box is a claim `doctrina delivery check` holds you to. -->
|
|
32
|
+
|
|
33
|
+
- [ ] Every project's change is merged and its pipeline ran green
|
|
34
|
+
- [ ] The delivery was validated in the target environment, evidence linked
|
|
35
|
+
|
|
36
|
+
## Observability
|
|
37
|
+
|
|
38
|
+
<!-- What was watched after the release — monitor, dashboard or query, the
|
|
39
|
+
window, and what it showed. At least one line before the close. -->
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[[b]]Absorb playbook[[/b]][[g]] — an existing codebase becomes specs; agent-executed (ADR 0005).[[/g]]
|
|
2
|
+
|
|
3
|
+
The intake holds the code INVENTORY, not a description. Describe what the
|
|
4
|
+
system does TODAY, as the code shows it — never what someone may want it
|
|
5
|
+
to do. Execute in order, in a single linear pass:
|
|
6
|
+
|
|
7
|
+
1. Read [[c]].doctrina/intake.md[[/c]] (the inventory), then what it points at:
|
|
8
|
+
the README, the documentation and decisions it lists, the agent
|
|
9
|
+
instructions already written, and per UNIT its manifest, its entry
|
|
10
|
+
points, one or two files per candidate area and its tests. Breadth
|
|
11
|
+
first — do not read the whole repository. Candidate areas are a hint
|
|
12
|
+
from the folder layout, not capabilities.
|
|
13
|
+
|
|
14
|
+
2. Fill [[c]].doctrina/product.md[[/c]] with what is observable: what the system
|
|
15
|
+
is, who uses it, what it does. Every business fact the code cannot tell
|
|
16
|
+
you (why a rule exists, who the users are, what is out of scope) goes
|
|
17
|
+
under [[c]]## Open questions[[/c]] for the human — never guess intent. So does
|
|
18
|
+
every line of the inventory's "What this inventory could not see".
|
|
19
|
+
|
|
20
|
+
3. One capability per area of behaviour you READ. In a repository of
|
|
21
|
+
several units, a capability stays inside one unit unless the code
|
|
22
|
+
itself shares it (a library both import). For each:
|
|
23
|
+
[[c]]doctrina spec new <capability>[[/c]]
|
|
24
|
+
then write EARS requirements for the behaviour the code shows, with
|
|
25
|
+
[[c]]**Implementation:** implemented[[/c]] and [[c]]**Source:**[[/c]] naming the real
|
|
26
|
+
folders it owns (ADR 0027; `validate` refuses a Source that matches no
|
|
27
|
+
file). Keep it a BASELINE: a few requirements per area now; the specs
|
|
28
|
+
grow with every change that touches them.
|
|
29
|
+
|
|
30
|
+
4. No requirement without the code that does it:
|
|
31
|
+
- Write behaviour only from code or a test you read — never from a
|
|
32
|
+
folder name, a comment, a TODO, commented-out code, a disabled flag
|
|
33
|
+
or a route that answers "not implemented".
|
|
34
|
+
- Numbers, limits, roles, statuses and messages exactly as the code
|
|
35
|
+
writes them. A value you did not see is a question, not a default.
|
|
36
|
+
- Where the docs and the code disagree, the code is what the system
|
|
37
|
+
does today; the disagreement is an open question.
|
|
38
|
+
- Existing decisions are recorded, not re-decided:
|
|
39
|
+
[[c]]doctrina decision new "<title>"[[/c]] citing the document they came from.
|
|
40
|
+
|
|
41
|
+
5. Acceptance criteria cite the tests that already exist. Mark one
|
|
42
|
+
[[c]][verified][[/c]] only when a cited test really proves it; everything else
|
|
43
|
+
stays [[c]][unverified][[/c]] — an honest gap is the point of the absorb.
|
|
44
|
+
|
|
45
|
+
6. Declare the build gate — [[c]]doctrina verify --init[[/c]] writes the commands
|
|
46
|
+
the inventory found; confirm each runs what its name says — and own the
|
|
47
|
+
seams the inventory lists under Interfaces (ports, env names, OpenAPI,
|
|
48
|
+
protobuf, GraphQL, events) as a contract:
|
|
49
|
+
[[c]]doctrina contract new <name>[[/c]]
|
|
50
|
+
|
|
51
|
+
7. Check the facts before the gates. Hand each spec, with its Source, to a
|
|
52
|
+
reader that did not write it (a reviewer subagent, or a fresh session):
|
|
53
|
+
for every requirement, find the code that does it; strike what cannot
|
|
54
|
+
be found, and list what the code does that no requirement says.
|
|
55
|
+
|
|
56
|
+
8. Gates — fix everything they report:
|
|
57
|
+
[[c]]doctrina clarify --all[[/c]]
|
|
58
|
+
[[c]]doctrina validate --fix[[/c]] (a spec written by hand leaves index.json behind; --fix heals that and reports the rest)
|
|
59
|
+
[[c]]doctrina coverage[[/c]]
|
|
60
|
+
|
|
61
|
+
9. Show the human the Open questions, then close the absorb:
|
|
62
|
+
[[c]]doctrina intake --converted[[/c]]
|
|
63
|
+
{{WORKSPACE_STEP}}
|