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.
Files changed (68) hide show
  1. package/README.md +3 -2
  2. package/package.json +1 -1
  3. package/src/commands/adapter.js +84 -0
  4. package/src/commands/analyze.js +14 -3
  5. package/src/commands/change.js +164 -17
  6. package/src/commands/clarify.js +1 -204
  7. package/src/commands/close.js +18 -1
  8. package/src/commands/context.js +81 -9
  9. package/src/commands/contract.js +1 -0
  10. package/src/commands/delivery.js +356 -0
  11. package/src/commands/doctor.js +21 -1
  12. package/src/commands/handoff.js +11 -2
  13. package/src/commands/index-rebuild.js +23 -0
  14. package/src/commands/init.js +28 -1
  15. package/src/commands/intake.js +60 -3
  16. package/src/commands/metrics.js +43 -3
  17. package/src/commands/next.js +41 -6
  18. package/src/commands/prime.js +125 -7
  19. package/src/commands/review.js +39 -3
  20. package/src/commands/skill.js +2 -1
  21. package/src/commands/status.js +28 -0
  22. package/src/commands/upgrade.js +33 -0
  23. package/src/commands/validate.js +18 -0
  24. package/src/commands/verify.js +32 -4
  25. package/src/commands/work.js +126 -7
  26. package/src/commands/workspace.js +419 -0
  27. package/src/index.js +100 -66
  28. package/src/lib/actions.js +44 -4
  29. package/src/lib/adapters.js +143 -1
  30. package/src/lib/args.js +33 -1
  31. package/src/lib/clarify-scan.js +221 -0
  32. package/src/lib/code-inventory.js +770 -0
  33. package/src/lib/commands.js +52 -5
  34. package/src/lib/context-slice.js +149 -0
  35. package/src/lib/delivery.js +286 -0
  36. package/src/lib/docs-impact.js +36 -1
  37. package/src/lib/fs-ops.js +23 -0
  38. package/src/lib/intake-model.js +1 -1
  39. package/src/lib/json-out.js +9 -1
  40. package/src/lib/metrics-model.js +23 -1
  41. package/src/lib/playbook.js +1 -1
  42. package/src/lib/shell.js +45 -0
  43. package/src/lib/skill-export.js +95 -0
  44. package/src/lib/snapshot.js +43 -2
  45. package/src/lib/task-card.js +195 -0
  46. package/src/lib/views.js +52 -3
  47. package/src/lib/work-model.js +66 -2
  48. package/src/lib/workspace-agents.js +306 -0
  49. package/src/lib/workspace-guard.js +65 -0
  50. package/src/lib/workspace-locate.js +141 -0
  51. package/src/lib/workspace-tools.js +160 -0
  52. package/src/lib/workspace-view.js +391 -0
  53. package/src/lib/workspace.js +323 -0
  54. package/templates/AGENTS.md.template +17 -14
  55. package/templates/README.md +11 -1
  56. package/templates/adapters/claude/.claude/settings.json.template +18 -0
  57. package/templates/adapters/cursor/.cursor/hooks.json.template +8 -0
  58. package/templates/adapters/cursor/.cursor/rules/00-doctrina.mdc.template +14 -9
  59. package/templates/adapters/cursor/.cursor/{commands/doctrina-context.md.template → skills/doctrina-context/SKILL.md.template} +6 -0
  60. package/templates/adapters/cursor/.cursor/{commands/doctrina-next.md.template → skills/doctrina-next/SKILL.md.template} +6 -0
  61. package/templates/adapters/cursor/.cursor/{commands/doctrina-status.md.template → skills/doctrina-status/SKILL.md.template} +6 -0
  62. package/templates/adapters/cursor/.cursor/{commands/doctrina-why.md.template → skills/doctrina-why/SKILL.md.template} +6 -0
  63. package/templates/adapters/cursor/.cursor/{commands/doctrina-work.md.template → skills/doctrina-work/SKILL.md.template} +6 -0
  64. package/templates/delivery.md.template +39 -0
  65. package/templates/playbooks/absorb.md.template +63 -0
  66. package/templates/playbooks/bootstrap.md.template +18 -6
  67. package/templates/playbooks/delivery.md.template +44 -0
  68. 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
- codebase: `doctrina work --from-diff`). After conversion the specs are the
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.** Start every session with `doctrina prime` (gates, rules,
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`.
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:* a request arrives — BEFORE scaffolding, especially if it smells like an incident.
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).
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 [<capability>] --concat` assembles the read pack in one
115
- call: this `AGENTS.md` → `product.md` → the capability spec → open changes →
116
- accepted ADRs. Run it for ANY task; it skips `changes/archive/`. Skills are
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`
@@ -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/commands/doctrina-*.md.template native slash commands (core loop)
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
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "sessionStart": [
5
+ { "command": "doctrina prime --hook cursor" }
6
+ ]
7
+ }
8
+ }
@@ -1,16 +1,21 @@
1
1
  ---
2
- description: Doctrina canonical rules — always read AGENTS.md first
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 pointer rule
6
+ # Doctrina
7
7
 
8
- This project uses the open AGENTS.md standard as the single source of
9
- truth for agent-readable rules. Before answering or editing, read the
10
- canonical file at `{{AGENTS_MD_PATH}}`.
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
- The Doctrina framework artifacts (specs, decisions, changes) live under
13
- `.doctrina/`. Read order is documented in `AGENTS.md`.
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 duplicate rules here. If a rule belongs in AGENTS.md, put it
16
- there and re-read this project.
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-context
3
+ description: Load the Doctrina read pack for a task, in read order
4
+ disable-model-invocation: true
5
+ ---
6
+
1
7
  # /doctrina-context — read pack for {{PROJECT_NAME}}
2
8
 
3
9
  Assemble the context before doing any work.
@@ -1,3 +1,9 @@
1
+ ---
2
+ name: doctrina-next
3
+ description: Show and act on the next recommended Doctrina workflow step
4
+ disable-model-invocation: true
5
+ ---
6
+
1
7
  # /doctrina-next — next step for {{PROJECT_NAME}}
2
8
 
3
9
  Decide the next step from the framework's own signals.
@@ -1,3 +1,9 @@
1
+ ---
2
+ name: doctrina-status
3
+ description: One-glance Doctrina project health and gate status
4
+ disable-model-invocation: true
5
+ ---
6
+
1
7
  # /doctrina-status — health of {{PROJECT_NAME}}
2
8
 
3
9
  Report the project's health and gate status.
@@ -1,3 +1,9 @@
1
+ ---
2
+ name: doctrina-why
3
+ description: Explain a capability's provenance (intent → proof → ADRs → history)
4
+ disable-model-invocation: true
5
+ ---
6
+
1
7
  # /doctrina-why — provenance for {{PROJECT_NAME}}
2
8
 
3
9
  Explain why a capability exists and is shaped as it is.
@@ -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}}