# Doctrine Loop

> 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.

- Skill: `cyberuni/doctrine-loop` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add cyberuni/doctrine-loop`
- Raw SKILL.md: https://api.skillmd.com/api/skills/cyberuni/doctrine-loop/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: cyberuni (https://skillmd.com/u/cyberuni)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/cyberuni/doctrine-loop

---


# 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 `<cr-ref>`** — 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.mts` over the project `ledger/` directory and membership-test
the mission's `<cr-ref>` against the set it returns, e.g.

```
node -e 'import("../plan-retirement/scripts/retire-plans.mts").then(m => console.log([...m.distilledCrRefs(process.argv[1])].join("\n")))' <ledger-dir>
```

`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": "<cr-ref>"` vs
`"distills":"<cr-ref>"`), a line-wrapped or reordered object — where a naive no-space substring grep
(`"distills":"<cr-ref>"`) **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 `<cr-ref>` 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.<hash>.jsonl`; mint `<hash>` 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: <cr-ref>` 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.

