SDD Combat-Log Governance
The durable, harness-agnostic record of a spec's missions — what was produced, what was judged, what
was corrected, and the strategy distilled from it. This skill defines the shape; the tracked
deletion of a retired plan is the plan-retirement skill.
Two faces, two homes
The record has two complementary faces: current-state in spec.md frontmatter
(contract), the durable history in a sibling ledger/ directory of per-writer shard files, sibling to the root spec.md.
| Face | Home | Shape | Mutability | Holds |
|---|---|---|---|---|
| Current-state | spec.md frontmatter |
produced-by (map by role) + approval (map by gate) |
overwritten — last write wins | the standing present: who produced each artifact, the latest CR's verdict per gate |
| Ledger | ledger/ dir (root sibling), one <cr-ref>.<hash>.jsonl shard per CR per writer |
one JSON object per line, appended to the writer's own shard | immutable — appended, never edited | the durable history: every CR's run-start leash block + gate verdict + strategy |
approval is standing, not historical — the one durable spec is flowed through by many CRs, and
spec.md approval holds only the latest CR's verdict (overwritten each time). The durable
per-CR record ("CR #34's diff was approved by X") is a gate ledger line, keyed by cr. There is
no per-CR approval block and no sidecar file.
The ledger is operational provenance, not contract — the ledger/ shards are never frozen and never
gated: writers keep appending across the whole lifecycle, including while spec.md + the .feature
are frozen at approved.
Two logs: the combat log (plan) vs the ledger (sibling dir)
Provenance splits by lifetime. Mid-flight detail is per-mission and tracked with the work, then removed at retro (durable in git history); the durable record is sparse and outlives the CR.
- Combat log —
.agents/plans/<cr-ref>.log.jsonl, beside the plan brief. Holds the chatty mid-flightreport/correction/haltlines. Tracked (committed, kept in the PR), deleted at retro once distilled and the source is done/merged. Already one file per CR, so it never had the shared-file merge problem. - Ledger — the
ledger/directory, sibling to the rootspec.md. Holds only the sparse durableleash/gate/strategylines, as one<cr-ref>.<hash>.jsonlshard per CR per writer. Never deleted.
Sharded storage (ADR-0020). Each writer appends only to its own shard, so no two writers ever
touch the same file — concurrent appends (two branches, or two sessions sharing one working tree) are
non-colliding by construction. A single shared ledger.jsonl conflicted on every concurrent mission
(EOF-append merge conflict) or was silently clobbered by a same-tree fork; sharding removes the shared
path, so no merge driver is used or needed. The reader globs ledger/*.jsonl (plus a legacy
ledger.jsonl if present) and concatenates. <hash> is 6 random hex minted once per writer-session
(random, not a machine/host/user id — that would leak identity); same session + same CR → same shard.
"Combat log" always means the live per-mission log in the plan; "ledger" always means the durable
sibling ledger/ directory. They are never the same store.
Entry shapes
One JSON object per line (JSON Lines). Every line carries a seq (append order within its shard —
its shard's own line count, restarting per shard, never a global counter), an optional pseudonymous
handle, and a kind. Combat-log lines additionally carry a write-time UTC ts; ledger
lines carry no wall-clock time (below). Seven kinds, split by tier: report / correction / halt
→ the combat log; leash / gate / strategy / followup → the ledger. Every line carries an optional
cr (the one project ledger spans many CRs against the one durable spec; outer-loop strategy lines may
omit it).
Safe-to-publish floor (committed-record rule). The combat log is committed → every line is published to git history permanently ("deleted at retro" is tree-only) and a distilled line may go upstream via Forge. The floor binds all fields:
- Categorical only — structured fields are enums; the free-text
summary/detailgive the decision or its class, commit-message-grade. - Never committed: email, OS usernames, hostnames, absolute paths, session/machine ids, secrets, code, prompts, literal values, raw numbers (token/cost) — those stay in the uncommitted transcripts.
- Identity is a pseudonym (
handle, below), neveruser.email.
Write-time ts — combat-log lines only. report / correction / halt carry a UTC ts (ISO-8601)
stamped at write-time — the doctrine loop reads the committed combat log post-merge (possibly another
machine), when the session clock is gone; within a mission ts orders those lines and feeds the pre-merge
coarse-duration signal the efficiency dimension reads from the raw transcripts. Ledger lines (leash /
gate / strategy) carry no ts — they are the forever-public durable record, and a wall-clock stamp
on a committed cross-machine artifact leaks activity timing/timezone for no load-bearing gain (nothing
reads ledger ts; ordering within a shard is seq; the cross-mission timeline is git history). Legacy
ledger lines written before ADR-0020 carry a ts and are grandfathered (append-only, never rewritten).
Identity — the per-entry handle. report / correction / strategy carry a handle (the
writer's pseudonym); a gate line keeps by (the ratifier). Resolution at write-time: SDD_HANDLE
(env) if set, else omit handle and fall back to the git commit author; never user.email,
never a git config read. The in-file handle / by is advisory, not proof — a self-asserter
can write any string, so the git commit signature plus positional authority are the control, not the
field.
report — per-subagent dispatch (combat log)
{"seq": 3, "ts": "2026-06-28T18:30:11Z", "handle": "unional", "kind": "report", "role": "spec-producer", "agent": "sdd:automaton", "outcome": "pass", "summary": "wrote 14 scenarios covering the ledger expansion"}
role is the production role dispatched; agent is the plugin-qualified agent name; outcome is
pass | fail.
correction — correction-with-cause (combat log)
One line per correction: a gate rejection, a producer⇄judge iteration, or a Council kick-back. The
matchable cause is the load-bearing field; at retro the doctrine loop folds recurring causes into
the ledger's strategy count.
{"seq": 7, "ts": "2026-06-28T18:41:02Z", "handle": "unional", "kind": "correction", "correction-kind": "gate-reject", "cause": "coverage-gap", "detail": "spec gate rejected — no negative scenario for the malformed-entry path"}
correction-kind— the closed setgate-reject | judge-iteration | council-kickback(the occasion, not the cause).cause— a minimal, discovered enum (the matchable category of why). Grounded so far:Cause Means coverage-gapa use case or operation lacked a covering scenario design-overreachthe design added a mechanism the architecture did not need spec-feature-contradictionthe spec.mdbody and the.featureasserted contradictory behaviorprose-impl-contradictiona skill's own operating docs or a sibling design doc asserted behavior the shipped implementation no longer has Growth: closed at any moment, discovered from usage — a new value is added only when a real recurring correction has no category. Adding one is an edit to this governance, ratified by the Council (a producer/judge/conductor never edits the enum).
Off-enum candidate discipline (the write-time nudge). When the conductor writes a
causeand no enum value fits, it writes the off-enum string intocauseanyway and flags the linecause-candidate: true— so the value stays countable as a proposal for enum growth instead of silently failing closed. This is a visibility nudge, not a write-blocking linter: the write always succeeds, and forcing an ill-fitting enum value would only relabel the silent drop as a mislabel. An absentcausestill fails closed (it breaks cross-mission matchability) — the nudge governs only the no-value-fits case and licenses no omission. Acause-candidatevalue that recurs is exactly the signal the Council reads when deciding the ratified growth above; the flag makes the accumulating candidate legible instead of invisible.Efficiency is a categorical correction class the committed log is designed to carry — the conductor flagging notable token-waste (a class, never raw counts), so the post-merge doctrine loop keeps the dimension. Its concrete
correction-kind/causeare not seeded; they enter by the same Council-ratified growth, and the numeric depth stays transcript-only (the floor admits no raw token number).cause-candidate— optional boolean.truemarks an off-enumcausewritten under the off-enum candidate discipline above as a proposed enum-growth value (kept present and countable, not silently dropped). Omitted orfalseon an on-enumcause. The same flag applies to agateline's off-enum stop cause (below).Durability discipline (the conductor's write duty). A
correctionis a discrete line, never left folded only into a verdictwhy(the doctrine loop matchescause, not prose):- At a gate reached via a judge-reject→fix→pass, the self-asserting conductor appends the
correctionline (correction-kind: judge-iteration, a matchablecause) before the gatewhyit summarizes. A gate that passed clean with no iteration appends none. - At mission finalize, a mission carrying a real correction whose line was never flushed
writes it now — creating the combat log if none exists — so the
causesurvives even the no-log mission class (a mission with no correction forces nothing). The forced line stays a combat-logcorrection, never a ledger line (the tier split above is invariant); its durability is the retro distillation of the committed log into the ledger'sstrategycount.
- At a gate reached via a judge-reject→fix→pass, the self-asserting conductor appends the
halt — a mid-flight stop, not at a gate (combat log)
The agent halts mid-phase (a hard floor, an input it cannot supply, a blast radius it will not cross).
A gate-time stop is a gate line (verdict: pause); this halt line is its mid-flight twin, so "why I
halted" is as durable as "why I went". Flush it to the committed log during the mission — the
doctrine loop reads only the committed log post-merge.
{"seq": 5, "ts": "2026-06-28T18:50:33Z", "handle": "unional", "kind": "halt", "phase": "explore", "why": {"floor": "clearance", "blast": "high — would drop scenarios from a frozen suite", "novelty": "low", "confidence": "high"}}
phase—intake | explore | deliver | handoff, where the mission stopped.why— the same categorical block theapprovalmap carries (floor/blast/novelty/confidence), classes only — never the raw blocker content.
gate — the durable per-CR gate verdict (ledger)
{"seq": 2, "kind": "gate", "cr": 34, "gate": "spec", "verdict": "approve", "by": "unional", "cause": "dimension", "frozen": ["intake/intake.feature", "mission/mission.feature"]}
gate—spec | impl.verdict—approve | pause | reject.by— a human name (ratified) oragent(self-asserted, provisional; carries thewhyderivation).cause—dimension | clearance | ceiling(the stop cause, distinct from acorrection's matchablecause): a gradient riskdimension, theclearancehard floor (a narrowing), or theceiling(Compatibility) cap. The off-enum candidate discipline applies here too — a stop cause with no enum fit (e.g. a novel floor) is written off-enum and flaggedcause-candidate: true, never silently dropped.frozen— the suite files this verdict froze (spec-gateapproveonly), so the ledger answers "what was frozen as of CR #34" standalone — no git walk.
leash — the conductor's run-start autonomy block (ledger)
The conductor's initial strategy evaluation, written once at run start: the run-level leash
reach + the approach[] containment methods. It is the conductor's autonomy bar for the mission —
not the Scanner's strategy, carries no ratified field, and is never counted as
pending strategy. The write is owned by the conductor (start-mission).
{"seq": 1, "kind": "leash", "cr": "disambiguate-strategy-kind", "leash": "auto-spec", "by": "user", "blast": "medium", "approach": ["no-spike", "worktree"]}
leash — auto-none | auto-spec | auto-all; by — derived | user; blast — the assessed
radius; approach[] — containment methods. The ceiling is not recorded (session-local). Pre-rename
historical run-start blocks appear as kind: strategy and are grandfathered (append-only ledger).
strategy — drafted strategy (ledger)
The Scanner records drafted strategy; this contract defines the shape, the write is owned by
the doctrine-loop Scanner. It carries the distilled recurrence count for a cause (in evidence).
{"seq": 1, "handle": "sdd-scanner", "kind": "strategy", "recommendation": "codify the coverage-gap pattern as a spec-format-governance check", "evidence": ["coverage-gap x3 across sdd-foo, sdd-bar, sdd-baz"], "ratified": false}
ratified: false means the Council holds keep-or-cut — unratified strategy never enters the corpus.
The distills subject. A strategy drafted from a Ship (→ implemented) or Kill
(→ deprecated) records the one mission it was distilled from in a distills field carrying that
mission's <cr-ref> — the same identifier that names the plan and the mission's cr on leash /
gate lines:
{"seq": 2, "handle": "sdd-scanner", "kind": "strategy", "distills": "referenced-artifact-escalation", "recommendation": "...", "evidence": ["cross-ref: d2-correction-line-durability", "cross-ref: ba6a39"], "ratified": false}
distills names the subject (the mission the line was drafted from); the cr-refs in evidence
are cross-references the recommendation leans on — never confuse the two. distills is the
machine-checkable hook the retirement sweep keys on to confirm a plan was distilled before deleting
its combat log (sdd:plan-retirement — the gate keys on distills, never an evidence mention,
and an unratified entry still counts). Milestone / drift / token-waste strategy that has no
single subject mission omits distills — only a Ship or Kill distillation gates a retirement.
The disposition subject (open | resolved). Before drafting, the Scanner validates each
plan/log-surfaced candidate against current code (a persisted plan or log is history, a
hypothesis about a gap — not present truth). The disposition field records that validation verdict:
disposition: open(the default; a line without the field grandfathers asopen) — current code does not resolve the candidate. It is an actionable recommendation: it counts toward pending strategy and drives the Scanner's issue emission.disposition: resolved— current code already resolves the candidate (built / fixed / superseded). The entry is a tombstone, not a recommendation: it carries the resolving current-code evidence inevidence, emits no issue, and is not counted toward pending strategy. It exists so the cut is auditable and a later run does not silently re-surface the same closed candidate.
{"seq": 3, "handle": "sdd-scanner", "kind": "strategy", "disposition": "resolved", "recommendation": "no action — coverage-gap mechanism already shipped", "evidence": ["current-code: checkReferencedArtifacts + checkUseCaseCoverage live in spec-gate/scripts/check-spec-state.mts"], "ratified": false}
disposition is set once at write and never flipped (the append-only invariant) — the Scanner's
pre-draft validation cut is a distinct act from the Council's keep-or-cut on a drafted
disposition: open line. Pending strategy counted at the gateway is kind: strategy,
ratified: false, disposition: open-or-absent — a disposition: resolved line is excluded.
followup — a recorded follow-up (ledger)
The durable record of work handoff identified but held out of scope. Written by the conductor at handoff, unconditionally — no permission, no forge, no human — and before any filing to the forge is attempted. It is a ledger kind, never a combat-log kind: the combat log is deleted from the tree at retro, and a follow-up must outlive its mission.
{"seq": 4, "kind": "followup", "cr": "github-237-handoff-followups", "class": "blocking", "summary": "Operator's admission (proposeEdge) has no dedupe against RAW cycles for follow-up edges", "contradicts": "handoff proposes follow-ups; nothing yet admits them", "evidence": ["cyberfleet-plugin/operator README claims single-writer admission, unimplemented"]}
class—blocking(the follow-up contradicts a completion claim the mission already made; the line names that claim incontradicts) orbacklog(genuinely new territory —contradictsis omitted). A finding that the mission's own frozen contract was wrong is not afollowupat all — it is an Oracle-lens revert inside that mission, never routed here.contradicts— required whenclass: blocking; names the completion claim the follow-up contradicts.evidence— the categorical support for the classification, commit-message-grade (the same floor as every other field).- No filed-state, ever. The line is never edited to mark it filed — the ledger is append-only. What is still outstanding is re-derived at each drain by deduping against the forge's existing issues, open or closed (matching only open ones would re-file a duplicate for a follow-up already filed and resolved).
- A proposal, not a verdict. Recording a
followupline grants nothing on its own — admission to the mission graph is the graph's single writer's act; the conductor writes no node or edge here.
Write ownership
Append-only; each writer adds lines to its own shard with the next seq within that shard, never
editing another writer's shard, and never editing or deleting a prior line. Full matrix in
sdd:ownership-governance:
| Writer | May append | To |
|---|---|---|
| conductor | report, correction, halt |
the combat log (plan *.log.jsonl) |
| conductor | run-start leash block (leash reach + approach[]) |
the ledger |
| conductor | self-asserted gate (by: agent) |
the ledger |
| conductor | followup (record, at handoff, unconditionally) |
the ledger |
gate skill (spec-gate), in-session |
human-ratified gate (by: <name>) |
the ledger |
| doctrine-loop Scanner | strategy |
the ledger |
| producers / judges | nothing | — |
A human-ratified gate line follows the positional authority rule (sdd:lifecycle-governance):
only the in-session position holding the user channel writes by: <name>.