doctrina-cli 0.11.0 → 0.13.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.
@@ -13,13 +13,13 @@ export const COMMAND_NAMES = [
13
13
  // bootstrap / day-to-day
14
14
  "init", "intake", "work",
15
15
  // authoring
16
- "spec", "change", "contract", "decision", "skill",
16
+ "spec", "change", "contract", "decision", "skill", "intent",
17
17
  // read / orient
18
18
  "prime", "context", "show", "search", "status", "next", "why", "handoff", "constitution",
19
19
  // gates
20
20
  "analyze", "clarify", "validate", "coverage", "trace", "review", "verify", "close", "doctor",
21
21
  // maintenance
22
- "templates", "hooks", "index", "watch", "metrics", "report", "completion",
22
+ "templates", "hooks", "index", "watch", "metrics", "report", "completion", "upgrade",
23
23
  ];
24
24
 
25
25
  // The full operation surface — every `doctrina <command> [<subcommand>]` a
@@ -40,8 +40,10 @@ export const OPERATIONS = [
40
40
  ["spec list", "List specs with version, status, and size"],
41
41
  ["spec set", "Edit spec headers / a criterion mark and resync the index"],
42
42
  ["change new", "Open a change proposal"],
43
- ["change apply", "Apply spec deltas (ADDED/REMOVED auto, MODIFIED manual)"],
44
- ["change archive", "Archive an applied change"],
43
+ ["change apply", "Apply spec deltas (ops blocks mechanically; batch ids ok)"],
44
+ ["change archive", "Archive an applied change (batch ids ok)"],
45
+ ["change check", "Pre-close dry-run: everything close would refuse, listed first"],
46
+ ["change tick", "List/tick the unchecked boxes (tasks + verification; --all)"],
45
47
  ["change diff", "Preview spec deltas (line diff for MODIFIED)"],
46
48
  ["change abandon", "Discard an open change cleanly (recorded in the ledger)"],
47
49
  ["contract new", "Own the integration surface (ports, env, interfaces)"],
@@ -56,6 +58,8 @@ export const OPERATIONS = [
56
58
  ["skill list", "List skills with their descriptions"],
57
59
  ["skill sync", "Mirror skill frontmatter descriptions into index.json"],
58
60
  ["skill suggest", "Surface fix-shaped lessons worth a skill (--write scaffolds)"],
61
+ ["intent add", "Append a new product intent anchor post-intake (- [SC5] ...)"],
62
+ ["intent list", "List product.md intent anchors in document order"],
59
63
  ["analyze", "Inspect a change folder before applying"],
60
64
  ["clarify", "Smell-test a Markdown file for ambiguity (--all for the tree)"],
61
65
  ["prime", "Session primer: gates, rules, open work, next steps in one read"],
@@ -83,6 +87,7 @@ export const OPERATIONS = [
83
87
  ["metrics", "Local git-derived adoption metrics (no network)"],
84
88
  ["report", "Markdown digest for a period: changes, gates, git summary"],
85
89
  ["completion", "Print bash/zsh/pwsh completions (generated from the catalog)"],
90
+ ["upgrade", "Bring an existing project up to the installed CLI (--write applies)"],
86
91
  ];
87
92
 
88
93
  // The Commands block of `doctrina --help`, generated from OPERATIONS so the
@@ -91,6 +96,102 @@ export function surfaceHelp() {
91
96
  return OPERATIONS.map(([op, summary]) => ` ${op.padEnd(21)}${summary}`).join("\n");
92
97
  }
93
98
 
99
+ // ---------------------------------------------------------------------------
100
+ // The CLI-owned AGENTS.md command-surface block (operator review 2026-07-19,
101
+ // meta-conclusion): for an agent, AGENTS.md IS the discovery interface — it
102
+ // never runs `--help` spontaneously — so every command absent from the hub
103
+ // was invisible for ~35 changes (`prime`, `handoff`, `doctor`, `show`, ...).
104
+ // The fix is a marker-delimited section GENERATED from OPERATIONS, so the
105
+ // hub's catalog is the CLI's catalog by construction: `init` scaffolds it,
106
+ // `templates check` flags it stale, and `templates update --write` (hence
107
+ // `doctrina upgrade --write`) regenerates exactly the marked span and
108
+ // nothing else. Content between the markers is CLI-owned; everything
109
+ // outside remains the adopter's, under the additive-only guarantee.
110
+
111
+ export const SURFACE_BEGIN =
112
+ "<!-- doctrina:surface:begin — CLI-owned block, generated from the installed command catalog. Refreshed by `doctrina upgrade --write`; edits inside are overwritten. -->";
113
+ export const SURFACE_END = "<!-- doctrina:surface:end -->";
114
+
115
+ // Workflow grouping for the generated block. A test asserts the flattened
116
+ // names equal COMMAND_NAMES, so a new command cannot ship ungrouped (and
117
+ // therefore cannot ship invisible to AGENTS.md).
118
+ export const SURFACE_GROUPS = [
119
+ ["Start", ["init", "intake", "work"]],
120
+ ["Author", ["spec", "change", "contract", "decision", "skill", "intent"]],
121
+ ["Read / orient", ["prime", "context", "show", "search", "status", "next", "why", "handoff", "constitution"]],
122
+ ["Gates", ["analyze", "clarify", "validate", "coverage", "trace", "review", "verify", "close", "doctor"]],
123
+ ["Maintain", ["templates", "hooks", "index", "watch", "metrics", "report", "completion", "upgrade"]],
124
+ ];
125
+
126
+ // Per-command usage hints appended to the generated reference — arguments and
127
+ // the flags an agent reaches for daily. Cosmetic only; the operation list
128
+ // itself always comes from OPERATIONS.
129
+ const SURFACE_HINTS = {
130
+ work: '"<prompt>" (--capability · --chore · --from-diff · --quiet)',
131
+ context: "[<cap>] --concat",
132
+ validate: "(--fix)",
133
+ coverage: "--strict",
134
+ trace: "--strict",
135
+ clarify: "--all (--lang pt|en)",
136
+ close: "<id...>",
137
+ upgrade: "--write",
138
+ prime: "(session start)",
139
+ handoff: "(before compaction/handover)",
140
+ };
141
+
142
+ // The full managed section, heading included, between the markers.
143
+ export function surfaceMarkdown() {
144
+ const subsByCmd = new Map();
145
+ for (const [op] of OPERATIONS) {
146
+ const [cmd, sub] = op.split(" ");
147
+ if (!subsByCmd.has(cmd)) subsByCmd.set(cmd, []);
148
+ if (sub) subsByCmd.get(cmd).push(sub);
149
+ }
150
+ const renderCmd = (cmd) => {
151
+ const subs = subsByCmd.get(cmd) ?? [];
152
+ const hint = SURFACE_HINTS[cmd] ? ` ${SURFACE_HINTS[cmd]}` : "";
153
+ return subs.length > 0 ? `\`doctrina ${cmd} ${subs.join("|")}\`` : `\`doctrina ${cmd}${hint}\``;
154
+ };
155
+ const lines = [
156
+ "## Doctrina command surface (generated — reach for these, don't hand-author)",
157
+ "",
158
+ "Every operation of the installed CLI, grouped by workflow. The CLI",
159
+ "scaffolds from canonical templates and keeps `index.json` in sync, so",
160
+ "prefer it over writing artifacts by hand. Details: `doctrina --help`.",
161
+ "",
162
+ ];
163
+ for (const [label, cmds] of SURFACE_GROUPS) {
164
+ lines.push(`- **${label}:** ${cmds.map(renderCmd).join(" · ")}`);
165
+ }
166
+ lines.push("");
167
+ lines.push("Session bookends: `doctrina prime` to orient at session start;");
168
+ lines.push("`doctrina handoff` before compaction or handover. Continuous:");
169
+ lines.push("`doctrina watch`. Capture lessons: `doctrina skill suggest`.");
170
+ return lines.join("\n");
171
+ }
172
+
173
+ // The block as written to AGENTS.md: markers wrapping the generated section.
174
+ export function surfaceBlock() {
175
+ return `${SURFACE_BEGIN}\n${surfaceMarkdown()}\n${SURFACE_END}`;
176
+ }
177
+
178
+ // Locate the managed block in an AGENTS.md text. Returns
179
+ // { start, end, inner } (string offsets; inner excludes the marker lines)
180
+ // or null when either marker is absent.
181
+ export function findSurfaceBlock(text) {
182
+ const begin = text.match(/<!--\s*doctrina:surface:begin[\s\S]*?-->/);
183
+ if (!begin) return null;
184
+ const endRe = /<!--\s*doctrina:surface:end\s*-->/;
185
+ const end = endRe.exec(text.slice(begin.index + begin[0].length));
186
+ if (!end) return null;
187
+ const innerStart = begin.index + begin[0].length;
188
+ return {
189
+ start: begin.index,
190
+ end: innerStart + end.index + end[0].length,
191
+ inner: text.slice(innerStart, innerStart + end.index).replace(/^\r?\n|\r?\n$/g, ""),
192
+ };
193
+ }
194
+
94
195
  // Extract the set of top-level `doctrina <command>` references that appear in
95
196
  // CODE context (fenced blocks or inline backtick spans) of a Markdown file —
96
197
  // AGENTS.md, typically. Restricting to code context avoids matching prose like
package/src/lib/scan.js CHANGED
@@ -25,6 +25,15 @@ export function listHeader(text, name) {
25
25
  return m ? m[1].trim() : null;
26
26
  }
27
27
 
28
+ // The machine-readable "**Depends on:** defects, decisions" spec header: the
29
+ // sibling capabilities this spec builds on. Returns [] when absent, "n/a", or
30
+ // empty. One parser so index, why, context, and review agree.
31
+ export function parseDependsOn(text) {
32
+ const raw = specHeader(text, "Depends on");
33
+ if (!raw || /^n\/a\b/i.test(raw.trim()) || raw.trim() === "—") return [];
34
+ return raw.match(/[a-z][a-z0-9][a-z0-9-]*/g) ?? [];
35
+ }
36
+
28
37
  function dirEntries(dir) {
29
38
  if (!isDir(dir)) return [];
30
39
  return readdirSync(dir).filter((e) => !e.startsWith(".")).sort();
@@ -87,6 +96,13 @@ export function deriveIndex(projectRoot, current) {
87
96
  const realizesRaw = specHeader(text, "Realizes");
88
97
  const realizes = realizesRaw ? (realizesRaw.match(/[A-Z]+\d+/g) ?? []) : prev?.realizes;
89
98
  if (realizes && realizes.length) entry.realizes = realizes;
99
+ // Depends on: sibling capabilities this spec builds on (0.11.0 field
100
+ // review — specs cited each other only in prose, unreadable by machine).
101
+ // Stored as a capability-id list so `why` can show the graph, `context`
102
+ // can pull dependencies into the read pack, and `review` can flag
103
+ // dependents of a touched capability.
104
+ const depends = specHeader(text, "Depends on") !== null ? parseDependsOn(text) : prev?.depends_on;
105
+ if (depends && depends.length) entry.depends_on = depends;
90
106
  out.artifacts.specs.push(entry);
91
107
  }
92
108
 
@@ -17,8 +17,17 @@
17
17
  // set-criterion 1: verified
18
18
  // replace-criterion 2: [verified] new text — verified by `test/x.test.ts`
19
19
  // append-criterion [unverified] new signal — verified by `test/y.test.ts`
20
+ // append-requirement event: When X occurs, the system shall do Y.
21
+ // replace-requirement ubiquitous 2: The system shall do Z.
20
22
  // ```
21
23
  //
24
+ // The requirement verbs (operator review 2026-07-19 §4.1) cover the dominant
25
+ // MODIFIED case — inserting or rewording EARS bullets in a `## Requirements
26
+ // (EARS)` subsection — so a typical delta no longer falls back to a manual
27
+ // merge. Because append-* verbs resolve their position/number at APPLY time,
28
+ // concurrent open changes appending to the same spec cannot collide on
29
+ // numbering: order of application decides, mechanically.
30
+ //
22
31
  // A `# comment` or blank line inside the block is ignored. When a delta has
23
32
  // no ops block, apply falls back to the manual-merge pointer (backward
24
33
  // compatible).
@@ -48,9 +57,13 @@ export function extractOps(deltaText) {
48
57
 
49
58
  // Find the body of the first ```ops fenced block, or null if absent. The
50
59
  // info string must be exactly "ops" (case-insensitive) so a normal code
51
- // fence in the prose is never mistaken for operations.
60
+ // fence in the prose is never mistaken for operations. HTML comments are
61
+ // stripped first: the scaffolded delta template carries an EXAMPLE ops block
62
+ // inside its instructional comment, and executing an example against a real
63
+ // spec is exactly the surprise this guard removes.
52
64
  function matchOpsBlock(text) {
53
- const m = text.match(/^[ \t]*```[ \t]*ops[ \t]*\r?\n([\s\S]*?)^[ \t]*```[ \t]*$/m);
65
+ const visible = text.replace(/<!--[\s\S]*?-->/g, "");
66
+ const m = visible.match(/^[ \t]*```[ \t]*ops[ \t]*\r?\n([\s\S]*?)^[ \t]*```[ \t]*$/m);
54
67
  return m ? m[1] : null;
55
68
  }
56
69
 
@@ -92,11 +105,50 @@ function parseOpLine(line) {
92
105
  if (!rest) return { verb, error: "append-criterion is missing its text" };
93
106
  return { verb, value: rest };
94
107
  }
108
+ case "append-requirement": {
109
+ // append-requirement <section>: <text>
110
+ const colon = rest.indexOf(":");
111
+ if (colon < 0) return { verb, error: `append-requirement needs "<section>: <text>" (got "${rest}")` };
112
+ const section = normalizeSection(rest.slice(0, colon).trim());
113
+ const value = rest.slice(colon + 1).trim();
114
+ if (!section) return { verb, error: `append-requirement: unknown section "${rest.slice(0, colon).trim()}" (use ${SECTION_TOKENS})` };
115
+ if (!value) return { verb, error: "append-requirement is missing the requirement text" };
116
+ return { verb, section, value };
117
+ }
118
+ case "replace-requirement": {
119
+ // replace-requirement <section> <n>: <text>
120
+ const colon = rest.indexOf(":");
121
+ if (colon < 0) return { verb, error: `replace-requirement needs "<section> <n>: <text>" (got "${rest}")` };
122
+ const head = rest.slice(0, colon).trim().split(/\s+/);
123
+ const value = rest.slice(colon + 1).trim();
124
+ const n = Number(head[head.length - 1]);
125
+ const section = normalizeSection(head.slice(0, -1).join(" "));
126
+ if (!section) return { verb, error: `replace-requirement: unknown section "${head.slice(0, -1).join(" ")}" (use ${SECTION_TOKENS})` };
127
+ if (!Number.isInteger(n) || n < 1) return { verb, error: `replace-requirement needs a positive bullet number (got "${rest}")` };
128
+ if (!value) return { verb, error: `replace-requirement ${n} is missing the requirement text` };
129
+ return { verb, section, n, value };
130
+ }
95
131
  default:
96
132
  return { verb: null, error: `unknown operation "${verb}"` };
97
133
  }
98
134
  }
99
135
 
136
+ // EARS subsection aliases -> the canonical token used to match the spec's
137
+ // `### ` heading under `## Requirements (EARS)`. Deterministic: first word of
138
+ // the heading, folded ("Unwanted-behavior (must-not)" -> "unwanted").
139
+ const SECTIONS = {
140
+ ubiquitous: "ubiquitous",
141
+ event: "event", "event-driven": "event",
142
+ state: "state", "state-driven": "state",
143
+ unwanted: "unwanted", "unwanted-behavior": "unwanted", "must-not": "unwanted",
144
+ optional: "optional",
145
+ };
146
+ const SECTION_TOKENS = "ubiquitous|event|state|unwanted|optional";
147
+
148
+ function normalizeSection(raw) {
149
+ return SECTIONS[raw.toLowerCase()] ?? null;
150
+ }
151
+
100
152
  // Apply a parsed op list to a spec's Markdown text. Pure: returns
101
153
  // { text, applied: string[], errors: string[] } and never throws. A failed
102
154
  // op is recorded in `errors` and leaves the text unchanged for that op, so
@@ -132,6 +184,10 @@ function applyOne(text, op) {
132
184
  return replaceCriterion(text, op.n, op.value);
133
185
  case "append-criterion":
134
186
  return appendCriterion(text, op.value);
187
+ case "append-requirement":
188
+ return appendRequirement(text, op.section, op.value);
189
+ case "replace-requirement":
190
+ return replaceRequirement(text, op.section, op.n, op.value);
135
191
  default:
136
192
  return { error: `unknown operation "${op.verb}"` };
137
193
  }
@@ -221,6 +277,90 @@ export function replaceCriterion(text, n, value) {
221
277
  return { text: loc.lines.join("\n"), summary: `replace-criterion ${n}` };
222
278
  }
223
279
 
280
+ // The `### <Section>` blocks under `## Requirements (EARS)`. Returns
281
+ // { lines, sections: { <token>: { headingIndex, endIndex, bullets: [lineIndex] } } }
282
+ // or null when the Requirements section is absent. A section's token is the
283
+ // first word of its heading, folded through SECTIONS ("Unwanted-behavior
284
+ // (must-not)" -> "unwanted"). endIndex is exclusive: the line index of the
285
+ // next heading (### or ##) after the section, or lines.length.
286
+ function locateRequirements(text) {
287
+ const lines = text.split(/\r?\n/);
288
+ let inRequirements = false;
289
+ let current = null;
290
+ const sections = {};
291
+ for (let i = 0; i < lines.length; i++) {
292
+ const line = lines[i];
293
+ if (/^##\s+/.test(line) && !/^###/.test(line)) {
294
+ if (current) { current.endIndex = i; current = null; }
295
+ inRequirements = /^##\s+Requirements\b/i.test(line);
296
+ continue;
297
+ }
298
+ if (!inRequirements) continue;
299
+ const h = line.match(/^###\s+(\S+)/);
300
+ if (h) {
301
+ if (current) current.endIndex = i;
302
+ const token = SECTIONS[h[1].toLowerCase().replace(/[^a-z-]/g, "")] ?? null;
303
+ current = { headingIndex: i, endIndex: lines.length, bullets: [] };
304
+ if (token) sections[token] = current;
305
+ continue;
306
+ }
307
+ if (current && /^\s*-(\s|$)/.test(line)) current.bullets.push(i);
308
+ }
309
+ return Object.keys(sections).length > 0 || /^##\s+Requirements\b/im.test(text)
310
+ ? { lines, sections }
311
+ : null;
312
+ }
313
+
314
+ // A list item may wrap over indented continuation lines ("- When X,\n
315
+ // the system shall ..."). Inserting right after the item's FIRST line
316
+ // splits it from its continuation — the bug the 0.13.0 dogfood close
317
+ // caught on this very repo's cli spec. From the item's first line, the
318
+ // item ends after every following line that is indented content.
319
+ function endOfItem(lines, startIndex) {
320
+ let i = startIndex;
321
+ while (i + 1 < lines.length && /^\s+\S/.test(lines[i + 1])) i += 1;
322
+ return i;
323
+ }
324
+
325
+ // append-requirement <section>: <text> — add "- <text>" at the end of the
326
+ // EARS subsection. A lone "-" placeholder bullet (the untouched scaffold) is
327
+ // replaced instead of appended after, so the first real requirement does not
328
+ // land under an empty stub.
329
+ export function appendRequirement(text, section, value) {
330
+ const loc = locateRequirements(text);
331
+ if (!loc) return { error: "append-requirement: spec has no '## Requirements (EARS)' section" };
332
+ const sec = loc.sections[section];
333
+ if (!sec) return { error: `append-requirement: no '### ${section}...' subsection found under Requirements` };
334
+ const placeholder = sec.bullets.length === 1 && /^\s*-\s*$/.test(loc.lines[sec.bullets[0]]);
335
+ if (placeholder) {
336
+ loc.lines[sec.bullets[0]] = `- ${value}`;
337
+ } else if (sec.bullets.length > 0) {
338
+ loc.lines.splice(endOfItem(loc.lines, sec.bullets[sec.bullets.length - 1]) + 1, 0, `- ${value}`);
339
+ } else {
340
+ // Empty section: insert after the heading (skipping one blank line if present).
341
+ let at = sec.headingIndex + 1;
342
+ while (at < sec.endIndex && loc.lines[at].trim() === "") at += 1;
343
+ loc.lines.splice(at, 0, `- ${value}`);
344
+ }
345
+ return { text: loc.lines.join("\n"), summary: `append-requirement ${section}: ${value.slice(0, 60)}` };
346
+ }
347
+
348
+ // replace-requirement <section> <n>: <text> — replace the nth bullet (1-based)
349
+ // of the EARS subsection.
350
+ export function replaceRequirement(text, section, n, value) {
351
+ const loc = locateRequirements(text);
352
+ if (!loc) return { error: "replace-requirement: spec has no '## Requirements (EARS)' section" };
353
+ const sec = loc.sections[section];
354
+ if (!sec) return { error: `replace-requirement: no '### ${section}...' subsection found under Requirements` };
355
+ const real = sec.bullets.filter((i) => !/^\s*-\s*$/.test(loc.lines[i]));
356
+ if (n > real.length) {
357
+ return { error: `replace-requirement: section "${section}" has ${real.length} requirement${real.length === 1 ? "" : "s"}, no #${n}` };
358
+ }
359
+ const indent = loc.lines[real[n - 1]].match(/^(\s*)/)[1];
360
+ loc.lines[real[n - 1]] = `${indent}- ${value}`;
361
+ return { text: loc.lines.join("\n"), summary: `replace-requirement ${section} ${n}` };
362
+ }
363
+
224
364
  // append-criterion <text> — add a new numbered criterion after the last one
225
365
  // in the section.
226
366
  export function appendCriterion(text, value) {
@@ -231,6 +371,6 @@ export function appendCriterion(text, value) {
231
371
  }
232
372
  const last = loc.items[loc.items.length - 1];
233
373
  const nextN = Math.max(...loc.items.map((it) => it.n)) + 1;
234
- loc.lines.splice(last.lineIndex + 1, 0, `${last.indent}${nextN}. ${value}`);
374
+ loc.lines.splice(endOfItem(loc.lines, last.lineIndex) + 1, 0, `${last.indent}${nextN}. ${value}`);
235
375
  return { text: loc.lines.join("\n"), summary: `append-criterion ${nextN}` };
236
376
  }
File without changes
@@ -22,37 +22,31 @@ prompt and approves; do not make them run the CLI or hand-author artifacts.
22
22
  spec each, `doctrina clarify --all` + `doctrina validate`, then flip the
23
23
  intake to `Status: converted`). After conversion the specs are the only
24
24
  source of truth — never edit `intake.md` to change requirements.
25
- - **Day-to-day.** Turn any request into a change with `doctrina work
26
- "<prompt>"` and follow the printed playbook (spec delta → tasks →
27
- implement → analyze → apply → archive → validate). Unsure what to do
28
- next? Run `doctrina next`. Need to read? Run `doctrina context`. Ask the
29
- human only on genuine ambiguity.
30
-
31
- ## Doctrina command surface (reach for these — don't hand-author)
32
-
33
- The CLI scaffolds from canonical templates and keeps `index.json` in sync,
34
- so prefer it over writing artifacts by hand. Full list: `doctrina --help`.
35
-
36
- - **Read / orient (any task, first & last):** `doctrina status` (health) ·
37
- `doctrina context [<cap>] --concat` (read pack) · `doctrina next` (what's
38
- next) · `doctrina why <cap>` (provenance) · `doctrina constitution`
39
- (standing rules) · `doctrina spec list` · `doctrina decision list` ·
40
- `doctrina search <query>`
41
- - **Start:** `doctrina work "<prompt>"` · `--from-diff` (backfill from
42
- existing code) · `--chore` (no-spec infra/docs/build)
43
- - **Scaffold (manual creation stays available):** `doctrina spec new <cap>`
44
- (`--bug`) · `doctrina decision new "<title>"` · `doctrina change new <id>`
45
- · `doctrina contract new <id>` · `doctrina skill new <slug>`
46
- - **Advance / close:** `doctrina change apply|archive|diff <id>` · `doctrina
47
- decision accept|land|supersede <n>` · `doctrina spec set <cap> [...]` ·
48
- `doctrina close <id>` (whole close in one pass)
49
- - **Gates (ground truth, not guesses):** `doctrina validate` (`--fix`) ·
50
- `doctrina verify` · `doctrina review` (conformance vs specs/ADRs) ·
51
- `doctrina coverage --strict` · `doctrina trace --strict` · `doctrina
52
- contract check` · `doctrina clarify --all` · `doctrina analyze <id>`
53
-
54
- Continuous: `doctrina watch` re-syncs and re-orients on every save.
55
- Capture lessons: `doctrina skill suggest` surfaces skills worth writing.
25
+ - **Day-to-day.** Start every session with `doctrina prime` (gates, rules,
26
+ open work in one read). Turn any request into a change with `doctrina
27
+ work "<prompt>"` and follow the printed playbook (spec delta → tasks →
28
+ implement → close). Preview a close with `doctrina change check <id>`;
29
+ close with `doctrina close <id>`. Unsure what's next? `doctrina next`.
30
+ Before compaction or handover: `doctrina handoff`. Ask the human only on
31
+ genuine ambiguity.
32
+
33
+ <!-- doctrina:surface:begin — CLI-owned block, generated from the installed command catalog. Refreshed by `doctrina upgrade --write`; edits inside are overwritten. -->
34
+ ## Doctrina command surface (generated — reach for these, don't hand-author)
35
+
36
+ Every operation of the installed CLI, grouped by workflow. The CLI
37
+ scaffolds from canonical templates and keeps `index.json` in sync, so
38
+ prefer it over writing artifacts by hand. Details: `doctrina --help`.
39
+
40
+ - **Start:** `doctrina init` · `doctrina intake` · `doctrina work "<prompt>" (--capability · --chore · --from-diff · --quiet)`
41
+ - **Author:** `doctrina spec new|list|set` · `doctrina change new|apply|archive|check|tick|diff|abandon` · `doctrina contract new|list|check` · `doctrina decision new|accept|land|supersede|list` · `doctrina skill new|list|sync|suggest` · `doctrina intent add|list`
42
+ - **Read / orient:** `doctrina prime (session start)` · `doctrina context [<cap>] --concat` · `doctrina show` · `doctrina search` · `doctrina status` · `doctrina next` · `doctrina why` · `doctrina handoff (before compaction/handover)` · `doctrina constitution`
43
+ - **Gates:** `doctrina analyze` · `doctrina clarify --all (--lang pt|en)` · `doctrina validate (--fix)` · `doctrina coverage --strict` · `doctrina trace --strict` · `doctrina review` · `doctrina verify` · `doctrina close <id...>` · `doctrina doctor`
44
+ - **Maintain:** `doctrina templates list|check|update` · `doctrina hooks install` · `doctrina index rebuild` · `doctrina watch` · `doctrina metrics` · `doctrina report` · `doctrina completion` · `doctrina upgrade --write`
45
+
46
+ Session bookends: `doctrina prime` to orient at session start;
47
+ `doctrina handoff` before compaction or handover. Continuous:
48
+ `doctrina watch`. Capture lessons: `doctrina skill suggest`.
49
+ <!-- doctrina:surface:end -->
56
50
 
57
51
  ## Stack and tooling
58
52
 
@@ -7,21 +7,27 @@
7
7
  For ADDED: include the full new spec body below. On apply, the body is
8
8
  written verbatim to the target path.
9
9
 
10
- For MODIFIED: include only the sections that change, with clear
11
- before/after markers. Bookkeeping edits (flip a criterion, bump the
12
- version, set a header) can be made mechanical by adding a fenced `ops`
13
- block — on apply the CLI executes it against the target spec, all ops or
14
- none (ADR 0007). Anything the ops verbs cannot express (rewriting prose)
15
- stays a by-hand merge; a MODIFIED delta with no `ops` block prints a
16
- manual-merge pointer.
10
+ For MODIFIED: prefer a fenced `ops` block — on apply the CLI executes it
11
+ against the target spec, all ops or none (ADR 0007). The verbs cover
12
+ headers, acceptance criteria, AND the EARS requirement bullets, so a
13
+ typical delta applies mechanically end to end; only free-prose rewrites
14
+ (Purpose, Maturity, ...) stay a by-hand merge. A MODIFIED delta with no
15
+ `ops` block prints a manual-merge pointer.
17
16
 
18
17
  ```ops
19
18
  set-header Implementation: verified — durable adapter (`src/db.ts`)
20
19
  bump-version minor
21
20
  set-criterion 1: verified
22
21
  append-criterion [unverified] new signal — verified by `test/x.test.ts`
22
+ append-requirement event: When <trigger>, the system shall <action>.
23
+ replace-requirement ubiquitous 2: The system shall <action>.
23
24
  ```
24
25
 
26
+ Requirement sections: ubiquitous | event | state | unwanted | optional.
27
+ append-* ops resolve numbering/position at APPLY time, so several open
28
+ changes appending to the same spec never collide on numbers — order of
29
+ application decides.
30
+
25
31
  For REMOVED: the body may be empty; on apply, the target spec file is
26
32
  deleted and the capability is recorded in the change archive only.
27
33
  -->
@@ -24,7 +24,15 @@ ADR 0006). Tag product.md success-criteria bullets with an anchor —
24
24
  "- [SC1] ..." — and list the ones this spec delivers above. An "active"
25
25
  spec with no Realizes: header warns under `doctrina validate`; if the
26
26
  capability genuinely maps to no product anchor (internal tooling), keep
27
- the header and write "n/a — <why>" to record that on purpose.
27
+ the header and write "n/a — <why>" to record that on purpose. Need a NEW
28
+ anchor for intent born after the intake? `doctrina intent add "<text>"`.
29
+
30
+ Optional: declare the sibling capabilities this spec builds on with a
31
+ machine-readable header — "**Depends on:** defects, decisions". It feeds
32
+ `doctrina why` (the dependency graph), `doctrina context <cap>` (the
33
+ dependencies join the read pack), and `doctrina review` (dependents of a
34
+ touched capability are flagged). Citing them only in prose keeps every
35
+ tool blind to the link.
28
36
  -->
29
37
 
30
38
  ## Purpose