doctrina-cli 0.1.0 → 0.2.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 (57) hide show
  1. package/README.md +27 -5
  2. package/package.json +4 -2
  3. package/src/commands/analyze.js +5 -5
  4. package/src/commands/change.js +113 -16
  5. package/src/commands/clarify.js +81 -18
  6. package/src/commands/context.js +129 -0
  7. package/src/commands/decision.js +91 -8
  8. package/src/commands/hooks.js +3 -3
  9. package/src/commands/index-rebuild.js +109 -0
  10. package/src/commands/init.js +58 -7
  11. package/src/commands/intake.js +166 -0
  12. package/src/commands/metrics.js +190 -0
  13. package/src/commands/next.js +108 -0
  14. package/src/commands/search.js +93 -0
  15. package/src/commands/skill.js +58 -5
  16. package/src/commands/spec.js +49 -7
  17. package/src/commands/templates.js +103 -4
  18. package/src/commands/validate.js +76 -9
  19. package/src/commands/work.js +215 -0
  20. package/src/index.js +26 -3
  21. package/src/lib/diff.js +103 -0
  22. package/src/lib/ears.js +108 -0
  23. package/src/lib/fs-ops.js +12 -0
  24. package/src/lib/scan.js +185 -0
  25. package/src/lib/templates.js +2 -2
  26. package/templates/AGENTS.md.template +83 -0
  27. package/templates/README.md +85 -0
  28. package/templates/adapters/aider/CONVENTIONS.md.template +11 -0
  29. package/templates/adapters/amp/README.md +13 -0
  30. package/templates/adapters/claude/CLAUDE.md.template +11 -0
  31. package/templates/adapters/codex/README.md +12 -0
  32. package/templates/adapters/continue/.continue/rules/00-doctrina.md.template +11 -0
  33. package/templates/adapters/copilot/.github/copilot-instructions.md.template +11 -0
  34. package/templates/adapters/cursor/.cursor/rules/00-doctrina.mdc.template +16 -0
  35. package/templates/adapters/devin/README.md +14 -0
  36. package/templates/adapters/factory/README.md +13 -0
  37. package/templates/adapters/gemini/GEMINI.md.template +11 -0
  38. package/templates/adapters/jules/README.md +13 -0
  39. package/templates/adapters/windsurf/.windsurfrules.template +11 -0
  40. package/templates/change/design.md.template +24 -0
  41. package/templates/change/proposal.md.template +26 -0
  42. package/templates/change/spec-delta.md.template +20 -0
  43. package/templates/change/tasks.md.template +17 -0
  44. package/templates/decision.md.template +56 -0
  45. package/templates/doctrina/changes/.gitkeep +0 -0
  46. package/templates/doctrina/changes/archive/.gitkeep +0 -0
  47. package/templates/doctrina/decisions/.gitkeep +0 -0
  48. package/templates/doctrina/index.json.template +19 -0
  49. package/templates/doctrina/product.md.template +31 -0
  50. package/templates/doctrina/skills/.gitkeep +0 -0
  51. package/templates/doctrina/specs/.gitkeep +0 -0
  52. package/templates/doctrina/templates/.gitkeep +0 -0
  53. package/templates/hooks/pre-commit.sample +7 -0
  54. package/templates/hooks/watch.sample +21 -0
  55. package/templates/skill.md.template +49 -0
  56. package/templates/spec-bug.md.template +40 -0
  57. package/templates/spec.md.template +52 -0
@@ -0,0 +1,103 @@
1
+ // Minimal line diff (LCS-based) for `change diff`. Specs are capped at a
2
+ // few hundred lines, so the O(n*m) table is fine and keeps us at zero deps.
3
+
4
+ export function diffLines(aText, bText) {
5
+ const a = splitLines(aText);
6
+ const b = splitLines(bText);
7
+ const n = a.length;
8
+ const m = b.length;
9
+
10
+ // LCS length table.
11
+ const table = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
12
+ for (let i = n - 1; i >= 0; i--) {
13
+ for (let j = m - 1; j >= 0; j--) {
14
+ table[i][j] = a[i] === b[j]
15
+ ? table[i + 1][j + 1] + 1
16
+ : Math.max(table[i + 1][j], table[i][j + 1]);
17
+ }
18
+ }
19
+
20
+ // Backtrack into a flat op list.
21
+ const ops = [];
22
+ let i = 0;
23
+ let j = 0;
24
+ while (i < n && j < m) {
25
+ if (a[i] === b[j]) {
26
+ ops.push({ type: "same", text: a[i] });
27
+ i += 1;
28
+ j += 1;
29
+ } else if (table[i + 1][j] >= table[i][j + 1]) {
30
+ ops.push({ type: "del", text: a[i] });
31
+ i += 1;
32
+ } else {
33
+ ops.push({ type: "add", text: b[j] });
34
+ j += 1;
35
+ }
36
+ }
37
+ while (i < n) ops.push({ type: "del", text: a[i++] });
38
+ while (j < m) ops.push({ type: "add", text: b[j++] });
39
+ return ops;
40
+ }
41
+
42
+ // Render ops as unified-style hunks with `context` lines around changes.
43
+ export function formatUnified(ops, { context = 3, aLabel = "a", bLabel = "b" } = {}) {
44
+ const out = [`--- ${aLabel}`, `+++ ${bLabel}`];
45
+ const hunks = collectHunks(ops, context);
46
+ let aLine = 1;
47
+ let bLine = 1;
48
+ let cursor = 0;
49
+ for (const [start, end] of hunks) {
50
+ // Advance line counters across the skipped region.
51
+ for (; cursor < start; cursor++) {
52
+ if (ops[cursor].type !== "add") aLine += 1;
53
+ if (ops[cursor].type !== "del") bLine += 1;
54
+ }
55
+ let aLen = 0;
56
+ let bLen = 0;
57
+ for (let k = start; k < end; k++) {
58
+ if (ops[k].type !== "add") aLen += 1;
59
+ if (ops[k].type !== "del") bLen += 1;
60
+ }
61
+ out.push(`@@ -${aLine},${aLen} +${bLine},${bLen} @@`);
62
+ for (; cursor < end; cursor++) {
63
+ const op = ops[cursor];
64
+ const prefix = op.type === "add" ? "+" : op.type === "del" ? "-" : " ";
65
+ out.push(prefix + op.text);
66
+ if (op.type !== "add") aLine += 1;
67
+ if (op.type !== "del") bLine += 1;
68
+ }
69
+ }
70
+ if (hunks.length === 0) out.push("(no differences)");
71
+ return out.join("\n");
72
+ }
73
+
74
+ function collectHunks(ops, context) {
75
+ // Indices of changed ops.
76
+ const changed = [];
77
+ for (let k = 0; k < ops.length; k++) {
78
+ if (ops[k].type !== "same") changed.push(k);
79
+ }
80
+ if (changed.length === 0) return [];
81
+ const hunks = [];
82
+ let start = Math.max(0, changed[0] - context);
83
+ let end = Math.min(ops.length, changed[0] + context + 1);
84
+ for (let c = 1; c < changed.length; c++) {
85
+ const k = changed[c];
86
+ if (k - context <= end) {
87
+ end = Math.min(ops.length, k + context + 1);
88
+ } else {
89
+ hunks.push([start, end]);
90
+ start = Math.max(0, k - context);
91
+ end = Math.min(ops.length, k + context + 1);
92
+ }
93
+ }
94
+ hunks.push([start, end]);
95
+ return hunks;
96
+ }
97
+
98
+ function splitLines(text) {
99
+ const lines = text.split("\n");
100
+ // Drop a single trailing empty line produced by a final newline.
101
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
102
+ return lines;
103
+ }
@@ -0,0 +1,108 @@
1
+ // Structural EARS checks for capability specs. The clarify command catches
2
+ // word-level smells; this catches grammar-shape problems per EARS section.
3
+ // All findings are warnings: specs evolve, validation should not block.
4
+
5
+ const SECTION_RULES = [
6
+ {
7
+ match: /^###\s+Ubiquitous/i,
8
+ name: "Ubiquitous",
9
+ check(req) {
10
+ if (/^when\b/i.test(req)) return "event-shaped requirement (starts with \"When\") — move to ### Event-driven";
11
+ if (/^while\b/i.test(req)) return "state-shaped requirement (starts with \"While\") — move to ### State-driven";
12
+ if (/^where\b/i.test(req)) return "optional-shaped requirement (starts with \"Where\") — move to ### Optional";
13
+ if (!/\bshall\b/i.test(req)) return "missing \"shall\" — ubiquitous form is \"The system shall ...\"";
14
+ return null;
15
+ },
16
+ },
17
+ {
18
+ match: /^###\s+Event-driven/i,
19
+ name: "Event-driven",
20
+ check(req) {
21
+ if (!/^when\b/i.test(req)) return "expected \"When <trigger>, the system shall ...\"";
22
+ if (!/\bshall\b/i.test(req)) return "missing \"shall\" after the When-trigger";
23
+ return null;
24
+ },
25
+ },
26
+ {
27
+ match: /^###\s+State-driven/i,
28
+ name: "State-driven",
29
+ check(req) {
30
+ if (!/^while\b/i.test(req)) return "expected \"While <state>, the system shall ...\"";
31
+ if (!/\bshall\b/i.test(req)) return "missing \"shall\" after the While-state";
32
+ return null;
33
+ },
34
+ },
35
+ {
36
+ match: /^###\s+Unwanted/i,
37
+ name: "Unwanted-behavior",
38
+ check(req) {
39
+ if (!/\bshall\b/i.test(req)) return "missing \"shall\" — unwanted-behavior form is \"The system shall not ...\"";
40
+ if (!/\b(not|no|never)\b/i.test(req)) return "missing a negation — unwanted-behavior requirements forbid something";
41
+ return null;
42
+ },
43
+ },
44
+ {
45
+ match: /^###\s+Optional/i,
46
+ name: "Optional",
47
+ check(req) {
48
+ if (!/^where\b/i.test(req)) return "expected \"Where <feature is present>, the system may ...\"";
49
+ if (!/\bmay\b/i.test(req)) return "missing \"may\" — optional requirements use may, not shall";
50
+ return null;
51
+ },
52
+ },
53
+ ];
54
+
55
+ // Check one spec text. Returns [{ line, message }]. Only applies to specs
56
+ // that declare a "## Requirements (EARS)" section; bug-shape specs and
57
+ // free-form specs are skipped entirely.
58
+ export function checkEars(text) {
59
+ const lines = text.split("\n");
60
+ const findings = [];
61
+
62
+ let inRequirements = false;
63
+ let rule = null;
64
+ let req = null; // { line, text }
65
+
66
+ const flush = () => {
67
+ if (req && rule) {
68
+ const message = rule.check(req.text.trim());
69
+ if (message) findings.push({ line: req.line, message: `${rule.name}: ${message}` });
70
+ }
71
+ req = null;
72
+ };
73
+
74
+ for (let i = 0; i < lines.length; i++) {
75
+ const line = lines[i];
76
+
77
+ if (/^##\s+Requirements \(EARS\)/i.test(line)) {
78
+ inRequirements = true;
79
+ continue;
80
+ }
81
+ if (!inRequirements) continue;
82
+ if (/^##\s+/.test(line)) {
83
+ // Left the Requirements section.
84
+ flush();
85
+ break;
86
+ }
87
+ if (/^###\s+/.test(line)) {
88
+ flush();
89
+ rule = SECTION_RULES.find((r) => r.match.test(line)) ?? null;
90
+ continue;
91
+ }
92
+ if (/^-\s+/.test(line)) {
93
+ flush();
94
+ req = { line: i + 1, text: line.replace(/^-\s+/, "") };
95
+ continue;
96
+ }
97
+ // Continuation of the current bullet (indented text or nested bullet).
98
+ if (req && /^\s+\S/.test(line)) {
99
+ req.text += " " + line.trim();
100
+ }
101
+ }
102
+ flush();
103
+ return findings;
104
+ }
105
+
106
+ export function isEarsSpec(text) {
107
+ return /^##\s+Requirements \(EARS\)/im.test(text);
108
+ }
package/src/lib/fs-ops.js CHANGED
@@ -73,3 +73,15 @@ export function lineCount(p) {
73
73
  if (text.length === 0) return 0;
74
74
  return text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
75
75
  }
76
+
77
+ // Normalise a path to forward slashes for display and comparison. CLI
78
+ // output is part of the contract (tests, docs, agents parse it), so it
79
+ // must not change shape between platforms.
80
+ export function toPosix(p) {
81
+ return p.split(path.sep).join("/");
82
+ }
83
+
84
+ // path.relative with stable, platform-independent forward slashes.
85
+ export function relPath(from, to) {
86
+ return toPosix(path.relative(from, to));
87
+ }
@@ -0,0 +1,185 @@
1
+ import path from "node:path";
2
+ import { readdirSync } from "node:fs";
3
+ import { isDir, isFile, read, walk } from "./fs-ops.js";
4
+ import { today } from "./dates.js";
5
+ import { parseFrontmatter } from "../commands/skill.js";
6
+ import { parseOperation, parseCapabilityFromDelta } from "../commands/change.js";
7
+
8
+ // Parse a spec-style header line: **Name:** value
9
+ export function specHeader(text, name) {
10
+ const m = text.match(new RegExp(`^\\*\\*${name}:\\*\\*\\s+(.+)$`, "m"));
11
+ return m ? m[1].trim() : null;
12
+ }
13
+
14
+ // Parse an ADR/proposal-style header line: - **Name:** value
15
+ export function listHeader(text, name) {
16
+ const m = text.match(new RegExp(`^-\\s+\\*\\*${name}:\\*\\*\\s+(.+)$`, "m"));
17
+ return m ? m[1].trim() : null;
18
+ }
19
+
20
+ function dirEntries(dir) {
21
+ if (!isDir(dir)) return [];
22
+ return readdirSync(dir).filter((e) => !e.startsWith(".")).sort();
23
+ }
24
+
25
+ // Regenerate the index object from the artifacts on disk. The files are
26
+ // the source of truth; fields with no on-disk source (project name,
27
+ // framework_version, product metadata) are carried over from `current`.
28
+ export function deriveIndex(projectRoot, current) {
29
+ const dot = path.join(projectRoot, ".doctrina");
30
+ const date = today();
31
+ const cur = current?.artifacts ?? {};
32
+
33
+ const out = {
34
+ $schema_version: current?.$schema_version ?? "0.1.0",
35
+ project: current?.project ?? path.basename(projectRoot),
36
+ framework_version: current?.framework_version ?? "0.0.0",
37
+ last_updated: current?.last_updated ?? date,
38
+ artifacts: {
39
+ product: cur.product ?? {
40
+ path: ".doctrina/product.md",
41
+ status: "active",
42
+ version: "0.1.0",
43
+ last_updated: date,
44
+ },
45
+ specs: [],
46
+ decisions: [],
47
+ changes: [],
48
+ changes_archive: [],
49
+ skills: [],
50
+ },
51
+ };
52
+
53
+ // Specs — headers inside each spec.md win over the previous index.
54
+ const specsDir = path.join(dot, "specs");
55
+ for (const cap of dirEntries(specsDir)) {
56
+ const p = path.join(specsDir, cap, "spec.md");
57
+ if (!isFile(p)) continue;
58
+ const text = read(p);
59
+ const prev = (cur.specs ?? []).find((s) => s.id === cap);
60
+ out.artifacts.specs.push({
61
+ id: cap,
62
+ path: `.doctrina/specs/${cap}/spec.md`,
63
+ status: specHeader(text, "Status") ?? prev?.status ?? "active",
64
+ version: specHeader(text, "Version") ?? prev?.version ?? "0.1.0",
65
+ last_updated: specHeader(text, "Last updated") ?? prev?.last_updated ?? date,
66
+ });
67
+ }
68
+
69
+ // Decisions — NNNN-slug.md files; Status/Date headers win.
70
+ const adrDir = path.join(dot, "decisions");
71
+ for (const f of walk(adrDir)) {
72
+ const base = path.basename(f);
73
+ const m = base.match(/^(\d{4})-.*\.md$/);
74
+ if (!m) continue;
75
+ const text = read(f);
76
+ const prev = (cur.decisions ?? []).find((d) => d.id === m[1]);
77
+ const titleMatch = text.match(/^#\s+ADR\s+\d{4}\s*[—-]\s*(.+)$/m);
78
+ const entry = {
79
+ id: m[1],
80
+ path: `.doctrina/decisions/${base}`,
81
+ title: titleMatch ? titleMatch[1].trim() : prev?.title ?? base,
82
+ status: listHeader(text, "Status") ?? prev?.status ?? "proposed",
83
+ date: listHeader(text, "Date") ?? prev?.date ?? date,
84
+ };
85
+ const supersedes = listHeader(text, "Supersedes");
86
+ if (supersedes && supersedes !== "—") entry.supersedes = supersedes;
87
+ const supersededBy = listHeader(text, "Superseded by");
88
+ if (supersededBy && supersededBy !== "—") entry.superseded_by = supersededBy;
89
+ out.artifacts.decisions.push(entry);
90
+ }
91
+
92
+ // Open changes — every directory except archive/.
93
+ const changesDir = path.join(dot, "changes");
94
+ for (const id of dirEntries(changesDir)) {
95
+ if (id === "archive") continue;
96
+ if (!isDir(path.join(changesDir, id))) continue;
97
+ const prev = (cur.changes ?? []).find((c) => c.id === id);
98
+ const proposalPath = path.join(changesDir, id, "proposal.md");
99
+ const proposal = isFile(proposalPath) ? read(proposalPath) : "";
100
+ const titleMatch = proposal.match(/^#\s+Change\s+\S+\s*[—-]\s*(.+)$/m);
101
+ out.artifacts.changes.push({
102
+ id,
103
+ title: titleMatch ? titleMatch[1].trim() : prev?.title ?? id,
104
+ path: `.doctrina/changes/${id}`,
105
+ status: listHeader(proposal, "Status") ?? prev?.status ?? "proposed",
106
+ opened: listHeader(proposal, "Date") ?? prev?.opened ?? date,
107
+ });
108
+ }
109
+
110
+ // Archived changes — folder name carries the applied date and id.
111
+ const archiveDir = path.join(changesDir, "archive");
112
+ for (const name of dirEntries(archiveDir)) {
113
+ const m = name.match(/^(\d{4}-\d{2}-\d{2})-(.+)$/);
114
+ if (!m || !isDir(path.join(archiveDir, name))) continue;
115
+ const prev = (cur.changes_archive ?? []).find((c) => c.path?.endsWith(name));
116
+ const proposalPath = path.join(archiveDir, name, "proposal.md");
117
+ const proposal = isFile(proposalPath) ? read(proposalPath) : "";
118
+ const titleMatch = proposal.match(/^#\s+Change\s+\S+\s*[—-]\s*(.+)$/m);
119
+ const specsAffected = [];
120
+ for (const deltaPath of walk(path.join(archiveDir, name, "specs"))) {
121
+ if (!deltaPath.endsWith("delta.md")) continue;
122
+ const text = read(deltaPath);
123
+ const capability = parseCapabilityFromDelta(text, deltaPath);
124
+ if (capability) {
125
+ specsAffected.push({ capability, operation: parseOperation(text) ?? "MODIFIED" });
126
+ }
127
+ }
128
+ out.artifacts.changes_archive.push({
129
+ id: m[2],
130
+ title: titleMatch ? titleMatch[1].trim() : prev?.title ?? m[2],
131
+ path: `.doctrina/changes/archive/${name}`,
132
+ status: "applied",
133
+ applied: m[1],
134
+ specs_affected: specsAffected,
135
+ });
136
+ }
137
+
138
+ // Skills — frontmatter description wins.
139
+ const skillsDir = path.join(dot, "skills");
140
+ for (const f of walk(skillsDir)) {
141
+ if (!f.endsWith(".md")) continue;
142
+ const id = path.basename(f, ".md");
143
+ const prev = (cur.skills ?? []).find((s) => s.id === id);
144
+ out.artifacts.skills.push({
145
+ id,
146
+ path: `.doctrina/skills/${id}.md`,
147
+ description: parseFrontmatter(read(f), "description")
148
+ ?? prev?.description ?? "<edit me — one-sentence summary>",
149
+ last_updated: prev?.last_updated ?? date,
150
+ });
151
+ }
152
+
153
+ return out;
154
+ }
155
+
156
+ // Deterministic stringify (sorted keys) so two indexes compare by content,
157
+ // not by key insertion order.
158
+ export function stableStringify(value) {
159
+ if (Array.isArray(value)) {
160
+ return "[" + value.map(stableStringify).join(",") + "]";
161
+ }
162
+ if (value && typeof value === "object") {
163
+ const keys = Object.keys(value).sort();
164
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(value[k])).join(",") + "}";
165
+ }
166
+ return JSON.stringify(value);
167
+ }
168
+
169
+ // Content equality ignoring the root last_updated timestamp. Absent
170
+ // artifact categories compare equal to empty ones so pre-skills indexes
171
+ // are not flagged for a purely structural difference.
172
+ export function indexesMatch(a, b) {
173
+ if (!a || !b) return false;
174
+ const normalize = (x) => {
175
+ const clone = { ...x };
176
+ delete clone.last_updated;
177
+ const arts = { ...(clone.artifacts ?? {}) };
178
+ for (const cat of ["specs", "decisions", "changes", "changes_archive", "skills"]) {
179
+ arts[cat] = arts[cat] ?? [];
180
+ }
181
+ clone.artifacts = arts;
182
+ return clone;
183
+ };
184
+ return stableStringify(normalize(a)) === stableStringify(normalize(b));
185
+ }
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
- import { read, walk, write } from "./fs-ops.js";
3
+ import { read, toPosix, walk, write } from "./fs-ops.js";
4
4
 
5
5
  // Resolve the path to the .doctrina/templates/ directory shipped with this
6
6
  // package. The package layout is:
@@ -59,7 +59,7 @@ export function loadTemplateTree(templatesDir, subdir) {
59
59
  const root = path.join(templatesDir, subdir);
60
60
  const files = walk(root);
61
61
  return files.map((templatePath) => {
62
- const relativePath = path.relative(root, templatePath);
62
+ const relativePath = toPosix(path.relative(root, templatePath));
63
63
  const body = templatePath.endsWith(".gitkeep") ? "" : read(templatePath);
64
64
  return { templatePath, relativePath, body };
65
65
  });
@@ -0,0 +1,83 @@
1
+ # AGENTS.md — {{PROJECT_NAME}}
2
+
3
+ Operational source of truth for AI coding agents working in this
4
+ repository. Follows the open AGENTS.md standard.
5
+
6
+ ## What this repo is
7
+
8
+ {{PROJECT_DESCRIPTION}}
9
+
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
+ ## Working from intent
15
+
16
+ - **Bootstrap.** If `.doctrina/intake.md` exists with `Status: pending`,
17
+ the specs have not been written yet. Before anything else, run
18
+ `doctrina intake` and execute the printed bootstrap playbook in one
19
+ linear pass: fill `product.md`, derive capabilities, author one EARS
20
+ spec each, run the quality gates (`doctrina clarify --all`,
21
+ `doctrina validate`), then flip the intake to `Status: converted`. Ask
22
+ the user only when the intake is genuinely ambiguous. After conversion
23
+ the specs are the only source of truth — never edit `intake.md` to
24
+ change requirements.
25
+ - **Day-to-day.** Turn a brief request into a scaffolded change with
26
+ `doctrina work "<prompt>"`, then follow the printed work playbook
27
+ (spec delta → tasks → implement → analyze → apply → archive → validate).
28
+
29
+ ## Stack and tooling
30
+
31
+ <!-- Replace with the project's actual stack. Keep this section short. -->
32
+ - Runtime:
33
+ - Package manager:
34
+ - Test runner:
35
+ - Linter / formatter:
36
+
37
+ ## Commands
38
+
39
+ <!-- Use exact, copy-pasteable commands. Avoid prose. -->
40
+ ```
41
+ # install
42
+ # build
43
+ # test
44
+ # lint
45
+ ```
46
+
47
+ ## Repository structure
48
+
49
+ <!-- Outline the top-level directories an agent needs to know about. -->
50
+
51
+ ## Conventions and boundaries
52
+
53
+ - Specs in `.doctrina/specs/<capability>/spec.md` are the current truth.
54
+ - ADRs in `.doctrina/decisions/` are immutable; supersede instead of edit.
55
+ - Active change proposals live in `.doctrina/changes/<id>/`.
56
+ - Archived changes in `.doctrina/changes/archive/` are out of the default
57
+ read path; consult only when explicitly debugging history.
58
+
59
+ ## How to read context efficiently
60
+
61
+ 1. Read this `AGENTS.md`.
62
+ 2. Read `.doctrina/product.md`.
63
+ 3. Read the relevant `.doctrina/specs/<capability>/spec.md`.
64
+ 4. Read any open `.doctrina/changes/<id>/`.
65
+ 5. Read `.doctrina/decisions/` filtered by `Status: accepted`.
66
+ 6. If the task matches a skill in `.doctrina/skills/`, load the
67
+ relevant skill on demand (read its `description:` / `when:`
68
+ frontmatter; load the full body only when the trigger fires).
69
+
70
+ Keep this file under 150 lines. Density beats prose. Use exact commands.
71
+
72
+ ## Definition of done
73
+
74
+ A change is done when:
75
+ - All tasks in the change's `tasks.md` are checked.
76
+ - Deltas have been merged into affected spec files.
77
+ - The change folder has been moved to `.doctrina/changes/archive/`.
78
+ - Any architectural decisions are recorded as ADRs with `Status: accepted`.
79
+ - `.doctrina/index.json` has been updated.
80
+
81
+ ## What never goes in this file
82
+
83
+ Tutorials, project history, secrets, generated content, session notes.
@@ -0,0 +1,85 @@
1
+ # Doctrina templates
2
+
3
+ Canonical scaffolds consumed by the `doctrina` CLI.
4
+
5
+ ## Placeholder syntax
6
+
7
+ Templates use `{{TOKEN}}` — double curly braces around an uppercase,
8
+ digit, and underscore token. Substitution is a single global string
9
+ replace. No conditionals, loops, or includes.
10
+
11
+ ## Canonical tokens
12
+
13
+ | Token | Meaning | Default if not supplied |
14
+ |-------|---------|-------------------------|
15
+ | `{{PROJECT_NAME}}` | Human-readable project name | basename of target directory |
16
+ | `{{PROJECT_DESCRIPTION}}` | One-sentence description | empty string |
17
+ | `{{DATE}}` | YYYY-MM-DD of init or artifact creation | system date |
18
+ | `{{CAPABILITY}}` | Slug for a new capability spec | required, no default |
19
+ | `{{CHANGE_ID}}` | Change folder identifier | required, no default |
20
+ | `{{CHANGE_TITLE}}` | Short change title | required, no default |
21
+ | `{{DECISION_NUMBER}}` | Zero-padded ADR number (e.g. `0007`) | next available |
22
+ | `{{DECISION_TITLE}}` | ADR title | required, no default |
23
+ | `{{DECISION_SLUG}}` | kebab-case slug derived from `{{DECISION_TITLE}}` | derived |
24
+ | `{{AGENTS_MD_PATH}}` | Path to root `AGENTS.md` from the adapter location | per adapter |
25
+
26
+ CLI authors and template authors share this vocabulary. Adding a token
27
+ requires extending the templates spec at `.doctrina/specs/templates/spec.md`.
28
+
29
+ ## Inventory (v0)
30
+
31
+ ```
32
+ README.md this file
33
+ AGENTS.md.template root rules for the target repo
34
+ doctrina/ the .doctrina/ skeleton
35
+ product.md.template
36
+ index.json.template
37
+ specs/.gitkeep
38
+ changes/.gitkeep
39
+ changes/archive/.gitkeep
40
+ decisions/.gitkeep
41
+ skills/.gitkeep
42
+ templates/.gitkeep
43
+ spec.md.template one capability spec (EARS)
44
+ spec-bug.md.template bug-shape capability spec
45
+ skill.md.template one on-demand skill
46
+ change/
47
+ proposal.md.template
48
+ tasks.md.template
49
+ design.md.template
50
+ spec-delta.md.template
51
+ decision.md.template one ADR (Nygard/MADR)
52
+ hooks/
53
+ pre-commit.sample installed by `doctrina hooks install`
54
+ watch.sample on-save validation wrapper (manual wiring)
55
+ adapters/
56
+ claude/CLAUDE.md.template
57
+ codex/README.md
58
+ cursor/.cursor/rules/00-doctrina.mdc.template
59
+ copilot/.github/copilot-instructions.md.template
60
+ gemini/GEMINI.md.template
61
+ aider/CONVENTIONS.md.template
62
+ windsurf/.windsurfrules.template
63
+ continue/.continue/rules/00-doctrina.md.template
64
+ amp/README.md
65
+ devin/README.md
66
+ factory/README.md
67
+ jules/README.md
68
+ ```
69
+
70
+ ## Adapter strategy
71
+
72
+ | Agent | What gets installed | Why |
73
+ |-------|--------------------|-----|
74
+ | `claude` | `CLAUDE.md` at project root with `@AGENTS.md` import | Claude Code reads `CLAUDE.md` natively and supports `@file` includes. |
75
+ | `codex` | nothing | OpenAI Codex CLI reads `AGENTS.md` natively; an adapter file would only add drift surface. |
76
+ | `cursor` | `.cursor/rules/00-doctrina.mdc` with `alwaysApply: true` | Cursor's `.mdc` rules live in a dedicated directory; the file is a thin pointer. |
77
+ | `copilot` | `.github/copilot-instructions.md` | GitHub Copilot's repository-level custom instructions path. |
78
+ | `gemini` | `GEMINI.md` at project root | Gemini CLI's native context file. |
79
+ | `aider` | `CONVENTIONS.md` at project root | Aider reads CONVENTIONS.md as cached read-only context. |
80
+ | `windsurf` | `.windsurfrules` at project root | Windsurf's rules file convention. |
81
+ | `continue` | `.continue/rules/00-doctrina.md` | Continue.dev's rules directory. |
82
+ | `amp` / `devin` / `factory` / `jules` | nothing | AGENTS.md-native (same posture as `codex`); the directory marks the support contract and gives future per-agent affordances a home. |
83
+
84
+ Adapter files stay under 30 lines so they cannot meaningfully drift from
85
+ `AGENTS.md` and so they do not pay a noticeable per-turn token tax.
@@ -0,0 +1,11 @@
1
+ # CONVENTIONS.md — {{PROJECT_NAME}}
2
+
3
+ Aider reads this file as read-only context (CONVENTIONS.md is
4
+ loaded automatically and cached). This project routes all
5
+ operational rules through the open AGENTS.md standard.
6
+
7
+ See `{{AGENTS_MD_PATH}}` for the canonical operational source of
8
+ truth. The Doctrina framework artifacts (specs, decisions,
9
+ changes) live under `.doctrina/`.
10
+
11
+ Read order is documented in AGENTS.md.
@@ -0,0 +1,13 @@
1
+ # Sourcegraph Amp adapter
2
+
3
+ Sourcegraph Amp reads `AGENTS.md` natively as part of its
4
+ support for the open AGENTS.md standard (per the Linux
5
+ Foundation's December 2025 announcement). No adapter file is
6
+ installed for Amp.
7
+
8
+ This directory exists so the templates inventory is symmetric
9
+ across supported agents, and so future Amp-specific affordances
10
+ have an obvious home.
11
+
12
+ If you are reading this because `doctrina init --agent amp` did
13
+ not create any files: that is the intended behaviour.
@@ -0,0 +1,11 @@
1
+ # CLAUDE.md — {{PROJECT_NAME}}
2
+
3
+ This project uses the open AGENTS.md standard as the single source of
4
+ truth for agent-readable rules. The canonical file is at the repository
5
+ root; this `CLAUDE.md` is a thin pointer so Claude Code's auto-loading
6
+ picks it up.
7
+
8
+ @{{AGENTS_MD_PATH}}
9
+
10
+ The Doctrina framework artifacts (specs, decisions, changes) live under
11
+ `.doctrina/`. Read order is documented in `AGENTS.md`.
@@ -0,0 +1,12 @@
1
+ # Codex CLI adapter
2
+
3
+ OpenAI Codex CLI reads `AGENTS.md` natively (Codex is one of the
4
+ reference implementations of the open AGENTS.md standard). No adapter
5
+ file is installed for Codex.
6
+
7
+ This directory exists so the templates inventory is symmetric across
8
+ supported agents, and so future Codex-specific affordances (for example
9
+ custom subagent definitions) have an obvious home.
10
+
11
+ If you are reading this because `doctrina init --agent codex` did not
12
+ create any files: that is the intended behaviour.
@@ -0,0 +1,11 @@
1
+ # Doctrina pointer — {{PROJECT_NAME}}
2
+
3
+ This project uses the open AGENTS.md standard as the single source of
4
+ truth for agent-readable rules. Before answering or editing, read the
5
+ canonical file at `{{AGENTS_MD_PATH}}`.
6
+
7
+ The Doctrina framework artifacts (specs, decisions, changes) live
8
+ under `.doctrina/`. Read order is documented in AGENTS.md.
9
+
10
+ Do not duplicate rules in this file. If a rule belongs in AGENTS.md,
11
+ put it there.
@@ -0,0 +1,11 @@
1
+ # Copilot instructions — {{PROJECT_NAME}}
2
+
3
+ This repository uses the open AGENTS.md standard as the single source
4
+ of truth for agent-readable rules. Before suggesting code or
5
+ completions, consult the canonical file at `{{AGENTS_MD_PATH}}`.
6
+
7
+ The Doctrina framework artifacts (specs, decisions, changes) live
8
+ under `.doctrina/`. Read order is documented in AGENTS.md.
9
+
10
+ Do not duplicate rules here. If a rule belongs in AGENTS.md, put it
11
+ there.