SDD Gate-Validation Governance
What makes a spec's state legal, and how a gate records its verdict. The field schema and transitions
are in sdd:lifecycle-governance; this skill is the legality layer on top of them. It carries no
leash — the self-clear-vs-escalate bar that derives how far an agent may self-assert is the
conductor's autonomy bar (baked into start-mission), a hard floor + a three-dimension gradient.
Legal-state tuples
The mechanical authority is spec-gate/scripts/check-spec-state.mts — run it
(node <skill>/scripts/check-spec-state.mts) to enforce; if node is unavailable, apply the same
rules by reading frontmatter. The (status, markers, .feature, approval) tuple of the root
spec.md — plus the durable gate lines globbed from its sibling ledger/ shards (a legacy ledger.jsonl still counts) — is illegal when:
status: approvedwith no.feature— a spec requires a frozen.featureto be approved.status: approvedorimplementedwith any<!-- open: -->markers — markers block the gate.approvalnames a gate other thanspecorimpl.- an
approval.<gate>has averdictother thanapprove,pause, orreject. - an
approval.<gate>isverdict: pausecarrying aby— a pause is always the agent's act and omitsby. - an
approval.<gate>isverdict: approvewith noby— an approve must record its approver. - an
approval.<gate>isby: agentwith nowhyblock — a self-assertion must record its derivation. - an
approval.<gate>has an off-enumcause(notdimension/clearance/ceiling) without acause-candidate: trueflag — the stop cause is a closed enum grown only by ratification, so an unflagged off-enum value fails closed; acause-candidate: true-flagged off-enum value is legal (the off-enum candidate discipline,sdd:combat-log-governance). - an
approval.<gate>isverdict: pauseon a gate the spec has already passed (spec onceapproved/implemented, impl onceimplemented). status: approvedorimplementedwith noapproval.specverdict: approve+by— the spec gate has no recorded ratification.status: implementedwith noapproval.implverdict: approve+by— the impl gate has no recorded ratification.status: approvedorimplementedwith nogatelinegate: spec,verdict: approvein the siblingledger/shards — the durable gate floor is missing. Theapprovalmap is the overwritten current-state twin; the ledger line is the immutable durable twin, so a status advance with no ledger gate line is an unenforced gate.status: implementedwith nogatelinegate: impl,verdict: approvein the ledger — the impl-gate durable floor is missing.
implemented is backed by the impl gate's runtime suite run, not a stored flag (ADR-0017); its
static guard here is the recorded approval.impl ratification. Open markers at draft are permitted
(they block only the gate, not the draft state). A draft requires no ledger shard (no gate
has run); the durable floor applies only once a gate has advanced the status.
Reject illegal tuples before any other gate work. If check-spec-state.mts changes, this list
follows it — the script is the source of truth, this prose is the readable mirror.
Per-node spec-type checks
Same fail-closed class, enforced by the same helper. A capability node README's spec-type marker
must agree with its shape (sdd:spec-format-governance):
spec-type: referencewith a sibling.feature— illegal (a reference artifact is suite-less by design).spec-type: referencewith no## Subjectsection — illegal (the reference descriptor is required).spec-type: behavioralwith no## Use Casessection — illegal (a behavioral spec maps use cases to scenarios).- a node README carrying any lifecycle field (
status,project-path,approval,produced-by, or a retiredaligned/spec-layout/strategy) — illegal: lifecycle frontmatter is root-spec.md-only (sdd:lifecycle-governance); a node carries only itsspec-typemarker.
The two gates
| Gate | Transition | Object judged |
|---|---|---|
| spec gate | Draft → Approved | spec.md + the .feature (no implementation required) |
| impl gate | Approved → Implemented | the implementation vs the frozen .feature |
producer ≠ judge survives the gate fold: even though gates are no longer a fixed station, the judge
stays a distinct actor from the producer, and never patches what it grades.
Sync is derived, not stored (no aligned flag)
There is no aligned field (ADR-0017). "Synced" is two properties at two layers, each derived or
judged — never a stored boolean:
- Contract layer (
spec.md↔.feature) — judged at the spec gate by the Builder coverage lens; implementation is not required, and spike code is excluded as scaffolding. - Impl layer (impl ↔ frozen
.feature) — derived by running the frozen suite; the impl gate advances toimplementedonly when every impl-judge passes, else it staysapprovedand surfaces the blocker. - Per-node settled state is the
@frozenscan; what is in flux now is the.plan.mdtodos.
status alone carries the lifecycle — no segment-level toggle.
No-resolvable-producer fails closed
A required production role always resolves to a real producer — a plugin agent or the SDD default
for that role. When a gate runs and a required role has no resolvable producer (not a plugin
agent and not even an SDD default), the gate fails closed with a blocker; it advances nothing.
This is a structural error, the same fail-closed class as a malformed produced-by entry or an
unflagged off-enum cause (a cause-candidate: true-flagged one is legal; sdd:combat-log-governance). Distinct from availability: a recorded
producer whose plugin is merely uninstalled is flagged, not blocked.