engine

package
v0.0.0-...-69654ad Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 22, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

Documentation

Overview

Package engine implements run activation — the fat transaction of engine-core §3.2 and engine-spec §2 (TDD docs/tdd/engine-spine.md §5.3).

The package carries no agent, model, or LLM vocabulary of any kind (docs/design/genericity.md). It binds workflows to issues by their declared `[match]` predicates, pins what a run must reproduce, freezes the issue state a context bundle will read, harvests the commands an operator approved, and expands the step topology. Every one of those is scheduling, dependency, or packaging work; none of them reads a key inside `params` or `metadata`, and `executor` stays the opaque string §11.1 makes it.

Index

Constants

View Source
const (
	ParamField      = "field"
	ParamMethod     = "method"
	ParamHoldSpread = "hold_spread"
	ParamOutput     = "output"
)

Aggregate params, as §7.1's table names them. Core reads exactly these four keys of the opaque bag and V28 refuses any other, so "the engine ignored my param" is a register-time sentence rather than a run-time mystery.

View Source
const (
	MethodMedian = "median"
	MethodMax    = "max"
	MethodMin    = "min"
)

The three reductions of §7.3. There are exactly three because §2 names exactly three; each addition would be core acquiring an opinion about what is worth computing (§13).

View Source
const (
	KeyMembers          = "members"
	KeyHeld             = "held"
	KeyDemotedFrom      = "demoted_from"
	KeyOperatorResolved = "operator_resolved"
	// KeyOperatorNote carries the operator's decision text into the resolved
	// payload (DKT-84). Before it, `operator_resolved` was the whole message:
	// a fixer downstream was told a decision existed and given nothing to act
	// on — the flag transited, the content did not.
	KeyOperatorNote = "operator_note"
	// KeyOperatorSetFrom records the computed value an operator's correction
	// replaced (DKT-42), mirroring `demoted_from`'s trail discipline: the
	// value that was not taken stays readable beside the one that was.
	KeyOperatorSetFrom = "operator_set_from"
)

Output keys of the aggregate's own payload (§7.6). They are STATISTICS AND PACKAGING vocabulary: `members` is a cluster's inputs, `held` is whether the spread reached the bound, `demoted_from` is a position in the declared order that was not the result, and `operator_resolved` is whether a person accepted it. None names a domain.

View Source
const (
	// RegistrationKindSchema and RegistrationKindWorkflow are the only two
	// things core REGISTERS. Everything else under `.docket/config/` is PINNED,
	// which is exactly what §2 says core does with instance files it does not
	// understand.
	RegistrationKindSchema   = "schema"
	RegistrationKindWorkflow = "workflow"

	// RegistrationNew and RegistrationUnchanged are F21's `outcome` values.
	RegistrationNew       = "new"
	RegistrationUnchanged = "unchanged"
)
View Source
const (
	OnDuplicateRefuse = "refuse"
	OnDuplicateSkip   = "skip"
)

OnDuplicateRefuse and OnDuplicateSkip are `--on-duplicate`'s two settings.

Refusing is the default and stays the default: a duplicate usually means the operator is about to double-count real spend, and the append-only ledger is right to say so. What was wrong was the GRANULARITY — the whole batch aborted on the first already-recorded row, and cross-wave duplicates are structural (a gate probed in wave N and seated in wave N+1 emits usage in both journals), so conductors hand-filtered rows seven times across three sessions (DKT-241).

View Source
const (
	// RowMatched: the row renders today exactly as it was stored.
	RowMatched = "matched"
	// RowRecorded: the row's step has moved OFF the scheduler — recorded,
	// failed, parked at waiting-human. Its absence from the recomputation is
	// the dispatch working, not a conflict (DKT-10/DKT-65), and it does not
	// make the verify fail.
	RowRecorded = "recorded"
	// RowShifted: the step is still offerable, but renders differently than
	// it did at open — an attempt or status moved mid-dispatch.
	RowShifted = "rendering-shifted"
	// RowMissing: the step is still NON-TERMINAL and yet is no longer
	// offerable — claimed since the manifest opened, or excluded by a scope
	// conflict with a row admitted since. The narrower, more alarming case.
	RowMissing = "genuinely-missing"
)

The verdicts a stored manifest row can draw (DKT-243).

View Source
const (
	EventRunStarted     = "run-started"
	EventRunActivated   = "run-activated"
	EventRunPaused      = "run-paused"
	EventRunResumed     = "run-resumed"
	EventRunAbandoned   = "run-abandoned"
	EventRunDone        = "run-done"
	EventIssuePromoted  = "issue-promoted"
	EventIssueAbandoned = "issue-abandoned"

	// The live-status mirror kinds (DKT-294). `issue-in-progress` covers BOTH
	// directions that land an issue in `in-progress`: claim's `todo -> in-progress`
	// and review's bidirectional return once nothing on the issue is still
	// `waiting-human` — the two are one concept ("the issue is actively being
	// worked"), not two kinds artificially split by which edge produced it.
	// `issue-review` is the other direction: a gate/vote step of the issue
	// parked `waiting-human`, so an operator or voter is now who the issue is
	// waiting on. Both earn their place on §9 item 2's argument like every kind
	// above: the issue mirror moving is a transition, and before this change
	// nothing recorded it — `in-progress` and `review` were validated and
	// rendered but never written (engine-spec §2).
	EventIssueInProgress = "issue-in-progress"
	EventIssueReview     = "issue-review"

	// Step-lifecycle kinds (phase 3).
	EventStepReady      = "step-ready"
	EventStepClaimed    = "step-claimed"
	EventStepHeartbeat  = "step-heartbeat"
	EventStepRecorded   = "step-recorded"
	EventGateStarted    = "gate-started"
	EventGateRecorded   = "gate-recorded"
	EventStepRouted     = "step-routed"
	EventStepFailed     = "step-failed"
	EventStepSkipped    = "step-skipped"
	EventStepSuperseded = "step-superseded"
	EventStepResolved   = "step-resolved"
	EventStepApproved   = "step-approved"
	EventStepRejected   = "step-rejected"
	EventLeaseReaped    = "lease-reaped"

	// Loop and join kinds (phase 4). `loop-entered` records a `fix-loop`
	// routing that actually entered a loop — not one that hit `max_fix_loops`
	// and became `waiting-human`, which is a step-routed to `waiting-human` and
	// is not a loop entry at all.
	EventLoopEntered   = "loop-entered"
	EventJoinCompleted = "join-completed"

	// Gate and trust kinds (stage 4, gates-trust §6.4). The closed set gains
	// exactly four, and they are listed in the TDD so §9 item 2's check keeps
	// passing rather than silently widening.
	//
	// `gate-unmatched` is SEPARATE from `gate-recorded` so an operator
	// following the feed sees a refusal to execute as its own event, rather
	// than as a result they must open and inspect. `gate-rerun` precedes each
	// flaky re-run (§5.6) for the same at-least-once observability reason
	// `gate-started` exists.
	//
	// `trust-added` / `trust-removed` are what make the self-trust residual
	// (T9) auditable: a session that grants itself a command mid-run leaves the
	// grant in the run's own trail, so a retro can find it. They carry the
	// ARGV HASH, never the argv — a trusted command's arguments must not land
	// in an event feed a run report renders.
	EventGateUnmatched = "gate-unmatched"
	EventGateRerun     = "gate-rerun"
	EventTrustAdded    = "trust-added"
	EventTrustRemoved  = "trust-removed"

	// Vote-step lifecycle kinds (gates-trust §8.1 phases 2 and 4).
	//
	// RECORDED AMENDMENT: §6.4 of that TDD enumerates "exactly four" kinds this
	// stage adds and does not list these two, while §8.1's own lifecycle table
	// requires both by name ("writes a `vote-opened` event", "writes a
	// `vote-tallied` event with the score"). The two sections of one document
	// disagree; §8.1 is the operative requirement because it specifies
	// behavior, and §6.4's count is bookkeeping that did not travel with it.
	// So the closed set gains SIX at this stage, not four, and the deviation is
	// filed rather than made silently.
	//
	// They earn their place on the same argument §6.4 makes for
	// `gate-unmatched`: a vote opening and a vote tallying are transitions an
	// operator follows in the feed, and §9 item 2 requires every transition be
	// attributable. Without them a proposal appears with nothing explaining
	// which step opened it.
	EventVoteOpened  = "vote-opened"
	EventVoteTallied = "vote-tallied"

	// The held-cluster kind (stage 5, payloads-thresholds §7.7).
	//
	// The closed set gains exactly ONE, and it earns its place on the same
	// argument every kind above earns it: §9 item 2 requires every transition
	// to be attributable, and materializing a `<step>-held` question is a
	// transition an operator follows in the feed. Without it a gate step nobody
	// declared appears in a run with nothing explaining where it came from.
	//
	// The resolution needs no new kind whichever way the hold was minted.
	// `step-approved`/`step-rejected` already say what an operator did to a
	// gate, and a human-minted hold is a human gate in every respect the verbs
	// can observe (H3). A VOTE-MINTED hold reports through the vote kinds while
	// the tally runs (`vote-opened`, `vote-tallied`, `step-routed`), and through
	// the same approve/reject kinds once a failed tally parks it for an
	// operator — so every one of its transitions is already attributable to the
	// party that made it.
	EventStepHeld = "step-held"

	// Dispatch kinds (stage 6 group 2, docs/tdd/runs-dispatch.md §5).
	//
	// The closed set gains exactly THREE, and each is a transition §9 item 2
	// requires to be attributable. `dispatch-abandoned` is named by engine-spec
	// §2 VERBATIM — "a dispatch TTL lazily auto-abandoned by `next`
	// (event-logged)" — so it is the one kind here the spec asks for by
	// implication rather than the TDD choosing.
	//
	// `dispatch-closed` and `dispatch-abandoned` are SEPARATE kinds rather than
	// one `dispatch-ended` with a reason, for the same argument that keeps
	// `gate-unmatched` separate from `gate-recorded`: an operator following the
	// feed must see "the batch was reconciled" and "the batch was given up on"
	// as different events, not as one event they have to open and inspect. The
	// two mean opposite things about whether the relay's work is accounted for.
	//
	// `dispatch-abandoned` carries `data.reason` — `ttl` for P14's lazy path,
	// `abandoned` for P21's explicit one — so the feed distinguishes a crashed
	// relay's manifest expiring from an operator retiring it.
	EventDispatchOpened    = "dispatch-opened"
	EventDispatchClosed    = "dispatch-closed"
	EventDispatchAbandoned = "dispatch-abandoned"

	// The write-reap acknowledgment kind (§6.2).
	//
	// It earns its place on A3: "the ack must be ATTRIBUTABLE" is a requirement
	// of the mechanism, and §9 item 2 requires every transition to be traceable.
	// Releasing write headroom is a transition — a successor becomes claimable
	// that was not — and without this kind the release would appear in the feed
	// as nothing at all, with the run simply starting to offer write-class work
	// again for no recorded reason.
	//
	// It carries `data.acked_by`, which is the VERB (A8: `guard-spawn` |
	// `dispatch-open`) and never a user identity, because core has no identity
	// model.
	EventReapAcknowledged = "reap-acknowledged"

	// Stage 7's two kinds (docs/tdd/events-follow.md §6, §7.3).
	//
	// `events-pruned` earns its place on the argument every kind above earns it
	// — §9 item 2 requires every transition to be attributable — plus one that
	// is specific to it: THE PRUNE IS THE ONE TRANSITION THAT DESTROYS
	// EVIDENCE. The record that it happened is the only thing standing between
	// a trimmed log and a log that looks like it was never written, and a prune
	// leaving no event would be indistinguishable from a run that simply made
	// fewer transitions. Its `seq` is above everything it deleted, so the record
	// of the deletion survives the deletion.
	//
	// `run-budget-set` is the kind S6's reasoning did NOT already cover. That
	// stage refused a `budget-breached` kind on the grounds that `run-paused`
	// already anchors the fact; the counter does not apply here, because no
	// existing kind anchors "the cap moved". Without it, a run that breached at
	// 12 and later admitted a claim at 20 would have a trail with nothing
	// explaining the difference — the exact gap operations.md §4 warned about
	// when the only way to raise a cap was to edit the database.
	EventEventsPruned = "events-pruned"
	EventRunBudgetSet = "run-budget-set"

	// The post-completion annotation kind (DKT-35).
	//
	// It earns its place on §9 item 2's argument like every kind above:
	// merging metadata onto a finished step's record is a mutation of the run
	// record, and a record that changed with no event would be evidence that
	// rewrote itself. The event carries the annotation verbatim, so what was
	// added survives a later annotation overwriting the same key.
	EventStepAnnotated = "step-annotated"

	// The tenancy kind (DKT-61).
	//
	// It earns its place on §9 item 2's argument, extended one step: a project
	// row is the thing every other row is attributed TO, so a project that
	// appears with no event is a hole underneath the whole attribution chain.
	// Every issue, run, and step in the store says which project it belongs to,
	// and until this kind existed the project itself said nothing about where
	// it came from.
	//
	// The absence was measured, not theorized. Attributing one junk row to the
	// verb that minted it took a hand-join of raw table timestamps against nine
	// session transcripts, because the store held no other record of the act.
	//
	// It carries `cwd`, `identity`, and `verb` — where the invocation ran, what
	// it resolved to, and which command it was — which are exactly the three
	// facts that reconstruction had to recover by hand. Like `trust-added` it
	// has NO RUN: registration precedes any run of the project by definition.
	EventProjectRegistered = "project-registered"

	// The repin kind (DKT-408).
	//
	// It earns its place on §9 item 2's argument in the form `events-pruned`
	// made famous: a repin MOVES THE RUN'S RECORDED AGREEMENT. The `pins` rows
	// are what every packet, render, and payload validation verifies bytes
	// against, and after a repin they no longer say what completed steps
	// actually worked under — this event is what still does. It carries the
	// old sha AND the new one per changed ref, plus the operator's reason, so
	// the agreement any given step consumed stays recoverable from the trail:
	// steps recorded before this event's seq worked under `old_sha256`, steps
	// claimed after it work under `new_sha256`. One event per changed ref, all
	// in the repin's own transaction — the old hash's survival must not depend
	// on a second commit landing.
	EventRunRepinned = "run-repinned"

	// The spawn carve-out kind (DKT-236).
	//
	// It earns its place on §9 item 2's argument in its sharpest form: this
	// event records a hold being STEPPED PAST. Without it, a spawn admitted
	// over an open reap hold is indistinguishable in the record from a spawn
	// nothing was holding — the one case where "no event" and "nothing
	// happened" say the same thing while meaning opposite things.
	//
	// It carries `carve_out` (which rule admitted the spawn), `proposal` (the
	// open question the batch exists to decide), and `hold` (the refusal text
	// it was admitted over), which are exactly the three facts an auditor
	// asking "why was this allowed?" needs.
	EventSpawnAdmitted = "spawn-admitted"
)

The CLOSED SET of event kinds (§7.6). Phases 2 and 3 named the kinds they emit; phase 4 completes the set with the two loop/join kinds and enforces it.

§9 item 2 — "every transition is attributable to `next`, a gate, a threshold, or human input" — is checkable only because this set is closed. A call site passing a bare string literal would put a kind outside the set into the table and make the check vacuous, so the writer REFUSES a kind that is not one of these (see eventKinds and recordEvent), and TestEventKindsAreAClosedSet enumerates the constants against the writer's own table.

View Source
const (
	VerdictPass = "pass"
	VerdictFail = "fail"
	// VerdictUnmatched is a FIRST-CLASS OUTCOME, added at S4 (gates-trust §6.2).
	//
	// §4, verbatim: "each must match a trust entry … or it is NOT EXECUTED and
	// reported as unmatched." It is not a pass, not a skip, and not an error
	// that aborts the run: the step routes per its `on_fail`, because a
	// workflow whose check cannot run has not passed its check. The opposite
	// reading — "we couldn't check, so carry on" — is precisely what makes a
	// security control decorative.
	VerdictUnmatched = "unmatched"
	// VerdictSkipped marks a gate that COULD NOT MEASURE its tree (DKT-169):
	// the step's recorded worktree was already swept, so nothing ran. It is
	// distinct from `fail` because a fail row with a null exit and empty
	// output is indistinguishable, to a verifier told to read recorded gate
	// output, from a gate that ran and the change failed it — and the two need
	// opposite responses (reconstruct-and-remeasure vs. judge the failure).
	// For routing it still counts as not-pass, same as `unmatched`: "we
	// couldn't check, so carry on" is what makes a control decorative.
	VerdictSkipped = "skipped"
)

Gate verdicts, per §11.4.

View Source
const (
	// ResolveRetry RESETS THE RETRY BUDGET for the step instance (§2: "retry =
	// attempts reset") by moving `attempt_base` to the current attempt —
	// `attempt` itself stays monotonic, because it is the usage ledger's key
	// half (DKT-86/DKT-90). This is a different counter from the issue-level
	// `attempt` v6 declared monotonic — claims-leases §5 anticipated exactly
	// this. The step's is a live retry budget against `max_attempts`; the
	// issue's is a permanent trail. Both statements stay true because they are
	// about different rows.
	ResolveRetry = "retry"
	// ResolveSkip routes the step `skipped`.
	ResolveSkip = "skip"
	// ResolveAbandonIssue abandons the issue within this run.
	ResolveAbandonIssue = "abandon-issue"
	// ResolveOverridePass passes the step as though its gates had allowed it —
	// the operator taking responsibility for a decision the engine would not
	// make. It records the GENERIC RoutingPass and does NOT evaluate the
	// step's threshold (DKT-470): a threshold that would have interposed
	// another step over the accepted payload is not consulted, and that
	// interposed step is skipped unconditionally. OverridePassSkipsInterposedTargets
	// names the steps this will skip, for a caller to warn with before this
	// commits.
	ResolveOverridePass = "override-pass"
	// ResolveFixRound authorizes ONE more fix loop for the issue and enters
	// it, minting a fresh fix+review round (DKT-237).
	//
	// It exists because exhausting `max_fix_loops` parked the issue with no
	// way back in. After HRN-26's third round verify-ac read 7/14 acceptance
	// criteria unmet and design-qa held 2 blockers, and the workflow scheduled
	// no further round — so the fix was built OUTSIDE the engine: a
	// general-purpose agent, a 1,128-insertion commit cherry-picked with no
	// judge review as a step, and ~100,923 output / 21.9M cache-read tokens in
	// no ledger. The engine offering no sanctioned re-entry is what made that
	// the reasonable move.
	//
	// It is DISTINCT FROM RETRY, and the distinction is the whole point.
	// `retry` re-runs the parked step — the check that reported the problem —
	// which answers the same question again. This says the problem is real and
	// authorizes another round of WORK on it: a new fix body, a new review,
	// judged like every other round.
	ResolveFixRound = "fix-round"
	// ResolveRerunGates RE-RUNS A COMPLETED STEP'S GATES without re-executing
	// the step (DKT-259).
	//
	// It exists because the only lever for "the gate was wrong, the work was
	// fine" was `retry`, which re-executes everything. Most of the RUN-13 epoch
	// retries were exactly this case — a gate that failed because a trust entry
	// was missing or an environment was broken, fixed out of band, with the
	// step's own output never in question. Paying a full re-execution for that
	// is expensive, and it is also DESTRUCTIVE: the re-execution diffs a tree
	// that already contains the work, which is how RUN-13 STEP-132's
	// `issue.diff` came to be replaced by 0 bytes.
	//
	// The mechanism is the saga's own: rewind `saga_stage` to `recorded` — the
	// point at which the artifact is in and the gates have not run — and
	// resume. Every gate stage re-runs in declared order and routing follows,
	// exactly as they would have the first time. The step never returns to
	// `pending`, so nothing re-offers it and no worker re-executes it, and the
	// recorded artifact is untouched.
	//
	// It is NOT `retry` with a flag. `retry` says "the work may be wrong, do it
	// again"; this says "the work stands, measure it again". They differ in
	// what they preserve, which is the only thing an operator picking between
	// them cares about.
	ResolveRerunGates = "rerun-gates"
)

The `step resolve --as` vocabulary (§6.10, §2).

View Source
const (
	// PinOK: the pinned bytes are still what the pin recorded.
	PinOK = "ok"
	// PinChanged: the ref resolves, and to different bytes. This is the one
	// that blocks work — every verb that reads the ref refuses (CONFLICT).
	PinChanged = "changed"
	// PinMissing: the run depends on the ref and it is no longer there.
	PinMissing = "missing"
)

Pin verdicts. They are constants rather than free strings because a consumer branches on them, and `--json` makes them part of the wire contract.

View Source
const (

	// ArtifactKindIssueDiff is the kind the engine records a computed VCS diff
	// under (§6.7.1). It is engine-produced rather than step-declared, which is
	// why it is a constant here and not a value read from a definition.
	ArtifactKindIssueDiff = "issue.diff"
)

The engine-produced input forms of §6.7.

View Source
const DispositionAbandoned = "abandoned"

DispositionAbandoned is the one issue-level terminal ruling core records as an event, and therefore the only one this section can report.

An issue that COMPLETED leaves an activity-log row and a trail comment and no event, and it needs none: its steps are `done` and the step sections already say so. Abandonment is the asymmetric case — the steps go terminal carrying whatever they last said, and the ruling that terminalized them is recorded nowhere a run reader looks.

View Source
const GapArtifactKind = "gap"

GapArtifactKind is the auxiliary kind every executor step may record beside its declared emit (DKT-72). It is PACKAGING vocabulary, like `held` and `operator_resolved`: "work surfaced outside this step's scope" names no domain. Before it existed, every rendered brief promised the channel while no workflow declared the kind — so agents narrated gaps into findings bodies, believed they had recorded them, and the residue evaporated.

View Source
const MetadataMaxBytes = 16 << 10

MetadataMaxBytes caps one completion's `--metadata` bag.

The bag is opaque, so nothing else bounds it. Without a cap a worker can push an artifact-sized body into a column the R7 rollup groups BY DISTINCT VALUE, turning a report into a wall of unique multi-kilobyte strings — a slow, silent degradation of the one read surface this path exists to feed.

16 KiB is roughly three orders of magnitude above the motivating use (a handful of short routing keys, ~120 bytes): generous enough that no legitimate KV bag meets it, small enough that the rollup stays legible.

It is a CONSTANT, not config, following db.ArtifactMaxBytes. A new engine config key is a surface addition needing its own spec line, and no evidence exists that any instance needs a different value.

View Source
const RoutingPass = "pass"

RoutingPass is §11.2's default: no threshold matches ⇒ pass.

View Source
const UsageSourceBackfilled = "backfilled"

UsageSourceBackfilled is the default `source` for a back-filled row.

It is a DEFAULT, not an enumeration: `--source` overrides it with any string, because core enumerating valid sources would be core holding an opinion about who is allowed to have measured the work — the same reasoning that keeps `unit` opaque.

Variables

View Source
var AggregateMethods = []string{MethodMedian, MethodMax, MethodMin}

AggregateMethods is the closed vocabulary, in §2's own order, for V28's error message and for SKILL.md's table.

Functions

func ActorCounts

func ActorCounts(conn *sql.DB, runID int) (map[Actor]int, error)

ActorCounts rolls the feed up per actor — the report's fifth section (E21).

It is here rather than in the report because the attribution is this file's subject, and a rollup computed beside the table cannot disagree with it.

func AnnotateStep

func AnnotateStep(conn *sql.DB, stepID int, metadata string, nowMS int64) (*db.Step, error)

AnnotateStep merges opaque metadata onto a finished step's record.

It refuses a step that has not reached a terminal status: a live step's metadata lands with its record (`step complete --metadata`), under its holder's token, and a side channel into an in-flight record would bypass exactly that authorization.

func BlockedReason

func BlockedReason(sched *Scheduler, step *db.Step) string

BlockedReason names WHY a step is not `ready`, "" for any step EffectiveStatus does not render `pending` (DKT-470's second fix).

EffectiveStatus already asks the §6.3 predicate and keeps only the bool; this asks the same question over the same snapshot and keeps the ReadyCondition instead — the two cannot disagree because they walk the identical branches. Without it, an operator staring at `step show` on a step whose interposed routing had already been decided AGAINST it (its threshold's routing step recorded a different routing, permanently) saw the same bare `pending` a step one predecessor away from ready shows, with no way to tell "not yet" from "not ever" short of reading the event log.

func BudgetBreachReason

func BudgetBreachReason(spend, cap float64, instance string) string

BudgetBreachReason is B21's shape:

budget: spend <N> of cap <M> reached at <instance>

A BARE-NUMBER STATEMENT NAMING NO UNIT (§1.1's second leak, closed). There is no currency, no token, and no rate: `--budget 12` means "stop when the accrued number reaches 12", and what the number counts is the workflow author's business. `%g` renders 12 as "12" and 12.5 as "12.5" rather than padding either into a fixed-point shape that would imply a denomination.

func DecodeMetadataBag

func DecodeMetadataBag(raw, label string) (map[string]any, error)

DecodeMetadataBag parses one opaque KV bag, refusing anything that is not a JSON OBJECT.

The object requirement is the one shape core insists on, and it is not core reading the bag: a KV bag with no keys has nothing to merge and nothing for the rollup to group by. db.MetadataRollup already SKIPS a non-object row at read time, and that tolerance exists for rows written before any writer validated them (R10: a read verb that refused because one row held odd bytes would be useless during exactly the run an operator wants to inspect). It is not a licence to write such rows now — refusing at the write end is what keeps the tolerance from becoming the norm.

Exported so internal/cli's `vote cast --metadata` (vote_cast.go) can decode through the SAME "is this a JSON object" rule rather than a second copy of it — the two differ only in what wraps this decode (the vote's own size cap against `db.VoteMetadataMaxBytes`, checked by the caller on the returned bag).

func DiscrepancyReason

func DiscrepancyReason(ds []Discrepancy) string

DiscrepancyReason renders a list for a refusal, one line per row.

Every line carries its RESOLUTION, because §2 enumerates the resolutions and a refusal that named the problem without the way out would make reconciliation a documentation lookup rather than an operator's act.

It names the STEP ID beside the instance (DKT-315). Instances repeat across issues in one run — `design-qa@1` is four different steps in a four-issue run — so a refusal naming instances alone could not be acted on: the other documented way out, `dispatch backfill-usage --step STEP-N`, needs the id, and no read verb exposed which rows the probe was counting. The id is already on the row; it was simply not rendered.

func EffectiveStatus

func EffectiveStatus(sched *Scheduler, step *db.Step) string

EffectiveStatus is §6.2's computed status, for any read verb.

A `pending` step reads as `ready` when the §6.3 predicate holds. A claimed step whose lease has LAPSED reads as `pending` — the v6 discipline, computed at read and never written back — so "status never lies because nobody called next" is true for steps as it is for leases. NOTHING HERE WRITES.

func EffectiveStatusCounts

func EffectiveStatusCounts(conn *sql.DB, runID int, nowMS int64) ([]model.StatusCount, error)

EffectiveStatusCounts rolls one run's steps up by EFFECTIVE status (§6.2), for `run status` — the same status LoadStepView and RunStepList report, so the rollup can never contradict `step show`/`step list` (DKT-468).

It exists because the rollup used to GROUP BY the raw column while the verb's own contract said "this verb computes effective status": a claimed step whose lease had lapsed counted as `claimed` here and read as `ready` in `step show`, and an operator holding both outputs at the same moment had no way to tell which surface was lying. Neither was — they answered different questions — but two answers to "what is this step's status" is exactly the divergence the effective-status discipline exists to prevent. READ-ONLY, like every §6.2 computation: the transaction is always rolled back and no reap runs.

func EventKinds

func EventKinds() []string

EventKinds returns the closed set, for the tests and the audit that enumerate it.

It is derived from the writer's own membership table rather than from a second list, so an audit cannot pass because it checked a stale copy.

func ForceReapStep

func ForceReapStep(conn *sql.DB, stepID int, reason string, nowMS int64) error

ForceReapStep is `docket step reap` (DKT-83): an operator or relay that has ESTABLISHED an executor is dead clears its claim now, instead of waiting out the full lease TTL.

Liveness was TTL-only, and the TTL cannot be sized right in both directions: raised to cover healthy long writers, it multiplies how long a dead agent's claim blocks its row (a claim whose process died two minutes in once sat until expiry). The engine cannot probe a process it did not start — the write-reap acknowledgment says so — but the RELAY that spawned the executor can, and this verb is the channel for what it observed.

TOKEN-FREE, like approve/resolve: the authority is repository access plus the assertion, recorded with `--reason`, that the holder is gone. It is not an eviction primitive a bystander reaches casually — a forced reap of a LIVE worker has exactly the risks a lease expiry has, which is why every consequence is the expiry reap's own: same event kind (`lease-reaped`, with `data.forced` and the reason distinguishing it), same write-class headroom hold, same return of the step to the pool.

func FormatDispatchID

func FormatDispatchID(id int) string

FormatDispatchID renders a dispatch's display identity.

It follows the `RUN-N` / `STEP-N` convention of every other engine entity rather than exposing a bare integer, so a `CONFLICT` naming a dispatch names something an operator can pass back to a verb.

func GitDiff

func GitDiff(dir, base string, scope []string) (string, error)

GitDiff computes a diff over the given path globs. THIS IS THE ONE DECLARED VCS COUPLING (engine-spec §7), and it is confined to this function.

It runs OUTSIDE any transaction, always (§6.7.1 D1). A failure is not fatal to the saga: a repository that is not a git checkout, or a scope matching nothing, yields an empty diff rather than wedging a run. The engine's job is to record what the tree says, and "nothing" is a truthful answer.

The D5 hazard engine-spine.md §6.7.1 recorded — "a process recording a step on behalf of work done somewhere else captures ITS OWN tree, silently" — is closed by the dir argument: the saga passes the step's recorded worktree (`--worktree` at complete/record), falling back to the invoking checkout's root. An empty dir keeps the old cwd behavior for callers that have neither.

base is the commit-ish `dir`'s tree is compared against, empty meaning `HEAD` (dir's own tip). DKT-11: a `--worktree`-recorded step commits its change IN THAT WORKTREE, so `dir`'s working tree and `dir`'s own HEAD are identical by the time this runs — `git -C dir diff HEAD` then reports nothing, even though the worktree's HEAD carries a real commit the shared checkout does not have. Passing the shared checkout's HEAD as base makes the diff "what did this worktree add", not "is this worktree's tree dirty". Worktrees share one object database, so `base` resolves from `dir` even though it names a commit reachable only via another checkout's ref.

func HeartbeatStep

func HeartbeatStep(
	conn *sql.DB, stepID int, token string, nowMS int64,
) (*model.Lease, error)

HeartbeatStep extends a live lease held by token (§6.10). It does not touch `attempt`: a heartbeat is not a new claim.

func HighestOrdinals

func HighestOrdinals(tx *sql.Tx, runID, issueID int) (map[string]int, error)

HighestOrdinals returns, per step NAME, the highest ordinal that has an instance for this issue.

It is the basis of §11.3's "issue completion is evaluated over highest-ordinal instances only". Per NAME rather than one number for the issue, because the two differ exactly where it matters: after a loop entry, `implement` has instances only at ordinal 0 while `review` has them at 0 and 1, and a single issue-wide maximum would rule `implement@0` out of the completion check and call the issue complete without it.

func IsVoteStepProposal

func IsVoteStepProposal(conn *sql.DB, proposalID int) (bool, error)

IsVoteStepProposal reports whether a proposal was opened by an engine vote step — its create keyed under the vote-step scope prefix.

`vote close` (DKT-114) is for CONVERSATIONAL proposals whose decision was made another way. A vote step's proposal is the step's own machinery: closing it underneath the step would not route the step, and the run is moved past an uncast vote with `docket step resolve` instead — so the close verb asks this first and refuses with that guidance.

func LatestIssueDisposition

func LatestIssueDisposition(conn *sql.DB, issueID int) (*model.IssueRunDisposition, error)

LatestIssueDisposition reads the LAST terminal ruling any run recorded about its work on one issue (DKT-404), or nil when no run ever abandoned it.

KEYED BY ISSUE, DELIBERATELY UNBOUNDED BY RUN. The run report answers "what did THIS run decide"; a reader of `issue show` is holding an issue and no run at all, and the abandonment they need to see is frequently not in the newest run — RUN-14 abandoned four issues whose replacements ran two runs later, and those four have been sitting at `todo` ever since with the disposition reachable only through `events list`.

The LAST ruling, not every one: an issue can be abandoned by a run, replanned and abandoned again, and what a reader must not misread is the CURRENT state. The earlier rulings stay in the event log, which is the right place for a history.

func MoveRun

func MoveRun(
	conn *sql.DB, runID int, verb string, to model.RunStatus,
	from []model.RunStatus, reason string, nowMS int64,
) (*model.Run, []string, error)

MoveRun applies one operator lifecycle transition and records its event atomically. `verb` names the CLI verb for the refusal message; `from` is the closed set of statuses the transition may leave.

The event's `data` carries `from`, `to`, and the operator's `reason` — `from` because the interesting question about a transition is what it left, and `reason` because "abandoned" alone does not answer the question somebody will ask about a run that ended without completing. The actor is `human` by the kind's attribution mapping, which is the truth: nothing in the engine pauses, resumes, or abandons a run BY THIS VERB'S ROUTE — every automatic transition goes through the reconciliation rollup (reconcile.go) instead: a budget breach writes its own `run-paused` with `data.reason = "budget"` (distinguishable by exactly that field), and the rollup's own resume/pause carry no `data` at all, which is how an event consumer tells an operator verb from an automatic one. DKT-68: the rollup's automatic resume deliberately DECLINES to fire while a run sits at `waiting-human` on an unresolved budget breach (`runs.breach_reason` still set) — that run can only return to `active` through this verb, or through a cap raise that itself resolves the breach (DKT-80). The []string it returns is the run's OUTSTANDING WORKTREES (DKT-116) — non-nil only on an abandon; see recordedWorktreesTx.

func OpenVoteProposal

func OpenVoteProposal(
	conn *sql.DB, step *db.Step, spec *workflow.Step, nowMS int64,
) (int, error)

OpenVoteProposal is §8.1 phase 2: the first engine invocation that observes a ready vote step without a proposal creates one.

LAZY AND IDEMPOTENT, following the saga's own discipline: the proposal is created by whichever invocation gets there first, and the idempotency key makes a double-invocation produce ONE proposal. No daemon watches for ready vote steps; nothing is scheduled.

func OverridePassSkipsInterposedTargets

func OverridePassSkipsInterposedTargets(conn *sql.DB, stepID int) []string

OverridePassSkipsInterposedTargets names the interposed step(s) an `override-pass` on stepID will never route to (DKT-470).

ResolveOverridePass always records the GENERIC RoutingPass, whatever the step's threshold would have decided over the accepted payload — it does not evaluate the threshold at all. For a step with no threshold, or a threshold with no step-name routing target, that generic pass IS the right answer and there is nothing to warn about. For a step whose threshold interposes another step (§11.2's "route to a step name"), it is a silent bypass: the interposed step is unconditionally skipped as soon as this resolution commits (reconcile.go's skipUnroutedTargets), REGARDLESS of whether the threshold's condition was actually met. RUN-36/VPL-153 hit exactly this — override-pass on an aggregate whose accepted payload had 17 findings `>= high`, against a threshold that would have routed to `security-vote` on that condition, instead silently skipped the vote with no warning that this is what override-pass does.

It answers a question about the step's DEFINITION, which the resolution it precedes does not change, so it is independent of ResolveStep and safe to call and print before that call commits anything — the operator sees the blast radius of what they are about to approve.

func PinDriftNotice

func PinDriftNotice(drift []PinVerdict, runRef string) string

PinDriftNotice renders drifted pins for a human surface, one line per pin, naming both hashes and the recovery verb. Empty for no drift.

func PinReportReason

func PinReportReason(r *PinReport) string

PinReportReason renders the unsound pins for a refusal, one clause each, with BOTH hashes — an operator needs them to decide between restoring the file and starting a new run, which is the same pair every pin refusal already names.

func ReapAckProposalKey

func ReapAckProposalKey(runID int, seq int64) string

ReapAckProposalKey is the idempotency key an ack-reap ballot is created under, so the acknowledgment that satisfies it can find it.

THE ENGINE OWNS THIS CONVENTION EVEN THOUGH THE ENGINE DOES NOT CREATE THE BALLOT. A conductor routing a reap to a panel opens the proposal; the engine applies the acknowledgment. Only one of the two can be the definition, and it has to be the side that must find the row later — a convention living in the creator would leave the finder guessing, which is how these ballots came to be uncloseable in the first place.

A conductor that does not use it loses nothing it had: its proposal simply is not auto-closed, exactly as today.

func ReapHoldReason

func ReapHoldReason(reaps []db.ReapAck) string

ReapHoldReason renders A11's guidance: each seq, and the exact flag to pass.

It NAMES THE FLAG rather than describing it, because §2's "surfaced by `guard spawn`" is what makes the mechanism discoverable rather than documented — an operator reading the refusal should be able to copy the next command out of it.

The `guard spawn` entry point lands in GROUP 3 (§6.2's table); this group ships `dispatch open --ack-reap`, which is the NEW RELAY's path and is what makes the mechanism complete without group 3. The text names both because the hold is what it is regardless of which verb clears it, and a message that omitted the other entry point would go stale the moment group 3 lands.

func RecordProjectRegisteredEvent

func RecordProjectRegisteredEvent(conn *sql.DB, reg ProjectRegistration, atMS int64) error

RecordProjectRegisteredEvent writes a `project-registered` event (DKT-61).

Exported for the same reason RecordTrustEvent is: registration happens in the CLI's root hook, outside this package, and the alternative — letting the hook write its own INSERT — would put a kind into the events table without passing the closed-set check that makes the set closed.

func RecordTrustEvent

func RecordTrustEvent(conn *sql.DB, kind string, grant TrustGrant, atMS int64) error

RecordTrustEvent writes a `trust-added` / `trust-removed` event (§3.6).

It is exported because the trust CLI is where a grant happens, and the CLI is outside this package. It takes the ARGV HASH rather than the argv, and the parameter type says so: a trusted command's arguments must not land in an event feed a run report renders, and a helper that ACCEPTED an argv would make that mistake available to the next caller.

The event has NO RUN. `events.run_id` is nullable, and a trust grant is a user-level act that may happen with no run in flight at all — attributing it to an arbitrary run would be a fabrication. What makes it useful is the TIMESTAMP: a run whose trail brackets this event is a run during which the grant happened, which is exactly the retro question T9's residual raises.

IT ALSO RECORDS WHO (DKT-263). The timestamp answers "during which run", and that was always the smaller half of the question — bracketing wall-clock still could not say which of two concurrent sessions widened what code may execute. `Actor` and `Cwd` answer it directly. Neither is authenticated and neither claims to be: events_read's actor CLASS says a person did this, which is a classification; these two are the person's own account of themselves, which is an identity. A grant is the one act in the system that widens what may execute, so its trail should not require a join against a clock.

func RegistrationOrder

func RegistrationOrder(paths []string) []string

RegistrationOrder sorts config files into the order auto-registration must apply (TDD §4.6).

**Schemas register in full before workflows.** A workflow that names a schema which is not registered yet is a hard VALIDATION_ERROR at `workflow register` — the only reading under which §11.2's "validated against the registered schema at register time" is true — so the zero-touch path never reaches that refusal only because the ORDER guarantees it cannot. That is a contract, not luck, and it is written here, in the stage that creates the dependency, because S6 would otherwise discover it as a bug.

Within each group the order is lexical by path, for determinism.

The DIRECTORY SCAN that feeds this landed at S6 in autoregister.go, against this signature. F1 requires the scan to hand its paths here rather than re-implement the ordering, so this stays the ONE definition of what the order is — and F2/F3 in autoregister_test.go assert the behavior it buys: activation over a config tree holding a workflow and the schema it names SUCCEEDS, which it could not if the order were lexical across everything.

func RenderFenceReport

func RenderFenceReport(w interface{ Write([]byte) (int, error) }, reports []FenceReport)

RenderFenceReport writes §7.7 S1's human-mode report.

"VERBATIM" MEANS EVERY BYTE IS ACCOUNTED FOR, NOT THAT RAW BYTES REACH THE TERMINAL (T18, §5.7). The command and the reason render through the escaping renderer, which is LOSSLESS: a command containing no control bytes renders identically to its stored form, and one that does renders its escapes visibly rather than executing them against the operator's cursor. D14's whole backstop is that what is displayed is what is approved, so any divergence between the two is an attack on the ratified control, not a cosmetic bug.

func RenderGatePreflight

func RenderGatePreflight(w interface{ Write([]byte) (int, error) }, rows []GatePreflight)

RenderGatePreflight writes the human-mode warning, and writes NOTHING when every gate resolves.

Silence on success is the point. An activation already prints a bound-issue roster, a pin count, a fence report, and any scope warnings; a fifth block saying "all 6 gates are fine" on every run is how an operator learns to skip the region of the screen where the one that is not fine will appear.

A STUBBED gate is listed too, under its own heading. It resolves and will run, so it is not a warning — but "secret-scan will execute /usr/bin/true" is a fact about this run's assurance that belongs beside the roster the operator is approving (DKT-265).

func RenderHoldPolicy

func RenderHoldPolicy(w interface{ Write([]byte) (int, error) }, p HoldPolicy)

RenderHoldPolicy writes the activation's one-line disclosure.

IT PRINTS ONLY WHEN SOMETHING IS CONFIGURED. A project that has never touched these keys gets the default — one operator decides — and saying so on every activation would be a line about a feature nobody is using, in a report whose unread regions are the problem the gate preflight above is careful about.

A HALF-CONFIGURED PAIR IS THE CASE THIS EXISTS FOR, so it prints loudest: the operator has said something and the engine is doing something else, which is precisely the silence the issue objects to.

func RenderRegistrationReport

func RenderRegistrationReport(
	w interface{ Write([]byte) (int, error) }, regs []Registration, pinned int,
)

RenderRegistrationReport is F20: the human-mode `Registered` block.

One line per file, `<name>@<version> <path> (new | unchanged)`, in REGISTRATION ORDER — schemas first, then workflows — followed by a `Pinned` count. The order is the scan's own, not a re-sort, so what an operator reads is the sequence that actually ran.

F23: an activation that registered NOTHING prints no block at all — not an empty one — so F17's dormancy is visible in the output. A repo with no `.docket/config/` produces exactly the activation output v9 produced.

THE PATH AND THE NAME GO THROUGH THE ESCAPER (T18). Both come from the repo's filesystem and a workflow's own `[pipeline] name`, which in a cloned repo are attacker-supplied strings heading for a TERMINAL — the same class as a fence command, and closed the same way. That matters most in exactly the case §9.5 is about: a malicious clone whose config directory this block is describing.

func ResolveInputArtifacts

func ResolveInputArtifacts(
	tx *sql.Tx, sched *Scheduler, step *db.Step, spec *workflow.Step,
) ([]*db.Artifact, error)

ResolveInputArtifacts is §6.7 resolved to the ARTIFACT ROWS themselves, for a consumer that needs a field the §11.4 bundle does not carry.

It exists because the bundle is the WORKER-FACING shape: a `ContextInput` has a `body` and no `payload`, since §11.4 gives an executor the artifact's text and nothing else. The `aggregate` builtin reduces PAYLOADS (§2), so it needs the row.

The RULE is not duplicated — both this and resolveInputs walk the same declared-position loop over the same resolveDeclaredInput, so the `done`-only filter, the ordinal scoping, and the within-input sort have exactly one implementation. What differs is only which columns the caller reads.

The engine-produced forms (`issue.body`, `issue.diff`) resolve to NO ARTIFACT here. `issue.body` has no artifact row at all, and neither carries a payload — they are prose and a diff, not a JSON array of clusters. A caller wanting them as text still has the bundle.

func ResolveRunFilter

func ResolveRunFilter(conn *sql.DB, ref string) (int, error)

ResolveRunFilter maps `--run RUN-N` onto a run id, refusing a run that does not exist.

The refusal matters for a CURSOR feed specifically: `events list --run RUN-99` over a missing run would otherwise return an empty page, and a consumer polling it would wait forever on a run that was never there.

func RunFloorTx

func RunFloorTx(tx *sql.Tx, runID int) (float64, error)

RunFloorTx is §4.3, the whole of it:

SELECT COALESCE(SUM(s.expected_cost), 0)
  FROM events e JOIN steps s ON s.id = e.step_id
 WHERE e.run_id = ? AND e.kind = 'step-claimed'

Every clause of §4.3's table falls out of this query rather than needing its own code:

  • B9, retries re-accrue: a reaped step claimed again writes a SECOND `step-claimed` event, and the SUM counts both. Nothing is released on reap, on fail, or on abandon — the work was attempted and the attempt is what cost something.
  • B10, loop entries re-accrue: a `fix` step at ordinal 1 is a DIFFERENT step row with its own `expected_cost`, claimed and event-logged independently. `max_fix_loops` therefore bounds the floor BY CONSTRUCTION, which is engine-core §7's "bounded loops bound the floor" arriving from §11.3 rather than from arithmetic here.
  • B11, a superseded, skipped, or never-claimed step contributes nothing: the accrual is per CLAIM EVENT, and a step never claimed produced none.
  • B5, the value accrued is the STEP ROW's `expected_cost`, materialized at expansion from the pinned definition and never re-read from the live `workflows` table. A run pins its definitions; its floor is computed from what it pinned.

It is exported because `run report` computes the same number from outside this package and the two must not be able to disagree — a report that recomputed the floor its own way would be a second source of truth for the number a breach is attributed to.

func ScopesIntersect

func ScopesIntersect(a, b []string) bool

ScopesIntersect reports whether two scope-glob LISTS conflict.

S1: an empty or absent list never excludes and is never excluded (engine-core §5: "Scope-less issues declare `scope = []` and never exclude"). This is the dormancy case as well as the common one — every pre-existing issue carries NULL — so it is the first branch rather than a special case buried in the loop.

func StaleLineage

func StaleLineage(tx *sql.Tx, step *db.Step) (bool, error)

StaleLineage is the INERT half of §11.3 (2), and §7.3 (3) exactly.

A step whose ordinal is below its issue's current `loop_count` belongs to a superseded lineage: a later loop entry replaced the work it is part of. It still finishes — it was claimed before the sweep and killing a running worker is not what "superseded" means — and its routing is still RECORDED on the step for the ledger. But the routing applies NO downstream effect: no supersede, no re-expansion, no issue status change, no loop increment.

THIS IS WHERE A NAIVE IMPLEMENTATION RACES. A slow `verify@0` completing after `fix@1` has started must not re-route the issue on stale ordinal-0 findings — it would enter a second loop for a question ordinal 1 has already moved past, and with `max_fix_loops = 2` it would burn the budget for a loop nobody asked for. The ordinal comparison is the necessary half of the guard, and TestStaleLineageRoutingIsInert is its test.

BUT THE COUNTER ALONE OVER-REACHES (DKT-540). A loop entry replaces only the `after_loop` downstream set and the loop bodies; a step OUTSIDE that set — a branch parallel to the loop, or `implement` upstream of it — keeps its existing instance as the issue's CURRENT one, at an ordinal the counter has moved past. Reading `ordinal < loop_count` as "superseded" declared exactly those steps stale: RUN-41's `verify@0` routed `pass` after its issue's review chain had looped once, the routing was recorded and then applied nothing — no `step-skipped` for its interposed `verify-tribunal@0`, no issue completion — and the gate sat `pending` until an operator resolved it by hand. So staleness additionally requires what "a later loop entry replaced the work it is part of" literally means: a later instance of the SAME step exists. The sweep's own invariant (ensureSupersededHaveSuccessors) guarantees one for every step a loop entry actually superseded, so the two readings agree wherever the sweep reached — and disagree only where it never did, which is precisely where the routing must stay live.

func StepDefinitions

func StepDefinitions(conn *sql.DB, runID int) (map[int]*workflow.Definition, error)

StepDefinitions loads the PARSED definition of every workflow a run's steps were expanded from, keyed by workflow id.

It restores from `workflows.parsed`, never by re-parsing `body` — the parsed JSON is the PINNED INTERPRETATION (TDD §4.1). Re-parsing the TOML here would mean a parser change silently re-interpreting a definition a run already pinned, which is the thing version pinning exists to prevent. That rule applies with more force at S3 than at activation: activation happens once, but every readiness computation, every context assembly, and every routing decision for the life of the run comes through here.

It is keyed by workflow id rather than by name because a run may legally bind two issues to two different versions of the same pipeline, and the step row records which one it was expanded from.

func StepDefinitionsTx

func StepDefinitionsTx(tx *sql.Tx, runID int) (map[int]*workflow.Definition, error)

StepDefinitionsTx is StepDefinitions inside a transaction, for readers that must see rows the open transaction wrote — the dry run's, whose steps exist only there and are about to be discarded.

func StepRowFor

func StepRowFor(sched *Scheduler, step *db.Step, ttls ttlConfig) (model.StepRow, error)

StepRowFor renders one step's `next row` at its EFFECTIVE status, for the read verbs (`step show`) and for the claim response's `context.step`. It writes nothing.

func ThresholdOrder

func ThresholdOrder(threshold map[string]string) []string

ThresholdOrder returns a step's threshold routings in DECLARED order.

TOML tables decode into Go maps, which lose declaration order, so the order is reconstructed deterministically: the §11.2 non-step routings first in their specified sequence, then step-name routings sorted. That is not the author's literal file order — TOML cannot give it back — but it IS a total, reproducible order, which is what first-match-routes actually requires. Two identical runs must route identically; a map range would not guarantee that.

func UsageBudgetBreachReason

func UsageBudgetBreachReason(spend, cap float64, unit, instance string) string

UsageBudgetBreachReason is BudgetBreachReason for the MEASURED dimension (DKT-238).

It NAMES THE UNIT, and that is the one place these two messages differ. The declared cap counts a number whose meaning is the workflow author's business, so naming a denomination there would be core inventing one; the measured cap counts a unit the operator themselves configured, and a breach that did not say which one leaves them unable to tell a token cap from a seconds cap — or to tell this stop from a declared-cost stop, which a different verb fixes.

Types

type AbandonIssueOutcome

type AbandonIssueOutcome struct {
	Run   string `json:"run"`
	Issue string `json:"issue"`
	// Steps lists the instances moved to `failed-routed`, in id order.
	Steps []string `json:"steps"`
	// RunStatus is the run's status AFTER the rollup — the abandoned issue may
	// have been the last unfinished work, in which case the run is now done.
	RunStatus string `json:"run_status"`
	// Worktrees is the issue's recorded worktrees (DKT-116) — what its steps
	// declared at record time and no close will ever sweep now. Excluded from
	// the frozen v1 payload; the CLI's v2 wrapper and the event carry it.
	Worktrees []string `json:"-"`
}

AbandonIssueOutcome reports what AbandonIssueInRun did.

func AbandonIssueInRun

func AbandonIssueInRun(
	conn *sql.DB, runID, issueID int, reason string, nowMS int64,
) (*AbandonIssueOutcome, error)

AbandonIssueInRun is the per-issue disposition (DKT-28): every remaining step of ONE issue stops, with a reason, and the run and its other issues continue.

Before this verb every path out of a mis-routed issue was blocked or terminal: `step resolve` applies to parked steps and the mis-routed issue's steps were `pending`; a fix step with no `max_attempts` re-offers forever and never parks; `run pause` parks the RUN while the steps stay pending; and the only guard-satisfying end was `run abandon` — terminal for the WHOLE run, taken under protest with other issues cleanly delivered.

The steps take the SAME `failed-routed` terminus the `abandon-issue` routing produces (reconcile.go), and the issue's own status is deliberately NOT forced terminal for the routing's reason exactly: this is a statement about the RUN's work on the issue, and triage stays the operator's.

type ActionResult

type ActionResult struct {
	// Kind is the produced artifact's kind — `params.output` (§4.3.1).
	Kind string
	// Body is the artifact body.
	Body string
	// Payload is the structured half, as JSON text — the PLAIN ARRAY (§6.3 S2).
	// The S3/S4 `{"stub":true,…}` wrapper is history and is never written again;
	// the marker now rides in `artifacts.stub`, which a result this stage
	// produces leaves at 0.
	Payload string

	// Held indexes the output payload's elements whose spread tripped
	// `hold_spread` (§7.4). A non-empty list defers the step's routing into the
	// saga's `held` stage (§7.7).
	Held []int

	// Results are the per-attempt records to write into `action_results`.
	// A builtin produces exactly one, with NULL argv and exit; a trusted
	// command produces one per flaky attempt.
	Results []ActionResultRow

	// Failed reports that the computation did not succeed. The step routes per
	// its effective `on_fail` and NO ARTIFACT IS WRITTEN — a computation that
	// could not run has not produced a result to record. B3 makes this a STEP
	// failure rather than an engine error: a workflow authoring mistake must not
	// wedge a run.
	Failed bool
	// Reason explains a failure, for the routing record and the operator.
	Reason string
}

ActionResult is one action's outcome.

M-a (§6.4): the seam returns Held and Results as well as a payload, and neither was avoidable. §2 requires the held-cluster outcome, and §6.3 requires per-attempt records; a seam that could only return a payload could express neither.

type ActionResultRow

type ActionResultRow struct {
	Action     string
	Ordinal    int
	Argv       []string
	Exit       *int
	DurationMS int64
	Output     string
	Truncated  bool
	Verdict    string
	Reason     string
	// Builtin marks a result core computed itself — the field that tells an
	// `aggregate` apart from a trusted command that happened to succeed.
	Builtin bool
	// TrustEntry names the entry that authorized this, for the audit record.
	TrustEntry string
	// ArgvSHA256 is the canonical hash of the candidate argv, for trust_cache.
	ArgvSHA256 string
	// Prefix records that a prefix entry authorized it.
	Prefix bool
}

ActionResultRow is one recorded attempt, in `action_results`' shape plus the trust-audit fields the saga writes alongside it.

It mirrors GateResultRow exactly, and the mirror is the point: `run report` (S6) reads one pattern twice rather than two patterns once.

type ActionRunner

type ActionRunner interface {
	Run(ctx context.Context, a ActionSpec, sc StepContext) (ActionResult, error)
}

ActionRunner computes one action step's payload.

The saga is written against this interface and calls it OUTSIDE every transaction, which is what makes §6's "no subprocess ever executes inside a transaction" a property of the saga's structure rather than of any runner's good behavior.

type ActionSpec

type ActionSpec struct {
	// Name is the `action` value. Core carries it opaquely: it is either a
	// builtin's name or a trust entry's name, and nothing else about it is read.
	Name string
	// Params is the opaque KV bag, verbatim. CORE NEVER READS A KEY INSIDE IT
	// for a non-builtin action (§6.2) — a trusted command's params are its
	// author's business. The builtin reads exactly the four keys §2 names for
	// it, and V28 refuses any other.
	Params map[string]any
	// Output is `params.output`: the artifact kind this step produces (§4.3.1).
	// It is lifted out of Params so the saga never reaches into the bag.
	Output string

	// Inputs is the payload set the computation reduces: THE CONCATENATED
	// PAYLOADS OF THE STEP'S DECLARED `inputs` ARTIFACTS, resolved per §6.7 and
	// already shape-validated (§2, amended).
	//
	// It is NOT the step's own recorded payload. An action step that has never
	// run has none, so reading one would make every aggregate reduce over nil in
	// production and leave the flow producible only by a dispatcher that claimed
	// the step and wrote its input by hand — reconcile.py reborn as a
	// claim+complete shim, which D13 forbids and which `claim`'s §6.15 branch now
	// refuses outright.
	//
	// V29 is untouched by this: it rejected the predecessor's SCHEMA as the ORDER
	// source, which stays the step's own declared `payload`. The ORDER and the
	// DATA are two questions, and `inputs` is the declaration that answers the
	// second — ordinal-aware, `done`-only, and in the author's declared order.
	Inputs []map[string]any
	// Order is the step's PINNED payload schema's ordered index, or nil when the
	// step declares none. The builtin refuses to reduce without it (V29's
	// runtime half); a trusted command never sees it.
	Order OrderResolver
	// Validate validates a produced payload against the step's declared schema,
	// or is nil when the step declares none. It is a function rather than a
	// compiled document so the seam does not drag the schema package into every
	// test that builds a spec.
	Validate func(payload []byte) error
	// Context is the §11.4 context object as `docket step context --json` emits
	// it — the bytes a trusted command receives on stdin (§6.2). It is
	// assembled by the saga BEFORE the runner is invoked, because assembling it
	// needs a transaction and a subprocess may never run inside one.
	Context []byte
}

ActionSpec is one action step to compute, normalized from §11.1.

type ActivateOptions

type ActivateOptions struct {
	// FilePins are `--pin PATH` (repeatable): arbitrary operator-supplied
	// files pinned by path and content hash. engine-spec §2 is explicit that
	// this is "how the reference instance pins its contracts, fragments, and
	// policy without core knowing what they are" — so core reads bytes, hashes
	// them, stores the path, and never opens the content again except to serve
	// `context.pins`.
	FilePins []string
	// NowMS is the activation timestamp, injected so a test can pin it and
	// two activations of the same inputs produce comparable rows.
	NowMS int64
	// DryRun computes the whole activation and DISCARDS it (§7.7 S4), so an
	// operator sees what a run would bind and invoke before committing.
	DryRun bool
	// Reason is the operator's optional stated reason for activating, recorded
	// on the run-activated event's data when non-empty — matching `run budget
	// --set --reason`, `run abandon --reason`, and `step reap --reason`.
	Reason string
}

ActivateOptions are `docket run activate`'s inputs beyond the run itself.

type ActivateResult

type ActivateResult struct {
	Run *model.Run
	// IssuesBound is every issue in the run, bound and snapshotted.
	IssuesBound int
	// IssuesExpanded is the subset whose phase expanded THIS activation —
	// phase-1 issues at first activation, newly-unblocked phases at a
	// re-activation (RA1).
	IssuesExpanded int
	StepsCreated   int
	// ExpectedCostTotal is the run-wide SUM of the steps' expected_cost as
	// this activation leaves it — every step the run holds, including any a
	// PRIOR activation already created, computed inside the SAME transaction
	// so a --dry-run projects the steps it provisionally created (DKT-54's
	// activation half: cap-vs-cost in front of a panel before any step
	// exists for `step list` to enumerate). This is the whole roster, NOT
	// this activation's delta — see ExpectedCostAdded for that (DKT-517: a
	// conductor read this total as a 5-step increment when the real
	// increment, ExpectedCostAdded, was a fifth of it).
	//
	// A skipped step (workflow.StatusSkipped) is still an inserted row with
	// its own expected_cost, so it counts toward this sum exactly as an
	// un-skipped step does — the sum reads the `steps` table, not step
	// status.
	ExpectedCostTotal float64
	// ExpectedCostAdded is the SUM of expected_cost across only the steps
	// THIS activation created — ExpectedCostTotal's increment, named
	// distinctly so a conductor cannot mistake the whole roster for the
	// delta (DKT-517). On a first activation the run holds no prior steps,
	// so this equals ExpectedCostTotal; on a re-activation it is the newly
	// expanded phase's cost alone. Same skipped-step accounting as
	// ExpectedCostTotal.
	ExpectedCostAdded float64
	PinsRecorded      int
	FencesHarvested   int
	// PromotedIssues names, by display id, every issue stage 7 moved
	// `backlog -> todo` this activation (DKT-102/DKT-94: a count alone
	// answers "how many" and not "which ones" — an operator approving a
	// `--dry-run`'s roster needs the ids, the same way `issues_bound`'s
	// count-only shape was already the DKT-94 complaint elsewhere). Populated
	// identically on a dry run and a real activation, since promotion is
	// computed the same way on both — only a real activation commits it.
	PromotedIssues []string
	// BoundIssues names, by display id, every issue bound this activation, paired
	// with the exact workflow@version it bound to (DKT-94). This is the roster
	// `IssuesBound`'s count was missing — an operator approving a `--dry-run`
	// needs to know WHAT was bound, not just how many.
	BoundIssues []BoundIssue
	// Reactivation reports whether this was a re-activation of an already
	// `active` run, so the verb can say "expanded 2 new phases" rather than
	// implying a first activation.
	Reactivation bool
	// ContextWarnings names every step whose closure exceeded
	// `context.warn_bytes` (§5.5). The WARN cap does not refuse — only the
	// ERROR cap does — so these ride out on the result rather than as an
	// error, and the verb chooses the channel: stderr in human mode, a flag on
	// the row in JSON mode. Returning them rather than printing them here
	// keeps the engine free of an output dependency it has no other use for.
	ContextWarnings []ContextWarning
	// ScopeWarnings names every issue that declared no scope at all while
	// binding a workflow whose steps occupy the tree (lintUnscopedHolders).
	//
	// It travels here for ContextWarnings' reason and takes the same stance: the
	// condition is a planning omission rather than an illegal state, so it warns
	// and proceeds, and the verb picks the channel — stderr in human mode, an
	// array in JSON.
	ScopeWarnings []ScopeWarning
	// Registered is what auto-registration acted on, in registration order —
	// schemas first, then workflows (docs/tdd/runs-dispatch.md §9.7 F20/F21).
	//
	// F23: an activation that registered nothing leaves this NIL and the verb
	// prints no `Registered` block — not an empty one — so F17's dormancy is
	// VISIBLE IN THE OUTPUT rather than merely true underneath it.
	Registered []Registration
	// PinsFromConfig counts the files under `.docket/config/` that were pinned
	// rather than registered (F4). They are counted, not listed: a fragment tree
	// can hold hundreds of files and none of them is a decision an operator
	// needs to read before approving.
	PinsFromConfig int

	// Fences is the §7.7 trust report: every harvested fenced command and
	// whether a trust entry authorizes it (gates-trust §7.7, threat T16).
	//
	// engine-spec §2 requires activation to surface "what activation will bind
	// — including every harvested fenced command, verbatim". S3 harvested and
	// stored; this stage adds the TRUST STATUS, because a verbatim list an
	// operator cannot act on is only half the mechanism: they need to see which
	// commands will actually run before the run, not after.
	Fences []FenceReport
	// GatePreflight is DKT-255's static check: every gate the bound workflows
	// DECLARE, resolved against this repo's trust store before the run.
	//
	// The fence report above answers the same question for a HARVESTED command
	// and nothing asked it of a declared gate, so the gap was discovered
	// mid-run, one gate at a time, when the gate fired and found nothing to
	// run. All 34 gate-unmatched events of the epoch were missing entries, and
	// every one was knowable here.
	//
	// It WARNS, on the same footing as ScopeWarnings and the fence report: some
	// gates are legitimately absent on some machines, and activation is not the
	// place to make that a hard stop.
	GatePreflight []GatePreflight
	// HoldPolicy is who will answer a hold this run mints (DKT-266) — a panel
	// when both `vote.hold.*` keys are set, one operator otherwise.
	//
	// It is reported because a configured governance surface that does nothing
	// and a broken one look identical from outside, and telling them apart took
	// a source audit. The engine was doing exactly what it was told.
	HoldPolicy HoldPolicy `json:"hold_policy"`
	// DryRun reports that nothing was written, so a caller cannot mistake a
	// discarded activation for a real one.
	DryRun bool
	// ProjectedStatus and ProjectedActivatedAtMS are set ONLY on a dry run
	// (DKT-96/DKT-100/DKT-109): what `Run.Status`/`Run.ActivatedAtMS` would
	// become if this activation committed. `Run` itself renders the run AS IT
	// ACTUALLY IS — the still-committed row, read before activateTx's stage 7
	// mutated it in the discarded transaction — so a dry run can never be
	// mistaken, field-for-field, for a real activation. On a real activation
	// both are zero/nil: `Run` already carries the committed projected state,
	// so a second copy would be redundant.
	ProjectedStatus        model.RunStatus
	ProjectedActivatedAtMS *int64
	// contains filtered or unexported fields
}

ActivateResult reports what one activation did, for the verb to render.

func Activate

func Activate(conn *sql.DB, runID int, opts ActivateOptions) (*ActivateResult, error)

Activate is the fat transaction (engine-core §3.2, engine-spec §2; TDD §5.3).

ONE transaction, seven stages in order, each failing the whole activation:

  1. Bind — exactly one registered workflow's [match] per issue
  2. Lint the work DAG — planner.BuildDAG + TopoSort over depends_on
  3. Pin — the bound workflows and every --pin file
  4. Snapshot — issue bodies, and {title, kind, labels, scope}
  5. Harvest fences — declared tags only, literal, hashed
  6. Expand phase 1 — lazily, for issues whose predecessors are satisfied
  7. Promote and flip — backlog -> todo, run -> active, events written

NOTHING EXECUTES INSIDE THIS TRANSACTION (§6: "No subprocess ever executes inside a transaction"). Activation runs no gate, no action, no command. It READS files — for pins and their hashes — which is not execution.

The transaction being fat is what makes the failure modes clean: a `--pin` path that does not exist aborts everything, leaving no run rows, no steps, and no pins. Pinning is never partial, because a partially-pinned run is a run that cannot reproduce itself and cannot say so.

type Actor

type Actor string

Actor is one of the FOUR causes engine-spec §9 item 2 requires every transition to be traceable to, verbatim: *"every transition in events traceable to next/gate/threshold/human input."*

It is a closed enumeration for the same reason the event kinds are: an attribution surface with a free-string actor could answer "some other thing" and item 2's audit would be vacuous.

const (
	// ActorNext is the SCHEDULER: readiness, reaping, joins, loop entry, the
	// TTL auto-abandon, and promotion.
	ActorNext Actor = "next"
	// ActorGate is a DETERMINISTIC CHECK — gates and actions both, since an
	// action is a check whose verdict is computed rather than declared.
	ActorGate Actor = "gate"
	// ActorThreshold is COMPUTED ROUTING: which way a step went, and every
	// status a routing produces.
	ActorThreshold Actor = "threshold"
	// ActorHuman is an OPERATOR VERB — including one a harness relays on an
	// operator's behalf, which is exactly the boundary item 2 exposes.
	ActorHuman Actor = "human"
)

func ActorFor

func ActorFor(kind string) (Actor, bool)

ActorFor reports which of the four causes an event kind is attributable to, and whether the kind is in the table at all.

The second return is not decoration: §9 item 2's audit asks whether EVERY event maps, and a lookup that returned `""` for an unknown kind would let the audit read an unattributed event as attributed to nothing in particular.

type ActorCount

type ActorCount struct {
	Actor string `json:"actor"`
	Count int    `json:"count"`
}

ActorCount is one row of E21's rollup: a cause, and how many of the run's transitions it accounts for.

The actor is a string on the wire rather than an enum, matching every other count row in this document, and the ORDER is a total one (§4.10 R9) so two reports of the same rows are byte-identical.

type AggregateOutcome

type AggregateOutcome struct {
	// Payload is the output payload, one element per input element (§7.6).
	Payload []map[string]any
	// Held indexes the elements whose spread tripped `hold_spread`. It is a
	// list rather than a count because §7.7's materialization has to be able to
	// say WHICH clusters are open, and a count cannot.
	Held []int
}

AggregateOutcome is one aggregation's result.

func Aggregate

func Aggregate(
	payloads []map[string]any, params AggregateParams, order OrderResolver,
) (*AggregateOutcome, error)

Aggregate reduces each input element's cluster to a single value of the same field (§7.2–§7.5).

EACH ELEMENT OF THE INPUT PAYLOAD IS ONE CLUSTER. `params` carries no grouping key because clustering is the JUDGED half and reconciliation is the COMPUTED half (engine-core §6) — so the grouping arrives in the payload, as an array under the field, and G2 makes a scalar there a one-member cluster.

G2 is the property that makes the machinery safe to adopt: over a flat, unclustered payload `aggregate` is the IDENTITY. Every value passes through, nothing is held, nothing is demoted, and the threshold sees exactly what it would have seen without the action — so an operator can introduce clustering later without a behavior cliff.

type AggregateParams

type AggregateParams struct {
	Field      string
	Method     string
	HoldSpread int
	Output     string
}

AggregateParams is the validated form of §7.1's table.

func ParseAggregateParams

func ParseAggregateParams(params map[string]any) (AggregateParams, error)

ParseAggregateParams reads §7.1's four keys out of the opaque bag.

It is the ONE place core reads inside `params`, and it reads exactly the keys §2 names. The same rules run at register time (V28) so a typo'd `method = "medain"` is refused before a run spends the inputs it would have aggregated; this exists as well because a definition can reach the engine through a database restored from elsewhere.

type ArtifactIndexEntry

type ArtifactIndexEntry struct {
	Artifact string `json:"artifact"`
	Kind     string `json:"kind"`
	Producer string `json:"producer,omitempty"`
	Executor string `json:"executor,omitempty"`
	Issue    string `json:"issue,omitempty"`
	SHA256   string `json:"sha256"`
	Bytes    int    `json:"bytes"`
	// Supersedes names the artifact this one REVISES, e.g. `ARTIFACT-71`
	// (DKT-70). A held cluster's resolution records a new artifact rather than
	// annotating the old one, and the two share a kind and a sha256 — so this
	// index showed one operator decision as a second unit of work, and ledger
	// mining, which counts artifacts as evidence of work, counted it twice.
	// A rollup counting work should skip entries that carry it.
	Supersedes string `json:"supersedes,omitempty"`
}

ArtifactIndexEntry is R6's row: what was produced, by whom, and how big — never the body.

Executor and Issue attribute the producer (DKT-79): `producer` alone is the fanout ordinal (`review@0#2`), which says WHERE in the topology an artifact came from and nothing about WHO — the opaque executor hint the definition declared is the axis a judge-value question actually groups by. Issue rides for the same collision instance labels have everywhere: two issues on one workflow share every instance name.

type BackfillOutcome

type BackfillOutcome struct {
	Written int          `json:"written"`
	Steps   int          `json:"steps"`
	Source  string       `json:"source"`
	Skipped []SkippedRow `json:"skipped,omitempty"`
}

BackfillOutcome is what the back-fill did: rows written, steps touched, the source they carry, and every row skipped as already recorded.

type BackfillRow

type BackfillRow struct {
	Step     int
	Unit     string
	Quantity float64
}

BackfillRow is one unit's quantity for one step, as named on the command line. There is deliberately NO attempt field: see BackfillUsage.

type BoundIssue

type BoundIssue struct {
	IssueID  string `json:"issue"`
	Workflow string `json:"workflow"`
}

BoundIssue names one issue this activation bound, by display id, and the exact workflow@version it bound to (DKT-94: `issues_bound` reported a count with no roster, and the only place bound-issue identity appeared at all was `scope_warnings`, keyed by internal numeric ids — not the display ids a conductor reads or writes back into its own plan). Populated identically on a dry run and a real activation, since binding is computed the same way on both — only a real activation commits it.

type BudgetSource

type BudgetSource string

BudgetSource names where an effective cap came from — the report's R6 line (§4.10 R2) and nothing else. It is scheduling vocabulary: a flag, a config key, or neither.

const (
	// BudgetFromRun is `run start --budget N` (B1's first branch).
	BudgetFromRun BudgetSource = "run"
	// BudgetFromConfig is `docket config budget.default` (B1's second branch).
	BudgetFromConfig BudgetSource = "config"
	// BudgetUnlimited is B1's third branch: 0 at both levels.
	BudgetUnlimited BudgetSource = "unlimited"
)

func BudgetSourceOf

func BudgetSourceOf(cap, configDefault float64) BudgetSource

BudgetSourceOf names where a run's stored cap came from — R6's line.

It COMPARES rather than reads a stored source, because §2.3 adds no `budget_source` column and inventing one would be a silent deviation from the column table this stage ratified. The comparison is exact in every case that matters and ambiguous in exactly one that does not: a run whose `--budget` happened to equal the config default reads as `config`. Both answers name the same number, and the number is what an operator asking "why didn't it stop?" needs.

type ClaimOptions

type ClaimOptions struct {
	Owner string
	// TTLOverride is an explicit `--ttl`. Zero means resolve from the
	// workflow's [limits] then config, per §6.4's precedence.
	TTLOverride int64
	NowMS       int64
}

ClaimOptions are `step claim`'s inputs beyond the step itself.

type ClaimResult

type ClaimResult struct {
	Step           string   `json:"step"`
	Token          string   `json:"token"`
	LeaseExpiresMS int64    `json:"lease_expires_ms"`
	Context        *Context `json:"context"`

	// Attempt and RowVersion surface under --json=v2 only, per
	// reliability-delta §6.3: a claim is a mutation and advances the version.
	Attempt    int `json:"-"`
	RowVersion int `json:"-"`
}

ClaimResult is §11.4's `claim response`, field for field:

{ step, token, lease_expires_ms, context }

THE SUBJECT KEY IS `step`, exactly as §11.4 specifies, and `context` is the full bundle. That is what closes the deviation (§6.4.1): S2 landed claims on issues, where the subject key is `issue` and no context bundle is defined, and filed the nominal deviation. This stage implements the spec's shape verbatim at the level it was written for, so the deviation is CLOSED by demonstration rather than amended.

func ClaimStep

func ClaimStep(conn *sql.DB, stepID int, opts ClaimOptions) (*ClaimResult, error)

ClaimStep takes a lease on a step and returns the token AND the context bundle in ONE response — "one atomic mediation: an unclaimed executor has nothing, a claimed one has everything" (engine-core §8).

IT IS THREE PHASES AS OF STAGE 4 (gates-trust §7.6.1, recorded as M-c), and the reason is structural: `pre = true` gates run AT CLAIM (§11.1), a pre-gate is a subprocess, and engine-spec §6 forbids a subprocess inside a transaction. So:

1  transaction A  reap, readiness (R8), CAS claim, started_ms,
                  status -> claimed, step-claimed event        COMMITTED
2  no transaction pre-gates run, one at a time, each result
                  committing in its own small transaction
3  transaction B  context assembly (now including the pre-gate
                  results), input materialization, row-version
                  read, and the lease refresh (§7.6.1.1)       COMMITTED

WHAT IS PRESERVED, precisely, because "claim is atomic" is a ratified property (engine-core §5):

  • The MUTUAL-EXCLUSION guarantee is unchanged. Exactly one claimant wins, and it wins in transaction A, on the same CAS. Losers still get CONFLICT.
  • The "token and context in one response" guarantee is unchanged. The CALLER still receives both in one response, which is what engine-core §8 is about — what the caller observes.

WHAT CHANGES: a crash between phase 1 and phase 3 leaves a claimed step whose caller never got a response. That is ALREADY the pre-existing behavior for a crash between the old single commit and the response reaching the caller, and it is handled by the same mechanism — the lease expires and the step is re-offered with attempt++ (§9 item 4).

A step with NO pre-gates takes the identical path with an empty phase 2, so the overwhelmingly common case is byte-identical to S3's behavior.

type CloseOutcome

type CloseOutcome struct {
	Dispatch string `json:"dispatch"`
	Run      string `json:"run"`
	Status   string `json:"status"`
	Reason   string `json:"close_reason"`
	// Accepted lists the steps closed over under `--accept-missing-usage`
	// (P19), as "STEP-N instance". It is part of the RECORD rather than of the
	// message: §2 says the flag "records the acceptance", and a list only
	// stderr carried would be gone by the time anyone audited. It names ids
	// because instances repeat across issues (DKT-315).
	Accepted []string `json:"accepted,omitempty"`
}

CloseOutcome reports what a closing verb did, so the CLI renders the fact rather than re-deriving it.

type CompleteOptions

type CompleteOptions struct {
	Token string
	// WorkDir is `--worktree`: the checkout the work actually happened in.
	// Persisted on the step at stage 0 and read by the diff stage, so a
	// conductor recording on behalf of an executor in another worktree
	// captures THAT tree rather than its own (G7/G8). Empty means "the
	// invoking checkout", resolved at diff time.
	WorkDir string
	// Artifact is the body from --artifact-file.
	Artifact []byte
	// Payload is the JSON from --payload-file, or nil.
	Payload []byte
	// Usage is `--usage`. It is STORED and ENFORCES NOTHING until S6 — the same
	// reasoning as `--budget`: the wire shape is §11.4's and it lands whole, so
	// the S6 upgrade adds enforcement rather than a flag.
	Usage string
	// Metadata is `--metadata`, opaque KV merged onto the step's own.
	Metadata string
	// Gaps are `--gap-file` bodies (repeatable): out-of-scope problems the
	// worker surfaced. Each records an auxiliary artifact of GapArtifactKind
	// beside the declared emit AND materializes a backlog issue related to the
	// step's own, in the same transaction (DKT-72).
	Gaps [][]byte
	// GapIssues, when non-nil, receives the refs of the issues materialized
	// from Gaps — the caller's channel for telling the operator where the
	// residue landed.
	GapIssues *[]string
	NowMS     int64
}

CompleteOptions are `step complete`'s inputs.

type Context

type Context struct {
	Step   model.StepRow  `json:"step"`
	Issue  ContextIssue   `json:"issue"`
	Inputs []ContextInput `json:"inputs"`
	Pins   []ContextPin   `json:"pins"`
	// LoopEntry is the ordinal k and the routing that entered it, NULL at k=0
	// (§6.4). It is a pointer so the wire shape carries `null` rather than a
	// zero-valued object, which a consumer would have to know to ignore.
	LoopEntry *LoopEntry `json:"loop_entry"`
	// Metadata is the definition's opaque KV, verbatim. Core never reads a key
	// inside it (genericity.md).
	Metadata map[string]any `json:"metadata,omitempty"`
	// PreGates carries the results of the step's `pre = true` gates, in
	// declared order (gates-trust §7.6.3, amendment A5).
	//
	// PRESENT ONLY WHEN THE STEP DECLARES PRE-GATES — absent, not empty,
	// otherwise, which is the rule the v6 `lease` object established. §11.4's
	// `context` line names no member for these while §11.1 requires the results
	// be "included in the context bundle"; the amendment proposes this name.
	PreGates []PreGateResult `json:"pre_gates,omitempty"`
	// TargetSHA and TargetWorktree are the machine-readable target ref
	// (DKT-24): the commit the step's resolved `issue.diff` tree stood at, and
	// the producing record's declared worktree path — good while that checkout
	// is still on disk (it is swept at integration). Lifted from the diff
	// artifact's own round-record payload, so they are exactly as reproducible
	// as the input they describe.
	//
	// Before these, the target commit rode a PROSE convention ("the
	// change-summary's first line carries the sha") and every reviewing
	// consumer re-derived the tree via `git archive | tar -x` — or burned
	// reasoning proving tree-equivalence when integration had already minted a
	// new sha. Both absent when the resolved diff carries no round record.
	TargetSHA      string `json:"target_sha,omitempty"`
	TargetWorktree string `json:"target_worktree,omitempty"`
	// Resolution is the ruling recorded on THIS STEP that sent it back for
	// rework — the routing, and the note whoever decided it wrote (DKT-247).
	//
	// A resolve/approve note was audit-trail only: the packet rendered header,
	// frozen body, inputs, pins, and output spec, and nothing else. So a
	// ruling issued BETWEEN rounds — the operator's answer to the very
	// question that parked the step — could not reach the retry it authorized,
	// and a conductor applied rulings as its own repo commits instead (agw:
	// 3df53c4, b9182fa — both worked, neither sanctioned).
	//
	// SCOPED TO THIS STEP'S OWN ROW. A note on another step is a fact about
	// that step, and rendering it here would put one instance's ruling in
	// another's packet — the collision the instance-label ambiguity already
	// makes easy. Read from `steps.routing`, which holds the CURRENT routing
	// record for this step alone.
	//
	// nil when the step carries no routing record, so a first-round packet is
	// byte-identical to what it always was.
	Resolution *ContextResolution `json:"resolution,omitempty"`
}

Context is §11.4's `context`, field for field:

{ step: <next row>, issue: {id, title, body_snapshot, kind, labels, scope},
  inputs: [{artifact, kind, producer_step, body}], pins: [{path, sha256}],
  loop_entry, metadata }

func AssembleContext

func AssembleContext(
	tx *sql.Tx, sched *Scheduler, step *db.Step, ttls ttlConfig,
) (*Context, error)

AssembleContext builds one step's context bundle inside tx.

It takes a transaction because `step claim` mints the token and assembles the bundle in ONE transaction — "one atomic mediation: an unclaimed executor has nothing, a claimed one has everything" (engine-core §8).

func ReadContext

func ReadContext(conn *sql.DB, stepID int, nowMS int64) (*Context, error)

ReadContext re-emits a step's context bundle READ-ONLY, no token required (§11.4). It is `step context`'s implementation.

IT WRITES NOTHING — not even a reap. §6.3 confines lazy reaping to `next`/`claim`, and this is neither: a read verb that reaped would make "I only looked at it" untrue, and would let a `--meta` query change an attempt counter.

func (*Context) Meta

func (c *Context) Meta() ContextMeta

Meta measures a bundle's sections.

type ContextGateOutcome

type ContextGateOutcome struct {
	Gate string `json:"gate"`
	// Verdict is `fail`, `unmatched`, or `skipped` — never `pass`, since a
	// passing gate is not why the step came back.
	Verdict string `json:"verdict"`
	// Reason is the recorded diagnosis, verbatim. It is what separates "the
	// trust entry is missing" from "the tree was gone" from a real failure,
	// and it is the field a classifier reads after the verdict.
	Reason string `json:"reason,omitempty"`
}

ContextGateOutcome is one non-passing gate as the rework packet carries it.

It is the LAST attempt per gate, matching the routing rule (§5.6 F4): a gate that failed twice and passed on the third try did not fail, and a packet that listed all three would invite a reader to conclude otherwise.

type ContextInput

type ContextInput struct {
	Artifact string `json:"artifact"`
	Kind     string `json:"kind"`
	// ProducerStep is the rendered instance identity of the step that produced
	// it, or "" for an engine-produced artifact (`issue.diff` at activation).
	ProducerStep string `json:"producer_step"`
	Body         string `json:"body"`
	// Payload is the artifact's STRUCTURED half as stored — JSON text, or ""
	// when the producing step declared none.
	//
	// CARRIED VERBATIM: no re-encoding, no re-validation, no key inspection.
	// Core validated the shape once, at completion (§6.8 stage 0), and the
	// schema register is S5's. Re-parsing here would let assembly reject an
	// artifact the engine already accepted — a packet that fails on data the
	// ledger holds is a worse failure than the one this field fixes.
	//
	// It is `omitempty` so a payload-less input serializes exactly as it did
	// before this field existed: every workflow that does not use payloads sees
	// byte-identical bundles and byte-identical packets.
	//
	// Without it, a step whose contract requires its inputs' structure had only
	// the prose — and recovering the rest meant reading engine storage directly,
	// which voids the pinning, the reproducibility, and §6.6's no-live-state
	// rule that the packet exists to provide.
	Payload string `json:"payload,omitempty"`
}

ContextInput is one resolved input artifact, in §11.4's shape.

type ContextIssue

type ContextIssue struct {
	ID           string   `json:"id"`
	Title        string   `json:"title"`
	BodySnapshot string   `json:"body_snapshot"`
	Kind         string   `json:"kind"`
	Labels       []string `json:"labels"`
	Scope        []string `json:"scope"`
}

ContextIssue is §11.4's `context.issue`. EVERY field but `id` comes from a snapshot column — that is what makes §9 item 5's mid-run edit immunity hold, and it is why §5.1.1 made title/kind/labels/scope snapshot columns rather than a join against `issues`.

type ContextMeta

type ContextMeta struct {
	IssueBytes    int `json:"issue_bytes"`
	InputsBytes   int `json:"inputs_bytes"`
	PinsBytes     int `json:"pins_bytes"`
	MetadataBytes int `json:"metadata_bytes"`
	TotalBytes    int `json:"total_bytes"`
	// TemplatePinned reports whether the template a render would use is pinned.
	// An UNPINNED template is reported so the reproducibility gap is visible
	// rather than assumed (§6.11.1) — a packet rendered through an unpinned
	// file is reproducible only to the extent the operator chose.
	TemplatePinned bool `json:"template_pinned"`
}

ContextMeta is `step context --meta`'s per-section byte counts — the closure-size record of engine-core §8.

It is a SIBLING object rather than a mutation of `context`, so the golden bundles (§8.3) are unaffected by asking for it (§6.4). A `--meta` that spliced counts into the bundle would make the goldens depend on a flag.

type ContextPin

type ContextPin struct {
	Path   string `json:"path"`
	SHA256 string `json:"sha256"`
}

ContextPin is §11.4's `pins` element. §11.4 gives pins ONE shape, so a workflow pin renders its `path` as `workflow:name@version` rather than growing a second shape (§6.4).

type ContextResolution

type ContextResolution struct {
	// Routing is the recorded routing — `retry`, `fix-loop`, `waiting-human`,
	// and so on. It is carried beside the note because "do X" reads very
	// differently under a retry than under an override.
	Routing string `json:"routing"`
	// Note is what the decider wrote, verbatim. Empty when the routing was
	// recorded without one — the routing alone is still worth rendering,
	// since it says why the step is being asked again.
	Note string `json:"note,omitempty"`
	// Gates names the gates that did NOT pass on the attempt this routing
	// ended, with their verdicts and reasons (DKT-261).
	//
	// It exists so a relay can tell an ENVIRONMENTAL failure from a CAPABILITY
	// one without a second query. That distinction is the hard and valuable
	// part of an escalation ladder: all three genuine capability-suspect
	// retries of the RUN-25/26 epoch were in fact environmental, so a naive
	// "gate failed twice, escalate" rule would have escalated three times and
	// helped zero times.
	//
	// DKT-254 is what makes the distinction readable rather than a guess. A
	// gate that COULD NOT MEASURE now records `skipped` and parks the step for
	// an operator instead of routing `on_fail`, so it never reaches a retry at
	// all; `unmatched` says the command was never trusted here; only `fail`
	// means a measurement was taken and the work did not pass it. A ladder that
	// escalates on `fail` alone is the rule the epoch's evidence supports.
	//
	// Empty on a step whose routing had nothing to do with gates, which is most
	// of them.
	Gates []ContextGateOutcome `json:"gates,omitempty"`
}

ContextResolution is the routing that sent a step back, and its note.

type ContextWarning

type ContextWarning struct {
	Instance string `json:"instance"`
	IssueID  string `json:"issue"`
	Bytes    int    `json:"context_bytes"`
	Cap      int    `json:"warn_bytes"`
}

ContextWarning is one step whose closure passed `context.warn_bytes` without reaching `context.error_bytes` — visible before spend, per engine-core §8, and not yet a refusal.

type Discrepancy

type Discrepancy struct {
	Kind     DiscrepancyKind `json:"kind"`
	Step     string          `json:"step"`
	Instance string          `json:"instance"`
	// Resolution is §2's enumerated way out, rendered for this specific row.
	// engine-spec §2 enumerates the resolutions precisely so reconciliation is
	// an operator's act rather than a guess the engine makes (§14).
	Resolution string `json:"resolution"`
}

Discrepancy is one unreconciled fact about a run, with the resolution §2 enumerates for it.

COMPUTED, NEVER STORED (§3.2). There is no `dispatch_discrepancies` table, because storing one would create a second source of truth that could disagree with the rows it summarizes — and the disagreement would be resolved by whichever code path a reader happened to call.

type DiscrepancyKind

type DiscrepancyKind string

DiscrepancyKind names one of engine-core §5's TWO classes. There are exactly two and there will not be a third without a spec amendment, so they are constants rather than free strings.

const (
	// DiscrepancyClaimedUnrecorded is D1: a step in `claimed`/`running` whose
	// activity is older than `dispatch.grace`.
	DiscrepancyClaimedUnrecorded DiscrepancyKind = "claimed-but-unrecorded"
	// DiscrepancyMissingUsage is D2: a step that reached a terminal status after
	// the run's activation with zero `usage_ledger` rows, IN A RUN THAT HAS EVER
	// OPENED A DISPATCH.
	DiscrepancyMissingUsage DiscrepancyKind = "usage-rows-missing"
)

type Engine

type Engine struct {
	Gates   GateRunner
	Actions ActionRunner
	// DiffFn computes an issue's VCS diff over its snapshotted scope (§6.7.1
	// D1). It is a field so a test can supply a deterministic one: the real
	// implementation shells out to git, which is THE ONE declared VCS coupling
	// (engine-spec §7), and a test that invoked it would depend on the working
	// tree it is supposed to prove immunity from.
	DiffFn func(dir, base string, scope []string) (string, error)
	// HeadFn resolves a checkout's current HEAD commit, for the round-delta
	// record (DKT-106). A field for DiffFn's reason exactly: the real one
	// shells out to git. "" means "could not resolve", and every consumer
	// treats that as "record no head, compute no delta".
	HeadFn func(dir string) string
	// IsAncestorFn reports whether `sha` is an ancestor of (or equal to)
	// execRoot's HEAD, and whether the question could be answered at all
	// (DKT-193). A field for DiffFn's reason: the real one shells out to git.
	// `known = false` means every consumer stays silent — an unanswerable
	// question is not evidence of staleness.
	IsAncestorFn func(execRoot, sha string) (ancestor, known bool)
	// TreeMatchFn reports whether execRoot's HEAD still carries `sha`'s TREE
	// on the paths `sha`'s work touched — or, where that question has no
	// evidence to answer with, whether the two carry the same root tree
	// outright (DKT-424, DKT-451) — and whether either could be answered at
	// all. A field for DiffFn's reason: the real one shells out to git.
	//
	// It is IsAncestorFn's ACQUITTAL, never its accuser: staleTargets asks it
	// only about a sha ancestry already disproved, and only a `match = true`
	// changes the outcome. `known = false` therefore leaves DKT-193's verdict
	// exactly as it stood — a probe that cannot answer must not silence a
	// warning it did not disprove, which is the opposite direction from
	// IsAncestorFn's own fail-open and deliberately so.
	TreeMatchFn func(execRoot, sha string) (match, known bool)
}

Engine carries the two execution seams and drives the saga.

The runners are fields rather than package-level values precisely so S4 and S5 swap them by constructing a different Engine — "one constructor call and nothing else" (§5.6, §6.13).

func NewEngine

func NewEngine() *Engine

NewEngine builds the S5 engine: the REAL gate runner, the REAL action runner, and the real git diff.

THIS IS THE CONSTRUCTOR SWAP engine-spine §6.13 promised — "the saga is written against the interface, so S5 changes one constructor call and nothing else". The promise holds FOR THE SEAM'S INVOCATION: `runRoutingStage` calls `e.Actions.Run(...)` outside every transaction with a fully-populated ActionSpec, and that call site is where it always was.

docs/tdd/payloads-thresholds.md §6.4 records honestly the four places more moves — the seam's return shape (M-a), the routing stage's `held` branch (M-b), DecideStep's materialized branch (M-c), and stage 0's schema validation with the threshold's resolver (M-d). None deviates from an engine-spec line, so the note is not an amendment.

The repo root is resolved HERE rather than passed by every call site, so the four `internal/cli` callers are genuinely unchanged. A repo that cannot be resolved yields a runner whose gates all report `unmatched` — fail-closed, which is the same direction every other unknown in this stage takes.

func (*Engine) AbandonDispatch

func (e *Engine) AbandonDispatch(
	conn *sql.DB, runID int, reason string, nowMS int64,
) (*CloseOutcome, error)

AbandonDispatch is P21: it closes the open manifest UNCONDITIONALLY.

NO DISCREPANCY BLOCKS IT, and that is the whole point: §2 provides "explicit `dispatch abandon` for a crashed relay", and the relay is gone and cannot resolve anything. A version that checked discrepancies first would be a recovery verb that refuses to recover, which is how a crashed relay wedges a run — the exact failure §2's recovery design exists to make impossible.

func (*Engine) BackfillUsage

func (e *Engine) BackfillUsage(
	conn *sql.DB, runID int, rows []BackfillRow, source string,
	onDuplicate string, nowMS int64,
) (*BackfillOutcome, error)

BackfillUsage records usage for steps whose claimant could not report it.

ONE TRANSACTION for the whole batch. A back-fill that half-applied would leave a dispatch that is neither closable nor honestly re-runnable, and an operator re-running the verb after a partial failure would hit the ledger's unique key on the rows that did land. `--on-duplicate=skip` does not weaken this: a skipped row writes nothing, so the batch is still all-or-nothing over the rows it actually records.

THE ATTEMPT IS THE STEP'S RECORDED ATTEMPT, and there is no way to name a different one. Back-filling an arbitrary historical attempt is rewriting history: the ledger's (step_id, attempt, unit) key exists so a retried step's second attempt records BESIDE its first, and a flag that let a caller choose the number could forge a row against an attempt that never ran or overwrite the accounting of one that did. Refused by omission is the strongest refusal available — there is no flag to misuse.

`source` is written EXPLICITLY on every row. InsertUsageRowTx defaults an empty source to UsageSourceReported, which means "a claimant said so"; a back-fill falling through that default would label a relay's reconstruction as the claimant's own report and destroy the distinction the column exists to preserve.

func (*Engine) BackfillVoteUsage

func (e *Engine) BackfillVoteUsage(
	conn *sql.DB, proposalID int, rows []VoteBackfillRow, source string, nowMS int64,
) error

BackfillVoteUsage records panel spend a relay measured but the seats could not report at cast time (DKT-115).

The step back-fill above cannot receive it: tribunal seats carry a proposal id, never a step id, so governance cost — measured at up to the whole of a run's visible spend — had no ledger path at all once the casts had landed. `vote cast --usage` remains the seat's own report; this is the relay's reconstruction, distinguishable forever by the `source` column v17 added.

ROWS ATTACH TO A SEAT'S CAST. A seat that never cast has no row to attach to and is refused by name — usage on a vote nobody cast is spend on a decision that did not happen, and inventing a cast to hold it would put a phantom seat in the tally's own table.

ONE TRANSACTION for the whole batch, for the same reason the step back-fill is: a half-applied batch would strand its remainder behind the ledger's unique key on a re-run.

func (*Engine) ClaimStepWithGates

func (e *Engine) ClaimStepWithGates(
	conn *sql.DB, stepID int, opts ClaimOptions,
) (*ClaimResult, error)

ClaimStepWithGates is ClaimStep with a gate runner, so pre-gates execute.

The runner is a PARAMETER rather than a package-level value because `ClaimStep` is a package-level function with no access to `e.Gates` — §7.1's M-c names exactly this. The nil-runner form keeps every existing caller and every S3 test working unchanged: with no runner, a declared pre-gate records `unmatched` rather than executing, which is the fail-closed direction.

func (*Engine) CloseDispatch

func (e *Engine) CloseDispatch(
	conn *sql.DB, runID int, acceptMissingUsage bool, nowMS int64,
) (*CloseOutcome, error)

CloseDispatch is P18, P19, P20, and P22.

It closes the open manifest ONLY IF no discrepancy exists. With one, it refuses `CONFLICT` enumerating each discrepancy and its resolution — because "unreconciled batch" is an engine-refusal state (§2) and a close that proceeded over one would be the silent proceeding the whole mechanism exists to prevent.

WITH `--accept-missing-usage` IT DOES NOT REQUIRE AN OPEN DISPATCH (DKT-315). That combination is the one documented way out of a usage-rows-missing refusal, and requiring a manifest to reach it made the refusal a cycle: `next` would not offer work until the discrepancy cleared, and the named way to clear it needed a dispatch that was not open. Harness RUN-14 sat in exactly that state — active, 190 steps done, unadvanceable by any normal verb — from 2026-08-19.

The flag RECORDS AN ACCEPTANCE, which is a statement about steps, not about a manifest. So with no dispatch open it settles the accepted steps, writes the same event against the run, and reports what it accepted. Without the flag the refusal is unchanged: closing a manifest that is not open is still a conflict, because there is nothing to close.

func (*Engine) CompleteStep

func (e *Engine) CompleteStep(conn *sql.DB, stepID int, opts CompleteOptions) error

CompleteStep runs the saga from stage 0, or resumes it from wherever it stopped.

It is safe to call at any time on any step: a step not in the saga starts it (token required), and one already in it resumes (token not required, and not consulted). That is what makes a crashed worker's saga finish under the next `next`, the next `claim`, or an explicit re-invocation, with no operator action.

func (*Engine) DecideStep

func (e *Engine) DecideStep(conn *sql.DB, stepID int, approve bool, note string, nowMS int64) error

DecideStep is `step approve` and `step reject` — §6.10's human-gate verbs.

NO TOKEN. A human gate is not claimed, so there is no lease to authorize against; the authority is the operator's access to the repository, which is the same authority `issue close` has always relied on.

func (*Engine) DecideStepValue

func (e *Engine) DecideStepValue(
	conn *sql.DB, stepID int, approve bool, note, value string, nowMS int64,
) error

DecideStepValue is DecideStep carrying `--value` (DKT-42): an operator's corrected value for a held cluster's aggregated field, validated against the pinned schema's declared enum and applied only on approve of a materialized held step. Every other decision passes "" and is DecideStep unchanged.

func (*Engine) DriveRunLifecycles

func (e *Engine) DriveRunLifecycles(conn *sql.DB, runID int, nowMS int64) error

DriveRunLifecycles advances every engine-run lifecycle a run has ready — the vote phases (open, read, route) and the action steps — to quiescence.

A LOOP, because routing cascades: an action's routing un-defers a step whose readiness exposes another action, and a routed vote can do the same. Each pass reloads the snapshot (the previous pass's routings made it stale — the DKT-55 lesson applied here by recomputation) and stops on the first pass that routed nothing. The pass count is bounded by the run's step count plus one: every routing pass moves at least one step OFF ready, so a hypothetical non-converging chain is an engine fault the bound turns from a hang into a finished call.

Errors are engine faults, exactly as next.go classifies them: B3 already routes a step that cannot run per its `on_fail`, so anything failing here is a database the next verb would hit too, and reporting beats proceeding.

func (*Engine) DriveVoteProposal

func (e *Engine) DriveVoteProposal(conn *sql.DB, proposalID int, nowMS int64) error

DriveVoteProposal is DriveRunLifecycles addressed by PROPOSAL: the hook the `vote cast` verb calls when its cast reached quorum, which knows the ballot it filled but not the run it belongs to.

The run is recovered from the proposal's own idempotency key — `vote-step:<run>:<issue>:<instance>`, the link OpenVoteProposal recorded at phase 2 — rather than from a column on the proposal, for loadVoteProposalsTx's reason: the link already exists and is authoritative, and a second copy could disagree with it. A proposal with no such key is an ad-hoc, operator-created ballot bound to no step; there is nothing to route, and the call is a no-op rather than an error.

func (*Engine) FailStep

func (e *Engine) FailStep(conn *sql.DB, stepID int, token, note, metadata string, nowMS int64) error

FailStep is `step fail` — the explicit-failure counterpart to `complete`.

It consumes an attempt and routes per `on_fail` when attempts are EXHAUSTED, per the status machine (§6.2: "attempts exhausted ⇒ waiting-human", via the step's effective routing). Below the limit the step returns to `pending` and is re-offered, which is the ordinary retry path.

metadata is `--metadata` (parity with `complete`'s): the same opaque KV bag, merged over the step's own with the same mergeMetadata and written with the same db.SetStepMetadataTx stageZero uses — a shared write path, not a duplicated one. It SURVIVES INTO A RETRY (§1.6): a failed attempt's bag merges into the step's row like any other, so the next attempt's completion or failure overlays on top of it. A worker that reports why it failed has produced the most valuable metadata in the run, and discarding it at retry would lose exactly the diagnostic an operator wants.

Validated BEFORE the transaction opens, same as stage zero's C5 ordering: a refusal costs the caller nothing and consumes no attempt.

func (*Engine) GuardSpawn

func (e *Engine) GuardSpawn(
	conn *sql.DB, runID int, opts SpawnOptions,
) (*GuardVerdict, error)

GuardSpawn answers `docket guard spawn`: may the relay start this batch?

G5 — it allows iff BOTH:

(a) the proposed rows byte-match the open dispatch, and
(b) no unacknowledged write reaps exist.

G7: WITH NO OPEN DISPATCH AND NO `--rows`, (a) IS VACUOUSLY SATISFIED. A harness that does not use dispatch manifests still gets (b) — the reap check — which is the half §2 assigns to this verb BY NAME ("surfaced by `guard spawn`"). Requiring a manifest would make the reap-ack mechanism unavailable to any relay that batches differently, which would be core deciding how a harness must batch.

G8: WITH `--rows` AND NO OPEN DISPATCH, IT IS A DENIAL, not a vacuous pass. The relay believes it is spawning a batch the engine never issued, and that belief is exactly the drift this verb exists to catch.

C11'S RESIDUAL, STATED: between this allow and the relay's actual spawn, the dispatch could be abandoned or a lease reaped. THE GUARD IS AN EARLY CHECK, NOT A LOCK, and the real enforcement remains where it has always been — `step claim`'s CAS. This is the same honest bound `guard gate` carries at S3, and it is recorded rather than papered over.

func (*Engine) HeldResolutionStaleTargets

func (e *Engine) HeldResolutionStaleTargets(
	conn *sql.DB, heldStepID int, nowMS int64,
) []StaleTarget

HeldResolutionStaleTargets is DKT-414's resolve-time advisory: the SAME recorded-target-vs-shared-HEAD ancestry check `dispatch open` runs (DKT-193), asked at the moment a materialized held step's resolution commits — because that is when the check can still change what happens next.

The gap it closes: under the staged closure, the downstream verify/review rows are ALREADY in an open dispatch when an operator resolves a held reconcile, so no `dispatch open` runs between the resolution and their execution. RUN-26/FLX-141 resolved a held cluster while the shared branch HEAD had moved off the recorded target sha; verify and two 3-seat panels then consumed packets rendered from a tree the branch no longer carried, rejected 3-0, and each seat re-derived by git forensics the divergence the engine already knew — the next round's `dispatch open` named exactly it, one phase too late to save the ~3-4 cost units already burned.

It REUSES DKT-193's two primitives rather than forking either half: staleTargetCandidates collects (the steps the resolution just un-blocked, judged over the run's current ready set), and staleTargets judges the shas against the shared checkout — so its answer is, by construction, the same rows with the same reason the next `dispatch open` would emit. That shared judge is also why DKT-424's known false positive (a cherry-pick integration mints a new sha, so an ancestry test fails while the TREE is identical) reproduces here verbatim — and why its fix, landing in staleTargets, heals this advisory with no change needed here.

ADVISORY, NEVER A REFUSAL, exactly stale_targets'/pin_drift's posture: the resolution this rides on has already committed, completed steps keep their recorded provenance untouched, and a false positive that merely warns costs far less than one that blocks. For the same reason every failure inside — an unreadable step, a snapshot that cannot load — returns nil rather than an error: absence of evidence is not staleness, and a divergence probe must not turn a resolution that succeeded into a verb that failed.

nil for any step that is not materialized, so the ordinary resolve/approve paths on declared steps stay exactly as they were. A resolution that does NOT end the hold (sibling clusters still open, `--as retry`) finds the routing step still gated, no downstream step ready, and warns about nothing — the advisory fires when packets actually become consumable.

func (*Engine) NextSteps

func (e *Engine) NextSteps(conn *sql.DB, runID int, limit int, nowMS int64) (*ReadySteps, error)

NextSteps computes a run's ready steps, reaping expired leases on the way.

THIS IS ONE OF THE TWO PLACES A READ-SHAPED VERB MAY WRITE (§6.3: "lazy lease reaping happens here and at claim, and nowhere else"). Every other read verb — `step show`, `step context`, `run status` — computes effective status and writes nothing, exactly as v6 established for issues.

The reap and the readiness computation share ONE transaction, and in that order: a step reaped by this call must be offered by this call, or a dispatcher would have to poll twice to see work that is already available.

func (*Engine) OpenDispatch

func (e *Engine) OpenDispatch(
	conn *sql.DB, runID int, limit int, ackSeqs []int64, nowMS int64,
) (*Manifest, error)

OpenDispatch records a batch manifest for a run (§5.2).

P1: it computes the ready set EXACTLY AS `next` DOES — the same LoadScheduler, the same predicate, the same SortSteps — by calling the same helper `next` calls. The identity is structural rather than asserted: a change to readiness reaches both verbs or neither, which is what TestManifestMatchesNext checks by comparing bytes rather than by re-deriving the answer.

P5: it PERFORMS THE SAME LAZY REAP `next` does before computing. It is a scheduling verb offering a batch, and offering a stale step that a reap would have freed would make the manifest wrong the moment it was written.

The acks (§6.2) are applied FIRST, inside the same transaction, because a new relay taking over from a crashed one acknowledges as part of claiming its next batch — and an ack applied after the ready set was computed would produce a manifest that omitted exactly the write-class steps the ack just released.

func (*Engine) ResolveStep

func (e *Engine) ResolveStep(
	conn *sql.DB, stepID int, as, note string, nowMS int64,
) error

ResolveStep is `step resolve --as retry|skip|abandon-issue|override-pass` — §6.10's `waiting-human` resolutions.

func (*Engine) ResumeSaga

func (e *Engine) ResumeSaga(conn *sql.DB, stepID int, nowMS int64) error

ResumeSaga advances a step's saga from its recorded resume point, to completion.

RESUME IS LAZY AND IDEMPOTENT. Each stage's transaction is CAS-guarded on the expected `saga_stage`, so two concurrent engine invocations resuming the same saga produce exactly one advance; the loser matches zero rows, re-reads, and either finds the work done or advances what is now current.

func (*Engine) RunActionStep

func (e *Engine) RunActionStep(conn *sql.DB, stepID int, nowMS int64) error

RunActionStep enters an action step's saga ENGINE-SIDE — no claim, no token, no worker artifact (§6.15 as amended).

It is the other half of `claim` refusing an action step. Action steps are the engine's deterministic half (AC-2): the saga computes the builtin from the step's declared `inputs`, records the artifact the computation produced, and routes. Nothing a worker could supply belongs in that sequence, which is why there is no stage-0 equivalent here rather than a stage 0 with its checks relaxed — a token to authorize, a body to size-cap, and a payload to validate are all facts about a HOLDER, and this step has none.

The stage-1 commit is therefore just the two rows the routing stage needs to find: the status, and the resume point. The ARTIFACT IS NOT WRITTEN HERE — runRoutingStage writes the action's own, and writing an empty one first would leave a crashed action indistinguishable from one that computed nothing.

Idempotent by the same CAS every stage uses: a step already in the saga skips straight to the resume, so two concurrent `next` invocations produce one run.

func (*Engine) VerifyDispatch

func (e *Engine) VerifyDispatch(
	conn *sql.DB, runID int, nowMS int64,
) (*VerifyResult, *RowMismatch, error)

VerifyDispatch recomputes the ready set and compares each stored manifest row against ITS OWN current rendering, BYTE FOR BYTE (§5.3) — with `Stage` excluded from that comparison (DKT-19, below).

P11 IS THE LOAD-BEARING PROPERTY AND IT IS A NEGATIVE ONE: this is a READ VERB AND IT WRITES NOTHING — INCLUDING NO REAP. It is the one scheduling-shaped verb that must not reap, because reaping would change the very ready set it was asked to compare against, and a verify that mutated its own subject could never fail. TestVerifyDoesNotReap asserts the step row still carries its stale owner afterward.

The transaction is therefore read-only by construction: it is rolled back unconditionally, never committed. That is stronger than "the code has no INSERT", because it also covers anything a helper might write.

DKT-10: a stored row whose step has since reached a TERMINAL status (§5.6's `db.StepTerminal`) is skipped rather than compared. `step record`/`step fail` retiring a step — or the step reaching `skipped`/`superseded` any other way — is exactly what a dispatch promises will keep happening while it is open (§5.1, P28 — a manifest is not a lock), so that step's absence from the recomputed ready set is the expected effect of the batch working, not a conflict shaped like one. The predicate is `db.StepTerminal`, the SAME one `missingUsage` reads to ask a related question (§5.8) — the classification itself is new to `verify`, not a line `discrepanciesTx`/`CloseDispatch` already drew: their split is by DISCREPANCY KIND, not terminal-vs-not, and close_reason "reconciled" means only "no discrepancy blocked", which is a different fact than the one this clause checks. A stored row is looked up by the STEP IT NAMES, never by position, so a step that recorded does not shift every row after it out of alignment: readiness among the other rows is judged unlimited (no `limit`) so a lower-ranked still-ready row is not mistaken for missing merely because new work outranks it now.

DKT-19: `Stage` (stage.go) is excluded from the comparison because it is a SET-RELATIVE ordering hint, not a fact about the row — its value depends on which OTHER rows share the current ready set, and that set legitimately shrinks across a dispatch's lifetime as siblings retire (or was truncated differently at open time by `--limit`). Comparing it verbatim reported a conflict on two kinds of untouched manifest: one opened with `--limit N` smaller than the unlimited set this function recomputes (a stored row's stage-0 rendering never saw the predecessor that stage depends on), and one whose stage-0 tree-holder had itself already retired terminal (its surviving stage-1 rows recompute unstaged, because their one predecessor is gone). Neither is a conflict — the set membership driving `Stage` changed for a reason `verify` already accounts for elsewhere in this function, and core makes no claim that `Stage` is enforced or stable (stage.go: "Core enforces nothing … that is the deliberate limit of a scheduling hint").

type Error

type Error struct {
	Code    ErrorCode
	Message string
	// Err is the underlying cause when there is one, so errors.Is still
	// reaches a db sentinel through this wrapper.
	Err error
}

Error is an activation failure carrying its taxonomy code.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

type ErrorCode

type ErrorCode string

ErrorCode classifies an activation failure so the CLI can map it onto the error taxonomy (TDD §5.5) without re-deriving the mapping from the message.

const (
	// CodeValidation is VALIDATION_ERROR (exit 3): a binding that resolves to
	// zero or several workflows, a work-DAG cycle, a run with no issues, or a
	// context bundle over the configured cap.
	CodeValidation ErrorCode = "VALIDATION_ERROR"
	// CodeNotFound is NOT_FOUND (exit 2): a run that does not exist, or a
	// `--pin` path that is missing or is not a regular file.
	CodeNotFound ErrorCode = "NOT_FOUND"
	// CodeConflict is CONFLICT (exit 4): re-activating a terminal run (RA5).
	CodeConflict ErrorCode = "CONFLICT"
	// CodeGone is GONE: an `events list --since` cursor below the retained
	// minimum (docs/tdd/runs-dispatch.md §8.6). No product code path reaches it
	// at this stage — nothing prunes until S7 — and the SHAPE ships here because
	// `--since` is the verb that must return it.
	CodeGone ErrorCode = "GONE"
)

func CodeOf

func CodeOf(err error) (ErrorCode, bool)

CodeOf reports the taxonomy code of an error raised by this package, and whether it was one.

type Event

type Event struct {
	Seq  int64  `json:"seq"`
	AtMS int64  `json:"at_ms"`
	Kind string `json:"kind"`
	// Run is `RUN-N`, OMITTED WHEN NULL per the `?` in §11.4's shape. A trust
	// event has no run (gates-trust §3.6) and attributing it to one would be a
	// fabrication.
	Run string `json:"run,omitempty"`
	// Step is the RENDERED INSTANCE IDENTITY (`name@k#i`) — matching every other
	// wire shape's step rendering — and omitted when NULL (E1).
	Step string `json:"step,omitempty"`
	// StepID is `STEP-N`: the joinable id (E4, the filed addition).
	StepID string `json:"step_id,omitempty"`
	// Issue is `DKT-N`, omitted when the event has none (DKT-74). Instance
	// labels COLLIDE across issues in one run — two issues on the same
	// workflow both have a `fix@1` — and a feed filtered by instance once
	// misattributed a pass-route to a reaped step over exactly that collision.
	// The column was always stored; the wire now carries it.
	Issue string `json:"issue,omitempty"`
	// Project is the NAME of the project the event belongs to — the run's, else
	// the issue's — and is empty for a store-level event (DKT-67).
	//
	// It exists because `--all-projects` had no project discriminator at all
	// once the prefix stopped being one: two projects can hold a `fix@1` step
	// and a `RUN-6`, and the ids alone do not say whose. Omitted when empty so a
	// single-project feed carries no redundant column.
	Project string `json:"project,omitempty"`
	// Data is the stored JSON object, VERBATIM. Core never reshapes it (E2);
	// §7.6's writer already normalized it to an object on the way in.
	//
	// It is json.RawMessage rather than a map so the bytes reach `--json`
	// unaltered: re-marshaling a decoded map would reorder keys and re-escape
	// strings, which for a consumer diffing two feeds is a change with no cause.
	Data json.RawMessage `json:"data"`
}

Event is §11.4's shape, field for field:

event { seq, at_ms, kind, run?, step?, data }

plus `step_id`, which is an ADDITION to that shape and is FILED as an amendment (§8.2 E4). The argument is that the shape is otherwise unjoinable: `step` and `data.instance` both give the HUMAN identity (`name@k#i`), and a consumer following the feed into `step show` needs the id it addresses steps by. Shipping the field and recording the note follows precedent — the spec's silence would otherwise be resolved silently by whoever implemented first.

type EventPage

type EventPage struct {
	Events []Event
	// Total counts MATCHING events before the slice, so `truncated` is
	// computable rather than guessed (reliability-delta §4.2).
	Total int
}

EventPage is one `--since` answer, with the truncation contract every list verb carries (E9, E11).

func ListEvents

func ListEvents(conn *sql.DB, q EventQuery) (*EventPage, error)

ListEvents serves `events list --since` (§8.3).

C9 — THE CURSOR RACE — is closed structurally rather than by a lock: the read is ONE `SELECT ... WHERE seq > ?` in ONE transaction, and `seq` is `AUTOINCREMENT` and monotonic, so an event inserted while this runs lands ABOVE the cursor and is returned by the NEXT call. No event is ever skipped and none is ever returned twice. TestCursorNeverSkipsUnderConcurrentInsert runs inserts against a looping reader and asserts the union is exactly the inserted set — the property stated as a test rather than as a comment.

The ordering is `seq ASC`, ALWAYS. There is no reverse mode (E7): a cursor feed that could run backwards is a cursor feed that skips.

type EventQuery

type EventQuery struct {
	// Since returns events with `seq > Since` — STRICTLY GREATER (E5), so a
	// consumer stores the last seq it saw and passes it back without re-reading
	// it. Zero is the default and returns from the beginning (E6).
	Since int64
	// RunID filters to one run using `idx_events_run_seq`. Zero is the
	// repo-wide feed, which is the only place a trust event is visible (E8).
	RunID int
	// Limit applies AFTER ordering (E9). Zero means the caller's default.
	Limit int
	// Tail selects the NEWEST N matching events instead of the oldest N.
	// Zero is off.
	//
	// This is a SELECTION change, not an ordering one. The returned page is
	// still `seq ASC` — the newest N rows, handed back oldest-first — so a
	// reader consumes them in the same direction as every other answer and the
	// last seq is still the cursor to store. E7's "no reverse mode" stands:
	// what varies is WHICH window of the feed is returned, never its direction.
	//
	// Tail is for the mid-incident question ("what just happened"), which the
	// cursor cannot answer without first paging through the entire history to
	// reach the end.
	Tail int
	// ProjectID scopes the feed to one project (v12); 0 is the whole store.
	//
	// An event's project is its RUN's when it has one, else its ISSUE's, else
	// the REPOSITORY ITS PAYLOAD NAMES (DKT-68) — `repo` for a trust change,
	// `identity` for a registration. A store-level event that names no
	// repository at all is a fact about the store and appears in every scoped
	// view, because a scoped feed that hid it would be an audit trail with a
	// blind spot; one that names another repository belongs to that
	// repository's trail, not to this one's. See eventFilter.
	ProjectID int
}

EventQuery is `events list`'s filters.

type ExecActionRunner

type ExecActionRunner struct {
	// RepoRoot is the working-tree root — the cwd every action runs in and the
	// containment boundary (gates-trust §5.2.1 R2).
	RepoRoot string
	// Identity is the project identity trust entries bind to (§3.4 P1). It
	// equals RepoRoot for env/local stores; under the global store it is the
	// worktree-stable project path.
	Identity string
	// LockPath is the tree lockfile serializing this project's tree-declaring
	// commands (§7.4 / A7).
	LockPath string
	// LoadStore reads the trust store. It is a FIELD so a test can supply a
	// sandbox store without the package growing a path-taking constructor —
	// gates-trust §9.5 SB3 forbids one, because every additional way to point
	// docket at a trust file is another way for repo content to point it
	// somewhere.
	LoadStore func() (*trust.Store, error)
}

ExecActionRunner is the ActionRunner that actually computes and, for a non-builtin, actually spawns.

func NewActionRunner

func NewActionRunner(paths RepoPaths) *ExecActionRunner

NewActionRunner builds the real runner. THIS IS THE CONSTRUCTOR SWAP engine-spine §6.13 promised, and §6.4 records honestly what else moved.

func (*ExecActionRunner) Run

Run computes one action step.

B1: RESOLUTION IS BUILTIN FIRST. An action core computes is never looked up in the trust store, so a trust entry cannot shadow a builtin and a builtin cannot be disabled by removing one.

type ExecRunner

type ExecRunner struct {
	// RepoRoot is the working-tree root — the cwd every gate runs in (§5.1)
	// and the containment boundary (§5.2.1 R2).
	RepoRoot string
	// Identity is the project identity trust entries bind to (§3.4 P1). It
	// equals RepoRoot for env/local stores; under the global store it is the
	// worktree-stable project path, which is what lets two checkouts of one
	// repository match the same entries.
	Identity string
	// LockPath is the tree lockfile serializing this project's tree gates
	// (§7.4).
	LockPath string
	// LoadStore reads the trust store. It is a FIELD so a test can supply a
	// sandbox store without the package growing a path-taking constructor —
	// §9.5 SB3 forbids one, because every additional way to point docket at a
	// trust file is another way for repo content to point it somewhere.
	LoadStore func() (*trust.Store, error)
	// NowMS stamps results. Injected so a test is not at the mercy of a clock.
	NowMS func() int64
}

ExecRunner is the GateRunner that actually executes.

func NewExecRunner

func NewExecRunner(paths RepoPaths) *ExecRunner

NewExecRunner builds the real runner. THIS IS THE CONSTRUCTOR SWAP §7.1 promised: NewEngine names it instead of PassThroughRunner, and the saga does not change around it.

func (*ExecRunner) Execute

Execute is Run's full-fidelity form: it returns every row to record rather than collapsing to one.

func (*ExecRunner) Run

Run executes one gate: match, then spawn only what matched.

The ORDER is the security property. Matching happens against an immutable store snapshot read ONCE (M1), and the matched entry's OWN ARGV is what executes — matching does not produce a permission that is later applied to an argv read from somewhere else, so there is no TOCTOU window (T4).

func (*ExecRunner) TrustRunner

func (r *ExecRunner) TrustRunner() *ExecRunner

TrustRunner reports the runner itself, so *ExecRunner satisfies the interface its own wrappers use.

type FenceReport

type FenceReport struct {
	Issue string `json:"issue"`
	Gate  string `json:"gate"`
	Tag   string `json:"tag"`
	// Ordinal is the command's position in body order, which is the order an
	// operator read it in and the order it will run in.
	Ordinal int `json:"ordinal"`
	// Command is the RAW STORED BYTES (§5.7 E4): JSON escaping is
	// encoding/json's job and the consumer is a program. Human-mode rendering
	// escapes at the print boundary instead, so the stored bytes stay exactly
	// what was harvested and hashed.
	Command string `json:"command"`
	Matched bool   `json:"matched"`
	// Entry names the trust entry that authorized it, when one did.
	Entry  string `json:"entry,omitempty"`
	Reason string `json:"reason,omitempty"`
}

FenceReport is one harvested command and its trust status (§7.7 S1/S2).

func BuildFenceReport

func BuildFenceReport(
	conn *sql.DB, runID int, loadStore func() (*trust.Store, error), identityPath string,
) ([]FenceReport, error)

BuildFenceReport resolves every harvested fence command against the trust store, for the run's bound issues.

The trust store is read ONCE for the whole report, the same snapshot discipline §7.2 M1 applies at the gate: a report assembled from several reads could disagree with itself mid-render.

type GateExecution

type GateExecution struct {
	// Results are the rows to record, in execution order.
	Results []GateResultRow
	// Verdict is the gate's routing verdict, conjunctive over Results.
	Verdict string
}

GateExecution is what one gate produced: one or more attempt records, since a fence gate matches PER LINE (§7.3 step 4) and a flaky command records each attempt individually (§5.6 F3).

type GatePreflight

type GatePreflight struct {
	// Gate is the gate name as the workflow declares it.
	Gate string `json:"gate"`
	// Workflows names every bound workflow declaring this gate, so an operator
	// adding one entry knows what it unblocks. Sorted, so two activations of
	// the same run render identically.
	Workflows []string `json:"workflows"`
	// Matched reports that a trust entry of this name resolves for this repo.
	Matched bool `json:"matched"`
	// Entry names the resolving entry, when one did.
	Entry string `json:"entry,omitempty"`
	// Stub reports that the resolving entry declared itself a placeholder
	// (DKT-265). A gate that WILL run and will measure nothing is a different
	// answer from one that will not run, and both are different from a real
	// check — an operator reading a green preflight should not have to open the
	// trust store to learn which they have.
	Stub bool `json:"stub,omitempty"`
	// Reason explains an unmatched gate, verbatim from the matcher, so the
	// preflight and the mid-run diagnostic say the same thing.
	Reason string `json:"reason,omitempty"`
}

GatePreflight is one declared gate and whether this machine can run it.

func BuildGatePreflight

func BuildGatePreflight(
	defs map[int]*workflow.Definition,
	loadStore func() (*trust.Store, error), identityPath string,
) ([]GatePreflight, error)

BuildGatePreflight resolves every gate the run's bound workflows declare against the trust store.

The store is read ONCE for the whole report, the same snapshot discipline §7.2 M1 applies at the gate and the fence report applies to itself: a report assembled from several reads could disagree with itself mid-render.

A gate is looked up BY NAME WITH A NIL ARGV, which is exactly what gate_exec.go's pre-match does — so a gate this reports as matched is a gate whose name resolves for this repo. It deliberately does not promise more: the argv check happens at spawn against the entry's own argv, and a preflight that pretended to settle it would tell an operator a gate is ready when it is not, which is worse than saying nothing.

type GateResult

type GateResult struct {
	Gate       string   `json:"gate"`
	Argv       []string `json:"argv"`
	Exit       int      `json:"exit"`
	DurationMS int64    `json:"duration_ms"`
	Output     string   `json:"output"`
	Truncated  bool     `json:"truncated"`
	Verdict    string   `json:"verdict"`
	Stub       bool     `json:"stub,omitempty"`
}

GateResult is one gate's outcome, in §11.4's `gate result` shape.

`Stub` is the field that makes the S3->S4 window safe. Every result this stage records carries `stub: true`, so an operator inspecting a run can tell a stubbed gate from a real one. A silent pass-through that looked identical to a real pass would be a trap for exactly the window where gates are specified but not yet executed — a green run would read as gate coverage it does not have.

type GateResultRow

type GateResultRow struct {
	Gate       string
	Ordinal    int
	Argv       []string
	Exit       *int
	DurationMS int64
	Output     string
	Truncated  bool
	Verdict    string
	Reason     string
	// TrustEntry names the entry that authorized this, for the audit record.
	TrustEntry string
	// ArgvSHA256 is the canonical hash of the candidate argv, for trust_cache.
	ArgvSHA256 string
	// Prefix records that a prefix entry authorized it.
	Prefix bool
	// Pre marks a pre-gate result (§7.6): an input to the step rather than a
	// judgment of it, and excluded from the saga's verdict by PG4.
	Pre bool
	// Stub is 1 ONLY for results the S3 pass-through produced. NOTHING THIS
	// RUNNER PRODUCES SETS IT (T11, N4) — the field survives so a migrated S3
	// row stays distinguishable forever, and so the fakes tests build on the
	// old seam keep recording honestly.
	Stub bool
	// StubEntry carries the matched entry's own `stub` declaration (DKT-265):
	// the command that ran was a placeholder, not the check its name implies.
	//
	// It is set on EVERY row that names a TrustEntry, including the ones whose
	// verdict is unmatched, skipped, or fail. The field describes the ENTRY,
	// not the outcome, and a reader diffing a gate's rows should not have to
	// wonder whether its absence on a failing row means "not a stub" or "we
	// only bother recording this when it passes".
	//
	// Distinct from Stub above, which is about which era of this codebase
	// produced the row. A row can be either, both, or neither.
	StubEntry bool
}

GateResultRow is one result this runner produced, in §11.4's shape plus the `reason` amendment (A6) and the `pre` marker.

Argv and Exit are POINTERS because an unmatched gate never ran: NULL is the honest encoding of "no process existed", and a zero exit on a gate that did not execute is the exact confusion T11 exists to prevent.

type GateRunner

type GateRunner interface {
	Run(ctx context.Context, g GateSpec, sc StepContext) (GateResult, error)
}

GateRunner executes one gate and returns its result. S3 ships PassThroughRunner; S4 ships the real one. The saga is written against this interface, so S4 changes one constructor call and nothing else.

type GateSpec

type GateSpec struct {
	// Name is the trusted gate name.
	Name string
	// Source is `fence:<tag>` when the gate's commands come from an issue
	// body's fenced block, or "" for a trusted gate resolved from the trust
	// file. Core reads the tag; what the commands mean is never its business.
	Source string
	// Pre marks a gate that runs at claim, with its results included in the
	// context bundle, rather than in order inside `complete` (§11.1).
	Pre bool
	// Commands are the harvested fence lines, verbatim, for a `fence:` gate.
	// They are carried rather than re-read so what runs is what was hashed at
	// activation — a post-activation edit cannot inject (engine-spec §4).
	Commands []string
	// CommandHashes are the SHA-256s activation stored alongside each command,
	// positionally aligned with Commands.
	//
	// S4 re-verifies each one against its stored command BEFORE spawning
	// (gates-trust §7.3 step 3). S3's snapshot already closes "the issue body
	// cannot inject"; this closes the narrower "the stored row cannot be
	// swapped". A direct database write is outside §2's trust boundary, but it
	// costs one hash to detect and a mismatch is refused rather than run.
	CommandHashes []string
}

GateSpec is one gate to run, normalized from §11.1's two spellings.

type GuardVerdict

type GuardVerdict struct {
	// Allowed is exit 0 when true, exit 2 when false.
	Allowed bool
	// Reason explains a denial. It goes to stderr in human mode and into the
	// JSON envelope's `error` under --json.
	Reason string
}

GuardVerdict is one guard's answer.

func GuardGate

func GuardGate(conn *sql.DB, stepName string, projectID int) (*GuardVerdict, error)

GuardGate answers `docket guard gate --step NAME`: does a PASSED gate step of that name exist for the active run?

"Passed" is `done` with a `pass` routing — the state `step approve` produces on a human gate, and the state a tallied approval produces on a vote gate. A step that reached `done` by any other route did not receive a decision, and a guard that accepted it would let an override stand in for a decision nobody made.

BOTH GATE KINDS ANSWER. The filter was `type="human"` alone, which meant converting a gate from `human` to `vote` — the same question, asked of several voters instead of one — silently stopped matching, and every hook checking that gate started denying with "no such step" while the gate itself sat approved. That is the strictness comment's own concern inverted: the decision WAS made, by the machinery §8 exists to run, and the guard was the only thing that could not see it. A tallied pass IS a decision, so it counts; nothing else about the test loosened, and a vote still open reads `pending` here and denies exactly as an unapproved human gate does.

projectID scopes the search to one project's runs; 0 answers over every project (see GuardStop). An approval is a decision about ONE project's gate, so a same-named gate in another project must not answer for it.

func GuardRecord

func GuardRecord(conn *sql.DB, runID, projectID int, nowMS int64) (*GuardVerdict, error)

GuardRecord answers `docket guard record`: is there no unreconciled dispatch?

G2: "unreconciled" is an open dispatch OR any discrepancy (§5.8) — THE SAME TWO PROBES `next` USES (P24/P25), COMPUTED BY THE SAME FUNCTION. That identity is the point rather than an economy: a guard and the scheduler disagreeing about whether a run is reconciled would let a harness record into a picture `next` had already refused to extend.

WHY `record` IS THE VERB A HARNESS WIRES BEFORE LETTING A WORKER CALL `step complete`: an unreconciled batch means the engine's picture of what is running is already wrong. Recording an artifact into that picture is how drift becomes durable.

G4: `--run` is OPTIONAL. Without it the guard answers over every non-terminal run, denying if ANY is unreconciled — matching `guard stop`'s existing all-active-runs shape, so a hook wired once keeps working as runs come and go.

projectID scopes the no-`--run` enumeration to one project; 0 answers over every project (see GuardStop). An EXPLICIT `--run` is exempt from scoping: naming a run is naming intent, and refusing a cross-project reference a hook spelled out would trade one surprise for another.

func GuardStop

func GuardStop(conn *sql.DB, projectID int, nowMS int64) (*GuardVerdict, error)

GuardStop answers `docket guard stop`: is the machine done working?

The base rule (§6.12): a step in `claimed`/`running`/`gated`/`pending` for an active AND DISPATCHED run blocks a stop. Four refinements narrow it to steps a stop would actually interfere with:

  • A run NOTHING HAS EVER HAPPENED TO does not block at all (DKT-71): never dispatched, and no step ever out of `pending`. Nothing was handed to anything, so there is nothing in flight for a stop to interrupt. See the probe in GuardStop.

  • A `waiting-human` step does NOT block — a run parked on a person is waiting for something a stop cannot interfere with.

  • A VOTE step whose proposal is OPEN does not block (DKT-107): its panel decides out-of-session, and yielding the turn is exactly how a session waits for it — the deny was a toll paid at every turn-end for as long as a panel deliberated. The exemption ends with the proposal: a decided proposal leaves a dispatchable step, which blocks again until `next` routes it. The same reading covers a `gated` routing step whose every unresolved held cluster is such a vote.

  • A `pending` step waiting only on its predecessors does not block on its own: whatever it waits on either blocks in its place or is exempt for a reason that covers the whole chain. Any other unreadiness — headroom, a paused run, a budget stop, an unacknowledged reap — still blocks, since those name work or acknowledgment the session owes before stopping. (A held cluster awaiting ONE OPERATOR still denies: the materialized human step is pending and ready, per H11's decided semantics.)

projectID scopes the question to one project's runs; 0 answers over every project — the same contract as RunListOptions.ProjectID. Scoping exists because the shared per-user store made "ANY active run" mean "any run on the MACHINE": a Stop hook firing in one repository was denied over another repository's run, which the hook could not distinguish from its own work (observed 2026-08-11, RUN-2). A stop only interferes with the invoking project's runs, so that is the default question; --all-projects asks the old one.

type HeldClusterLink struct {
	// Cluster is the element's POSITION in the routing step's payload array,
	// zero-based — the number the instance's `#N` suffix carries. Named
	// `cluster_index`, not `cluster`, precisely because it is not an id.
	Cluster int `json:"cluster_index"`
	// Clusters is how many the payload holds, which is what makes the index
	// legible: `#3` alone says nothing about whether that is the last one.
	Clusters int `json:"cluster_count"`
	// Artifact is the ARTIFACT-N the cluster's payload lives in — the answer
	// `step artifacts` on the held row cannot give, because the held row
	// produced nothing.
	Artifact string `json:"artifact"`
	// ProducerStep is the routing step instance that recorded it.
	ProducerStep string `json:"producer_step"`
	// contains filtered or unexported fields
}

HeldClusterLink is a materialized held step's provenance: WHICH cluster it decides, out of how many, and WHERE that cluster's payload lives.

It exists because a held row named none of it (DKT-239). `step artifacts` on a `reconcile-held@0#N` row answered "produced no artifacts" — true, and useless: the payload sits on the SYNTHESIZE step's artifact, not the hold's. `step show --json` named no cluster and no source artifact. And the `#N` suffix is the element's POSITION IN THE PAYLOAD, which reads as a cluster id and is not one. A conductor needed 8 calls, 3 tracebacks, and a reading of the aggregate's step-recorded event to find and disambiguate one payload.

The payload itself is deliberately NOT on the wire here — `step show` is a status check, and inlining a held cluster's body would make it a document dump, the same rule the report's artifact index follows. The artifact id is one `step context` away for anyone who wants the body.

type HoldPolicy

type HoldPolicy struct {
	// Rule is the configured `vote.hold.rule`, empty when unset.
	Rule string `json:"rule,omitempty"`
	// Voters is the configured `vote.hold.voters`, empty when unset.
	Voters []string `json:"voters,omitempty"`
	// Panel reports the effective answer: true when a hold this run mints will
	// be decided by a tally, false when it goes to one operator.
	//
	// It is a DERIVED field rather than something a reader recomputes from the
	// two above, because the derivation is the thing that was invisible: both
	// keys must be non-empty, and a half-configured pair silently means "one
	// operator" — which is the state an operator who set only one of them would
	// least expect and least easily notice.
	Panel bool `json:"panel"`
}

HoldPolicy is who will answer a hold this run mints (DKT-266).

A hold is the one step in a run no author declared — the engine mints it when a `hold_spread` trips — so who answers it is not visible anywhere a workflow author or a run's operator normally looks. It is `vote.hold.rule` plus `vote.hold.voters` in engine config, and if BOTH are set the hold is minted as a vote step; otherwise it is minted for one operator.

The audit that produced this issue found 52 step-held events, 50 resolved by an operator directly, and ZERO hold-panel votes, against config that reads `set` in both projects. The engine was doing exactly what it was told — the config postdates those runs, and TestHeldStepMintsVoteWhenConfigured has always proven the path. What was missing is that NOTHING SAYS WHICH POLICY IS IN FORCE, so a configured surface that is inert and one that is broken look identical, and answering "which is it" took a source audit.

func LoadHoldPolicy

func LoadHoldPolicy(conn *sql.DB, runID int) (HoldPolicy, error)

LoadHoldPolicy reads the run's project's hold-decision policy.

type IssueDisposition

type IssueDisposition struct {
	Issue string `json:"issue"`
	// Disposition is the ruling. One value today — the constant above — and a
	// field rather than a bare presence flag, so a second issue-level ruling
	// can land here instead of forcing a second section.
	Disposition string `json:"disposition"`
	// By is the step instance whose routing abandoned the issue, or empty when
	// an operator abandoned it from OUTSIDE the graph with `run abandon
	// --issue` — where no step decided anything and naming one would be a
	// fabrication.
	By string `json:"by,omitempty"`
	// Reason is the recorded ruling, VERBATIM and unabridged. Both abandon
	// paths capture it and neither published it: `run abandon --issue` puts it
	// in the event payload, the routing path in the deciding step's own
	// routing note. A renderer may show a head; this carries the whole thing.
	Reason string `json:"reason,omitempty"`
}

IssueDisposition is one issue's terminal ruling within this run (DKT-403).

It is a statement about THE RUN'S work on the issue, not about the issue: the `abandon-issue` routing deliberately does not force the issue itself to a terminal status (see abandonIssue), so this says "this run stopped" and the tracker says what became of the issue afterwards.

type LoopEntry

type LoopEntry struct {
	Ordinal int    `json:"ordinal"`
	Routing string `json:"routing"`
}

LoopEntry records which loop iteration a step belongs to.

type LoopOutcome

type LoopOutcome struct {
	// Entered reports whether the loop was actually entered. False means the
	// bound was hit and the routing became `waiting-human` (clause 1).
	Entered bool
	// Ordinal is the new loop ordinal when Entered, i.e. the issue's new
	// loop_count. Instances created by this entry carry it.
	Ordinal int
	// Routing is the routing that ACTUALLY applies after the bound is
	// considered: `fix-loop` on entry, `waiting-human` when bounded.
	Routing string
	// Reason explains a bound, for the operator resolving the parked step.
	Reason string
	// Superseded lists the instances the sweep terminated (clause 2).
	Superseded []string
	// Instantiated lists the instances this entry created — the loop bodies of
	// clause 3 and the re-instantiated chain of clause 4.
	Instantiated []string
}

LoopOutcome is what a `fix-loop` routing resolved to.

It exists because the routing transaction must know whether the loop was ENTERED or BOUNDED before it writes the step's status: an entry routes `fix-loop` and the step is `done`, a bound routes `waiting-human` and the step parks. Returning a struct rather than mutating through a pointer keeps that decision inspectable in a test.

func EnterLoop

func EnterLoop(
	tx *sql.Tx, step *db.Step, def *workflow.Definition, nowMS int64,
) (*LoopOutcome, error)

EnterLoop performs a `fix-loop` routing, inside the caller's routing transaction.

It is called ONLY when a routing resolved to `fix-loop` on a step whose lineage is live (see StaleLineage). The whole of §11.3 happens here, in one transaction with the step update that triggered it, because a partial loop entry — a counter raised with no bodies instantiated, or bodies instantiated twice — is not a state any later pass can repair without guessing. The routing STEP's own spec is deliberately not a parameter: nothing about a loop entry depends on which step routed. The counter is the issue's, the bound is the workflow's, the sweep set is `after_loop`'s downstream, and the instantiation is the definition's — so a `fix-loop` from `verify` and one from `reconcile` must produce identical effects. Taking the spec would invite a future reader to make one of those depend on it.

func EnterLoopAuthorized

func EnterLoopAuthorized(
	tx *sql.Tx, step *db.Step, def *workflow.Definition, nowMS int64,
) (*LoopOutcome, error)

EnterLoopAuthorized is EnterLoop under an EXPLICIT operator authorization — `step resolve --as fix-round`, which has just recorded a grant.

The difference is the non-convergence refusal, and only that: an operator who has read the park and asked for the round anyway has answered the question the park asked. Making them unable to is how a park that "names the way out" becomes a park with no way out — DKT-237's exact failure, reproduced by the guard that was meant to prevent a different one.

The BOUND is not waived here, because it does not need to be: a grant raises the effective maximum, so the same resolution already gets past it through LoopGrantsTx. Only the convergence check has no such counter to move.

type Manifest

type Manifest struct {
	Dispatch string `json:"dispatch"`
	Run      string `json:"run"`
	// OpenedSeq is the event seq at open time — the manifest's place in the log
	// (P2), and §6's boundary for "reaps this relay has not yet seen".
	OpenedSeq int64 `json:"opened_seq"`
	// ExpiresMS is not in §11.4's shape and is emitted alongside it, because P6
	// and P24 both require the refusal to NAME the expiry: a relay told only
	// that a dispatch is open cannot tell whether waiting is a strategy.
	ExpiresMS int64           `json:"expires_ms"`
	Rows      []model.StepRow `json:"rows"`
	// StaleTargets rides BESIDE the §11.4 shape for ExpiresMS's reason: a
	// conductor about to spend review budget on these rows needs the staleness
	// named (DKT-193). Never a row field — rows are hashed at open and
	// byte-compared at verify, and a live-derived fact would either freeze an
	// open-time answer into row_json or need normalizing away.
	StaleTargets []StaleTarget `json:"stale_targets,omitempty"`
	// BudgetHeld names the ready rows this manifest withheld for lack of
	// budget headroom, with the numbers (DKT-242). It rides BESIDE the §11.4
	// shape for StaleTargets' reason, and is `omitempty` for the same reason
	// `next`'s equivalent is empty by default: a run with no cap says nothing
	// new.
	//
	// Measured cost of its absence: with 0.9 headroom the engine offered 1 of
	// 5 ready judges and named no reason, and the round serialized around an
	// invisible wall for ~10 minutes and an extra wave cycle. `run budget`
	// already states the numbers honestly; the verb that DECIDES on them did
	// not.
	BudgetHeld string `json:"budget_held,omitempty"`
	// PinDrift is the run's unsound pins, when any (DKT-408) — the same
	// advisory channel StaleTargets rides, for the same reader at the same
	// moment: the conductor deciding whether to spend a wave on these rows.
	// Harness RUN-14 dispatched 3.5M tokens against a drifted pin set and
	// every step reading the drifted contract refused at render; the refusals
	// were correct and the spend was not. Advisory rather than a refusal
	// because drift blocks only the steps that READ a drifted ref — the
	// per-step CONFLICT at claim/render stays the enforcement — but the
	// conductor must get to decide before the wave, not discover it from
	// exit codes inside one.
	PinDrift []PinVerdict `json:"pin_drift,omitempty"`
}

Manifest is §11.4's `dispatch` wire shape: `{dispatch, run, opened_seq, rows}`.

type OrderResolver

type OrderResolver interface {
	// Position reports a value's index in the field's declared order, and
	// whether the field is ordered AND the value is in that order (§4.3 I4).
	Position(field, value string) (int, bool)
	// Ordered reports whether the field declares an order at all.
	Ordered(field string) bool
	// FieldType is the field's declared JSON `type`, or "" when the field
	// declares none or is not declared at all. Both fall back to S3's string
	// comparison, which is correct: a schema that constrains nothing has said
	// the value may be anything.
	FieldType(field string) string
	// Conservative is the field's declared `conservative_end` — which END of
	// the declared order the author considers the cautious one — or "" when
	// none was declared.
	//
	// It reports a DIRECTION, never a rank and never a value, so it cannot
	// become the second ordering API Position exists to be the only one of. Its
	// single reader is the aggregate's even-count median tie-break (DKT-267):
	// core still does not know which end of an order is bad, it asks the order.
	Conservative(field string) string
}

OrderResolver reports what the step's PINNED payload schema declares about a field (§5, §4.3).

It is deliberately narrow. Position is the only ordering API — there is no Less(a, b) a caller could invoke on a field with no declared order — so the type makes "compare two values of a field core has no order for" unrepresentable rather than merely discouraged. Ordered and FieldType exist for the two things the evaluator must SAY rather than guess: which of T3's three cases it hit, and whether a literal is compared as a string, a number, or a boolean.

A nil OrderResolver is the step-declares-no-`payload` case, and it is the exact S3 behavior — which is what makes the survival suite a re-run of the S3 table with nothing passed in.

type PacketFile

type PacketFile struct {
	Path   string `json:"path"`
	SHA256 string `json:"sha256"`
	Body   string `json:"body"`
}

PacketFile is one resolved file: where it came from, what it hashed to, and its bytes.

The path and hash ride along with the body so provenance survives INTO the rendered packet. A worker reading a composed document should be able to say which file a passage came from, and an operator reproducing a run should be able to check it.

type PassThroughRunner

type PassThroughRunner struct{}

PassThroughRunner is the S3 implementation: it returns a passing, stubbed result WITHOUT TOUCHING THE PROCESS TABLE.

Two consequences are asserted as tests, and stated here so neither is mistaken for an oversight: (a) a workflow whose gate WOULD fail still passes at this stage, and the QA section says so in a comment so nobody reads a green run as gate coverage; (b) `stub: true` appears in every result this stage records.

func (PassThroughRunner) Run

Run returns `{verdict: "pass", exit: 0, stub: true}`. It never spawns anything: §6's "No subprocess ever executes inside a transaction" holds trivially here, and it will still hold at S4 because the saga's gate stage is its own transaction-free stage.

type PinReport

type PinReport struct {
	Run string `json:"run"`
	// Pins is every pin the run holds, in a total order (kind, then ref), so
	// two checks of one unchanged run produce identical output.
	Pins []PinVerdict `json:"pins"`
	// Changed and Missing are the counts a caller branches on without
	// re-walking Pins.
	Changed int `json:"changed"`
	Missing int `json:"missing"`
}

PinReport is `docket run verify-pins`.

func VerifyPins

func VerifyPins(conn *sql.DB, runID int) (*PinReport, error)

VerifyPins checks a run's WHOLE pin set against what those refs resolve to now (DKT-297).

It exists because no verb answered "is this run's pin state sound". The verbs that check pins each check ONLY the pins they themselves read — `step render` verifies the template and its own step's packet files, `pinnedSchema` the one schema a step declares — which is right for them and made `step render` a misleading answer to the whole-run question. Harness RUN-14 had `contracts/synthesize-findings.md` drift at 21:20Z; `step render` returned exit 0 and a full packet for two steps that did not read that file, and an hour later every `synthesize` step in the run was unclaimable for exactly that reason. The conductor diagnosed it by comparing `run status --json` against `shasum` by hand, which is the ten-line check every seat was rewriting.

IT IS A READ, AND IT IS THE ONLY PLACE THE WHOLE SET IS COMPARED. Context assembly must never do this: §6.6 is explicit that assembly "never re-reads a file a pin names", because the hash IS the contract and re-reading would make a bundle depend on the working tree. This verb is the deliberate opposite — it asks about the tree, on purpose, and writes nothing.

func (*PinReport) Sound

func (r *PinReport) Sound() bool

Sound reports whether every pin still matches.

type PinVerdict

type PinVerdict struct {
	Kind   string `json:"kind"`
	Ref    string `json:"ref"`
	Status string `json:"status"`
	// Pinned is the hash the run recorded at activation.
	Pinned string `json:"pinned"`
	// Found is the hash the ref resolves to now — empty when it does not
	// resolve at all.
	Found string `json:"found,omitempty"`
	// Path is where a file pin actually resolved, so an operator restoring a
	// file does not have to guess which config root won.
	Path string `json:"path,omitempty"`
}

PinVerdict is one pin, checked.

func PinDrift

func PinDrift(conn *sql.DB, runID int) ([]PinVerdict, error)

PinDrift returns the unsound subset of a run's pin report — the rows a read surface states when it warns (DKT-408's remedy 2). nil when every pin is sound, so callers can gate rendering on emptiness alone.

It is VerifyPins minus the sound rows rather than its own walk, so the warning a surface prints and the report `verify-pins` exits 4 on can never name different pins.

type PreGateResult

type PreGateResult struct {
	Gate       string   `json:"gate"`
	Argv       []string `json:"argv"`
	Exit       *int     `json:"exit"`
	DurationMS int64    `json:"duration_ms"`
	Output     string   `json:"output"`
	Truncated  bool     `json:"truncated"`
	Verdict    string   `json:"verdict"`
	Reason     string   `json:"reason,omitempty"`
}

PreGateResult is a §11.4-shaped gate result as it rides in the context bundle (§7.6.3, amendment A5).

It is a distinct type from the row because the BUNDLE's shape is a wire contract: `argv` and `exit` are pointers so an unmatched pre-gate serializes them as null rather than as `[]` and `0`, which is the same honesty the table enforces (§4.2).

type ProjectRegistration

type ProjectRegistration struct {
	// ID is the project row that came into being.
	ID int
	// Name is the row's display name.
	Name string
	// Identity is the path the row is keyed by.
	Identity string
	// Cwd is the working directory the invocation ran from.
	Cwd string
	// Verb is the full command path (`step complete`, `issue create`) that
	// triggered the registration.
	Verb string
	// Prefix is the display prefix the row was given.
	Prefix string
}

ProjectRegistration is what a `project-registered` event records (DKT-61): where the invocation ran, what that resolved to, and which verb did it.

Cwd and Identity are SEPARATE fields even though they are usually the same path, because the case worth attributing is exactly the one where they differ: an executor running from a scratchpad directory registered a project under that directory's name, and only the pair says so.

type PruneQuery

type PruneQuery struct {
	// Before deletes events with `seq < Before` — STRICTLY LESS (P2), so
	// `--before N` and a cursor at `N-1` name the same boundary, which is the
	// convention every other seq comparison in this feed already follows.
	Before int64
	// BeforeRun deletes every event of one run, whatever its seq (P3). It is the
	// form operations.md §2 recommends — trim whole runs that reached a terminal
	// status, rather than the oldest N events across all of them, so the report
	// stays honest for every run still covered.
	BeforeRun int
	// RunID narrows Before to one run (P4). Zero is every run.
	RunID int
	// DryRun computes the answer and deletes nothing (P5).
	DryRun bool
	// NowMS is the transaction's clock, used for the retention boundary and for
	// the `events-pruned` event's timestamp. It is passed IN rather than read
	// here so the boundary and the event agree, and so a test can place the
	// window without waiting for wall-clock time to pass.
	NowMS int64
}

PruneQuery is one prune's target and its clock.

EXACTLY ONE OF Before / BeforeRun IS SET (P1). A destructive verb with a default target is how a log gets deleted by a typo, so "prune everything" is not expressible: the caller must name a boundary or a run.

type PruneResult

type PruneResult struct {
	Pruned          int   `json:"pruned"`
	RetainedMinimum int64 `json:"retained_minimum"`
	DryRun          bool  `json:"dry_run,omitempty"`
	// HeldByRetention counts rows the boundary protected (P14). It is reported
	// rather than silently subtracted: an operator who asked to prune 900 events
	// and pruned 200 must be told a policy held back the other 700, or they will
	// conclude the verb is broken.
	HeldByRetention int `json:"held_by_retention,omitempty"`
	// LiveRuns names the non-terminal runs that blocked the prune, for the
	// refusal message (P9).
	LiveRuns []string `json:"-"`
}

PruneResult is what a prune did, or would have done (P7).

It reports the NEW RETAINED MINIMUM alongside the count so a consumer can set its cursor without a second call — which matters because the call that invalidated the cursor is exactly this one.

func PruneEvents

func PruneEvents(conn *sql.DB, q PruneQuery) (*PruneResult, error)

PruneEvents is the whole verb: validate, refuse, delete, log — in ONE transaction.

THE REFUSAL PREDICATES ARE EVALUATED INSIDE THE DELETE'S TRANSACTION (F3), so the set refused and the set deleted are computed over one snapshot. A run that reaches `done` between a check and a delete does not widen what was deleted: it prunes on the next call, having been a live run for the whole of this one.

type ReadyCondition

type ReadyCondition string

ReadyCondition identifies one clause of the predicate, so a refusal names it.

const (
	// CondRunActive is R1: the run is `active`.
	CondRunActive ReadyCondition = "run is not active"
	// CondIssueDeps is R2: the issue's depends_on predecessors are satisfied.
	CondIssueDeps ReadyCondition = "the issue's dependencies are not satisfied"
	// CondPredecessors is R3: intra-workflow `after` predecessors are done,
	// and a fanned-out predecessor is joined.
	CondPredecessors ReadyCondition = "an `after` predecessor is not done"
	// CondUnrouted is R3's interposition clause (DKT-38): a step named as a
	// `threshold` step-name routing target is ready only once a routing
	// predecessor's RECORDED routing names it. Distinct from CondPredecessors
	// so a dispatcher staring at a pending gate whose predecessors are all
	// terminal can tell "the threshold routed elsewhere (or has not routed
	// yet)" from a join still open.
	CondUnrouted ReadyCondition = "no threshold has routed to this interposed step"
	// CondGateOpen is R3's SECOND interposition clause (DKT-168), the half
	// DKT-38 left out: a predecessor's threshold names an interposed gate,
	// and that gate has not resolved. §11.2: "on the gate's own pass execution
	// resumes at the routing step's ordinary downstream" — so until the gate
	// is terminal (routed to and tallied, or skipped because the routing chose
	// against it), the routing step's ordinary downstream waits. Without this,
	// a verify sharing a stage with a still-open security-vote was claimable
	// the moment their common predecessor recorded, and reached its own `pass`
	// while the vote was rejecting (RUN-25/VPL-129).
	CondGateOpen ReadyCondition = "an interposed gate on a predecessor has not resolved"
	// CondScope is R4: no scope conflict with a claimed/running step.
	CondScope ReadyCondition = "its scope conflicts with a claimed or running step"
	// CondHeadroom is R5: per-class concurrency headroom exists.
	CondHeadroom ReadyCondition = "no concurrency headroom in its class"
	// CondStatus is R6: the step is `pending` and `when` did not skip it.
	CondStatus ReadyCondition = "the step is not pending"
	// CondBudget is R7: budget headroom exists. S6 owns the check; at S3 the
	// seam returns true (§6.3), so this constant exists but never fires yet.
	CondBudget ReadyCondition = "no budget headroom"
)

type ReadySteps

type ReadySteps struct {
	Steps []model.StepRow
	// Total is the size of the OFFER (ready rows and staged rows alike)
	// BEFORE slicing — the v2 truncation contract (reliability-delta §4.2).
	// A post-limit count cannot distinguish "exactly N ready" from "N
	// returned, many more dropped", which is the silent drop the v2 envelope
	// exists to close.
	Total int
	// Reaped names the step instances whose leases this call reaped. Reaping is
	// a WRITE, and a verb that writes silently is one an operator cannot audit,
	// so the fact rides out on the result.
	Reaped []string
	// HeldReason names the unacknowledged bounded-class reaps holding headroom,
	// and the flag that clears them (§6.3's closing paragraph, A11's shape).
	// "Bounded" is writeClassOf's reading — any class with a finite max, judge
	// panels included — so the wording must not say "write": a reaped judge
	// holds its slot for the same reason a reaped writer does, and a message
	// that called it write-class sent a relay hunting a misclassification.
	//
	// It rides on the RESULT rather than being folded into the rows because a
	// headroom denial with nothing running is otherwise baffling: an operator
	// sees a class that is bounded at 1, no step claimed, and no work offered.
	// The refusal reports `CondHeadroom` — the existing constant — and this
	// carries the WHY alongside it. Empty on every run that never reaped a
	// bounded class, which is D3's dormancy visible on the wire.
	HeldReason string
	// LoopHeldReason names the rows this offer withheld behind an OPEN loop
	// body, and the body itself (DKT-61) — the eviction ClaimablePrefix and
	// the post-limit pass perform, which previously reached no field at all:
	// a dispatcher staring at an empty-for-the-judges offer could not tell a
	// waiting-human-parked fixer's indefinite hold from any other narrowing
	// without inspecting the body's own status. Same contract as HeldReason:
	// empty whenever nothing was withheld.
	LoopHeldReason string
	// BudgetHeldReason names the rows this offer withheld for lack of budget
	// headroom, with the numbers (DKT-242). Same contract as the two above:
	// empty whenever nothing was withheld, so a run with no cap carries no new
	// field on the wire.
	//
	// It rides on the RESULT for the same reason HeldReason does: a
	// dispatcher offered 1 of 5 ready judges, and then an empty `next` against
	// a run reporting 9 pending, cannot tell a budget wall from a graph that
	// has run dry — and the second reading makes it stop polling.
	BudgetHeldReason string
	// UnroutedReason names PENDING steps CondUnrouted holds back — an
	// interposed threshold target whose routing predecessor already decided,
	// terminally, against naming it (DKT-470). Unlike the three holds above,
	// this one is never resolved by anything ELSE finishing: the routing that
	// would have named this step already happened and recorded a different
	// routing, so the step is not "not yet" ready, it is never going to be
	// without an operator's own intervention (`step resolve` on it, or on the
	// routing predecessor before it records). Scoped to CondUnrouted alone —
	// not CondPredecessors or CondGateOpen, both ordinary "not yet" waits that
	// resolve themselves as the run progresses and would turn every ordinary
	// empty offer into a false alarm.
	UnroutedReason string
}

ReadySteps is one `next --run` answer: the offer — the ready set plus its staged closure (lookahead.go), in stage-major order — and the TRUE total before `--limit` truncated it.

type Registration

type Registration struct {
	// Kind is `schema` or `workflow`. The pinned-only files do not appear here;
	// they are counted (F20's `Pinned` line) rather than listed, because a
	// fragment tree can hold hundreds of files and none of them is a decision.
	Kind    string `json:"kind"`
	Name    string `json:"name"`
	Version int    `json:"version"`
	Path    string `json:"path"`
	SHA256  string `json:"sha256"`
	// Outcome is `new` or `unchanged`. F11's identical-bytes case is a SUCCESS
	// THAT CHANGES NOTHING, and reporting it as `unchanged` rather than omitting
	// it is what makes re-activation of an unedited repo legible: the operator
	// sees that the file was considered and found already registered.
	Outcome string `json:"outcome"`
}

Registration is one file the scan acted on — F21's wire row.

type RenderResult

type RenderResult struct {
	Packet string `json:"packet"`
	// Template names the template used: "default" for the embedded one, or the
	// file path.
	Template string `json:"template"`
	// TemplatePinned reports whether the template's bytes are pinned by the run.
	// It rides on the result so `--meta` can report an UNPINNED template and the
	// reproducibility gap is VISIBLE rather than assumed (§6.11.1) — a packet
	// rendered through an unpinned file is reproducible only to the extent the
	// operator chose.
	TemplatePinned bool `json:"template_pinned"`
}

RenderResult is one rendered work packet, with the provenance of the template that produced it.

func RenderStep

func RenderStep(
	conn *sql.DB, stepID int, templatePath string, nowMS int64,
) (*RenderResult, error)

RenderStep renders a step's work packet.

`templatePath` is `--template F`, or "" for the shipped default.

func RenderStepAs

func RenderStepAs(
	conn *sql.DB, stepID int, templatePath, executor string, nowMS int64,
) (*RenderResult, error)

RenderStepAs is RenderStep with the `{executor}` substitution resolved to a caller-supplied hint (DKT-70).

The declared hint is a DECLARATION, and some instances resolve it further at dispatch time — a policy table keyed by issue labels picks the actual executor after the engine has offered the step. Substitution used to bind to the declared hint alone, so a label-resolved executor could never receive its own contract: the corpus shipped per-resolved-hint files that no packet could ever name. The resolved hint arrives here, at render time, which is where substitution ALREADY re-derives — activation pinned the whole config tree, so the resolved contract verifies against its pin like any other entry, and a hint whose contract does not exist refuses loudly naming the exact path.

An empty executor is the declared behavior, unchanged. The override also lands on the rendered step row's `executor`, so the packet's `target:` line names who the work is actually for.

type RepinChange

type RepinChange struct {
	Kind      string `json:"kind"`
	Ref       string `json:"ref"`
	OldSHA256 string `json:"old_sha256"`
	NewSHA256 string `json:"new_sha256"`
	// Path is where the file pin resolved on disk, "" for registered-object
	// pins — the same field PinVerdict carries, for the same operator.
	Path string `json:"path,omitempty"`
}

REPIN — the recovery half of the pin story (DKT-408).

`verify-pins` (DKT-297) made drift visible; nothing made it survivable. When a corpus install replaces files under an instance-config root while a run is active or parked, every pin naming a replaced file mismatches disk forever: re-activation deliberately INHERITS the original pin set (RA2), so the only disposition was abandon + full re-plan. Four runs across three projects died exactly that way, one of them forfeiting 76 completed steps, and one of them existing only to reconcile the previous casualty of the same mechanism.

RepinRun is the explicit, operator-gated opposite of RA2's inheritance: it rewrites the run's CURRENT agreement — the `pins` rows — to what the refs resolve to on disk now, so the steps not yet claimed proceed under the new bytes. It is a separate verb rather than a flag on re-activation on purpose: re-activation inheriting pins is a guarantee in-flight work relies on, and a repin is a person deciding, with a recorded reason, that the agreement moves.

COMPLETED WORK'S PROVENANCE IS NEVER REWRITTEN. Two mechanisms enforce it at the data level rather than by convention:

  • The transaction refuses unless the run is quiesced: no step `claimed` (an executor mid-flight claimed under the old agreement, and its packet re-renders would silently flip contract mid-execution) and no dispatch open (a manifest is a frozen offer; its relay must not straddle the transition). It also refuses when every step is terminal — a run whose pins are referenced only by completed work has nothing left to recover, so a repin could only falsify history. Under those guards every step is either terminal (it consumed the old bytes wholly, before the repin event's seq) or pre-claim (it will consume the new bytes wholly) — no step ever straddles the old and new agreement.

  • The write touches ONLY the `pins` table. The tables that record what completed steps did — `steps`, `artifacts`, `step_inputs`, `events` — are never updated; the old sha is preserved in the same transaction as a `run-repinned` event per changed ref (old sha -> new sha), so the agreement a completed step worked under stays recoverable from the trail even after the current agreement moves. The event log is already the package's history mechanism (§9 item 2); a parallel pin-history table would be a second source of the same fact.

type RepinOutcome

type RepinOutcome struct {
	Run string `json:"run"`
	// Repinned is one entry per pin whose recorded hash moved, in the
	// (kind, ref) order the report walks — empty (never nil) on a no-op.
	Repinned []RepinChange `json:"repinned"`
	// Unchanged counts the pins that already matched disk.
	Unchanged int `json:"unchanged"`
}

RepinOutcome reports what RepinRun did.

func RepinRun

func RepinRun(conn *sql.DB, runID int, reason string, nowMS int64) (*RepinOutcome, error)

RepinRun re-pins a run's drifted pins to what their refs resolve to now.

type RepoPaths

type RepoPaths struct {
	// ExecRoot is the cwd every gate and action runs in, and the containment
	// boundary for their binaries (§5.2.1 R2/R3).
	ExecRoot string
	// Identity is what trust entries bind to (§3.4 P1) — the project, not the
	// checkout.
	Identity string
	// LockPath is the tree-mutex lockfile serializing this project's
	// tree-declaring gates (§7.4).
	LockPath string
}

RepoPaths carries the per-project execution facts a runner needs.

The three fields USED TO BE ONE VALUE — the parent of the .docket store stood in for all of them, which §3.4 P1 called "so a gate's binding check and its cwd cannot disagree". A store shared by every project is exactly what breaks that identity apart: where a command runs (ExecRoot), which project authorized it (Identity), and which lockfile serializes its tree (LockPath) are separate facts once the store no longer lives inside the repository. For env- and locally-resolved stores config.Resolve still yields ExecRoot == Identity == parent-of-store, so every existing setup sees byte-identical behavior.

type RowMismatch

type RowMismatch struct {
	Position int
	Stored   string
	Computed string
}

RowMismatch is P9's refusal detail: the FIRST differing position, with both renderings.

Position indexes the STORED MANIFEST's row list (`stored[i]` in VerifyDispatch) — the order the manifest was opened with — never the recomputed ready set, which a step recording, retiring, or simply losing rank can reorder freely without that reordering being what this field reports.

Both sides are carried rather than a diff, because §5.3 asks for "the differing bytes, so an operator can see whether a lease lapsed or a priority changed". A computed summary would be the engine's opinion about which of those happened; the bytes are the evidence. A step that legitimately recorded, failed, or otherwise reached a terminal status never reaches this struct at all — VerifyDispatch's DKT-10 clause skips it before the comparison runs — so an empty `Computed` here means something narrower and more alarming: the stored row's step is STILL non-terminal and yet is no longer offerable (renderRowOrAbsent explains the reading further).

type RowVerdict

type RowVerdict struct {
	Position int    `json:"position"`
	Step     string `json:"step"`
	Instance string `json:"instance"`
	Verdict  string `json:"verdict"`
	Stored   string `json:"stored,omitempty"`
	Computed string `json:"computed,omitempty"`
}

RowVerdict is one stored row's outcome. Stored and Computed carry the bytes on a non-matching verdict, for the reason RowMismatch does: the evidence rather than the engine's opinion about it. Both are empty on a `matched` or `recorded` row, where there is nothing to show.

type RunBudget

type RunBudget struct {
	Run      string  `json:"run"`
	Budget   float64 `json:"budget"`
	Source   string  `json:"source"`
	Floor    float64 `json:"floor"`
	Reported float64 `json:"reported"`
	Unit     string  `json:"unit,omitempty"`
	Spend    float64 `json:"spend"`
	// The MEASURED dimension (DKT-238), reported beside the declared one
	// because a run stopped on tokens and a run stopped on declared cost need
	// different answers and an operator must be able to tell which they have.
	//
	// All three are `omitempty` and absent when the dimension is dormant, so a
	// run that never armed it reads exactly as it always did.
	UsageBudget float64 `json:"usage_budget,omitempty"`
	UsageUnit   string  `json:"usage_unit,omitempty"`
	UsageSpend  float64 `json:"usage_spend,omitempty"`
	RowVersion  int     `json:"row_version"`
}

RunBudget is the read form's answer (B-4): the effective cap and the numbers it is compared against.

It carries the SAME quantities `run report` shows, computed the same way, so an operator deciding what to raise a cap to does not have to read a report to find out what the run has already spent.

func GetRunBudget

func GetRunBudget(conn *sql.DB, runID int) (*RunBudget, error)

GetRunBudget is the read form (B-4).

func SetRunBudget

func SetRunBudget(conn *sql.DB, runID int, budget float64, reason string, ifVersion *int, nowMS int64) (*RunBudget, error)

SetRunBudget writes a run's cap, event-logged, in one transaction.

`budget` is the new cap; ZERO MEANS UNLIMITED (B-1), the meaning the flag has carried since S3 and the one `budgetSnapshot.unlimited()` enforces. Raising and lowering are the same operation (B-2) — engine-spec §1 says "raise/lower a live cap", and a verb that refused to lower would be a verb that half exists.

type RunBudgetReport

type RunBudgetReport struct {
	// Cap is the effective cap, and Source is where it came from — R6's line,
	// which exists because B3's pinning property is surprising: a run started
	// before `budget.default` was set stays unlimited after. An operator asking
	// "why didn't it stop?" reads the answer here rather than in the source.
	Cap    float64 `json:"cap"`
	Source string  `json:"cap_source"`
	// Floor is §4.3's SUM over claim events — recomputed here, through the same
	// query enforcement runs, never read from `runs.usage_floor`. The cache
	// exists for the burn-rate line's convenience and a report that read it
	// would be publishing a number no decision was made against.
	Floor float64 `json:"floor"`
	// Reported is per-unit and NEVER summed across units (B19, §4.5): summing
	// {tokens: 4000, seconds: 12} to 4012 would be core asserting those add up.
	Reported []db.UnitTotal `json:"reported,omitempty"`
	// Unit is `budget.unit` — which of the above, if any, the cap counts.
	Unit string `json:"budget_unit,omitempty"`
	// Spend is `max(reported, floor)`: the quantity the cap is compared to.
	Spend float64 `json:"spend"`
	// BurnRate is floor per wall-clock hour. It is published and NOT projected:
	// core does not predict when a run will breach, because that is a policy
	// computation over a published number (§14).
	BurnRate float64 `json:"burn_rate"`
	// BreachReason is set only on a run a budget actually paused.
	BreachReason string `json:"breach_reason,omitempty"`
	// The MEASURED dimension (DKT-238): a second, independent cap over what
	// the ledger recorded rather than what step definitions declared.
	//
	// Beside the numbers above rather than folded into them, because they are
	// not commensurable — a raise tribunal deliberating 280 declared units
	// against a run that measured hundreds of millions of tokens was
	// deliberating over a proxy, and its own security seat said so. All three
	// are absent when the dimension is dormant.
	UsageCap   float64 `json:"usage_cap,omitempty"`
	UsageUnit  string  `json:"usage_budget_unit,omitempty"`
	UsageSpend float64 `json:"usage_spend,omitempty"`
}

RunBudgetReport is R2: the budget section.

Every number here is BARE. There is no currency, no token, and no rate: what the numbers count is the workflow author's business (§1.1's second leak, closed), and the report's job is to publish them so an instance can compute its own policy — a warn threshold, say — from a read verb (B27, B28).

type RunReport

type RunReport struct {
	Run *model.Run `json:"run"`
	// WallClockMS is activation -> now, or activation -> the terminal
	// transition. A run that never activated has none, and the field is absent
	// rather than zero: a field that is not a fact does not appear, which is the
	// v6 `lease` object's rule applied here.
	WallClockMS int64 `json:"wall_clock_ms,omitempty"`

	Budget RunBudgetReport `json:"budget"`

	// Steps is R3: the count by EFFECTIVE status, computed at read.
	Steps []model.StatusCount `json:"steps,omitempty"`
	// Attempts is R3's other half: per-step attempts, ordered by instance.
	Attempts []StepAttempt `json:"attempts,omitempty"`

	// Issues is R3's issue-level half (DKT-403): the TERMINAL RULINGS an
	// operator made about whole issues, which the step sections cannot carry.
	//
	// A step's row says how that step ended. It does not say that the question
	// the step parked on was later answered — and for the two abandon paths it
	// cannot: `run abandon --issue` terminalizes every remaining step WITHOUT
	// touching `routing`, so a step parked with "loop 4 would exceed
	// max_fix_loops" keeps rendering that park text as though it still stood,
	// and the operator's actual ruling lived only in an `issue-abandoned`
	// event that no section of this document read. RUN-32 shipped exactly that
	// shape: two resolved gates, both reported as open, and the next session
	// re-asked two decisions the operator had already made.
	Issues []IssueDisposition `json:"issues,omitempty"`

	// Actors is E21 (docs/tdd/runs-dispatch.md §8.7): the per-actor event
	// counts, folded into R3's neighborhood.
	//
	// It publishes §9 item 2's answer as a NUMBER rather than as an argument.
	// "Every transition in events traceable to next/gate/threshold/human input"
	// is checkable from this section alone: the four counts sum to the run's
	// event total, and an event attributable to nothing would make them not.
	// The events read surface is where an auditor goes for the individual rows;
	// this is the rollup that says whether they need to.
	Actors []ActorCount `json:"actors,omitempty"`

	Gates       []db.VerdictCount   `json:"gates,omitempty"`
	GateTrail   []db.ResultTrailRow `json:"gate_trail,omitempty"`
	Actions     []db.VerdictCount   `json:"actions,omitempty"`
	ActionTrail []db.ResultTrailRow `json:"action_trail,omitempty"`

	// Artifacts is R6: the INDEX — id, kind, producer instance, sha256, bytes.
	// NEVER THE BODIES. A rollup that inlined artifact bodies would turn a
	// status check into a document dump, and the bodies are one `step context`
	// away for anyone who wants them.
	Artifacts []ArtifactIndexEntry `json:"artifacts,omitempty"`

	// Metadata is R7, the genericity line at its thinnest: keys to distinct
	// values with counts, verbatim and uninterpreted.
	Metadata []db.MetadataKeyRollup `json:"metadata,omitempty"`

	// VoteMetadata is the same rollup over CAST VOTES (DKT-71): every
	// `--metadata` claim the run's vote-step proposals collected. Vote seats
	// are the one spend the usage ledger cannot see — a vote step is never
	// claimed — so this is where tribunal routing and cost claims become
	// verifiable from the run document.
	VoteMetadata []db.MetadataKeyRollup `json:"vote_metadata,omitempty"`

	// VoteUsage is DKT-95: the seats' own spend reports, summed per unit —
	// UsageByUnit's question asked of the casts, from the vote_usage ledger
	// (usage_ledger cannot key a seat: a vote step's attempt is permanently
	// 0). Beside VoteMetadata, this is what makes tribunal cost measurable
	// from the run document instead of an absent zero awaiting an operator's
	// backfill.
	VoteUsage []db.UnitTotal `json:"vote_usage,omitempty"`

	// VoteUsageCoverage is how many seat-casts reported their spend (DKT-257).
	//
	// It is NOT omitempty, and that is the whole point. `vote_usage` is
	// `omitempty`, so a run whose seats all reported nothing carried no key at
	// all — and an absent section reads as "no panels ran", which is a
	// different and much more comfortable claim than "panels ran and none of
	// them said what they cost". The ledger held ZERO rows for an entire store
	// epoch while 21+ seat-votes did real verification work, and nothing
	// anywhere distinguished that from a run with no panels.
	//
	// Core cannot supply the missing numbers — it cannot observe a
	// conductor-side seat's spend — but it can stop the silence from looking
	// like a zero.
	VoteUsageCoverage db.VoteUsageCoverage `json:"vote_usage_coverage"`

	// StepUsage is the ledger row by row — which step, which attempt, which
	// unit, how much, and who measured it. Budget.Reported is the same rows
	// summed per unit; this is the detail behind that headline.
	//
	// It exists because NOTHING exposed per-step usage (DKT-241). A
	// back-fill's duplicate refusal tells an operator to "check `docket run
	// report` before re-running", and that was impossible advice: the report
	// answered only per-unit totals, so the question the refusal raises —
	// WHICH steps already have usage — could be answered from no read verb at
	// all, and conductors hand-filtered batches by trial and error across
	// three sessions. `omitempty`, so a run whose ledger is empty is unchanged.
	StepUsage []db.StepUsageRow `json:"step_usage,omitempty"`
}

RunReport is the whole document — R1 through R7, in that order.

func LoadRunReport

func LoadRunReport(conn *sql.DB, runID int, nowMS int64) (*RunReport, error)

LoadRunReport builds the document. IT WRITES NOTHING.

type Scheduler

type Scheduler struct {
	// contains filtered or unexported fields
}

Scheduler answers readiness over one consistent snapshot of a run.

It is a value rather than a set of free functions because R3, R4, and R5 are questions about the SAME set of rows at the SAME instant: a predicate that re-read the step table between clauses could see a step both claimed (for scope) and pending (for headroom), and schedule two writers into one tree. Loading once and answering many times is what makes the snapshot consistent.

func LoadScheduler

func LoadScheduler(tx *sql.Tx, runID int, defs map[int]*workflow.Definition, nowMS int64) (*Scheduler, error)

LoadScheduler reads everything the predicate needs, once, inside tx.

func (*Scheduler) AwaitingDecision

func (s *Scheduler) AwaitingDecision(step *db.Step) bool

AwaitingDecision reports whether this step is a DECLARED gate whose turn has come and which nobody has decided yet — the issue-mirror's third `review` shape (DKT-334).

It is R3 and its two interposition clauses, R6, and R2, and deliberately NOT the rest of Ready(). R1 (run active), R4 (scope), R5 (class headroom) and R7 (budget) are all questions about scheduling EXECUTOR work: nobody claims a gate, it holds no tree and consumes no class or budget headroom, and pausing the run does not answer the question the gate is asking. A gate whose turn has come is outstanding against a human whatever the scheduler is doing with the rest of the run.

Materialized held steps are excluded because they are already counted by their own shape (a non-terminal `materialized` row), which needs no scheduler at all.

It lives here, on the Scheduler, rather than as a query in reconcile.go so that R3 has exactly ONE implementation. A second copy — "are this step's predecessors terminal", written out longhand next to the mirror — is what would drift at the first fanout join or interposed gate that mattered.

func (*Scheduler) Budget

func (s *Scheduler) Budget() (cap, floor, reported, spend float64, source BudgetSource, unit string)

Budget exposes the loaded snapshot's decided numbers to callers outside this package — the claim's enforcement path and the report.

It hands over the ANSWER rather than the apparatus, for the reason StepView does: a caller holding the raw rows could compute a different spend from the one the scheduler decided against, and the two would disagree exactly when it mattered.

func (*Scheduler) BudgetHoldReason

func (s *Scheduler) BudgetHoldReason() string

BudgetHoldReason names the rows this offer withheld for lack of budget headroom, with the numbers that decided it — DKT-242's "withheld: N steps, reason=budget headroom X < cost Y".

Same contract as LoopHoldReason and HeldReason: empty whenever nothing was withheld, so a run with no cap — B29's dormancy — carries no new field on the wire. A withheld step is not refused and not failed; it is simply not offered yet, and the difference between that and "there is no work" is the whole reason this string exists.

func (*Scheduler) ClaimablePrefix

func (s *Scheduler) ClaimablePrefix(sorted []*db.Step) []*db.Step

ClaimablePrefix narrows a SORTED ready set to the largest prefix-greedy subset whose members can be claimed AS A SET: no two admitted rows exclude each other on scope, no class is offered more rows than it has remaining headroom, and the offer's summed cost never exceeds remaining budget headroom (DKT-47).

R4 and R5 ask whether a step conflicts with something that HOLDS — a claimed or running scope holder, an occupied class slot. Neither has an opinion about two steps that are both merely READY, because neither holds anything yet — so `next` offered sets whose own members excluded one another, and a dispatcher that spawned the whole set watched every loser die on a claim refusal. RUN-3 lost 21 of 53 spawns to the scope half (CONFLICT at claim); RUN-2 bounced 5 write-class spawns off one slot to the class half, each dying on "no concurrency headroom" after paying its full worktree bootstrap (DKT-23). In both cases the offer was honest about each row in isolation and false about the set.

Greedy over the caller's order, which SortSteps has already made total (priority, then age, then id). That is what makes this FAIR as well as correct: the oldest ready step in a conflicting cluster always wins, every time it is asked, so a repeatedly-losing step cannot starve — it becomes the oldest and takes its turn. An arbitrary or map-ordered choice would schedule just as correctly and starve unpredictably, which is the failure RUN-3's review fanout hit going 0-for-5.

All three constraints — class headroom, scope, budget — are checked BEFORE any of the three is consumed: a row skipped for headroom grants no scope and charges no cost, a row skipped for scope occupies no slot and charges no cost, and a row skipped for budget occupies no slot and grants no scope. scopeAdmits is a pure predicate for exactly this reason — its grant (grantScope) is applied only once every constraint has passed, alongside the class and cost charges. A single pass consuming a resource mid-check would exclude a later row that fits, which is what a check-then-grant scope half did until DKT-47.

This narrows the OFFER, never the readiness predicate: a step held back here is still ready and is offered by the very next call once a slot, scope, or budget headroom frees. Nothing is written, and R4/R5/R7 remain the authority a claim is checked against.

DEPENDENT EVICTION. A row admitted here whose loop body — the same-issue, same-ordinal loop step whose `after_loop` chain covers it — is still open and unoffered is evicted from the offer as well, via blockingLoopBodyAbsent (stage.go), regardless of WHY the body is absent: rationed out of THIS offer by class headroom or scope (DKT-26), or excluded from readiness itself before it ever reached `sorted` (DKT-48; R7 budget, R5 class headroom, R4 scope). blockingLoopBodyAbsent asks the PINNED DEFINITION, not the offered set, which is what lets one predicate answer both cases — see its own doc comment for the reasoning and RUN-2's measured failures.

precedesInSet stays in use for assignStages' stage LABELS (a comparison between two rows of one offer), but eviction no longer runs it: it only ever saw a loop body that made it into `sorted` and was then rejected here, a proper subset of what blockingLoopBodyAbsent already catches by scanning every status of `later`'s run, offer membership included.

The evicted dependents are offered again once the predecessor stops blocking — usually because it records, but `db.StepTerminal` also lets a body sitting at `waiting-human` (a persisted NON-terminal status, alongside claimed/running/gated) hold the offer open-endedly, until an operator acts on it. That hold has no dedicated `ReadySteps` field or `ReadyCondition` of its own the way a scope or budget park does (next.go), so a dispatcher staring at an empty-for-the-judges offer cannot tell this case apart from any other narrowing without inspecting the loop body's own status.

The pass iterates to a fixed point because eviction and admission feed each other in both directions: the fixer sorts AFTER its judges (younger), so a single scan admits the dependents before it ever rejects their predecessor — and an evicted dependent frees a slot that may admit a row an earlier pass rationed out. Each iteration either returns or grows the evicted set, so the bound is the set's length.

func (*Scheduler) Expired

func (s *Scheduler) Expired(step *db.Step) bool

Expired reports whether a step's lease should be reaped: either the lease lapsed, or the step passed `max_step_duration` measured from `started_ms`.

The second half is the one that matters and is easy to omit. §11.1 makes `max_step_duration` a SCHEDULE-TO-CLOSE bound evaluated against `started_ms`, "independent of heartbeats — so a runaway executor cannot renew forever". A step past it is reaped even with a live lease and a fresh heartbeat, which is the entire difference between this and a lease timeout.

BOTH HALVES ARE SCOPED TO AN ACTIVE RUN. A TTL is a bet that a silent worker is dead and its step is better re-offered; on a run that is not active, R1 refuses to offer anything, so the bet has no payoff and the reap is pure loss — it clears a live worker's lease, returns the step to `pending`, and takes a write-reap hold on its class. The state is ordinary, not exotic: rollupRunTx pauses a run when ANY step is parked (`parked > 0`), leaving that step's siblings legitimately `claimed` while the run sits at `waiting-human`. That is the mid-wave human hold, and the clock ran against workers that were never in trouble.

This is a SUSPENSION, not an exemption. Nothing here writes `expires_ms`, so a step whose lease lapsed during a pause is reaped by the first `next` after the run returns to `active` — which is what preserves D1 (next.go's reap ordering) and `max_step_duration` alike, both of which are statements about runs the engine is actually scheduling. Deliberately NOT implemented by extending leases on resume: that would need a writer on the paused path, and the reap is already lazy, so there is nothing to fix at resume time.

func (*Scheduler) HeadroomDetail

func (s *Scheduler) HeadroomDetail(step *db.Step) string

HeadroomDetail renders the numbers behind a CondHeadroom refusal: the class's occupancy, the cap, and the workflow whose `[limits]` set it.

DKT-23's second ask. A dispatcher that just paid a full spawn to hear "no concurrency headroom in its class" could answer none of the questions that follow — what is the cap, how full is the class, who set the bound — without grepping the corpus. The counts come from the same snapshot the predicate used, so the message cannot cite different arithmetic from the refusal it explains.

func (*Scheduler) IssueAwaitingDecision

func (s *Scheduler) IssueAwaitingDecision(issueID int) bool

IssueAwaitingDecision reports whether ANY step of one issue is an undecided declared gate whose turn has come.

func (*Scheduler) LabelsFor

func (s *Scheduler) LabelsFor(issueID int) []string

LabelsFor is the frozen labels of the issue a step belongs to, for rendering a `next` row. It answers nothing about readiness.

func (*Scheduler) Limit

func (s *Scheduler) Limit(class string) workflow.Limit

Limit returns the effective `[limits]` entry for a class.

func (*Scheduler) LoopHoldReason

func (s *Scheduler) LoopHoldReason() string

LoopHoldReason renders the loop-body holds this scheduler's offer passes recorded, "" when they evicted nothing — ReapHoldReason's shape, for the DKT-61 case: rows withheld because their same-issue loop body is open and absent from the offer. Without it, a dispatcher staring at an empty-for-the-judges offer cannot tell this hold from any other narrowing short of inspecting the body's own status. At most three withheld instances are named; a reason listing forty is one nobody reads.

func (*Scheduler) QuorumMisses

func (s *Scheduler) QuorumMisses() []*db.Step

QuorumMisses returns the fanned-out predecessors of this run whose join has completed (every sibling terminal) but whose `done` count fell below `min_siblings` — J5's failure half.

It is separate from the readiness predicate because the two answer different questions. Readiness asks "may this step run?", and the answer for a missed quorum is no, forever — the successor's predecessor produced too few results and no amount of waiting changes that. J5 says what happens INSTEAD: "the fanned step routes per its `on_fail`". Without this, a missed quorum would leave the run silently wedged — every sibling terminal, the successor never ready, nothing left to complete and nothing to explain why.

The siblings are returned rather than routed here so the caller applies the routing in a transaction it owns, alongside the events, exactly as the saga does for an ordinary failure.

func (*Scheduler) Ready

func (s *Scheduler) Ready(step *db.Step) (bool, ReadyCondition)

Ready reports whether a step satisfies R1-R7, and when it does not, WHICH condition failed.

The clauses are evaluated in R1..R7 order and the first failure is reported. That order is not arbitrary: it goes from the cheapest and most global (is the run even active?) to the most local, so the reason a caller sees is the most useful one — "the run is paused" explains a whole stalled dispatcher, where "no headroom" on the same step would send someone hunting for a concurrency problem that is not there.

func (*Scheduler) Run

func (s *Scheduler) Run() *model.Run

Run returns the loaded run.

func (*Scheduler) SortSteps

func (s *Scheduler) SortSteps(steps []*db.Step)

SortSteps orders a ready set by PRIORITY THEN AGE (§2: "Ordering: priority then age").

Priority is the ISSUE's, ranked by planner.PriorityRank — the same ranking `next` uses, reused rather than redefined so the two verbs cannot disagree about what "higher priority" means. Age is the STEP's `created_at_ms`, and `id` is the final tie-break so the order is TOTAL and reproducible: two steps created in the same millisecond are common (one expansion writes them all), and without the id they would order nondeterministically, which would make the topology goldens flap.

func (*Scheduler) Steps

func (s *Scheduler) Steps() []*db.Step

Steps returns the loaded step set, for callers that render alongside the readiness answer.

func (*Scheduler) UnacknowledgedReaps

func (s *Scheduler) UnacknowledgedReaps() []db.ReapAck

UnacknowledgedReaps exposes the snapshot's open reaps for the REFUSAL MESSAGE.

A headroom denial with nothing running is otherwise baffling (§6.3's closing paragraph), so `next`'s human output additionally names the unacknowledged reaps and the flag that clears them. The rows come from the same snapshot the predicate used, so the message cannot name a different set of reaps from the one that caused the denial.

func (*Scheduler) UnroutedHoldReason

func (s *Scheduler) UnroutedHoldReason() string

UnroutedHoldReason names PENDING steps CondUnrouted holds back, "" when none are (DKT-470's second fix).

Unlike LoopHoldReason/BudgetHoldReason it is not accumulated while the offer is built — CondUnrouted is a fact about a step's OWN threshold routing, not a narrowing the offer's admission passes apply — so it is computed by asking every pending step directly rather than reading a map side effect wrote. Scoped to CondUnrouted alone, deliberately: it is the one interposition clause that names a TERMINAL fact (the routing that would have named this step already happened and recorded something else) rather than an ordinary "not yet" — CondPredecessors and CondGateOpen resolve themselves as the run progresses, and paging an operator about every step waiting on an unfinished predecessor would turn most quiet polls into false alarms.

func (*Scheduler) UsageBudget

func (s *Scheduler) UsageBudget() (cap, spend float64, unit string, armed bool)

UsageBudget exposes the MEASURED dimension's decided numbers (DKT-238), for the same reason Budget does: a caller re-deriving them from the ledger could compute a different spend than the one the scheduler enforced against.

type ScopeWarning

type ScopeWarning struct {
	IssueID  string `json:"issue"`
	Workflow string `json:"workflow"`
	Reason   string `json:"reason"`
}

ScopeWarning is one issue that binds a tree-holding workflow while declaring no scope at all — reported before the run rather than discovered when its steps race something.

type SkippedRow

type SkippedRow struct {
	Step     string `json:"step"`
	Instance string `json:"instance"`
	Attempt  int    `json:"attempt"`
	Unit     string `json:"unit"`
}

SkippedRow names one row --on-duplicate=skip passed over, so the skip is reported rather than silent.

type SpawnOptions

type SpawnOptions struct {
	// Rows is the JSON array a relay is about to spawn (G6), as raw bytes from
	// `--rows FILE` or stdin. Nil means the flag was not passed, which is a
	// DIFFERENT case from an empty array: the first asks only the reap half
	// (G7), the second proposes spawning nothing.
	Rows []byte
	// AckSeqs are `--ack-reap SEQ`, repeatable. G10: they are processed BEFORE
	// the predicate, so one invocation both acknowledges and answers — which is
	// what lets a relay's hook be a single command.
	AckSeqs []int64
	// DecidingVote is `--deciding-vote PROPOSAL-N`: the id of the OPEN
	// proposal the batch about to be spawned exists to decide (DKT-236).
	//
	// Without it the reap hold denies the one spawn that can clear it. The
	// sanctioned path for an ack-reap decision is a judge panel, and the
	// panel's spawn was denied for the exact state the panel exists to decide
	// — measured cost: one ~10h operator round trip, then two self-passed
	// `--ack-reap` calls with no panel at all, which is authorization creep
	// arriving by way of a deadlock.
	//
	// It relaxes the REAP half only, and never the row comparison: (a) is
	// about a relay spawning a batch the engine never issued, which has
	// nothing to do with who decides a hold.
	DecidingVote int
	NowMS        int64
}

SpawnOptions are `guard spawn`'s inputs beyond the run.

type StaleTarget

type StaleTarget struct {
	Instance   string `json:"instance"`
	Issue      string `json:"issue"`
	TargetSHA  string `json:"target_sha"`
	SharedHead string `json:"shared_head"`
	Reason     string `json:"reason"`
}

StaleTarget names one manifest row whose packet will render from a recorded target sha that is NO LONGER an ancestor of the shared checkout's HEAD (DKT-193). A conductor that selectively integrated an executor's work — a sanctioned action — silently invalidates downstream review packets rendered from the recorded sha; this is the structural signal that lets it be caught before review budget is spent on a diverged tree.

type StepArtifact

type StepArtifact struct {
	// Artifact is the ARTIFACT-N reference, matching the run report's
	// artifact index (R6) so the two surfaces name the same thing the same
	// way.
	Artifact string `json:"artifact"`
	Kind     string `json:"kind"`
	// Producer is the producing step's instance, e.g. `review@0#2`. Empty for
	// a run-scoped artifact with no producing step.
	Producer string `json:"producer,omitempty"`
	SHA256   string `json:"sha256"`
	// Bytes and PayloadBytes are the SIZES, always reported. A listing gives
	// these instead of the content so an operator can see what is there before
	// asking for a body that may be up to db.ArtifactMaxBytes.
	Bytes        int `json:"bytes"`
	PayloadBytes int `json:"payload_bytes,omitempty"`
	// Stub marks an artifact the S3/S4 stub runner produced — a computation
	// that did not actually run. `omitempty` matches the db.Artifact rule: an
	// artifact from a real run serializes with no `stub` key at all.
	Stub bool `json:"stub,omitempty"`
	// Supersedes names the artifact this one REVISES, e.g. `ARTIFACT-71`
	// (DKT-70) — see db.Artifact.Supersedes. `omitempty` keeps an original's
	// bytes exactly as they were.
	Supersedes  string `json:"supersedes,omitempty"`
	CreatedAtMS int64  `json:"created_at_ms"`

	// Body and Payload are the CONTENT, present only on a single-artifact
	// read. Both are omitempty so a listing's entries carry neither.
	Body    string `json:"body,omitempty"`
	Payload string `json:"payload,omitempty"`
}

StepArtifact is one artifact as a read verb reports it.

The `Body`/`Payload` fields are populated only by ReadArtifact; the listing leaves them empty and reports sizes instead. Sharing the type keeps the `artifact` reference and `kind` spelled identically in both, so a caller can list and then fetch without re-deriving anything.

func ListStepArtifacts

func ListStepArtifacts(conn *sql.DB, stepID int) ([]StepArtifact, error)

ListStepArtifacts reports what one step produced, WITHOUT the bodies.

IT WRITES NOTHING, matching every other read verb — no reap, no lease touch.

A step that produced nothing is not an error: it returns an empty list. Many steps legitimately produce no artifact, and turning that into a failure would make the verb unusable as the "what is here?" probe it exists to be.

func ReadArtifact

func ReadArtifact(conn *sql.DB, artifactID int) (*StepArtifact, error)

ReadArtifact reads ONE artifact in full, body and payload included.

IT WRITES NOTHING.

Reached by numeric id (the N in ARTIFACT-N), which is what the listing above and the run report's index both print — so the reference an operator already has on screen is the one this takes.

type StepAttempt

type StepAttempt struct {
	Step     string `json:"step"`
	Instance string `json:"instance"`
	// Issue is the issue this step was expanded for.
	//
	// It rides here for the collision instance labels have everywhere: two
	// issues on one workflow share every instance name, so `reconcile@3` alone
	// does not identify a row. It is also the JOIN KEY a reader needs to carry
	// an issue-level disposition (below) back onto the step that parked on it.
	Issue    string `json:"issue,omitempty"`
	Status   string `json:"status"`
	Attempts int    `json:"attempts"`
	// Routing is the step's recorded routing, with its reason when one was
	// given — `abandon-issue: the issue was abandoned...`, `skip: operator
	// selected ...`. Empty for a step that has not been routed yet, which is
	// itself the answer for a `pending` or `claimed` row.
	Routing string `json:"routing,omitempty"`
	// Vote is a vote step's proposal and how it tallied, `DKT-V38 rejected`.
	//
	// A vote step's `attempts` is permanently 0 — it is never claimed — so the
	// count that tells every other row apart from a row that did nothing says
	// nothing here. RUN-22's verify-tribunal rendered `skipped, attempts: 0`
	// after DKT-V38 convened, three seats deliberated for ~39.5k output tokens,
	// and the panel rejected 0-3-0; the `skipped` was the operator's post-tally
	// resolve. Beside `Routing` this carries BOTH facts, which is what the
	// issue asks for: the panel decided, and then a person disposed of it.
	//
	// Empty for every non-vote step, and for a vote step whose proposal was
	// never opened — which is exactly the never-convened case the reader needs
	// to tell apart.
	Vote string `json:"vote,omitempty"`
}

StepAttempt is one step's attempt count and how it ended.

`Routing` is the DKT-258 addition, and it is what makes the other three readable. A status alone collapses outcomes that need opposite responses: `skipped` is a tribunal that never convened AND one whose panel deliberated and was then resolved by an operator; `failed-routed` is a step that was measured and came back bad AND one that was cascade-terminated by an issue-abandon without ever being claimed. The engine records why in the step's own `routing`, and the report simply never carried it.

type StepContext

type StepContext struct {
	Instance string
	RunID    int
	IssueID  int
	// Scope is the issue's SNAPSHOTTED scope globs (DKT-63), exported to the
	// child as DOCKET_SCOPE so a diff-shaped gate can evaluate the change it
	// is gating rather than the whole dirty tree. Per-step gates over a
	// shared tree see every issue's in-flight edits; without the scope, one
	// issue's uncommitted work failed the next issue's gate three times in
	// one run, parking the run each time on an already-adjudicated fact.
	Scope []string
	// WorkRoot is the private worktree the step's gates should measure, or ""
	// for the shared checkout (DKT-9). The saga fills it with the step's own
	// recorded worktree; the pre-claim path fills it with the resolved target
	// worktree — the tree the producing step declared. Before this field,
	// every gate spawned in the shared checkout unconditionally, so a gate's
	// evidence could describe a HEAD the step under review never touched.
	WorkRoot string
}

StepContext is the step a gate runs for. It is deliberately thin at this stage: the pass-through runner reads none of it, and S4's real runner needs the identity and the working directory, not the closure.

type StepListEntry

type StepListEntry struct {
	Step string `json:"step"`
	// Run names the row's run. A run-scoped listing repeats one value, but an
	// issue-scoped one spans runs, and without it two rounds of the same
	// instance are indistinguishable (DKT-244).
	Run      string `json:"run"`
	Instance string `json:"instance"`
	Issue    string `json:"issue"`
	Kind     string `json:"kind"`
	Status   string `json:"status"`
	// BlockedReason is StepRow's field of the same name (DKT-470), on the
	// inventory row too: `step list` is where an operator scanning a whole
	// run for a stall notices one `pending` row sitting among steps that have
	// long since finished.
	BlockedReason string `json:"blocked_reason,omitempty"`
	// LeaseExpired is StepRow's field of the same name (DKT-489), on the
	// inventory row too: `step list` is where a caller reconciling this
	// listing against a `run repin` CONFLICT sees WHICH `ready` rows still
	// carry an unreaped claim the repin is refusing over.
	LeaseExpired bool `json:"lease_expired,omitempty"`
	Attempt      int  `json:"attempt"`
	// FailedAttempts / ReapedClaims are StepRow's fields of the same name
	// (DKT-490), on the inventory row too: `step list` is where an operator
	// scanning a run asks "why is this step on attempt 3", and the breakdown
	// is the answer — how many of those claims failed outright vs were reaped
	// with nothing measured.
	FailedAttempts int     `json:"failed_attempts,omitempty"`
	ReapedClaims   int     `json:"reaped_claims,omitempty"`
	ExpectedCost   float64 `json:"expected_cost"`
}

StepListEntry is one row of `step list --run`: the identity, effective status, and cost a budget projection reads — an INVENTORY row, not an offer. model.StepRow is the dispatch wire shape (executor hints, lease TTLs, packet identity) and would overpromise here.

func IssueStepList

func IssueStepList(conn *sql.DB, issueID int, nowMS int64) ([]StepListEntry, error)

IssueStepList answers `docket step list --issue ISSUE-N`: every step recorded for one issue, across every run that holds one, with the same effective status RunStepList computes.

It exists because the issue, not the run, is what a conductor has in hand when it asks "where did this get to" — reaching for --issue and finding only --run meant paging a whole run's listing through an external filter, and guessing at --run/--issue combinations that did not exist (DKT-244). Each run is scheduled on its own, because readiness is a question about one run's graph; the rows are then concatenated in run order. READ-ONLY, like RunStepList.

func RunStepList

func RunStepList(conn *sql.DB, runID int, nowMS int64) ([]StepListEntry, error)

RunStepList answers `docket step list --run RUN-N`: every step of one run, with its EFFECTIVE status (§6.2) and expected cost, in the (issue, id) order ListRunSteps defines.

It exists because no read verb enumerated a run's steps (DKT-54): `run status` rolls statuses up to counts, `next` offers ready rows only, and step ids are one store-wide sequence shared across projects — so a conductor building the budget projection its contract demands (done spend plus pending expected costs against the cap) could only guess contiguous ids, correct exactly as long as the store was quiet. READ-ONLY: the scheduler loads in a transaction that is always rolled back, and no reap runs.

type StepView

type StepView struct {
	Step *db.Step
	Row  model.StepRow
	// Routing and SagaStage are rendered by `step show` so an operator can see
	// why an unclaimed step is not `done`: a step mid-saga is engine-owned and
	// needs no lease to advance (§6.8).
	Routing   string
	SagaStage string
	// Owner and ExpiresMS describe a LIVE lease only. A lapsed one reports
	// neither, matching the v6 `lease` object's rule that a field which is not
	// a fact does not appear — and matching the effective status, which already
	// reads the lapsed lease as gone.
	Owner     string
	ExpiresMS int64
	// Gates are the step's recorded gate results, in insertion order (DKT-63).
	//
	// `step show` printed no gate section at all, so the surface an operator
	// reaches for when a step is `waiting-human` said nothing about the gates
	// that put it there — a conductor read this verb and the event feed on
	// 2026-08-16 and reported three failed gates as passes. The results were
	// always in the table; nothing rendered them.
	Gates []db.GateResultRow
	// HeldCluster is a materialized held step's provenance: which cluster it
	// decides, out of how many, and which artifact carries it (DKT-239).
	//
	// nil on every step that is not a materialized hold, so an ordinary step's
	// view is unchanged. On a hold it is the answer `step artifacts` cannot
	// give — that verb reports what the STEP produced, and a hold produces
	// nothing; the payload sits on the routing step's artifact.
	HeldCluster *HeldClusterLink
}

StepView is one step as a READ verb sees it: the row, its effective status already computed, and the lease facts `step show` renders.

It is a value rather than a (step, scheduler, config) triple because the scheduler and the TTL config are assembly machinery, and a CLI verb that held them could compute a DIFFERENT effective status from the one in the view. Handing over the answer rather than the apparatus is what keeps the read verbs unable to disagree with `next`.

func LoadStepView

func LoadStepView(conn *sql.DB, stepID int, nowMS int64) (*StepView, error)

LoadStepView reads one step at its effective status. IT WRITES NOTHING — not even a reap (§6.3 confines reaping to next/claim).

type ThresholdResult

type ThresholdResult struct {
	// Routing is the matched routing key, or RoutingPass when none matched.
	Routing string
	// Reason is set when T3 parked the step: it names the step, the routing
	// key, the predicate verbatim, and the cause, so an operator resolving it
	// can see exactly what could not be decided and why.
	Reason string
	// Parked reports a T3 park, distinguishing "routed to waiting-human because
	// a predicate SAID SO" from "parked because a comparison was undecidable".
	// Both end at `waiting-human`; only one is a gap S5 closes.
	Parked bool
}

ThresholdResult is one evaluation's outcome.

func EvaluateThreshold

func EvaluateThreshold(
	instance string, threshold map[string]string, order []string,
	payloads []map[string]any, schema OrderResolver,
) (ThresholdResult, error)

EvaluateThreshold applies a step's `threshold` table to a payload set.

Routings are evaluated TOP TO BOTTOM, FIRST MATCH ROUTES (§11.2). "Top to bottom" over a Go map needs a defined order, and the order is the DECLARED one — recovered from the definition's routing sequence — because a map's range order varies per run and a first-match rule over a varying order is a routing decision that changes between two identical runs.

type TrustGrant

type TrustGrant struct {
	Name       string
	ArgvSHA256 string
	Repo       string
	Global     bool
	Prefix     bool
	ReRunnable bool
	Tree       bool
	Flaky      bool
	Network    []string
	Timeout    string
	// Stub is the operator's declaration that this entry authorizes a
	// PLACEHOLDER rather than the check its name implies (DKT-265).
	//
	// It rides in the event for the same reason `tree` and `network` do: it is
	// what the grant is really granting. A feed that showed a `secret-scan`
	// entry being added, without saying it points at `/usr/bin/true`, records
	// the name of an assurance rather than the assurance.
	Stub bool
	// Actor is WHO ran the verb — the git identity, falling back to the OS
	// username (DKT-263). It is A CLAIM, NOT A VERIFIED FACT: `git config
	// user.name` is whatever the invoking environment says it is, and nothing
	// here authenticates it. That is the same footing step metadata stands on,
	// and it is worth having anyway — before this field, recovering by-whom
	// meant bracketing runs against wall-clock, which the retro had to do
	// twice.
	Actor string
	// Cwd is where the verb ran from.
	//
	// It is the DISAMBIGUATOR, and it is the field that does the real work on a
	// machine where every grant carries the same git identity: two concurrent
	// sessions are one operator with two working directories, and only the cwd
	// separates them. Same reasoning as ProjectRegistration's Cwd/Identity
	// pair — the case worth attributing is the one where the person is not the
	// distinguishing fact.
	Cwd string
}

TrustGrant is what a trust entry AUTHORIZES, in the shape gates-trust §3.6 records: "the gate name, the argv hash …, the repo binding, and the flags".

IT HAS NO ARGV FIELD, and that absence is load-bearing. §3.6 forbids the argv itself in the event body, so the type a caller must fill in cannot carry one — which is also why this is declared here rather than reusing the store's entry type. That type holds the argv one field access away from the event writer, and a mapping written by hand at the call site is where the hash discipline stays visible.

EVERY BEHAVIOR-AFFECTING PROPERTY IS A FIELD. A grant that recorded only the name and the hash left the two flags that widen what a command may do — `tree` and `network` — invisible in the feed, so a remove and a re-add milliseconds apart could widen egress with a trail that showed only that something named the same thing was re-approved.

type VerifyResult

type VerifyResult struct {
	Verified bool   `json:"verified"`
	Dispatch string `json:"dispatch"`
	Run      string `json:"run"`
	// StaleTargets is the same advisory OpenDispatch attaches (DKT-193),
	// recomputed against the shared checkout as it stands NOW — byte-equal
	// rows can still have gone stale since the open.
	StaleTargets []StaleTarget `json:"stale_targets,omitempty"`
	// Rows is every stored manifest row with its own verdict, in stored order
	// — DKT-243's per-row report.
	//
	// The comparison used to stop at the FIRST shifted row and hard-error, so
	// a dispatch where several steps had moved mid-flight (a park resolved, a
	// retry minted) reported one of them and hid the rest. ~16 occurrences
	// across three sessions, each costing a manual per-step confirm round
	// before a `close` that reconciles the same state without complaint. The
	// verb still FAILS when anything is off — that contract is unchanged —
	// but it now says everything it knows in one answer instead of one fact
	// per invocation.
	Rows []RowVerdict `json:"rows,omitempty"`
}

VerifyResult is `dispatch verify`'s answer (P8, P9).

type VoteBackfillRow

type VoteBackfillRow struct {
	Voter    string
	Unit     string
	Quantity float64
}

VoteBackfillRow is one unit's quantity for one SEAT of a proposal, as named on the command line (DKT-115).

type VoteOutcome

type VoteOutcome struct {
	// ProposalID is the proposal driving this step.
	ProposalID int
	// Status is the proposal's status, verbatim from the existing machinery.
	Status model.ProposalStatus
	// Verdict maps that status onto the gate verdict routing consumes:
	// approved => pass, rejected => fail. It is empty while the proposal is
	// still open, which is phase 3 and means "nothing to route yet".
	Verdict string
	// Score is the weighted score the existing tally computed, when there is
	// one.
	Score *float64
}

VoteOutcome is what a vote step's phase-4 read observed.

func ReadVoteOutcome

func ReadVoteOutcome(conn *sql.DB, step *db.Step, spec *workflow.Step) (*VoteOutcome, error)

ReadVoteOutcome is §8.1 phase 4: the first engine invocation after the proposal leaves `open` observes its status.

It READS. The tally happened inside db.CastVote when the last voter cast, and this does not recompute it — recomputing would be a second implementation of the rule, which is exactly what "used unchanged" forbids.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL