--- name: doctrine-loop description: "Partial Skill: invoke by name only — the SDD doctrine loop, the Strategist's outer loop run by the Scanner — invoked by the doctrine-loop delegate, not triggered by users directly." user-invocable: false metadata: internal: true --- # SDD Doctrine Loop The **outer loop** of the SDD model. Owned by the **Strategist** and run by its delegate, the **Scanner in the Bunker** (`sdd-scanner`), parallel to the conductor running the mission loop. It fires at **lifecycle granularity, never per-gate**: it watches every **mission** reach a terminal state, drafts **strategy** (forward recommendations to revise governances, conventions, skills) from persisted artifacts post-hoc, and surfaces it to the human **Council** for keep-or-cut. Ratified, strategy re-enters as a CR that re-tunes the **doctrine** (the SDD design rules and governances) and grows the **corpus**. Load `sdd:combat-log-governance` for the **shape** of a `strategy` ledger entry and the matchable `cause` enum — this skill defers the entry shape there and never restates it. ## The split: detect-and-draft vs keep-or-cut | Half | Holder | Cost | Effect | |---|---|---|---| | **Detect and draft** | the Scanner delegate | cheap, continuous, non-blocking | appends an **unratified** `strategy` entry to the ledger | | **Keep or cut** | the human Council | accountable, high-blast-radius | ratify → re-enter as a CR that re-tunes doctrine + grows corpus; cut → strategy stays out | No strategy enters the corpus without the Council's ratification. The Scanner is the **sole writer** of `strategy` entries; the conductor (`report` / `correction`) and the producers (nothing) never write them. ## The six use cases Each is an entry-point: a lifecycle-grained trigger, its post-hoc input, and the strategy drafted. **A single gate passing is not a trigger** — the loop fires only at the lifecycle granularity below. | Use case | Trigger | Input | Drafts | |---|---|---|---| | **Ship** | `→ implemented` (the impl gate writes it) | the concluded mission's combat log (**PRIMARY**) + *[opt]* transcripts | strategy from a successful mission | | **Kill** | `→ deprecated` (the deprecation path writes it) | the concluded mission's combat log — why it failed (**PRIMARY**) + *[opt]* transcripts | strategy from the failure | | **Milestone retro** | a human-held retro | the milestone's concluded combat logs | strategy across the milestone | | **Recurring pattern** | the same correction recurs across missions | the **distilled `cause` recurrence count** in the ledger (maintained mission-over-mission), never a re-scan of many raw logs | strategy to codify the pattern | | **Drift / staleness** | a now-false convention or governance contradiction | the corpus (conventions, governances) | a **PRUNE** strategy | | **Token-waste** | a flagged-waste `correction` in the log, **or** session token cost over a **configurable bound** (pre-merge) | the **categorical** efficiency `correction` from the committed log (**post-merge**); raw transcripts add numeric depth (**pre-merge / same-machine only**) | efficiency strategy | The Scanner **observes** the terminal transitions; it never writes a mission's `status`. ## Idempotent Ship/Kill: detect an already-distilled mission by parsing the ledger, never grepping The **Ship** (`→ implemented`) and **Kill** (`→ deprecated`) triggers can meet the **same** terminal transition more than once — a later pass, a resumed segment, a fresh re-invocation. So they are **idempotent**: **before drafting** Ship/Kill strategy for a mission, first detect whether that mission was **already distilled** — a prior `strategy` entry whose **`distills` field equals the mission's ``** — and if so, **draft nothing** (no duplicate strategy for a mission already recorded as distilled). **Determine distilled-ness by a structured JSONL parse, never a substring or regex text match.** The ledger shards are JSONL (one JSON object per line). **Reuse the existing parse contract** rather than hand-rolling one: run the exported **`distilledCrRefs`** engine from `../plan-retirement/scripts/retire-plans.mjs` (its `--distilled` flag) over the project `ledger/` directory and membership-test the mission's `` against the set it returns, e.g. ``` node "/scripts/retire-plans.mjs" --distilled ``` `distilledCrRefs` parses **each line** with `JSON.parse` (keying only on `kind === "strategy"` with a non-empty `distills`), so it is correct against **any** valid-JSON formatting the Scanner or another writer emits — a **space after the colon** in a pretty-printed entry (`"distills": ""` vs `"distills":""`), a line-wrapped or reordered object — where a naive no-space substring grep (`"distills":""`) **silently under-counts**, reports an already-distilled mission as undistilled, and **re-drafts** it. This is the same defect class as *grep is blind to wrapped terms*; a distilled-detection that under-counts causes false "undistilled" conclusions and wasted or duplicate drafting. Reusing the engine also inherits its exact semantics: it keys on the **structured `distills` field only** (an `` appearing merely inside another entry's `evidence` cross-references does **not** count as distilled), **tolerates malformed and blank lines** (they are skipped, not fatal), and counts an **unratified** distilling entry — the Scanner's default — the same as a ratified one (the question is what was distilled, not sign-off). This is a **read** over the ledger; it changes nothing you write. ## Inputs: combat log (contract) vs transcripts (enrichment) The Scanner reads **persisted artifacts post-hoc** — never live subagent context. - **Combat log** — PRIMARY input, the contract: the concluded mission's combat log (the plan's `*.log.jsonl`), read once at retro. Strategy is draftable from it **alone** for **every categorical dimension**; raw transcripts are additive, never required. - **Raw `.jsonl` transcripts** — optional enrichment, harness-specific, and **may be absent post-merge** (another machine, the session gone). The **sole** transcript-only piece is the *numeric* token-waste depth. The **token-waste dimension splits**: a coarse, **categorical** efficiency signal rides the committed log as a `correction` (the conductor flags a class — **no raw counts**), so the post-merge loop keeps the dimension; the **numeric** breakdown lives only in transcripts and is **threshold-gated + pre-merge / same-machine only** (run over a configurable bound or on demand, never under it without a request). No raw token-cost number is written to the committed log — only the categorical class (the safe-to-publish floor, `sdd:combat-log-governance`). ## Validate before drafting — a plan or log is a hypothesis, not present truth A persisted plan, combat log, or ledger line is **history**, never authoritative present truth. Every candidate improvement it surfaces — a gap to build, a defect to fix — is a **hypothesis about the current codebase** (the repo's own principle: *a source-read is a hypothesis, refuted by a repro against current code*). **Before you draft**, run a validation gate on each candidate: read the CURRENT code and decide whether the flagged gap still exists, or has since been **built / fixed / superseded** (by a later mission, a landed PR, a governance change). - **Resolved → cut.** A candidate current code already resolves is **cut**: draft **no** build-or-fix strategy and emit **no** issue. Record the cut **explicitly, never silently** — append a `strategy` entry marked **`disposition: resolved`** to your own shard, carrying the **resolving current-code evidence** (the repro that refuted the hypothesis) in `evidence`. A `disposition: resolved` line is a **tombstone**: not an actionable recommendation, so it emits no issue and is **not counted toward pending strategy** at the gateway. It exists so the cut is auditable and a later run does not silently re-surface the same closed candidate. - **Still open → draft.** A candidate current code does **not** resolve is a real improvement: draft `strategy` for it marked **`disposition: open`** (the default; a line without the field grandfathers as open), which counts toward pending strategy, and emit its issue (below). The gate applies to any candidate that asserts an **unmet gap or defect**, whether surfaced by a **plan or a combat log** (symmetric — each source has both a resolved→cut and a still-open→draft path). A pure distilled **retro lesson** (a success pattern, not a gap claim) carries no gap to validate and is drafted without a current-code check. Set the disposition **once at write** — never flip it (append-only); the Council's later keep-or-cut on a `disposition: open` line is a separate act (the keep-or-cut plan), not a field edit. The `disposition` field's shape is owned by `sdd:combat-log-governance`. This gate is what stops the loop reinforcing a **stale cache** — drafting "build X" for an X a later mission already built. ## Cold-instrument evidence — non-author measurement + ablation before a rule-level recommendation A self-authored measurement is untrustworthy exactly when it grounds a **rule-level decision**: the author's own blind spot is baked into the instrument meant to catch it (op6/#224 — a masked generator "proved" its own conclusion twice, each ratified then walked back; op6-m5/#254 — a proposed revival measured Δ=0 dead weight, caught before landing). Two rules extend the validate-before-drafting gate to your rule-level recommendations. This is **the canonical statement** of the non-author-evidence standard — other faces (`sdd:spec-producer-governance`'s dimension/cut grounding) reference it. - **Non-author evidence.** Draft a recommendation to **adopt a rule, drop a rule, or set a threshold** only when its grounding measurement was **produced by a party other than the proposer, *or* independently reviewed by such a party, *or* ablated against a freshly and adversarially constructed case**. A measurement on the proposer's **own generator or harness alone** is **withheld**: draft no recommendation on it, and record that it needs non-author or fresh-adversarial evidence. A measurement grounding **no** rule-level decision is unconstrained by this rule. - **Ablation before landing a revival.** Draft a candidate **reviving a dead measurement dimension** to land only when an ablation against a control shows the dimension is **loseable**; **state the revived rule abstractly with no worked example** (lifting a probe scenario's apparatus into the rule is **absorption** — the probe then grades nothing). A revival whose ablation shows **no difference** from the control is **dead weight**: **cut** it (a `disposition: resolved` tombstone, as above) and draft no land recommendation. ## Improvement output — validated-open findings become tracked issues The loop's **actionable output** for a validated-open improvement is a **new tracked issue** (`gh issue create`) — one titled, bodied issue per real improvement, **cross-linking the evidence** that drove it. This grounds the improvement plan in current code, not the stale narrative of a retired plan. The ledger `strategy` line stays the **provenance**; the issue is what a later mission is started from. - **Dedupe first.** Before filing, dedupe against the forge's existing issues — **open and closed** (at least two keyword combinations: the full title, then the core noun/verb) — and on a mixed set file only the unmatched. - **Emit is not dispatch.** Emitting an issue leaves the `strategy` **unratified** and spawns **no** mission — it opens no CR and admits nothing to the mission graph (that is the graph's single writer's act). Keep-or-cut stays the Council's; the issue re-enters SDD only when a **later** mission is started from it. - **Outward-publish floor.** Compose the issue body to the same outward-publish floor the handoff follow-up issues meet (owned by the handoff unit — do not restate it): **self-contained** (a reader who cannot see the mission's internal artifacts can act on it), **no production-internal artifact reference** (no ledger shard filename, no combat-log or plan-brief path), plus everything the committed-record floor bans (absolute paths, `$HOME`/`$USER`, usernames, secrets, raw numbers). Carry an **agent-filed marker** and name the evidence it was distilled from. ## Where strategy lands Every `strategy` entry lands in the **one project ledger** — the `ledger/` directory sibling of the root `spec.md` — written to the **Scanner's own shard** (`strategy..jsonl`; mint `` as 6 random hex once per session), so two concurrent Scanner runs write distinct shards and never contend. There is no per-spec log to route to under the project-spec model. The Scanner's `handle` is `sdd-scanner`. Every entry is **unratified** (`ratified: false`) and carries its **driving evidence** (the distilled `cause` recurrence that drove it), per the shape in `sdd:combat-log-governance`. The shard is append-only — the next `seq` within it, never an edit; ledger lines carry **no `ts`**. Every entry also carries its validation **`disposition`** (`open` for a drafted still-open improvement, `resolved` for a validation tombstone; see *Validate before drafting* above) — set once at write. **Record the distilled subject.** When the entry is drafted from a **Ship** (`→ implemented`) or **Kill** (`→ deprecated`), set `distills: ` to the **one mission it was distilled from** — distinct from the cross-referenced cr-refs in `evidence`. This is the machine-checkable hook `sdd:plan-retirement` keys on to confirm a plan was distilled before deleting its combat log, so a Ship/Kill distillation **must** carry it. **Milestone / drift / token-waste** strategy has no single subject mission and **omits** `distills` (`sdd:combat-log-governance`, *The `distills` subject*). ## Surfacing and ratification The Scanner **accumulates** unratified strategy and surfaces it **episodically** — never synchronously blocking a mission. The `sdd` gateway surfaces the **count of pending (unratified) strategy** when the Council re-enters; that is the entry point to keep-or-cut. On **ratify**, the strategy re-enters as a **new CR** that re-tunes the doctrine and grows the corpus (a ratified **PRUNE** removes the stale convention). On **cut**, it stays unratified and absent from the corpus. ## Stale plan frontmatter — deriving the retirement clearance set, never autofixing status A plan brief's frontmatter `status` can lag reality (its own todos finish, or its source issue closes, without the value being flipped) but there is **no legal terminal value** to autofix it into — the plan-level `status` is a two-value dispatch flag (`active | approved`), and the contract's own answer to "this mission is over" is **retirement**, not a status write (`sdd:combat-log-governance`'s home, `design/provenance-model.md`). So during its pass, for each brief under `.agents/plans/`, the Scanner cross-checks `todos-all-done` (every `todos[].status` completed) against `source-closed` (the declared `source` queried natively, the same way `plan-retirement`'s own clearance check queries it) — never writing `status` either way: - **Both agree terminal** → the cr-ref is included in the **retirement clearance set** the Scanner passes as `plan-retirement`'s existing `--retire` input. No new deletion mechanism — `plan-retirement` still runs its own gated sweep. - **Both agree non-terminal** → no clearance, no finding. - **They disagree** → excluded from the clearance set; the Scanner surfaces a flagged finding naming the disagreement in its pass summary (its only channel — it returns only its final message), for a human to resolve. A flagged finding is ephemeral and never a ledger write — it is neither a `kind: strategy` entry nor a `kind: report` line, and it is distinct from the validated-open-improvement finding above that becomes a tracked issue. ## Plan retirement Doctrine's **last retro step** — the gated, idempotent **tracked deletion** of a retired plan — is a separate unit (`sdd:plan-retirement`). The distill (writing `strategy` here) fires early, at `→ implemented`; the delete is a later step gated on source = `done`/merged **and** distilled. The source-cleared set `plan-retirement` consumes via `--retire` is, per above, the Scanner's derived retirement clearance set — never a raw, uncross-checked source-only judgment.