Documentation
¶
Overview ¶
Package engine implements run activation as one transaction.
The package carries no agent, model, or LLM vocabulary of any kind. 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 an opaque string.
Index ¶
- Constants
- Variables
- func ActionNames() (map[string]bool, error)
- func ActorCounts(conn *sql.DB, runID int) (map[Actor]int, error)
- func AnnotateStep(conn *sql.DB, stepID int, metadata string, nowMS int64) (*db.Step, error)
- func AuthorizeConductor(conn *sql.DB, runID int, token, verb string) error
- func BlockedReason(sched *Scheduler, step *db.Step) string
- func BudgetBreachReason(spend, cap float64, instance string) string
- func CastVote(conn *sql.DB, vote *model.Vote) (*db.CastVoteResult, error)
- func CheckWorkflowSource(sourcePath, registeredSHA string) *model.WorkflowSourceStatus
- func DecodeMetadataBag(raw, label string) (map[string]any, error)
- func DescribeWorkflowOrigin(s *model.WorkflowOriginStatus) string
- func DescribeWorkflowSource(s *model.WorkflowSourceStatus) string
- func DescriptionEditFrozenForActiveRuns(conn *sql.DB, issueID int) []string
- func DiscrepancyReason(ds []Discrepancy) string
- func EffectiveStatus(sched *Scheduler, step *db.Step) string
- func EffectiveStatusCounts(conn *sql.DB, runID int, nowMS int64) ([]model.StatusCount, error)
- func EventKinds() []string
- func ForceReapStepWith(conn *sql.DB, stepID int, opts ForceReapOptions) error
- func FormatDispatchID(id int) string
- func GateFingerprint(output string) string
- func GitCommitPatch(execRoot, sha string, scope []string) (string, error)
- func GitDiff(dir, base string, scope []string) (string, error)
- func GitRangePatch(execRoot, from, to string, scope []string) (string, error)
- func HeartbeatStep(conn *sql.DB, stepID int, token string, nowMS int64) (*model.Lease, error)
- func HighestOrdinals(tx *sql.Tx, runID, issueID int) (map[string]int, error)
- func HoldIssue(conn *sql.DB, runID, issueID int, opts HoldOptions) ([]int, error)
- func HoldStep(conn *sql.DB, stepID int, opts HoldOptions) error
- func IsVoteStepProposal(conn *sql.DB, proposalID int) (bool, error)
- func LatestIssueDisposition(conn *sql.DB, issueID int) (*model.IssueRunDisposition, error)
- func MoveRunWith(conn *sql.DB, opts MoveRunOptions) (*model.Run, []string, error)
- func OpenVoteProposal(conn *sql.DB, step *db.Step, spec *workflow.Step, nowMS int64) (int, error)
- func OverridePassSkipsInterposedTargets(conn *sql.DB, stepID int) []string
- func PinContent(conn *sql.DB, runID int, ref string) (string, error)
- func PinDriftNotice(drift []PinVerdict, runRef string) string
- func PinReportReason(r *PinReport) string
- func PrimeDetachedPreGateLockDir(cfg *config.Config)
- func ReapAckProposalKey(runID int, seq int64) string
- func ReapHoldReason(reaps []db.ReapAck) string
- func RecordConductorPanelOpened(conn *sql.DB, proposalID int) error
- func RecordProjectRegisteredEvent(conn *sql.DB, reg ProjectRegistration, atMS int64) error
- func RecordTrustEvent(conn *sql.DB, kind string, grant TrustGrant, atMS int64) error
- func RegistrationOrder(paths []string) []string
- func RenderFenceReport(w interface{ ... }, reports []FenceReport)
- func RenderGatePreflight(w interface{ ... }, rows []GatePreflight)
- func RenderHoldPolicy(w interface{ ... }, p HoldPolicy)
- func RenderRegistrationReport(w interface{ ... }, regs []Registration, pinned int)
- func ResolveInputArtifacts(tx *sql.Tx, sched *Scheduler, step *db.Step, spec *workflow.Step) ([]*db.Artifact, error)
- func ResolveRunFilter(conn *sql.DB, ref string) (int, error)
- func ResolveSeats(conn *sql.DB, runID int, seats []string, size string, labels []string) ([]model.VoterAssignment, error)
- func ResolveStepFilter(conn *sql.DB, ref string) (int, error)
- func RunAdvisories(conn *sql.DB, runID int) ([]UnexpandedIssue, []RetryEligible, error)
- func RunFloorTx(tx *sql.Tx, runID int) (float64, error)
- func ScopeEditFrozenForActiveRuns(conn *sql.DB, issueID int) []string
- func ScopeWithinDomain(scope, domain []string) bool
- func ScopesIntersect(a, b []string) bool
- func SetConfig(conn *sql.DB, projectID int, key, value, changedBy string) error
- func StaleLineage(tx *sql.Tx, step *db.Step) (bool, error)
- func StepDefinitions(conn *sql.DB, runID int) (map[int]*workflow.Definition, error)
- func StepDefinitionsTx(tx *sql.Tx, runID int) (map[int]*workflow.Definition, error)
- func StepRowFor(sched *Scheduler, step *db.Step, ttls ttlConfig) (model.StepRow, error)
- func ThresholdOrder(threshold map[string]string) []string
- func UnintegratedReason(steps []UnintegratedStep) string
- func UsageBudgetBreachReason(spend, cap float64, unit, instance string) string
- func ValidateCastEvidence(conn *sql.DB, proposalID int, f *model.Findings) error
- func ValidateEventKind(kind string) error
- func VoteProposalRun(conn *sql.DB, proposalID int) (int, bool, error)
- func WorkflowsReferencingSchema(conn *sql.DB, projectID int, name string, version int) ([]string, error)
- type AbandonDispatchOptions
- type AbandonIssueOptions
- type AbandonIssueOutcome
- type ActionResult
- type ActionResultRow
- type ActionRunner
- type ActionSpec
- type ActivateOptions
- type ActivateResult
- type ActivationHead
- type Actor
- type ActorCount
- type AggregateOutcome
- type AggregateParams
- type ArtifactIndexEntry
- type Attribution
- type Authority
- type AuthorityCount
- type BackfillOutcome
- type BackfillRow
- type BindingWarning
- type BlockedIssue
- type BlockerHop
- type BlockingIssue
- type BoundIssue
- type BudgetSource
- type CastFinding
- type CheckedIntegration
- type ClaimOptions
- type ClaimResult
- type ClassCap
- type ClassCapSource
- type CloseOutcome
- type ClusterComplementarity
- type ClusterSourceAttribution
- type CompleteOptions
- type ConditionsWaiver
- type ConductOptions
- type ConductResult
- type Context
- func AssembleContext(tx *sql.Tx, sched *Scheduler, step *db.Step, ttls ttlConfig) (*Context, error)
- func AssembleRecordedContext(tx *sql.Tx, sched *Scheduler, step *db.Step, ttls ttlConfig) (*Context, error)
- func ReadContext(conn *sql.DB, stepID int, nowMS int64) (*Context, error)
- func ReadLiveContext(conn *sql.DB, stepID int, nowMS int64) (*Context, error)
- type ContextGateOutcome
- type ContextInput
- type ContextIssue
- type ContextMeta
- type ContextPin
- type ContextResolution
- type ContextWarning
- type CorpusEntry
- type CorpusIndex
- type DecideOptions
- type DetachedPreGateRun
- type DetachedSagaRun
- type DiffFacts
- type Discrepancy
- type DiscrepancyKind
- type DoctorCheck
- type DoctorOptions
- type DoctorReport
- type DoctorVerdict
- type DroppedMetadataTarget
- type ElidedFile
- type Engine
- func (e *Engine) AbandonDispatchWith(conn *sql.DB, runID int, opts AbandonDispatchOptions) (*CloseOutcome, error)
- func (e *Engine) AnnotateIntegration(conn *sql.DB, stepID int, sha, metadata string, by Attribution, token string, ...) (*IntegrationAnnotation, error)
- func (e *Engine) BackfillUsage(conn *sql.DB, runID int, rows []BackfillRow, source string, onDuplicate string, ...) (*BackfillOutcome, error)
- func (e *Engine) BackfillVoteUsage(conn *sql.DB, proposalID int, rows []VoteBackfillRow, source string, ...) error
- func (e *Engine) ClaimStepRendered(conn *sql.DB, stepID int, opts ClaimOptions, templatePath, executor string) (*ClaimResult, *RenderResult, error)
- func (e *Engine) ClaimStepWithGates(conn *sql.DB, stepID int, opts ClaimOptions) (*ClaimResult, error)
- func (e *Engine) CloseDispatch(conn *sql.DB, runID int, acceptMissingUsage bool, skip IntegrationSkip, ...) (*CloseOutcome, error)
- func (e *Engine) CompleteStep(conn *sql.DB, stepID int, opts CompleteOptions) error
- func (e *Engine) DecideStepWith(conn *sql.DB, stepID int, opts DecideOptions) error
- func (e *Engine) DriveRunLifecycles(conn *sql.DB, runID int, nowMS int64) error
- func (e *Engine) DriveVoteProposal(conn *sql.DB, proposalID int, nowMS int64) error
- func (e *Engine) ExtendDispatch(conn *sql.DB, runID int, nowMS int64) (*Extension, error)
- func (e *Engine) FailStep(conn *sql.DB, stepID int, token, note, metadata string, nowMS int64) error
- func (e *Engine) GuardSpawn(conn *sql.DB, runID int, opts SpawnOptions) (*GuardVerdict, error)
- func (e *Engine) HeldResolutionStaleTargets(conn *sql.DB, heldStepID int, nowMS int64) []StaleTarget
- func (e *Engine) IntegrateSteps(conn *sql.DB, stepIDs []int, by Attribution, token string, nowMS int64) ([]StepIntegration, error)
- func (e *Engine) NextSteps(conn *sql.DB, runID int, limit int, nowMS int64) (*ReadySteps, error)
- func (e *Engine) OpenDispatch(conn *sql.DB, runID int, limit int, ackSeqs []int64, nowMS int64) (*Manifest, error)
- func (e *Engine) ReconcileDispatch(conn *sql.DB, runID int, rows []BackfillRow, source string, onDuplicate string, ...) (*ReconcileOutcome, error)
- func (e *Engine) ResolveStepWith(conn *sql.DB, stepID int, opts ResolveOptions) (*ResolveOutcome, error)
- func (e *Engine) ResumeSaga(conn *sql.DB, stepID int, nowMS int64) error
- func (e *Engine) ResumeStrandedSagas(conn *sql.DB, runID int, nowMS int64) error
- func (e *Engine) RunActionStep(conn *sql.DB, stepID int, nowMS int64) error
- func (e *Engine) RunDetachedPreGates(conn *sql.DB, stepID int, targetSHA string, nowMS int64) (*DetachedPreGateRun, error)
- func (e *Engine) RunDetachedSaga(conn *sql.DB, stepID int, nowMS int64) (DetachedSagaRun, error)
- func (e *Engine) VerifyDispatch(conn *sql.DB, runID int, nowMS int64) (*VerifyResult, *RowMismatch, error)
- func (e *Engine) WaiveStaleTargets(conn *sql.DB, runID int, instances []string, targetSHA, note string, ...) ([]WaivedTarget, error)
- type Error
- type ErrorCode
- type Event
- type EventPage
- type EventQuery
- type ExecActionRunner
- type ExecRunner
- type ExecutorLedger
- type ExecutorLedgerOptions
- type ExecutorLedgerRow
- type Extension
- type FailedGate
- type FenceReport
- type ForceReapOptions
- type GateExecution
- type GateOutcome
- type GatePreflight
- type GateResult
- type GateResultRow
- type GateRunner
- type GateSeat
- type GateSpec
- type GateStatusResult
- type GateTally
- type GateTarget
- type GateVerdict
- type GitPick
- type GuardVerdict
- func GuardGate(conn *sql.DB, stepName string, runID, projectID int) (*GuardVerdict, error)
- func GuardRecord(conn *sql.DB, runID, projectID int, nowMS int64) (*GuardVerdict, error)
- func GuardSpawnActive(conn *sql.DB, projectID, decidingVote int, usageJoin bool, nowMS int64) (*GuardVerdict, error)
- func GuardStop(conn *sql.DB, projectID int, nowMS int64) (*GuardVerdict, error)
- type HeldClusterLink
- type HeldStep
- type HoldOptions
- type HoldPolicy
- type Holder
- type IncompleteClaimError
- type IntegratedRange
- type IntegrationAnnotation
- type IntegrationCheck
- type IntegrationSkip
- type IssueDiffRepin
- type IssueDisposition
- type LoopEntry
- type LoopOutcome
- type Manifest
- type MemberCount
- type MoveRunOptions
- type OrderResolver
- type PacketFile
- type PacketGate
- type PassThroughRunner
- type PinEpoch
- type PinReport
- type PinVerdict
- type PinnedWorkflowStaleness
- type PolicyAssignment
- type PolicyPin
- type PreGateResult
- type ProjectRegistration
- type ProjectRegistryAudit
- type PruneQuery
- type PruneResult
- type ReadyCondition
- type ReadySteps
- type ReconcileOutcome
- type ReferenceVerdict
- type RefreshedBody
- type RefreshedFiles
- type RefreshedScope
- type Registration
- type RegistryAudit
- type RegistryAuditOptions
- type RegistryLag
- type RegistryOrphan
- type RenderResult
- type RepinChange
- type RepinOptions
- type RepinOutcome
- type RepoPaths
- type ResolveOptions
- type ResolveOutcome
- type RetryEligible
- type RoutingLintReport
- type RoutingWarning
- type RowMismatch
- type RowVerdict
- type RunBudget
- type RunBudgetReport
- type RunFact
- type RunFactOptions
- type RunNote
- type RunNoteRuling
- type RunReport
- type Scheduler
- func (s *Scheduler) AwaitingDecision(step *db.Step) bool
- func (s *Scheduler) Budget() (cap, floor, reported, spend float64, source BudgetSource, unit string)
- func (s *Scheduler) BudgetHoldReason() string
- func (s *Scheduler) ClaimablePrefix(sorted []*db.Step) []*db.Step
- func (s *Scheduler) ClassCaps() map[string]ClassCap
- func (s *Scheduler) ClassHolders(step *db.Step) []Holder
- func (s *Scheduler) Expired(step *db.Step) bool
- func (s *Scheduler) HeadroomDetail(step *db.Step) string
- func (s *Scheduler) IssueAwaitingDecision(issueID int) bool
- func (s *Scheduler) LabelsFor(issueID int) []string
- func (s *Scheduler) Limit(class string) workflow.Limit
- func (s *Scheduler) LoopHoldReason() string
- func (s *Scheduler) QuorumMisses() []*db.Step
- func (s *Scheduler) Ready(step *db.Step) (bool, ReadyCondition)
- func (s *Scheduler) Run() *model.Run
- func (s *Scheduler) ScopeHolders(step *db.Step) []Holder
- func (s *Scheduler) SizeFor(issueID int) string
- func (s *Scheduler) SortSteps(steps []*db.Step)
- func (s *Scheduler) Steps() []*db.Step
- func (s *Scheduler) UnacknowledgedReaps() []db.ReapAck
- func (s *Scheduler) UnroutedHoldReason() string
- func (s *Scheduler) UsageBudget() (cap, spend float64, unit string, armed bool)
- type ScopeWarning
- type SilentVoteSeat
- type SkippedRow
- type SourceWarning
- type SpawnOptions
- type StageError
- type StaleTarget
- type StepArtifact
- type StepAttempt
- type StepContext
- type StepIntegration
- type StepListEntry
- type StepRuling
- type StepView
- type ThresholdResult
- type TrustGrant
- type TrustProbeGate
- type TrustProbeResult
- type TrustProbeSkip
- type UnclaimedTarget
- type UnexpandedIssue
- type UnfiledCondition
- type UnintegratedStep
- type VerifyResult
- type VoteBackfillRow
- type VoteOutcome
- type VoterLedgerRow
- type WaivedTarget
- type WorkflowOriginIndex
Constants ¶
const ( ParamField = "field" ParamMethod = "method" ParamHoldSpread = "hold_spread" ParamOutput = "output" // ParamRouteAt is the routing floor (DKT-593): a value of the field's // declared order. Clusters whose reduced value sits at or above its // position are emitted to the step's output payload — the wire the // threshold and the loop read — and the rest go to the record. Like every // other value here it is an OPAQUE TOKEN compared only by position: core // does not know it is a severity, only where the author put it. ParamRouteAt = "route_at" // ParamSourceField names a property of each input element holding an // array of opaque source labels (DKT-2462) — a step ref such as // `review@0#0` naming the artifact a member came from. Core reads this key // ONLY to know which OTHER key to read: carry-through already moves an // array under any such name through to Aggregate's output untouched, so // this param adds no reduction behavior of its own. It exists so the run // report can group clusters by producing executor without core hardcoding // a corpus-owned name like `member_sources` — the same reason `route_at` // takes its floor as a value rather than assuming a field name. ParamSourceField = "source_field" )
Aggregate params. Core reads exactly these six keys of the opaque bag and register time refuses any other, so "the engine ignored my param" is a register-time sentence rather than a run-time mystery.
const ( MethodMedian = "median" MethodMax = "max" MethodMin = "min" )
The three reductions. There are exactly three because the specification names exactly three; each addition would be core acquiring an opinion about what is worth computing.
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" // KeyOperatorSetMirrors names the OTHER threshold fields a correction was // carried onto (DKT-1548). Those keys are the author's, not core's, so the // one occasion core writes them is recorded beside the decision that caused // it — a reader of the resolved element can see which facts the operator's // value displaced instead of having to infer it from value equality. KeyOperatorSetMirrors = "operator_set_mirrors" )
Output keys of the aggregate's own payload. 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.
const ( // RegistrationKindSchema and RegistrationKindWorkflow are the only two // things core REGISTERS. Everything else under `.docket/config/` is PINNED, // which is what core does with instance files it does not understand. RegistrationKindSchema = "schema" RegistrationKindWorkflow = "workflow" // RegistrationNew and RegistrationUnchanged are the report's `outcome` // values. RegistrationNew = "new" RegistrationUnchanged = "unchanged" )
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).
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" // RowParkHeld: the step is still pending, and its issue is parked on an // operator decision (CondIssueParked), which holds every pending row of // that issue — typically the staged successors of the step that parked. // Like the parked step's own `recorded` row, it does not make the verify // fail: the park, not a conflict, is what keeps it out of the recomputation. RowParkHeld = "held-by-park" )
The verdicts a stored manifest row can draw (DKT-243).
const ( // StageBackfill is stage 1: the usage rows the wave measured. StageBackfill = "backfill" // StageVerify is stage 2: the manifest still renders as it was stored. StageVerify = "verify" // StageClose is stage 3: the manifest is reconciled and retired. StageClose = "close" )
One-verb wave reconcile (DKT-580).
A conductor closing a wave typed the same pipeline every time — back-fill what the wave measured, verify the manifest still matches, close it — once per wave, five times in harness RUN-44 alone. Nothing about that pipeline is a decision: the ORDER is forced (usage must land before the close's probe reads it, and a close over a drifted manifest is the thing verify exists to stop), and every retype is a chance to name the wrong journal, pipe a stale file, or skip the verify entirely — the three ways a ledger silently starts lying.
THIS ADDS NO NEW SEMANTICS. It is the same three engine calls in the same order, and each stage is the EXACT function the standalone verb calls — BackfillUsage, VerifyDispatch, CloseDispatch — never a re-implementation. That is what makes the discrepancy probe see a reconciled back-fill exactly as it sees a hand-typed one: `usage_recorded` is set by the same MarkStepUsageRecordedTx inside the same BackfillUsage transaction, and `missingUsage` reads that column. There is no second codepath that could drift from the first.
WHAT "ATOMIC" MEANS HERE, precisely, because it is not one transaction and cannot be:
- Each stage is individually all-or-nothing. BackfillUsage is one transaction over the whole batch; VerifyDispatch writes nothing at all and is rolled back by construction; CloseDispatch is one transaction.
- A stage that fails STOPS THE SEQUENCE. Verify never runs on a failed back-fill, close never runs on a failed verify, and the run is left in whatever state the last SUCCESSFUL stage produced — never half-closed.
- Every failure NAMES ITS STAGE (StageError), so an operator reading a refusal knows which of the three verbs to reach for.
The three could not share one transaction even if that were wanted: VerifyDispatch's stale-target pass shells out to git and must therefore run with no transaction open (no subprocess inside a transaction), and its read-only-by-rollback property is the mechanism that makes it a verify at all. Nesting it inside a writing transaction would destroy the very property verify depends on.
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 the attribution 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. 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). The closed set gains exactly four, and // they are listed here so the attribution 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 for the same at-least-once observability reason // `gate-started` exists. // // `trust-added` / `trust-removed` are what make the self-trust residual // 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. // // The vote lifecycle requires both by name: opening a vote writes a // `vote-opened` event, and tallying writes a `vote-tallied` event with the // score. So the closed set gains SIX at stage 4, not four. // // They earn their place on the same argument that keeps `gate-unmatched`: // a vote opening and a vote tallying are transitions an operator follows in // the feed, and the attribution rule 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). // // The closed set gains exactly ONE, and it earns its place on the same // argument every kind above earns it: the attribution rule 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. 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). // // The closed set gains exactly THREE, and each is a transition the // attribution rule requires to be attributable. A dispatch whose TTL lapses // is lazily auto-abandoned by `next`, and that abandonment is event-logged // as `dispatch-abandoned`. // // `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 the lazy TTL path, // `abandoned` for the 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" // `dispatch-extended` is the append: rows the engine minted or readied // while the manifest was open, added to it rather than waiting for the // next open. It is its own kind for the reason `dispatch-closed` and // `dispatch-abandoned` are separate — an operator reading the feed must // see that the batch GREW, and against which stored manifest, without // opening a row. It carries `dispatch`, `rows` (the appended count), // `expires_ms`, and `extended_seq`: the log position the appended rows // were computed at, the same fact `opened_seq` records for the open. EventDispatchExtended = "dispatch-extended" // The write-reap acknowledgment kind. // // It earns its place because the ack must be ATTRIBUTABLE: that is a // requirement of the mechanism, and the attribution rule 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 (`guard-spawn` | // `dispatch-open`) and never a user identity, because core has no identity // model. EventReapAcknowledged = "reap-acknowledged" // Stage 7's two kinds. // // `events-pruned` earns its place on the argument every kind above earns it // — the attribution rule 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 earlier reasoning did NOT already cover. A // `budget-breached` kind was refused 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 gap that existed 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 the attribution 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 the attribution 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 the attribution 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 the attribution 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 batch gate-override kinds (DKT-546). // // Both earn their place on the attribution argument in the `spawn-admitted` // form: each records a park being STEPPED PAST on standing authority. // `gate-override-granted` is the authority being minted — one operator // ruling that a gate's failure signature is environmental for the rest of // the run — and it carries `gate#grantid` so the grant row the feed names // is findable. `step-batch-overridden` is the authority being SPENT: a // step whose failed gates were auto-passed under that ruling, carrying the // covering grant id(s). Without the second kind an auto-applied override // would be indistinguishable in the feed from an engine-computed pass — // the one case where "no event" and "the gates passed" say the same thing // while meaning opposite things. EventGateOverrideGranted = "gate-override-granted" EventStepBatchOverridden = "step-batch-overridden" // The stale-target waiver being minted (DKT-742) — the same attribution // argument as `gate-override-granted`: a warning that stops appearing is // otherwise indistinguishable in the record from a warning that stopped // being true, and this kind is what says an operator ruled rather than // git relented. It carries `targetsha#waiverid` so the waiver row the // feed names is findable. The waiver being SPENT records no event, // deliberately: the advisory is recomputed by `dispatch verify`, which // writes nothing by contract, and an advisory suppressed is not a // transition — no step changes status because a warning stayed quiet. EventStaleTargetWaived = "stale-target-waived" // The activation snapshot's scope being REFRESHED mid-run (DKT-869). // // It earns its place on the `run-repinned` argument, one column over: this // is the only other transition that moves a frozen premise while a run is // live. Every packet an issue's remaining steps render, and every diff they // record, read `run_issues.issue_snapshot`; without this kind a reader of // the ledger comparing two steps of one run would see two different // declared scopes with nothing between them explaining the difference — // which is exactly the drift the freeze exists to prevent, reintroduced // silently. It carries `from`, `to`, the step instances the refresh // reaches, and the operator's reason, so the discontinuity is dated and // attributable rather than inferred. EventIssueScopeRefreshed = "issue-scope-refreshed" // The activation snapshot's DESCRIPTION being refreshed mid-run (DKT-2291). // // It earns its place on the same argument as its scope twin, one column // over again: `run_issues.body_snapshot` is the other half of the premise a // packet states, and RUN-98/HRN-830 is what a run looks like when only one // half can move. That packet rendered AC1's superseded wording under a // `scope:` header already carrying the same operator ruling's other half — // partial staleness, where the fresh half is positive evidence the ruling // was applied. It carries the superseded and refreshed `body_sha256`, the // body it superseded, the step instances the refresh reaches, and the // operator's reason, so a reader comparing two steps of one run can date // and attribute the discontinuity rather than infer it. EventIssueBodyRefreshed = "issue-body-refreshed" // The activation snapshot's attachment LIST being re-frozen mid-run. The // `files` key of `run_issues.issue_snapshot` is what `issue.files` renders, // so a detach reaches a live run's packets only through this refresh. It // carries the frozen list, the live list that replaced it, the step // instances it reaches, and the operator's reason. EventIssueFilesRefreshed = "issue-files-refreshed" // A step's recorded `issue.diff` being RE-PINNED to another tree // (DKT-1034): `step resolve --worktree` recomputed the diff and its target // sha from a checkout an operator patched out of band, and recorded the // result as a new artifact superseding the step's previous one. // // It earns its place on the `run-repinned` argument, one column over // again: the round record is the frozen premise every downstream review // packet renders its target from, and DKT-725/DKT-741 settled that a // recorded reference is never silently re-derived. This is the one verb // that moves it on purpose, so the trail must date and attribute the move // — it carries the STALE sha and the RE-PINNED sha, the checkout measured, // and both artifact ids, so a reader comparing a review packet against // the commit the executor actually recorded can see where the two parted. // Without it, RUN-67's review round judging fe5db34e while the branch // carried e642afb would be indistinguishable in the record from a packet // that rendered correctly. EventIssueDiffRepinned = "issue-diff-repinned" // A run note being recorded (DKT-1079): `run note add` put a standing // statement into every packet the run renders from that moment on. // // It earns its place on the `run-repinned` argument, in yet another // column: a note is the third thing that changes what a step's packet // says while the run is live, beside a repin and a scope refresh. Two // renders of one step that differ by a `== RUN NOTE` section would // otherwise be indistinguishable in the record from the packet drift the // snapshot discipline exists to prevent. It carries the note's id and its // text verbatim, as `step-annotated` carries its annotation, so what // every later worker was told survives in the feed itself. EventRunNoteAdded = "run-note-added" // The conductor capability's mint (DKT-2465). `conductor-seated` records // that a run's conductor capability was minted or re-minted by `run // conduct` — the one token-free route to the capability the seven operator // verbs require, so the feed must name who took the seat and from where. // A first activation's mint needs no kind of its own: `run-activated` // already records that transition, and the capability is one of its // effects. EventConductorSeated = "conductor-seated" // A security-load-bearing config key being WRITTEN (DKT-2766, DKT-2767). // `docket config set` of any `vote.rule.<name>.*` key records one of // these in the same transaction as the write, carrying the key, the // scope, the prior and the new value, and who wrote it. // // It earns its place on the `run-repinned` argument in one more column: // a vote rule's threshold is a premise every tally under it is judged // against, and `config set` has no per-caller authorization — so a // rewrite of that premise is a transition an auditor must be able to // date and attribute, or a ballot approved at 0.5 is indistinguishable // in the record from one approved at the 0.8 the rule declared last // week. Detection where prevention is not built (operator decision, // 2026-09-17). Like `project-registered` it has NO RUN: a config write // belongs to a project, not to any run of it. EventConfigChanged = "config-changed" // EventDispatchCloseRefused (DKT-2758) records a `dispatch close` that // refused CONFLICT: unintegrated commits, no open dispatch, unreconciled // discrepancies, nothing to accept, or a lost close race. A refusal // changes no state, but it is a coordination fact a store-only retro // must be able to count, and without the event it reached only the // conversation that drove the wave. It carries the run, the reason, and // the dispatch when one was open. EventDispatchCloseRefused = "dispatch-close-refused" // EventClaimRefused (DKT-2776) records a `step claim` that refused // CONFLICT: an unclaimable kind, a step not ready (headroom included), // a lost CAS against a live lease, a claim lost while its pre-gates // ran, or a budget refusal. Like a refused close it moves no state of // its own, but contention is a coordination fact a store-only retro // counts. It carries the step, the requesting owner, and the reason. EventClaimRefused = "claim-refused" // EventStepReminted records a same-owner re-claim that re-keyed its own // live lease (reMintOwnClaim). The re-mint voids the previous token and // writes no `step-claimed`, so without this event a re-key, benign or // not, leaves no trace. It carries the step, the owner, and the attempt. EventStepReminted = "step-reminted" // EventVoteReseated and EventStepDeferred (DKT-2759) are facts the // conductor reports through `run fact add`: a vote seat it re-spawned, // and a step it held back on budget or behind a chain (the cause rides in // data). Both carry the actor and cwd that reported them. `dispatch // close` also derives a chain deferral (`data.source` is "engine", // `data.dispatch` names the manifest) behind a predecessor a wave // attempted. A predecessor skipped before its claim leaves no event, so // the conductor keeps filing every chain deferral. EventVoteReseated = "vote-reseated" EventStepDeferred = "step-deferred" // EventParkRetryEligible (DKT-4257) records that a step parked gap-only has // every depends_on or blocks prerequisite of its issue done, one of them linked or changed after the park, so the park is a retry // candidate for the advisor-first path. `dispatch open` writes it, once per // park (data.row_version names the park it was written for), because a // conductor that lost its parks to a context reset otherwise holds them // until an operator asks. EventParkRetryEligible = "park-retry-eligible" )
The CLOSED SET of event kinds. Phases 2 and 3 named the kinds they emit; phase 4 completes the set with the two loop/join kinds and enforces it.
The attribution rule — 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.
const ( // AuthorityOperator: a person decided, or a harness relayed that person's // decision. The commonest case, and the one a default would have hidden. AuthorityOperator = "operator" // AuthorityStandingGrant: a recorded standing authorization was applied — // a run-wide ruling covering this class of resolution. AuthorityRef names // it, and is REQUIRED, because a standing authorization that cites nothing // is the unattributable note this field replaces. AuthorityStandingGrant = "standing-grant" // AuthorityConductor: the conductor decided on its own reproduction, with // no operator in the loop. Distinguishing this from an operator's decision // is the whole reason the field exists: both used to read as a note. AuthorityConductor = "conductor" )
The three authorities a resolution can be made under (DKT-1899). The set is CLOSED because the conductor policy distinguishes exactly these cases, and a fourth spelling of one of them would make the rollup uncountable again.
const ( // EvidenceArtifact is `artifact:ARTIFACT-N` (a bare N is accepted and // rewritten to the canonical spelling before the cast records). EvidenceArtifact = "artifact" // EvidenceGate is `gate:<name>`, a gate name the run recorded a result // for, in any of its steps. EvidenceGate = "gate" )
Evidence reference schemes — the part before the first colon.
const ( VerdictPass = "pass" VerdictFail = "fail" // VerdictUnmatched is a FIRST-CLASS OUTCOME, added with the real runner. // // Each command must match a trust entry, or it is NOT EXECUTED and is // 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.
const ( // ResolveRetry RESETS THE RETRY BUDGET for the step instance ("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. 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.
const ( PinEpochActivation = "activation" PinEpochRepin = "repin" )
PinEpochOrigin values.
const ( // DetachedPreGateMeasured: the gates ran and their rows are recorded. DetachedPreGateMeasured = "measured" // DetachedPreGateReused: a complete result for this step and target was // already recorded, so nothing ran. DetachedPreGateReused = "reused" // DetachedPreGateRunning: another run holds this step and target's lock. DetachedPreGateRunning = "running" // DetachedPreGateStale: the step no longer resolves this target, so a // measurement of it would serve no claim. DetachedPreGateStale = "stale" // DetachedPreGateClosed: the step is over, or measures its own worktree // and never a sha, so nothing it does could serve this result. DetachedPreGateClosed = "closed" )
Outcomes of one detached run, as DetachedPreGateRun reports them.
const ( FindingBlocker = "blocker" FindingConcern = "concern" FindingSuggestion = "suggestion" )
The three lists a structured finding can come from — CastFinding.Kind's closed vocabulary, core's own and never stored text.
const ( // SeatPathVoteStep: the proposal is keyed under the run's vote-step // family (voteIdempotencyPrefix) — an engine-minted `type = "vote"` step // row, whose panel is seated in-wave. SeatPathVoteStep = "vote-step" // SeatPathConversationalGate: everything the run's machinery convened // OUTSIDE a vote step — a reap-ack ballot keyed under // ReapAckProposalKey's family, or a proposal whose text names the run (an // activation panel opened with `vote create`). These panels are seated // conductor-side. SeatPathConversationalGate = "conversational-gate" )
The two seating paths a run's vote seats are minted through (DKT-733). These are the values SilentVoteSeat.Path carries, and they are core's own closed vocabulary — derived from HOW the proposal joined the run's membership, never from anything a caster asserted.
const ( RunFactVoteReseated = EventVoteReseated RunFactStepDeferred = EventStepDeferred )
Run-fact kinds, as `--kind` spells them.
const ( DeferralBudget = "budget" DeferralChain = "chain" )
Deferral causes for RunFactStepDeferred.
const ( // DetachedSagaRan: this invocation held the lock and resumed the saga. DetachedSagaRan = "ran" // DetachedSagaRunning: another resumer holds the lock; nothing was run. DetachedSagaRunning = "running" )
Outcomes of RunDetachedSaga.
const ( StepCommentMaxBytes = 16 * 1024 StepCommentMaxCount = 4 )
Caps on the comments one step may record. The body cap is the artifact cap's order of magnitude on purpose: a comment is prose for a person, and a record larger than this belongs in an artifact the comment cites.
const ( DiffFieldLines = workflow.DiffFieldLines DiffFieldFiles = workflow.DiffFieldFiles DiffFieldEmpty = workflow.DiffFieldEmpty )
The reserved `diff.*` family (DKT-2063).
These three fields are NOT payload fields and never reach a payload schema. They name the engine's own measurement of the change a tree-holding step recorded, which is why they are reserved rather than registered: a step's payload is whatever its executor chose to emit, and `implement` emits a markdown change-summary with no payload at all — so the facts a change track wants to route on (how big was the change, was there one) exist in the ledger and no payload predicate can reach them.
They are ORDERED WITHOUT AN ORDERED_ENUM, and that is not a hole in T3. T3 exists because core does not know whether `high` outranks `medium`; it does know that 21 > 20. `diff.lines` and `diff.files` are counts the engine itself computed, compared numerically, so there is no order to guess and nothing to park on.
The names are the workflow package's (DKT-2518, DKT-2551): the validator decides at register time where a `diff.*` predicate may appear and what it may compare against, and this package imports that one, so the vocabulary lives there and is read here — one spelling, as VoteCastFields is.
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" // PinUnpinnedReference: not a verdict on a pin — there IS no pin. The // pinned bytes reach a file this run never snapshotted, so every step whose // packet resolves it refuses at claim (VALIDATION_ERROR). PinUnpinnedReference = "unpinned-reference" )
Pin verdicts. They are constants rather than free strings because a consumer branches on them, and `--json` makes them part of the wire contract.
const ( // ArtifactKindIssueDiff is the kind the engine records a computed VCS diff // under. 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.
const AttachmentMaxBytes = exec.CaptureCap
AttachmentMaxBytes caps the bytes one `issue.files` attachment may inline into a packet. It is the gate-output capture cap, the engine's existing bound on bytes headed into an executor's context, and a CONSTANT, not config.
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.
const FactSourceEngine = "engine"
FactSourceEngine marks a `step-deferred` event the engine derived itself, in `data.source`. A conductor-filed fact (`run fact add`) carries no 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.
const GateExecutedCopyAbsent = "executed-copy-absent"
GateExecutedCopyAbsent is the status of a gate whose relatively named script does not exist under the invoking exec root. A gate run there cannot open it and fails rather than running other bytes, so it is reported without making PinReport.Sound false.
const GateExecutedCopyChanged = "executed-copy-changed"
GateExecutedCopyChanged is the status of a gate whose script, named relative to the cwd, resolves under the invoking exec root to bytes other than the pinned ones: the copy a gate run from this checkout would execute is not the copy the run pinned. It makes PinReport.Sound false.
const GateStepCopyAbsent = "step-copy-absent"
GateStepCopyAbsent is the status of a gate whose relatively named script does not exist under a work root a step of the run recorded, or of the one row a work root that no longer exists gets. Like GateStepCopyChanged it never makes PinReport.Sound false.
const GateStepCopyChanged = "step-copy-changed"
GateStepCopyChanged is the status of a gate whose relatively named script resolves, under a work root a step of the run recorded, to bytes other than the pinned ones, or to a file that cannot be checked. A gate run in that work root would execute that copy. It never makes PinReport.Sound false, because an issue may legitimately edit a gate script.
const GateUnchecked = "unchecked"
GateUnchecked is the status of a declared gate whose script could not be checked at all: the trust store is unreadable, the repo identity does not resolve, or no trust entry matches the gate. It is distinct from GateUnpinned so a report with nothing checked never reads as all pinned, and like GateUnpinned it never makes PinReport.Sound false.
const GateUnpinned = "unpinned"
GateUnpinned is the status of a declared gate whose trusted argv names no file this run pinned. It is a report, not a refusal: it never makes PinReport.Sound false, because most trust entries invoke a build tool (`make <target>`) rather than a script, and failing every such run would turn a visibility feature into a blanket block.
const HolderUnacknowledgedReap = "unacknowledged-reap"
HolderUnacknowledgedReap is the status a Holder carries for a lapsed writer whose reap nobody has acknowledged: it holds a slot until the ack, whatever the step's own status says.
const IssueCommentArtifactKind = "issue-comment"
IssueCommentArtifactKind is the auxiliary kind a step may record beside its declared emit when its acceptance criterion requires an issue comment (DKT-4272). Like GapArtifactKind it is packaging vocabulary, always emittable, and needs no workflow declaration.
The engine posts the body as a comment on the step's OWN issue, in the same transaction as the recorded artifact. An executor in an isolated worktree then never runs `docket issue comment add` itself, which the harness refuses there.
THE TARGET IS THE STEP'S ISSUE AND NOTHING ELSE. A recorded artifact is executor-authored text, and letting it name another issue would let any step write into any issue's thread. The body is capped and the count per step is capped, so a step cannot flood a thread either.
const MetadataKeyIntegratedSHA = "integrated_sha"
MetadataKeyIntegratedSHA is the step-row key the annotation also writes, so the convention the docket-run skill already followed by hand (`--metadata '{"integrated_sha": ...}'`) keeps holding for readers of the row.
const MetadataMaxBytes = 16 << 10
MetadataMaxBytes caps one `--metadata` bag, on every verb that takes one — `claim`, `complete`, `fail`, `annotate`.
The bag is opaque, so nothing else bounds it. Without a cap a worker can push an artifact-sized body into a column the 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.
const RecordedHeadArtifactKind = "recorded-head"
RecordedHeadArtifactKind is the auxiliary kind a worktree-recorded step carries beside its declared emit: the commit its writer left in the worktree, captured at record time (DKT-4250). Like GapArtifactKind it is packaging vocabulary and needs no workflow declaration.
It exists because the commit is otherwise named only by the diff stage, and that stage runs LAST in the saga, after the completion gates. By then the writer's worktree may already be gone, so the one moment the head can be read from the worktree is the record itself.
const ResolutionIntegratedSHA = "integrated-sha"
ResolutionIntegratedSHA is the `resolution` the re-record's `issue-diff-repinned` event carries, distinguishing it from a re-pin that rode a `step resolve`.
const RoutingPass = "pass"
RoutingPass is the grammar's default: no threshold matches ⇒ pass.
const RunNoteAuthorityUnknown = "unknown"
RunNoteAuthorityUnknown labels a note whose stored authority is not one a writer can record: a row written before the column existed stores "", and nothing infers an authority for it.
const RunNoteMaxBytes = 16 << 10
RunNoteMaxBytes caps one note. A note rides EVERY packet of the run, so an oversized one is paid for by every step; 16 KiB — MetadataMaxBytes' figure, for the same reason — is three orders of magnitude above the motivating use (a gate name, why it is pre-existing, an issue id, and a disposition: a few hundred bytes) and small enough that a note stays a note. Bulk detail goes in the issue the note cites.
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.
const VoteUsageExcludedNote = "vote_usage is excluded from reported and spend: " +
"those sum the step usage_ledger the cap enforcement reads, and seat casts " +
"land in the separate vote_usage ledger; a vote step's declared " +
"expected_cost accrues to the floor instead"
VoteUsageExcludedNote is VoteUsageNote's one value, a constant so the JSON document and the rendered report cannot say it differently.
Variables ¶
var AggregateMethods = []string{MethodMedian, MethodMax, MethodMin}
AggregateMethods is the closed vocabulary, in the specification's own order, for the register-time error message and for SKILL.md's table.
var ErrEmptyTrustRoster = errors.New("the trust roster has nothing to probe")
ErrEmptyTrustRoster means there was nothing to probe — every entry was filtered out (empty roster, or every entry a declared action). That is a finding, not a pass, and is refused rather than reported as an empty success (gate-probe.js's "throws when the roster is empty" rule).
var ErrNotConductor = errors.New("not the run's conductor")
ErrNotConductor is the sentinel under an AUTH_ERROR refusal, so a caller mapping errors can test for it without matching the message.
var LaunchDetachedPreGates func(stepID int, targetSHA string) error
LaunchDetachedPreGates starts one detached pre-gate run for a step and a target sha, outside the calling process. nil means the mechanism is off and the engine runs every pre-gate inside the claim exactly as before.
The CLI installs its launcher in the binary's real entry point only. A test binary must never find one installed: the launcher re-executes os.Executable(), which under `go test` is the test suite itself.
var LaunchDetachedSaga func(stepID int) error
LaunchDetachedSaga starts one detached `step resume` for a step whose stage 1 just committed. Nil means no launcher is installed and the saga runs inline; the CLI installs it in the binary's real entry point only, for the reason LaunchDetachedPreGates documents.
Functions ¶
func ActionNames ¶
ActionNames returns every step name declared `action = "<name>"` across the instance-config workflow roots (DKT-1283 AC5) — the shared corpus and this repository's own `.docket/config/workflows/`.
It exists so a trust roster entry can be classified `gate` or `action` without grepping the TOML files, the way `docket-run`'s gate-probe.js used to: it read `action = "<name>"` out of every `workflows/*.toml` by regex because the trust roster itself carries no class marker. The engine already walks these same roots for auto-registration (scanConfigDirs); this reuses that walk and reads each workflow's OWN parsed grammar instead of a regex.
A workflow file that fails to parse is SKIPPED rather than refused: this is a classification aid for `trust list`/`trust probe`, not a registration gate, and a genuinely broken definition is refused where registration itself matters. The set is best-effort and errs toward under-classifying — a name this misses is reported as a "gate", which is the conservative direction (an action mistaken for a gate merely fails its stdin-less probe; a gate mistaken for an action would go unprobed).
func ActorCounts ¶
ActorCounts rolls the feed up per actor — the report's fifth section.
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 ¶
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 AuthorizeConductor ¶
AuthorizeConductor is authorizeConductor for a CLI verb whose write lives outside the engine (`vote commit`): the verb runs it before writing.
func BlockedReason ¶
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 readiness 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 ¶
BudgetBreachReason is the breach reason's shape:
budget: spend <N> of cap <M> reached at <instance>
A BARE-NUMBER STATEMENT NAMING NO UNIT. 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 CastVote ¶
CastVote is the engine half of `docket vote cast`: authorize the cast against the vote step's pinned switches, then record it through the existing tally. An ad-hoc proposal (no vote step behind it) and a materialized held step (whose minted name the pinned definition never declares) enforce nothing and tally exactly as before. An ad-hoc panel a run attributes also records its vote-opened and vote-tallied events on that run (recordConductorPanelEvents).
A refusal is a VALIDATION_ERROR, raised BEFORE the transaction opens, so a refused cast writes no vote row. The db sentinels (ErrNotFound, ErrConflict) come back unwrapped from the tally, as they always did.
func CheckWorkflowSource ¶
func CheckWorkflowSource(sourcePath, registeredSHA string) *model.WorkflowSourceStatus
CheckWorkflowSource reads the file at a registered workflow's recorded `source_path` and reports whether its bytes still hash to `source_sha256`.
It hashes with workflow.SHA256 — the SAME function registration hashed with — so a mismatch is a real difference in bytes and never an artifact of two hashers disagreeing.
The verdict is never an error: a missing file is a fact about the repository to report, not a failure of the reporting. Callers decide what each state is worth (see checkBoundSources for activation's disposition).
func DecodeMetadataBag ¶
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 (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 DescribeWorkflowOrigin ¶
func DescribeWorkflowOrigin(s *model.WorkflowOriginStatus) string
DescribeWorkflowOrigin renders one verdict as a human-readable clause, so every reader of this fact says the same thing about the same state — the shape DescribeWorkflowSource established.
func DescribeWorkflowSource ¶
func DescribeWorkflowSource(s *model.WorkflowSourceStatus) string
DescribeWorkflowSource renders one verdict as a human-readable clause, so `workflow show` and `run activate` say the same thing about the same state.
func DescriptionEditFrozenForActiveRuns ¶
DescriptionEditFrozenForActiveRuns is ScopeEditFrozenForActiveRuns for an `issue edit --description`: it names each live run that froze a different description for issueID and still holds a non-terminal step that will render it, and points at `run refresh-body`, the verb that makes the amendment reach that run's remaining steps.
The description has no live reader at all — a packet's REQUEST and issue.body both render `run_issues.body_snapshot` — so, unlike the scope advisory, there is no half of the edit that already took effect to report. It is advisory-only and swallows every error to nil for the same reasons.
func DiscrepancyReason ¶
func DiscrepancyReason(ds []Discrepancy) string
DiscrepancyReason renders a list for a refusal, one line per row.
Every line carries its RESOLUTION, because the resolutions are enumerated 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 ¶
EffectiveStatus is the computed status, for any read verb.
A `pending` step reads as `ready` when the readiness 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 ¶
EffectiveStatusCounts rolls one run's steps up by EFFECTIVE status, 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 effective-status 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 ForceReapStepWith ¶
func ForceReapStepWith(conn *sql.DB, stepID int, opts ForceReapOptions) error
ForceReapStepWith 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.
NO LEASE TOKEN — the holder's own token is exactly what a reap cannot require, since the premise is that the holder is dead. What the verb requires instead is the RUN's conductor capability (DKT-2465, conductor.go), plus the assertion, recorded with `--reason`, that the holder is gone: the relay that spawned the executor holds the capability, the executor it is reaping never did, and a sibling executor cannot clear a claim it merely wants out of its way. 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 SCHEDULING 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.
The one deliberate divergence is the ATTEMPT BUDGET (DKT-585): a reap carrying `data.forced` does not count the reaped attempt against `max_attempts`, because the verb's premise is that the holder died before an executor could fail — see the exemption below, and db.ExemptStepAttemptFromBudgetTx for why it is a +1 nudge of the base and never a touch of `attempt` itself.
func FormatDispatchID ¶
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 GateFingerprint ¶
GateFingerprint is the content half of a gate failure's signature (DKT-1796): a SHA-256 over the gate's capture with run-varying text removed, so one operator ruling covers the failure the operator actually read and nothing else.
(gate, exit, reason) alone is a GATE-WIDE waiver in practice. `Reason` is set only for a timeout or a refusal, so an ordinary failing gate's signature is (gate, exit, empty) and every later failure of that gate with that exit rides the same grant — including a real regression nobody looked at, which is the defect three DKT-V417 seats converged on.
THE NORMALIZATION IS DELIBERATELY MINIMAL, and under-normalizing is the safe direction. Text that survives normalization can only make two failures look DIFFERENT — a grant that stops matching parks a step for a human, which is the pre-DKT-546 posture. Text that is stripped makes two failures look the SAME, which re-creates the gate-wide waiver inside the control meant to remove it. So the rules below target the shapes of text that varies between two runs of one unchanged failure. They match by shape, not by role: a duration, a timestamp or clock time, or a `/`-led run of two or more path segments is elided WHEREVER it occurs, including inside assertion text (`want 3s, got 5s` and `want 4s, got 9s` both become `want <dur>, got <dur>`) and inside Go import paths (`github.com/ALT-F4-LLC/docket/internal/tui` becomes `github.com<path>/internal/tui`). What survives is the rest: test names (a subtest path nested three or more levels below its test keeps only its last two levels), file names, line numbers, the words around an elided value, and line order.
The exact rules, in the order normalizeGateOutput applies them, are the contract:
- ANSI CSI/OSC escape sequences are removed.
- Carriage returns are dropped and trailing horizontal whitespace on each line is removed, so CRLF and a progress redraw hash as their plain form.
- RFC3339-ish timestamps and `HH:MM:SS(.fff)` clock times become `<time>`.
- Absolute POSIX paths become `<path>/<tail>`: the leading directories are replaced and the LAST TWO segments are kept. A scratch root or worktree prefix varies per run; the tail is what names the file that failed, and dropping it would let any two failures in different files collide. The match is any `/`-led run of two or more segments, so it also rewrites the part of a Go import path or URL after its first `/`.
- Go-style durations (`1.234s`, `0.5ms`, `2m30.1s`) become `<dur>`. The literal `(cached)` token also becomes `<dur>`, but only where `go test` prints it in place of the duration on a passing-package summary line (`ok \t<pkg>\t(cached)`); anywhere else it survives.
- Integer-with-unit durations (`in 42ms`, `after 3s`) are covered by the same rule, which is why it accepts a bare integer mantissa.
- Leading and trailing blank lines are removed.
Nothing else is touched. In particular line ORDER is preserved and no line is deduplicated or sorted: a re-ordered failure set is a different failure set.
func GitCommitPatch ¶
GitCommitPatch is CommitPatchFn's real implementation: one commit's own patch, `git diff-tree -p` of sha against its parent (`--root` so a root commit renders too), pathspec'd to the issue's scope the way GitDiff is, and carrying GitDiff's own out-of-scope disclosure: the paths the commit touched outside the declared scope are named and their hunks follow under the same marked heading, so a resolution that reached beyond the scope cannot hide behind it. Untracked files do not exist in a commit, so there is no untracked half. A merge commit renders no patch under `diff-tree -p` and records an empty body — the annotation's contract names one ordinary commit.
func GitDiff ¶
GitDiff computes a diff over the given path globs. THIS IS THE ONE DECLARED VCS COUPLING, and it is confined to this function.
It runs OUTSIDE any transaction, always. 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 hazard of a process recording a step on behalf of work done somewhere else capturing 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 GitRangePatch ¶
GitRangePatch renders the combined patch of from..to, tree against tree, the way GitCommitPatch renders one commit: scoped, with the same out-of-scope disclosure. It is RangePatchFn's real implementation (the whole integrated range of a multi-commit writer).
func HeartbeatStep ¶
HeartbeatStep extends a live lease held by token. It does not touch `attempt`: a heartbeat is not a new claim.
func HighestOrdinals ¶
HighestOrdinals returns, per step NAME, the highest ordinal that has an instance for this issue.
It is the basis of the loop rule'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 HoldIssue ¶
HoldIssue parks every pending step of one issue in a run `waiting-human` with park class `held` (DKT-4273), ready and staged alike, and records one `step-held` event per step. It refuses, changing nothing, when a step of the issue is in flight (claimed, running, or gated) or when the issue has no pending step to hold, so a hold never reads as covering work it did not stop. The held steps leave the next dispatch's rows and are listed under the manifest's `held`; each releases through `step resolve --as retry`.
func HoldStep ¶
func HoldStep(conn *sql.DB, stepID int, opts HoldOptions) error
HoldStep parks one pending step `waiting-human` with park class `held`, and records one `step-held` event carrying the reason and the actor.
func IsVoteStepProposal ¶
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 ¶
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 MoveRunWith ¶
MoveRunWith 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.
EVERY MOVE REQUIRES THE RUN'S CONDUCTOR CAPABILITY on a bound run (DKT-2465, conductor.go): a pause, resume or abandon is the conductor's decision, and before the capability existed any executor sharing the checkout could park or end the run it was working in. The check is inside the transaction, after the run is found and before its status is inspected, so a caller without the capability learns only that the run exists.
func OpenVoteProposal ¶
func OpenVoteProposal( conn *sql.DB, step *db.Step, spec *workflow.Step, nowMS int64, ) (int, error)
OpenVoteProposal opens the proposal: 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 ¶
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 ("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 ResolveStepWith 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 PinContent ¶
PinContent is the bytes a run pinned at one config-relative path (DKT-2026).
A rendered packet names its pinned files by ref and sha256, so a contract telling a worker to test something against `policy.toml` named a document the worker had no sanctioned way to open: `policy resolve` answers with routing derived from that file, not with its text. This is the read that closes the gap, and it is deliberately the SAME LADDER the packet resolves entries through — config-relative ref first, the legacy absolute form second — so the bytes a reader gets here are the bytes a packet would have carried, never a working-tree copy that happens to sit at the path.
DRIFT REFUSES RATHER THAN PRINTS. The pin is the run's agreement about bytes; printing an edited file under the pinned ref would hand a worker content the run never agreed to, which is the same silent substitution the resolver refuses at render. The refusal names both hashes, as `run verify-pins` does.
THE RUN RESOLVES BEFORE ITS PIN SET IS READ. An absent run and a run that never activated both list zero pins, and the ladder would report either as an unpinned path with a repin remedy neither can use. A missing run is NOT_FOUND, as `run verify-pins` answers; an unactivated one has no pin set.
It writes nothing: no lease, no lock, no event.
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 ¶
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 PrimeDetachedPreGateLockDir ¶
PrimeDetachedPreGateLockDir fixes the in-flight lock directory from a config the caller already resolved, so no scheduler snapshot pays a git resolution for it. The CLI calls it once at entry, after config.Resolve and before any verb opens a transaction. A nil config leaves the lazy default.
func ReapAckProposalKey ¶
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 ¶
ReapHoldReason renders the hold's guidance: each seq, and the exact flag to pass.
It NAMES THE FLAG rather than describing it, because "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 a later group; 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 RecordConductorPanelOpened ¶
RecordConductorPanelOpened records a step-less panel's vote-opened on every run that attributes it and has not recorded it yet. `vote link` calls it once the link exists; a panel no run attributes records nothing.
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 ¶
RecordTrustEvent writes a `trust-added` / `trust-removed` event.
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 the self-trust 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.
BOTH ARE MANDATORY, AND THE REFUSAL LIVES HERE (DKT-595). The 2026-08-19 batch — 43 unattributed trust events, twelve of which widened network egress — came from a writer that supplied neither field, and the ledger could not say whether that writer was an older binary or a non-interactive path. That ambiguity is unrepairable after the fact, so it is closed at the door: this function, the single entry point through which a trust event reaches the table, refuses a grant whose Actor or Cwd is empty, BEFORE anything is written. Guarding here rather than only at the CLI call site is what makes the property hold for a future non-CLI writer too.
func RegistrationOrder ¶
RegistrationOrder sorts config files into the order auto-registration must apply.
**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 "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, beside the code that creates the dependency.
Within each group the order is lexical by path, for determinism.
The DIRECTORY SCAN that feeds this lives in autoregister.go. The scan hands its paths here rather than re-implement the ordering, so this stays the ONE definition of what the order is — and autoregister_test.go asserts 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 the human-mode report.
"VERBATIM" MEANS EVERY BYTE IS ACCOUNTED FOR, NOT THAT RAW BYTES REACH THE TERMINAL. 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. The report'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).
A gate whose match carries a warning gets its own `warning:` line, whether or not it is also a stub: it resolves and will run, but the operator has something to act on before it does.
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 renders 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.
An activation that registered NOTHING prints no block at all — not an empty one — so 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. 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 for 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 input resolution down to the ARTIFACT ROWS, for a consumer that needs a field the bundle does not carry.
It exists because the bundle is the WORKER-FACING shape: a `ContextInput` has a `body` and no `payload`, since an executor gets the artifact's text and nothing else. The `aggregate` builtin reduces PAYLOADS, 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 ¶
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 ResolveSeats ¶
func ResolveSeats(conn *sql.DB, runID int, seats []string, size string, labels []string) ([]model.VoterAssignment, error)
ResolveSeats resolves each named seat against the run's pinned policy.toml, returning the same {model, effort, variant} a vote step row's voter_assignments carry.
A CONVERSATIONAL GATE — ack-reap, activation, budget, fix-batch — has no step row for routing to ride on, so its panel is seated by a conductor whose contract forbids choosing a model, tier or effort. This is that lookup, from the run's own pinned policy: it calls the SAME (*policyDoc).ResolveSeat resolveRowPolicy calls, so the never-list merge, the [security] ceiling clamp, the [sizes] starting variant and the [escalation.fallback] redirect apply identically.
size stands in for the frozen Size a vote step row snapshots; empty resolves as an unsized row does. labels stands in for the issue labels the row snapshots. A conversational gate carries none of its own, so the caller supplies whatever governs the [security] sensitivity check and the [sizes] label fallback; empty means "not sensitive by label", exactly as an unlabelled row resolves.
EVERY SEAT RESOLVES OR THE CALL FAILS. A caller building a judge panel needs the whole roster, and returning the seats that happened to resolve would seat a panel nobody chose while hiding which seat the policy has no row for.
func ResolveStepFilter ¶
ResolveStepFilter maps `--step STEP-N` onto a step id, refusing a step that does not exist, for the same reason ResolveRunFilter refuses a missing run: a filter over nothing would answer with a successful empty page. Only the joinable id is accepted, never a `name@k#i` instance label, which collides across issues in one run.
func RunAdvisories ¶
func RunAdvisories(conn *sql.DB, runID int) ([]UnexpandedIssue, []RetryEligible, error)
RunAdvisories is `run status`'s pair of advisory lists, the unexpanded issues and the retry-eligible parks, read over one step list and one relations scan.
func RunFloorTx ¶
RunFloorTx is the floor query, plus DKT-584's vote-step accrual:
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'
+ SELECT COALESCE(SUM(expected_cost), 0)
FROM steps WHERE run_id = ? AND kind = 'vote'
AND status NOT IN ('skipped', 'superseded')
THE SECOND SUM IS DKT-584. A vote step is unclaimable by construction, so it can never write the `step-claimed` event the first sum counts — which made a declared `expected_cost` on a `type="vote"` step inert: the author stated what the panel would cost and the floor could not see it (RUN-34 ran 40.6% of its output tokens through seats the budget never saw). A vote step therefore accrues AT MATERIALIZATION — the step row's existence is the engine-owned fact, written at expansion (or when the engine mints a held ballot), exactly as the claim event is for executor work. It accrues once per row: a loop's next ordinal is a new row and re-accrues (the analogue of loop entries re-accruing), and a materialized held ballot accrues the configured `vote.hold.cost`. The reservation side is adjusted to match — see reservableCost: a vote step's cost is already IN the floor, so the headroom check and the offer must not reserve it a second time.
Every floor rule falls out of this query rather than needing its own code:
- 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.
- 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: "bounded loops bound the floor" arrives from loop expansion rather than from arithmetic here.
- A superseded, skipped, or never-claimed step contributes nothing: the accrual is per CLAIM EVENT, and a step never claimed produced none. A vote row accrues at materialization instead, so the second sum excludes skipped and superseded rows: a panel that is never routed costs nothing.
- 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. DKT-867 adds ONE exception, per claim rather than per step: a claim whose dispatcher declared a variant-scaled cost (`step claim --cost-multiplier`) accrues the scaled cost its own `step-claimed` event carries in `data.expected_cost`, and the step row's declaration is only the fallback. The event is still the engine-owned fact — the scaled number was committed WITH the claim, in the same append-only log the floor has always summed — so replay, audit, and the no-lost-update property are untouched. Events of non-terminal runs are unprunable, so enforcement can never lose a live run's scaled accruals.
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 ScopeEditFrozenForActiveRuns ¶
ScopeEditFrozenForActiveRuns names the live runs an `issue edit --scope` on issueID did NOT reach, and the abandon + re-plan path that would (DKT-741).
It fires only when the widen is actually invisible somewhere: the issue is bound into a NON-TERMINAL run that has already been activated (so a snapshot exists), that run still holds at least one NON-TERMINAL step for the issue (so a packet will still be rendered from the stale snapshot), and the live scope genuinely DIFFERS from what that run froze. A re-declaration of the same globs, an edit to an unactivated `planning` run, and an issue whose steps have all recorded are all silent — none of them has anything to discover.
It reports rather than refuses. The write to `issues.scope_globs` is real and does take effect for scheduling, so the operator is told what landed and what did not, not stopped. Advisory-only also means it is safe to call after the edit commits, which is where `issue edit` calls it: the answer is a property of the run's frozen snapshot, which the edit did not touch.
Every error is swallowed to nil, matching OverridePassSkipsInterposedTargets: an advisory that cannot be computed must never fail the verb that already succeeded.
func ScopeWithinDomain ¶
ScopeWithinDomain reports whether EVERY glob of a scope lies inside a workflow's declared `[match] domain_paths` (DKT-1182).
It is deliberately NOT ScopesIntersect, in two ways:
- It is DIRECTIONAL. "This issue's work belongs to that domain" is a containment question, and intersection answers a different one: `internal/**` intersects `internal/tui/**` while saying nothing about whether the issue is TUI work.
- It errs toward SILENCE, where ScopesIntersect errs toward reporting. The costs are reversed: a missed exclusion corrupts a working tree, while a spurious binding warning trains an operator to skim past the ones that are real. So EVERY glob must be inside — an issue scoped half to a domain and half elsewhere is genuinely cross-cutting, and which pipeline should own it is a judgment this lint has no basis to make.
An empty scope or an empty domain is never inside anything: "no scope declared" is lintUnscopedHolders' subject, and a workflow that declares no domain has opted out of this lint entirely.
func ScopesIntersect ¶
ScopesIntersect reports whether two scope-glob LISTS conflict.
S1: an empty or absent list never excludes and is never excluded. 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 SetConfig ¶
SetConfig is the engine's config write: db.SetConfigTx plus, for a vote-rule key, the `config-changed` event, committed together or not at all.
`changedBy` is the identity the activity log attributes issue edits to (config.DefaultAuthor at the CLI). It is recorded verbatim and unverified, like every other self-asserted name in the store — the point is a trail, not a proof. A write to a key outside the vote-rule family records nothing, and a refused write (unknown key, invalid value) records nothing because it wrote nothing.
func StaleLineage ¶
StaleLineage is the INERT half of the supersede rule, 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 ¶
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. 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 here 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 ¶
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 ¶
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 ¶
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 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 UnintegratedReason ¶
func UnintegratedReason(steps []UnintegratedStep) string
UnintegratedReason renders dispatch close's refusal, one line per finding — DiscrepancyReason's shape, for the same reason: an operator needs every unintegrated commit at once, not one refusal per retry.
func UsageBudgetBreachReason ¶
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.
func ValidateCastEvidence ¶
ValidateCastEvidence checks every evidence reference in f against the run the proposal was opened for, and CANONICALIZES artifact references in place (`artifact:3` becomes `artifact:ARTIFACT-3`) so the stored row and the run report spell one id one way.
A nil f, or one whose entries cite nothing, passes without a read — an ordinary cast is byte-for-byte the cast it always was, refusals included (db.CastVote still owns "no such proposal" for it). With evidence present, a missing proposal is NOT_FOUND, a proposal bound to no run and every unresolvable reference is VALIDATION, each naming the reference and why.
func ValidateEventKind ¶
ValidateEventKind refuses a `--kind` outside the closed set, naming the valid kinds. An unknown kind would otherwise match nothing and read as "no such event happened", the silent miss the filter exists to end.
func VoteProposalRun ¶
VoteProposalRun returns the run whose vote step opened a proposal. The second return is false for a proposal bound to no run: a conversational ballot, or a caller-forged key whose run part does not parse, which DriveVoteProposal likewise routes nowhere.
func WorkflowsReferencingSchema ¶
func WorkflowsReferencingSchema( conn *sql.DB, projectID int, name string, version int, ) ([]string, error)
WorkflowsReferencingSchema lists the workflow versions in one project that are still IN SERVICE and whose definition names schema `name@version` as a step's `payload`.
It is the question `docket schema deprecate` asks before retiring a version: a workflow that still binds and still references the schema would have its next registration or activation refused by the retirement (V25a's retired branch), so the verb refuses first and names the referencers instead of leaving that refusal for the next operator to find.
RETIRED WORKFLOW VERSIONS DO NOT COUNT. A workflow already out of binding cannot be newly activated, and a run that pinned it keeps its pins regardless; counting it would make a cleanup pass unable to retire a schema whose only referencers were themselves already cleaned up.
The definitions are read from the stored canonical form — what activation reads — rather than re-parsed from the recorded TOML, so what is checked is what would run.
Types ¶
type AbandonDispatchOptions ¶
AbandonDispatchOptions carries `dispatch abandon`'s inputs. Token is the run's conductor capability, required once the run is bound.
type AbandonIssueOptions ¶
type AbandonIssueOptions struct {
RunID int
IssueID int
// Reason is `--reason`, required.
Reason string
// Token is the run's conductor capability (DKT-2465, conductor.go):
// required on a bound run, ignored on an unbound one.
Token string
NowMS int64
}
AbandonIssueOptions are `run abandon --issue`'s inputs.
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 AbandonIssueInRunWith ¶
func AbandonIssueInRunWith(conn *sql.DB, opts AbandonIssueOptions) (*AbandonIssueOutcome, error)
AbandonIssueInRunWith 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.
It requires the run's conductor capability exactly as the whole-run abandon does (DKT-2465): the per-issue disposition ends a lane's work the same way, and an executor must not be able to end a sibling's.
type ActionResult ¶
type ActionResult struct {
// Kind is the produced artifact's kind — `params.output`.
Kind string
// Body is the artifact body.
Body string
// Payload is the structured half, as JSON text — the PLAIN ARRAY.
// The old `{"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`. A non-empty list defers the step's routing into the saga's
// `held` stage.
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. This is 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.
The seam returns Held and Results as well as a payload, and neither was avoidable. The aggregate builtin requires the held-cluster outcome, and the audit record 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` 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 "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 — a trusted command's params are its author's
// business. The builtin reads exactly six keys (field, method, hold_spread,
// output, route_at, source_field), and register time refuses any other.
Params map[string]any
// Output is `params.output`: the artifact kind this step produces.
// 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 and already
// shape-validated.
//
// 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 `claim` now refuses outright for an action
// step.
//
// The ORDER source is untouched by this: the register-time check rejects 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 (the runtime
// half of the register-time ordered-field check); 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 context object as `docket step context --json` emits it —
// the bytes a trusted command receives on stdin. 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 the workflow definition.
type ActivateOptions ¶
type ActivateOptions struct {
// FilePins are `--pin PATH` (repeatable): arbitrary operator-supplied
// files pinned by path and content hash. 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, 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
// HeadBranch and HeadCommit are the activating checkout's HEAD, read by
// the verb through config.GitHead on the configured exec root and
// recorded on the run-activated event's data (DKT-3263), so a close
// report can range `git log <head_commit>..HEAD` even after the shared
// branch is rewritten. Each is omitted when empty: outside a checkout
// the head is unknown, never invented.
HeadBranch string
HeadCommit 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.
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
// BlockedIssues names, by display id, every bound issue this activation
// left UNEXPANDED, each with the in-run depends_on predecessors still
// holding it and their status as this activation leaves it (DKT-1180: a
// re-activation that expanded nothing carried no field saying why, and the
// cause — a predecessor check gating on issues the blocked one had no edge
// into — took a source audit to find). Populated identically on a dry run
// and a real activation. Nil when every bound issue is expanded, so the
// ordinary run carries no key at all rather than an empty one.
BlockedIssues []BlockedIssue
// ConductorToken is the run's conductor capability, minted by a FIRST
// activation and returned here and nowhere else (DKT-2465): only its hash
// is stored. Empty on a re-activation, a dry run, and a run conducted
// before it activated (the seat's holder already has the token).
ConductorToken string
// 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`. 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
// SourceWarnings names every BOUND workflow whose registered source file
// could not be verified against `source_sha256` — and that activation did
// not refuse over (DKT-590). Drift on a binding this activation MADE is a
// refusal, so it appears here only in the three cases refusing would be
// wrong: an unreadable source (the registered bytes still reproduce; only
// their provenance is gone), drift under a binding inherited from an
// earlier activation (a mid-run edit stays a non-event), and drift
// under `registration.auto = false`, where a registry lagging the corpus is
// the operator's own standing decision.
//
// It travels here for ContextWarnings' reason and takes the same stance:
// the engine holds no output dependency, and the verb picks the channel —
// stderr in human mode, an array in JSON.
SourceWarnings []SourceWarning
// 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
// BindingWarnings names every issue that bound one workflow while its
// declared scope lies entirely inside ANOTHER registered workflow's declared
// `[match] domain_paths`, and lacks only the labels that other workflow
// requires (lintDomainScopeMismatch, DKT-1182).
//
// It is the exactly-one-WRONG-match signal. Activation refuses zero matches
// and refuses several, but a mis-labelled issue matches its wrong workflow
// exactly once, which is the one shape the binding rule cannot see — so it
// bound, ran, and lost the other pipeline's gates with no output saying so.
//
// It travels here for ContextWarnings' reason and takes the same stance:
// binding on the labels the operator applied is legal and sometimes
// deliberate, so this reports and proceeds, and the verb picks the channel.
BindingWarnings []BindingWarning
// Registered is what auto-registration acted on, in registration order —
// schemas first, then workflows.
//
// An activation that registered nothing leaves this NIL and the verb
// prints no `Registered` block — not an empty one — so a repo with no
// config directory is VISIBLY DORMANT IN THE OUTPUT rather than merely
// dormant underneath it.
Registered []Registration
// PinsFromConfig counts the files under `.docket/config/` that were pinned
// rather than registered — since DKT-581, only the PACKET CLOSURE the
// bound workflows reach (packet entries, their `packet_includes`, and
// policy.toml), not every file the scan walked. They are counted, not
// listed: a closure can hold dozens of files and none of them is a
// decision an operator needs to read before approving.
PinsFromConfig int
// NewPolicyPin names the policy.toml a RE-ACTIVATION pinned because the
// run's pin set held none. Waiting rows resolve model, effort, and variant
// against the pinned policy, so from this activation on they adopt a
// policy the run did not start under. Nil on a first activation and on a
// re-activation that pinned no new policy.toml; an inherited policy pin
// keeps its original hash and is never reported here.
NewPolicyPin *PolicyPin
// Fences is the trust report: every harvested fenced command and whether
// a trust entry authorizes it.
//
// Activation must surface what it will bind, including every harvested
// fenced command, verbatim. Harvesting stores the commands; this report
// 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
// RoutingWarnings is DKT-4275's dry-run-only array: bound issues without the
// security label that look security-shaped. It warns; it routes nothing.
RoutingWarnings []RoutingWarning
// RoutingLint states what that lint could check, on a dry run only (nil on
// a real activation, which never runs it). An empty RoutingWarnings is read
// against it: a lint with no lexicon and no labelled issue found nothing
// because it had nothing to compare against.
RoutingLint *RoutingLintReport
// 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.
ONE transaction, seven stages in order, each failing the whole activation:
- Bind — exactly one registered workflow's [match] per issue
- Lint the work DAG — planner.BuildDAG + TopoSort over depends_on
- Pin — the bound workflows and every --pin file
- Snapshot — issue bodies, and {title, kind, labels, scope}
- Harvest fences — declared tags only, literal, hashed
- Expand phase 1 — lazily, for issues whose predecessors are satisfied
- Promote and flip — backlog -> todo, run -> active, events written
NOTHING EXECUTES INSIDE THIS TRANSACTION: 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 ActivationHead ¶
type ActivationHead struct {
Branch string `json:"branch,omitempty"`
Commit string `json:"commit"`
}
ActivationHead is the checkout head the run's LATEST activation recorded on its run-activated event (DKT-3263): the head the shared branch stood at when the run's current roster was bound. Both fields are empty when no activation recorded one (the exec root was not a checkout, or the run activated before heads were recorded) — absence, never a guess.
func LatestActivationHead ¶
func LatestActivationHead(conn *sql.DB, runID int) (*ActivationHead, error)
LatestActivationHead reads ActivationHead for `run status` (DKT-3310). It returns nil when the latest run-activated event carries no commit.
It reads the row directly rather than through ListEvents: that is a cursor feed whose GONE probe refuses a zero cursor once anything was pruned, and a status read must not fail because old events were retired.
type Actor ¶
type Actor string
Actor is one of the FOUR causes the attribution rule requires every transition to be traceable to: every transition in events is traceable to next, a gate, a threshold, or 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 the attribution 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 the attribution audit // exposes. ActorHuman Actor = "human" )
func ActorFor ¶
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: the attribution 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 ¶
ActorCount is one row of the per-actor 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 so two reports of the same rows are byte-identical.
type AggregateOutcome ¶
type AggregateOutcome struct {
// Payload is the output payload, one element per emitted cluster.
// Without `route_at` that is one element per input element; with it, only
// the clusters whose reduced value's position reached the floor.
Payload []map[string]any
// Held indexes the PAYLOAD elements whose spread tripped `hold_spread`. It
// is a list rather than a count because held materialization has to be
// able to say WHICH clusters are open, and a count cannot. The indices are
// positions in Payload — the payload the artifact records and held
// resolution re-reads — which coincide with input positions whenever
// `route_at` is absent.
Held []int
// Recorded holds the clusters `route_at` routed below the floor (DKT-593):
// fully reduced — value, members, demotion trail — but destined for the
// record rather than the loop output. The caller writes them into the
// action's own audit row; they never enter the artifact payload, so the
// threshold and every downstream `inputs` reader see only the emitted set.
// Nil whenever `route_at` is absent, which is what keeps the absent case
// byte-identical to the pre-`route_at` builtin.
Recorded []map[string]any
}
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.
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 — so the grouping arrives in the payload, as an array under the field, and a scalar there is a one-member cluster.
The one-member rule 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
// RouteAt is the routing floor, or "" when the step declares none — the
// absent case, in which Aggregate emits every cluster exactly as it always
// has.
RouteAt string
// SourceField is the property name the run report groups clusters by, or
// "" when the step declares none. Aggregate itself never reads it — the
// verbatim carry-through already moves whatever key this names — it rides
// on AggregateParams purely so complementarityRollup can find it without
// re-parsing the step's raw params.
SourceField string
}
AggregateParams is the validated form of the aggregate params.
func ParseAggregateParams ¶
func ParseAggregateParams(params map[string]any) (AggregateParams, error)
ParseAggregateParams reads the six aggregate keys out of the opaque bag: `field`, `method`, `hold_spread`, `output`, `route_at` (the routing floor), and `source_field` (the per-element source-labels key name).
It is the ONE place core reads inside `params`, and it reads exactly the keys the specification names. The same rules run at register time 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 one index 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 Attribution ¶
Attribution is WHO ran an operator verb and from WHERE — the two claim-level fields a trust event carries (DKT-263/DKT-595), on a step ruling (DKT-2450).
`step approve`, `step reject`, `step resolve` and `step reap` are the four `human` kinds (events_read's attribution table) an auditor most needs a name on: each is a person, or a harness relaying one, deciding what the engine would not. Every one of them recorded only a note, so a ruling a harness relayed was indistinguishable in the feed from one a person typed, and the run report could list rulings without attributing them — while trust events already said who and from where, and reap acks said `acked_by`.
Neither field is authenticated, and neither claims to be: Actor is the git identity the invoking environment reports (config.DefaultAuthor — git user.name, then the OS username, then the literal "unknown"), and Cwd is the directory the verb ran from, the disambiguator between two concurrent sessions that share one identity. Both are resolved at the CLI call site, never inside the engine, for the reason RecordTrustEvent gives: the engine has no business shelling out to `git config`.
type Authority ¶
type Authority struct {
// Kind is one of the three constants above.
Kind string
// Ref names the standing authorization, on AuthorityStandingGrant only.
Ref string
}
Authority is UNDER WHAT AUTHORITY an operator verb was exercised (DKT-1899) — the companion to Attribution's who and from where.
Attribution answers "who ran this"; it cannot answer "were they entitled to, and by what". Across 254 override-pass resolutions those were tellable only from free text: 193 notes quoted an operator, 26 cited a standing authorization, 35 referenced no operator at all — three cases the conductor policy treats differently and no surface could count.
It is a SEPARATE type from Attribution rather than two more fields on it, because the two have different reaches. Attribution covers `step reap` and `run conduct` as well, which assert no resolution authority; Authority covers `run pause` and `run abandon`, which carry no attribution. Folding them together would force every verb to satisfy both contracts.
The zero value is the unanswered question, and require refuses it. Parsing happens at the CLI (ParseAuthority), so an invalid value is a VALIDATION error to the caller rather than a refusal from deep inside a transaction.
func ParseAuthority ¶
ParseAuthority turns the two flag strings into an Authority, enforcing the closed set and the standing-grant reference rule. It returns a validation error, which the CLI surfaces as VALIDATION_ERROR.
The empty kind is refused rather than defaulted: defaulting to `operator` would record the commonest answer for every caller that never answered, and an authority nobody asserted is exactly what this replaces.
func ParseRunNoteAuthority ¶
ParseRunNoteAuthority turns `--authority` into the authority a run note is recorded under: operator or conductor. Every other value, standing-grant included, is a validation error, and the empty value is refused rather than defaulted, as ParseAuthority refuses it.
type AuthorityCount ¶
AuthorityCount is one row of DKT-1899's rollup: an authority, and how many of the run's resolutions and run notes were made under it.
The rollup answers the question the note could not: of this run's resolutions, how many an operator decided, how many applied a standing authorization, and how many the conductor made on its own reproduction. The order is the three values' DECLARED order, not by count, for ActorCount's determinism reason, and an authority with no resolutions is omitted rather than shown as zero.
Resolutions recorded BEFORE this field existed carry no authority and are counted under none of the three: an uncountable resolution is exactly what this replaces, and inventing a bucket for it would put the guess back in the number.
type BackfillOutcome ¶
type BackfillOutcome struct {
Written int `json:"written"`
Steps int `json:"steps"`
// MetadataMerged counts the steps whose record took a back-fill metadata
// bag. Zero when no row carried one.
MetadataMerged int `json:"metadata_merged"`
Source string `json:"source"`
Skipped []SkippedRow `json:"skipped,omitempty"`
// Unclaimed names the targets no worker ever held. The rows for them ARE
// written — see BackfillUsage — so this is an advisory, not a refusal, and
// it is on the payload because `Warn` is suppressed in JSON mode and a
// conductor runs with `--json`.
Unclaimed []UnclaimedTarget `json:"unclaimed,omitempty"`
// MetadataDropped names the steps whose bag was dropped. As with Unclaimed,
// the usage rows for them are written and this is an advisory, not a
// refusal. A step that was reaped for retry lands here and not in
// Unclaimed, because a worker did claim it.
MetadataDropped []DroppedMetadataTarget `json:"metadata_dropped,omitempty"`
}
BackfillOutcome is what the back-fill did: rows written, steps touched, the source they carry, every row skipped as already recorded, every target that was never claimed, and every step whose metadata bag was dropped.
type BackfillRow ¶
type BackfillRow struct {
Step int
Unit string
Quantity float64
// Metadata is an optional opaque KV bag merged onto the step's record in the
// same transaction as the usage row. Core stores it verbatim and reads no
// key in it. On a step that has not reached a terminal status its usage row
// still lands, but the bag is dropped and the step is named on the outcome:
// a live step's record is written under its holder's token, and a back-fill
// has none. A bag that is not a JSON object refuses the batch whatever the
// step's status.
Metadata string
}
BackfillRow is one unit's quantity for one step, as named on the command line. There is deliberately NO attempt field: see BackfillUsage.
type BindingWarning ¶
type BindingWarning struct {
IssueID string `json:"issue"`
// BoundWorkflow is what activation actually bound, as `name@version` —
// named because it is the pipeline that will really run, and an operator
// reading only the domain half would not know what they are getting.
BoundWorkflow string `json:"bound_workflow"`
// DomainWorkflow is the workflow whose `domain_paths` contain the issue's
// scope, as `name@version` — the pipeline the paths say should own it.
DomainWorkflow string `json:"domain_workflow"`
// Scope is the issue's declared scope, verbatim and in declared order, so
// the warning carries the evidence rather than asking the reader to go find
// what the issue claims to touch.
Scope []string `json:"scope"`
// MissingLabels names what would move the binding: every `labels_all` entry
// the issue lacks, plus the `labels_any` list when it holds none of it.
// Never empty — an issue missing nothing already bound the other workflow,
// or hit the exactly-one-match refusal.
MissingLabels []string `json:"missing_labels"`
Reason string `json:"reason"`
}
BindingWarning is one issue whose LABELS and whose SCOPE disagree about which pipeline owns it: it bound `BoundWorkflow` on its labels, while every path it declares lies inside `DomainWorkflow`'s declared domain and the only thing keeping it out of that workflow is `MissingLabels` (DKT-1182).
type BlockedIssue ¶
type BlockedIssue struct {
IssueID string `json:"issue"`
BlockedBy []BlockingIssue `json:"blocked_by"`
}
BlockedIssue is one bound issue an activation did not expand, and why: the in-run depends_on predecessors that are not yet `done` (DKT-1180). It always carries at least one — an issue with no unsatisfied predecessor expands.
type BlockerHop ¶
BlockerHop is one issue on a blocker chain and its status.
type BlockingIssue ¶
BlockingIssue is one unsatisfied predecessor: its display id and the status keeping it unsatisfied — anything but `done`, the one status that releases a successor. Note that `abandon-issue` is not a status: the routing leaves the issue at `todo` with `resolution = abandoned`, so a `todo` predecessor here may be one the run has already given up on, and the successor will then expand only once an operator closes or re-plans it.
type BoundIssue ¶
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 budget line and nothing else. It is scheduling vocabulary: a flag, a config key, or neither.
const ( // BudgetFromRun is `run start --budget N`. BudgetFromRun BudgetSource = "run" // BudgetFromConfig is `docket config budget.default`, used when no // `--budget` was given. BudgetFromConfig BudgetSource = "config" // BudgetUnlimited is neither: 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 — the report's budget line.
It COMPARES rather than reads a stored source, because the runs table has no `budget_source` column and inventing one would be a silent deviation from the ratified schema. 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 CastFinding ¶
type CastFinding struct {
Proposal string `json:"proposal"`
Voter string `json:"voter"`
Role string `json:"role,omitempty"`
// Kind is which list the entry came from: blocker, concern, or suggestion.
Kind string `json:"kind"`
Text string `json:"text"`
// Evidence is the references the engine resolved when the cast recorded,
// in the caster's order and canonical spelling.
Evidence []string `json:"evidence,omitempty"`
// Unsupported marks an entry that cited nothing. A flag rather than the
// absence of Evidence, so a consumer never reads an omitted key as "not
// applicable" — the same discipline VoteUsageCoverage states for silence.
Unsupported bool `json:"unsupported,omitempty"`
}
CastFinding is one entry of one cast's structured findings on one of the run's proposals (DKT-2451), with the evidence it cited.
type CheckedIntegration ¶
type CheckedIntegration struct {
Step string `json:"step"`
Instance string `json:"instance"`
SHA string `json:"sha"`
// How is "ancestor" or "patch-equivalent" when this pass asked git,
// "resolved" when the recorded head is one `step annotate
// --integrated-sha` verified and re-recorded (annotate_integration.go) and
// git confirmed its ancestry again here, "skipped" when this close's
// --skip-integration-check reason vouched for the commit instead — or,
// with Prior set, the verdict the prior close recorded, carried forward
// unchanged.
How string `json:"how"`
// Prior names the earlier close of this run that accepted this sha —
// "DISPATCH-N", or "run" for an acceptance recorded with no manifest open
// — present only when this pass honored that record rather than asking
// git again. A writer sha that a conflicting cherry-pick was resolved by
// hand is NEVER an ancestor and never patch-equivalent, so without this
// every close after the one that accepted it would refuse on it again
// (RUN-90, four steps, every close for the rest of the run).
Prior string `json:"prior,omitempty"`
// BaseDiverged is DKT-3288's advisory: inherited commits under the step's
// recorded base that the shared branch no longer carries by patch (a
// mid-run rebase rewrote them). The step's own commits were judged
// patch-equivalent; these are named, never refused on.
BaseDiverged []string `json:"base_diverged,omitempty"`
}
CheckedIntegration is one write-class step's commit and how this close accepted it — the audit trail a run report reads (AC3), and the record a LATER close of the same run trusts (DKT-1787).
type ClaimOptions ¶
type ClaimOptions struct {
Owner string
// TTLOverride is an explicit `--ttl`. Zero means resolve from the
// workflow's [limits], then config.
TTLOverride int64
// Metadata is `--metadata`, the opaque KV bag the DISPATCHER knows at
// claim time, merged onto the step's own in the claim's own transaction
// (DKT-592).
//
// It exists because the facts a dispatcher knows when it hands a step out
// are exactly the facts a step that never completes would otherwise never
// record. Deferring them to `complete` means the steps that failed or
// crashed — the ones an operator most wants to characterize — are the only
// steps with no bag at all, and a rollup over those keys goes blind at
// precisely the rows that motivated it.
//
// Core reads no key here, as everywhere: the bag is stored and never
// interpreted.
Metadata string
// CostMultiplier is `--cost-multiplier`: the DISPATCHER's declaration that
// this claim will cost this many times the step's declared `expected_cost`
// (DKT-867). Zero means unset — the claim accrues the declared cost,
// byte-for-byte the pre-DKT-867 behavior.
//
// It exists because `expected_cost` is variant-blind: the definition
// declares one number per step template, and the party that resolves a
// step to an executor variant — routing an escalated retry to something
// far pricier — is the dispatcher, at claim time. The multiplier is that
// resolution priced in the cap's own currency, declared by the only party
// in a position to know it. Core stores, sums, and reserves the number and
// never interprets it (the genericity rule): what varied and by how much is the
// dispatcher's business, exactly as `--usage`'s units are the claimant's.
//
// The scaled cost participates in the SAME facts the declared cost always
// has: it is checked against the cap in the claim's own transaction and
// recorded in the claim's `step-claimed` event, so each attempt of a step
// accrues what ITS claim declared — an escalated re-claim accrues more
// than the cheap first attempt did (retry re-accrual, made
// variant-aware).
CostMultiplier float64
// RecoverySecret is the claimant's proof for a later same-owner re-claim.
// The first claim stores only its hash; a re-claim of a lease that stored
// one re-mints only when it presents the same secret (reMintOwnClaim).
RecoverySecret string
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: a claim is a
// mutation and advances the version.
Attempt int `json:"-"`
RowVersion int `json:"-"`
// ReMinted marks a response that re-keyed the caller's OWN standing lease
// rather than taking a new one (DKT-1564). Nothing about the claim changed
// — same attempt, same accrual, same recorded bundle — but the caller asked
// twice, and the honest answer says which of the two it got.
ReMinted bool `json:"-"`
}
ClaimResult is the `claim response`, field for field:
{ step, token, lease_expires_ms, context }
THE SUBJECT KEY IS `step`, and `context` is the full bundle. That is what closes the deviation: claims first landed 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".
IT IS THREE PHASES AS OF STAGE 4, and the reason is structural: `pre = true` gates run AT CLAIM, a pre-gate is a subprocess, and no subprocess may run inside a transaction. So:
1 transaction A reap, readiness, 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 COMMITTED
WHAT IS PRESERVED, precisely, because "claim is atomic" is a ratified property:
- The MUTUAL-EXCLUSION guarantee is unchanged. Exactly one claimant wins, and it wins in transaction A, on the same CAS. Losers still get CONFLICT — except a caller presenting the OWNER THAT ALREADY HOLDS the live lease, which is not a second claimant but the holder asking again, and is answered by re-minting its token (DKT-1564, reMintOwnClaim). One lease still carries one live capability, one attempt, and one accrual.
- The "token and context in one response" guarantee is unchanged. The CALLER still receives both in one response, which is what that guarantee 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++.
A step with NO pre-gates takes the identical path with an empty phase 2, so the overwhelmingly common case is byte-identical to the pre-gate-free behavior.
type ClassCap ¶
type ClassCap struct {
Max int
Source ClassCapSource
}
ClassCap is a class's effective max and what set it.
type ClassCapSource ¶
type ClassCapSource string
ClassCapSource names what set a class's effective max.
const ( // ClassCapWorkflow is a pinned workflow's `[limits]` max, with no // limits.max.<class> value set. ClassCapWorkflow ClassCapSource = "workflow" // ClassCapOverride is a numeric limits.max.<class> value. ClassCapOverride ClassCapSource = "override" // ClassCapAuto is limits.max.<class>=auto, derived from the host's CPUs. ClassCapAuto ClassCapSource = "auto" )
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`
// as "STEP-N instance". It is part of the RECORD rather than of the
// message: 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"`
// Integration is DKT-1284's verdict: every write-class step's recorded
// commit was found integrated into the shared branch ("verified", with
// the checked shas), or the operator named a reason to skip asking
// ("skipped"). It is never nil — a close that reached this far always
// ran one of the two.
Integration *IntegrationCheck `json:"integration,omitempty"`
// ConditionsWaived lists the sealed votes whose unfiled approve-with-concerns
// conditions `--no-conditions` closed over (DKT-4263).
ConditionsWaived []UnfiledCondition `json:"conditions_waived,omitempty"`
}
CloseOutcome reports what a closing verb did, so the CLI renders the fact rather than re-deriving it.
type ClusterComplementarity ¶
type ClusterComplementarity struct {
Step string `json:"step"`
Instance string `json:"instance"`
Issue string `json:"issue"`
// Unique counts clusters with exactly one member: the field's declared
// order had one voice on that value, corroborated by nothing.
Unique int `json:"unique"`
// Corroborated counts clusters with more than one member: the SAME
// measurement Anthropic's fanout-width case made ("only 12 vulnerabilities
// in common") — how much of the round agreed rather than merely occurred.
Corroborated int `json:"corroborated"`
// ByMemberCount is Unique and Corroborated's own detail, ordered by
// member count ascending (a total key): every cluster this round
// reduced, grouped by how many members it had.
ByMemberCount []MemberCount `json:"by_member_count,omitempty"`
// InputArtifacts is how many artifacts fed the step whose output this
// round reduced — `step_inputs` on that PRODUCER step, not on the
// aggregate step itself (an action step is never claimed and so never
// gets a `step_inputs` row of its own, per DKT-1054). Summed over every
// run step sharing the producer's declared name, so a fanned-out producer
// (`review@0#0..3`) counts every sibling's artifact once. Zero when the
// producer step cannot be resolved — an aggregate step with no
// `<step>.<kind>` input, which V3/V29 already make unregisterable, but a
// restored database is not required to have run today's validation.
InputArtifacts int `json:"input_artifacts"`
// PayloadUnreadable marks a step whose aggregate output does not decode
// as the array of objects `aggregate` promises, so an absent row is never read
// as "this round produced nothing" (the same discipline MetadataUnreadable
// states). The bag is not re-validated here: a read verb that
// refused over one odd artifact would be useless during exactly the run
// an operator wants to inspect.
PayloadUnreadable bool `json:"payload_unreadable,omitempty"`
}
ClusterComplementarity is DKT-2452's row, one per `aggregate` step instance.
type ClusterSourceAttribution ¶
type ClusterSourceAttribution struct {
Step string `json:"step"`
Instance string `json:"instance"`
Issue string `json:"issue"`
// Source is the opaque value itself — a step ref such as `review@0#0` in
// this corpus's convention, but core treats it as a string it groups,
// never as content it parses.
Source string `json:"source"`
// Executor is the hint the named step declared, when Source resolves to a
// step instance in this run. Empty when it does not resolve — a source
// naming a step outside this run, or a value that is not a step ref at
// all — kept rather than dropped, since an unresolved source is
// still a fact about the round.
Executor string `json:"executor,omitempty"`
// Clusters is how many of the round's clusters this source contributed a
// member to.
Clusters int `json:"clusters"`
}
ClusterSourceAttribution is DKT-2462's row, one per distinct source-field value seen across one `aggregate` step's round.
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. 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 here — the same
// reasoning as `--budget`: the wire shape lands whole, so enforcement is
// added later 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
// Comments are `--comment-file` bodies (repeatable): issue comments the
// step's acceptance criterion requires. Each records an auxiliary artifact
// of IssueCommentArtifactKind and posts as a comment on the step's own issue
// in the same transaction (DKT-4272).
Comments [][]byte
// Pending, when non-nil, is set true when the gates and routing were
// handed to a detached run instead of running inside this call.
Pending *bool
NowMS int64
}
CompleteOptions are `step complete`'s inputs.
type ConditionsWaiver ¶
type ConditionsWaiver struct {
Reason string
Token string
By Attribution
}
ConditionsWaiver is `dispatch close --no-conditions`. The zero value runs the check. A non-empty Reason waives it; Token is the run's conductor capability that authorizes the waiver on a bound run, and By attributes the run note.
type ConductOptions ¶
type ConductOptions struct {
// By is who is taking the seat and from where. REQUIRED, exactly as on a
// ruling: the event is the whole audit trail of a token-free verb that
// mints authority, so an unattributed one must not be written.
By Attribution
NowMS int64
}
ConductOptions are `run conduct`'s inputs.
type ConductResult ¶
type ConductResult struct {
Run *model.Run
// Token is the freshly minted capability. It is never stored; only its
// hash is.
Token string
// Rotated reports that a capability already stood and was retired by this
// mint — the fact the standing conductor will discover at its next ruling,
// stated up front to whoever just took the seat.
Rotated bool
}
ConductResult is what the verb returns: the token, ONCE.
func ConductRun ¶
func ConductRun(conn *sql.DB, runID int, opts ConductOptions) (*ConductResult, error)
ConductRun takes (or re-takes) a run's conductor seat: it mints the run's capability, retiring any standing one, and records who did it.
It refuses a TERMINAL run: there is nothing left to conduct, and a token minted on a done run would be a capability for verbs that all refuse it anyway. A planning run is conductable — `run abandon` applies to one — and its first activation then keeps the capability rather than minting a second.
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.
// 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.
Metadata map[string]any `json:"metadata,omitempty"`
// PreGates carries the results of the step's `pre = true` gates, in
// declared order, so the results are included in the context bundle.
//
// PRESENT ONLY WHEN THE STEP DECLARES PRE-GATES — absent, not empty,
// otherwise, which is the rule the v6 `lease` object established.
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"`
// DiffBase is the commit the resolved `issue.diff` was computed from, read
// from the same round record (DKT-3307), so a judge reviewing it knows
// the range without rebuilding the diff. Absent for a record that
// predates the field.
DiffBase string `json:"diff_base,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.
//
// TWO WRITERS PUT A RULING HERE, and together they are the ONLY reliable
// operator-steering channels into a packet (DKT-725): `step resolve --as
// retry|rerun-gates -m` leaves the note on the SAME row it re-executes,
// and `step resolve --as fix-round -m` stamps it onto the NEW round's
// instantiated rows (stampEntryRouting), because the parked row it was
// issued against is superseded and renders nothing again. Issue COMMENTS
// are not a context source at all — the source rule, by design — and a
// mid-run `description` edit never renders either, because
// `body_snapshot` froze at activation. `run refresh-body`
// (DKT-2291) is that freeze's one explicit exception, and it is a separate
// operator act rather than a consequence of the edit.
//
// 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"`
// Notes are the run's standing statements (DKT-1079): every `run note
// add` recorded against THIS RUN, in insertion order, whichever issue or
// step the bundle is for.
//
// It is the one steering channel that is run-wide rather than
// step-scoped. `Resolution` reaches the step a ruling was issued against
// (and the round it authorizes), which is right for a ruling about one
// step's work and useless for a fact about the whole run — RUN-70's
// conductor learned BEFORE DISPATCH that a required gate fails on clean
// HEAD, got the operator's disposition, filed the tracking issue, and had
// nowhere to put any of it that a packet reads: issue comments are an
// audit surface (the source rule, by design), the body froze at
// activation, and no step had a routing record yet. The executor
// re-derived the failure and filed a duplicate.
//
// omitempty, so a run with no notes renders every bundle byte-identically
// to what it always was.
Notes []RunNote `json:"notes,omitempty"`
}
Context is the step'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, resolving its inputs over the run's artifacts AS THEY STAND — the claim-time assembly.
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".
It is the right assembly for a step that has not been handed out: what it returns is what a claim made now would hand over. For a step that HAS been handed out, the answer to "what did this step see" is AssembleRecordedContext.
func AssembleRecordedContext ¶
func AssembleRecordedContext( tx *sql.Tx, sched *Scheduler, step *db.Step, ttls ttlConfig, ) (*Context, error)
AssembleRecordedContext builds a step's context bundle over the artifacts its claim RECORDED as its inputs (`step_inputs`), instead of the run's current artifact table (DKT-1054).
Resolution is a function of run state at assembly time, and run state keeps moving after a step is handed out: the fixture's `fix@1` declares `reconcile.findings`, binds `reconcile@0` at claim (the round that routed it), and then `review@1`, `synthesize@1`, and `reconcile@1` all complete AT ITS OWN ORDINAL. Re-resolving live after that binds `reconcile@1` — an artifact produced by reviewing fix@1's own diff — and reports it as the input fix@1 read. RUN-64's STEP-2992 read exactly that way (and STEP-2974, STEP-3002, and the RUN-53/56/61 passes before it), while `step_inputs` for the same row held ARTIFACT-2709 (`reconcile@0`) the whole time, written by the claim and read by nothing. The same drift moved `issue.diff` — and with it `target_sha` — onto a record superseded after the step ran (a re-pin, DKT-1034, revises a producer's diff in place at the same ordinal).
THE RESOLVERS ARE NOT DUPLICATED. This runs the identical assembly over a different artifact set — the recorded rows — so the declared-position order, the engine-produced forms (`issue.body` from the snapshot, an empty `issue.diff`, `gate-results` from the ledger), the prior-round pass, the dedupe, and the target lift are the same code the claim ran. Every recorded artifact was selected by exactly the rule that re-selects it here, and no artifact recorded after the claim is a candidate, so the result is the claim-time bundle: byte-identical inputs, however far the run has moved.
Two forms are ledger reads rather than artifact reads and stay so: `<step>.gate-results` and `<step>.vote-record` resolve the producer INSTANCE by the same ordinal rule and serve its recorded rows, which are append-only per instance. A self-declared `<self>.gate-results` therefore carries the step's completion-side gate rows on a read-back that were not yet recorded at claim; that is the one engine-produced input whose read-back can grow.
func ReadContext ¶
ReadContext re-emits a step's context bundle READ-ONLY, no token required It is `step context`'s implementation.
IT WRITES NOTHING — not even a reap. Lazy reaping is confined 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.
A step that has been handed out reads back the bundle its claim recorded (AssembleRecordedContext, DKT-1054): the inputs, and the target ref lifted from them, are the ones the worker was given, however far the run has moved since. A step not yet handed out — or back at `pending` for a retry — reads live, as the claim that will hand it out would assemble it (recordedClaim). ReadLiveContext asks the live question of any step.
func ReadLiveContext ¶
ReadLiveContext is ReadContext resolved over the run's artifacts AS THEY STAND, whether or not the step has been handed out — `step context --live`.
It answers a different question from ReadContext on a claimed step: not "what did this step see" but "what would a claim made now hand over" — the preview an operator wants before authorizing a retry, and the one reading every consumer had before DKT-1054, kept reachable by name rather than removed.
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 first non-pass row per gate from the routing reduction (routingGateRows, as failedGatesOverRows reads it), sorted by gate name, so the packet names exactly the gates the step routed on: a command retried to a pass or a gate whose latest rerun-gates execution passed is omitted, and a fence gate whose earlier line failed is named even when its last line passed.
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, against the schema
// register. 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 the no-live-state rule
// that the packet exists to provide.
Payload string `json:"payload,omitempty"`
}
ContextInput is one resolved input artifact, in the bundle'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 `context.issue`. EVERY field but `id` comes from a snapshot column — that is what makes the bundle immune to mid-run edits, and it is why title/kind/labels/scope are 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"`
// NotesBytes is the run notes' share (DKT-1079). Counted, because a note
// rides EVERY packet of the run: a cap computed without it would let a
// bundle exceed its declared limit by exactly the words a dispatcher
// added to all of them.
NotesBytes int `json:"notes_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 — 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.
It is a SIBLING object rather than a mutation of `context`, so the golden bundles are unaffected by asking for it. A `--meta` that spliced counts into the bundle would make the goldens depend on a flag.
type ContextPin ¶
ContextPin is one `pins` element. Pins have ONE shape, so a workflow pin renders its `path` as `workflow:name@version` rather than growing a second shape.
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, and not yet a refusal.
type CorpusEntry ¶
type CorpusEntry struct {
// Kind is RegistrationKindWorkflow or RegistrationKindSchema.
Kind string `json:"kind"`
Name string `json:"name"`
Version int `json:"version"`
Path string `json:"path"`
}
CorpusEntry is the CURRENT declaration of one registry name in the shared instance-config roots: the highest version any root declares, and the file declaring it.
type CorpusIndex ¶
type CorpusIndex struct {
// contains filtered or unexported fields
}
CorpusIndex is one scan of the instance-config roots, read as "what does the corpus declare RIGHT NOW, for both registries".
It is WorkflowOriginIndex's shape, with the two differences the audit needs: it carries the VERSION rather than only the path, and it covers SCHEMAS as well as workflows. The two indexes are kept apart rather than merged because WorkflowOriginIndex is built lazily on activation's refusal path, where paying for a schema pass to answer a workflow question would be a cost with no reader.
func ScanCorpus ¶
func ScanCorpus() (*CorpusIndex, error)
ScanCorpus scans the shared store root's config for the audit, and nothing else.
The repository-side root the other readers union in belongs to whichever checkout invoked the command, and the audit compares EVERY project's rows. Scanning it would classify other projects against this checkout's additions, so the same store would audit differently depending on the working directory.
The error is the SCAN's own refusal — a root that is a regular file, a dangling symlink — surfaced rather than swallowed, for orphan_registration's reason: those are exactly the states in which having looked nowhere would be reported as every registration being stale.
func (*CorpusIndex) Current ¶
func (i *CorpusIndex) Current(kind, name string) (CorpusEntry, bool)
Current returns what the corpus declares for one kind and name.
func (*CorpusIndex) Entries ¶
func (i *CorpusIndex) Entries() []CorpusEntry
Entries are every declaration the scan found, ordered by kind then name, for a caller that wants to render what "current" meant.
func (*CorpusIndex) Err ¶
func (i *CorpusIndex) Err() error
Err is the failure that stopped the build, if any.
func (*CorpusIndex) Roots ¶
func (i *CorpusIndex) Roots() []string
Roots are the canonicalized roots that were scanned, in precedence order.
func (*CorpusIndex) Scanned ¶
func (i *CorpusIndex) Scanned() bool
Scanned reports whether any instance-config root existed to look in.
type DecideOptions ¶
type DecideOptions struct {
// Approve is the verb: true for `approve`, false for `reject`.
Approve bool
// Note is `--note`.
Note string
// Value is `--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 "".
Value string
// By is who is ruling and from where. REQUIRED: an empty field refuses the
// decision before anything is written (see Attribution).
By Attribution
// Under is the authority the decision is made under (DKT-1899). REQUIRED:
// the zero value refuses the decision before anything is written.
Under Authority
// Token is the run's conductor capability (DKT-2465, conductor.go). It is
// REQUIRED on a bound run and ignored on an unbound one; the empty string
// authorizes nothing.
Token string
NowMS int64
}
DecideOptions are `step approve` and `step reject`'s inputs — the ResolveOptions shape, adopted when the ruling gained its attribution (DKT-2450): a fifth positional on `(approve, note, value, nowMS)` is how call sites end up passing the wrong string.
type DetachedPreGateRun ¶
type DetachedPreGateRun struct {
Step string `json:"step"`
TargetSHA string `json:"target_sha"`
Outcome string `json:"outcome"`
// Gates are the rows this run recorded, or the recorded rows it found
// complete — empty for every other outcome.
Gates []PreGateResult `json:"gates,omitempty"`
}
DetachedPreGateRun is what one detached run did, as `step pregate` reports it.
type DetachedSagaRun ¶
DetachedSagaRun is the child's report.
type DiffFacts ¶
type DiffFacts struct {
// Lines is added plus removed content lines.
Lines int
// Files is the number of files the diff touches.
Files int
// Empty reports that no content line changed.
Empty bool
}
DiffFacts is the engine's measurement of one step's recorded change, the values the reserved `diff.*` family evaluates over.
A nil *DiffFacts is "this step holds no tree", and every `diff.*` predicate is then an ordinary unknown field — the schema-less behavior, untouched. An ABSENT round record is a different fact and is a non-nil zero value: no change was measured, so Lines and Files are 0 and Empty is true. The two must not collapse, because `any(diff.empty == false)` has to be decided (false) for a step that recorded nothing, not left to the no-such-field path.
type Discrepancy ¶
type Discrepancy struct {
Kind DiscrepancyKind `json:"kind"`
Step string `json:"step"`
Instance string `json:"instance"`
// Resolution is the enumerated way out, rendered for this specific row.
// The resolutions are enumerated precisely so reconciliation is
// an operator's act rather than a guess the engine makes.
Resolution string `json:"resolution"`
}
Discrepancy is one unreconciled fact about a run, with the resolution the scheduling rules enumerate for it.
COMPUTED, NEVER STORED. 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 the TWO discrepancy 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, more than `dispatch.grace` ago, with zero // `usage_ledger` rows, IN A RUN THAT HAS EVER OPENED A DISPATCH. DiscrepancyMissingUsage DiscrepancyKind = "usage-rows-missing" )
type DoctorCheck ¶
type DoctorCheck struct {
Check string
Verdict DoctorVerdict
Detail string
}
DoctorCheck is one row: which check, its verdict, and a human-readable reason — the same shape whether the caller reads JSON or the rendered table.
type DoctorOptions ¶
type DoctorOptions struct {
// Cwd is the invoking process's working directory, canonicalized.
Cwd string
// DBPath is the store's database file — the same path the current seat
// already opened read-write to reach this verb, re-verified directly.
DBPath string
// ProjectID is the project the invocation resolved to, as the root hook
// decided it: a registered row's id, db.DefaultProjectID on a
// single-tenant store, or db.UnregisteredProjectID when Identity has no
// row. Doctor reports it and never changes it.
ProjectID int
// Identity is the invocation's project identity as config resolved it,
// or "" on a single-tenant store — named in the detail so an unbound
// report says which path has no project.
Identity string
// RunID is `--run`'s target, or 0 when absent — AC2's SKIP case.
RunID int
// SourceRoot is `--source PATH`, or "" when absent — install-drift's own
// SKIP case.
SourceRoot string
NowMS int64
}
DoctorOptions are one invocation's inputs.
type DoctorReport ¶
type DoctorReport struct {
Clean bool
Skipped bool
Checks []DoctorCheck
}
DoctorReport is `doctor`'s whole answer: one row per check, plus the two summary bits a caller branches on without re-walking the rows.
func Doctor ¶
func Doctor(conn *sql.DB, opts DoctorOptions) *DoctorReport
Doctor runs all seven checks and reports one row each, without short-circuiting on an early failure (AC1): a conductor deciding whether to attach needs every answer in one call, not the first one that went wrong.
type DoctorVerdict ¶
type DoctorVerdict string
DoctorVerdict is one check's answer.
const ( DoctorOK DoctorVerdict = "OK" DoctorFail DoctorVerdict = "FAIL" DoctorDrift DoctorVerdict = "DRIFT" DoctorSkip DoctorVerdict = "SKIP" DoctorWarn DoctorVerdict = "WARN" )
type DroppedMetadataTarget ¶
type DroppedMetadataTarget struct {
Step string `json:"step"`
Instance string `json:"instance"`
Status string `json:"status"`
}
DroppedMetadataTarget names one back-filled step whose metadata bag was dropped because the step had not reached a terminal status. Status is on the row for the same reason it is on UnclaimedTarget: it says why the bag had nowhere to land.
type ElidedFile ¶
ElidedFile is one generated file the render replaced with a stat line.
type Engine ¶
type Engine struct {
Gates GateRunner
Actions ActionRunner
// DiffFn computes an issue's VCS diff over its snapshotted scope. 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, 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)
// PatchContainedFn reports whether execRoot's HEAD carries `sha`'s PATCH —
// whether some commit on HEAD's side of their merge base is patch-id
// equivalent to each commit on `sha`'s side (DKT-1033) — and whether that
// question could be answered at all. A field for DiffFn's reason: the real
// one shells out to git.
//
// It is the FIRST question a disproved ancestry opens, ahead of
// TreeMatchFn, because it asks about the work rather than the tip: a
// sibling issue's integration or a conductor's follow-up patch on the same
// file moves HEAD's tree without touching this patch, and RUN-67 warned on
// all nine review rows for exactly that. Like TreeMatchFn it may only
// ACQUIT a sha ancestry already disproved; unlike TreeMatchFn, a
// definitive `contained = false` is the ONE measurement that lets the
// advisory call an integration diverged, because it is the one that
// actually measured the patch.
PatchContainedFn func(execRoot, sha string) (contained, known bool)
// StepPatchContainedFn is PatchContainedFn for a write-class step's own
// commit at dispatch close (DKT-3288), bounded below by the step's recorded
// base so only base..sha is judged; `diverged` names inherited base
// commits the shared branch no longer carries, for the close's advisory.
// An empty base asks the unbounded question. A field for DiffFn's reason.
StepPatchContainedFn func(execRoot, sha, base string) (contained, known bool, diverged []string)
// 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.
//
// Since DKT-1033 it is PatchContainedFn's FALLBACK: a tree comparison
// reads any later commit on the same paths as a difference, so its
// `match = false` is no longer allowed to word the advisory as a
// divergence — only as content that could not be matched.
TreeMatchFn func(execRoot, sha string) (match, known bool)
// ObjectExistsFn reports whether `sha` resolves as a COMMIT OBJECT from
// execRoot at all — `git cat-file -e <sha>^{commit}` — and whether that
// question could be answered (DKT-742). A field for DiffFn's reason: the
// real one shells out to git.
//
// It exists because IsAncestorFn's `known = false` conflates two states
// that read very differently to a packet consumer: "git could not answer"
// (git absent, not a repository — nothing to warn about) and "the object
// is not in the shared store at all" (a separate-clone worktree whose
// objects never reached the shared store, or a pruned+GC'd one). RUN-52's
// DKT-V253 was the second: all three vote seats ran `git cat-file -t` on
// the packet's target, found no object anywhere, and burned an
// investigation each — while the engine, which had asked git about that
// exact sha at dispatch open, had silently skipped it as unanswerable.
//
// staleTargets consults it only where ancestry was UNANSWERABLE, and only
// a definitive `exists = false, known = true` produces a warning — so the
// "absence of evidence is not staleness" posture holds for every state
// where git genuinely could not answer.
ObjectExistsFn func(execRoot, sha string) (exists, known bool)
// CommitPatchFn renders ONE commit's own patch — `sha` against its parent,
// read from execRoot and scoped like every `issue.diff` — for the verified
// integration annotation (annotate_integration.go), which re-records a
// step's diff from the commit the shared branch carries rather than from
// a working tree. A field for DiffFn's reason: the real one shells out to
// git, and a test injects a body. nil records an empty body.
CommitPatchFn func(execRoot, sha string, scope []string) (string, error)
// IntegrationRangeFn establishes, for the verified integration annotation,
// whether `tip` (the annotated sha) carries the writer's chain base..head by
// patch, and where the integrated range starts (DKT-4269). nil skips the
// check and records the commit's own patch, as before it existed. A field
// for DiffFn's reason: the real one shells out to git.
IntegrationRangeFn func(execRoot, tip, head, base string) IntegratedRange
// RangePatchFn renders the combined patch of from..to, scoped like
// CommitPatchFn, for the annotation's full-range re-pin. nil falls back to
// CommitPatchFn on the tip.
RangePatchFn func(execRoot, from, to string, scope []string) (string, error)
// IntegrationChainFn answers, read-only, whether a writer's base..head is a
// chain `step integrate` will pick, and lists its commits oldest first
// (DKT-4268). nil refuses the verb.
IntegrationChainFn func(execRoot, base, head string) ([]string, error)
// CherryPickFn is the engine's only git WRITE: it cherry-picks base..head
// onto the shared checkout with -x, all or nothing (integrate_git.go). nil
// refuses the verb, so an engine built without it, every test engine
// included, can never write the shared branch.
CherryPickFn func(execRoot, base, head string) (GitPick, error)
// contains filtered or unexported fields
}
Engine carries the two execution seams and drives the saga.
The runners are fields rather than package-level values precisely so a caller swaps them by constructing a different Engine — one constructor call and nothing else.
func NewEngine ¶
func NewEngine() *Engine
NewEngine builds the production engine: the REAL gate runner, the REAL action runner, and the real git diff.
THIS IS THE CONSTRUCTOR SWAP: the saga is written against the interface, so switching to the real runners changes one constructor call and nothing else. That 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. Four other places move with it: the seam's return shape, the routing stage's `held` branch, DecideStepWith's materialized branch, and stage 0's schema validation with the threshold's resolver.
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) AbandonDispatchWith ¶
func (e *Engine) AbandonDispatchWith( conn *sql.DB, runID int, opts AbandonDispatchOptions, ) (*CloseOutcome, error)
AbandonDispatchWith closes the open manifest UNCONDITIONALLY.
NO DISCREPANCY BLOCKS IT, and that is the whole point: the design 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 the recovery design exists to make impossible.
It requires the run's conductor capability: retiring the live manifest is the conductor's decision, and an executor sharing the checkout must not be able to pull a batch out from under it. The TTL abandon `next` performs stays token-free, because there the engine, not the caller, decides.
func (*Engine) AnnotateIntegration ¶
func (e *Engine) AnnotateIntegration( conn *sql.DB, stepID int, sha, metadata string, by Attribution, token string, nowMS int64, ) (*IntegrationAnnotation, error)
AnnotateIntegration records a verified integration for a finished write-class step: the sha's ancestry against the shared checkout's HEAD is established first, then the step's `issue.diff` is re-recorded over the integrated range and `integrated_sha` (plus any further metadata) is merged onto the step's record — all of it in one transaction after the git questions are answered.
It refuses: a malformed sha; a caller that does not present the step's run's conductor capability, when the run is bound to one; a step that is not terminal (a live step's record lands under its holder's token); a step that records no `issue.diff` of its own (nothing downstream reads a re-record of it); a step whose record names no commit (there is no divergence to resolve); an engine with no ancestry seam wired; an unanswerable ancestry question; and a sha that is not an ancestor of the shared checkout's HEAD.
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.
A TARGET NO WORKER EVER CLAIMED IS RECORDED AND REPORTED, never refused (DKT-993). Harness RUN-66's conductor back-filled a wave journal whose join heuristic had misattributed probe-agent tokens, and the rows landed on STEP-3146 (`design-qa@1`, `pending`, never claimed) and on a superseded step with no refusal, no warning, and no diagnostic — so `run report` showed spend on steps that never ran and nothing in the store said where it came from.
THE CONTRACT IS WARN-AND-RECORD, and the choice is deliberate. Trusting the relay stays the default: core cannot know that a step which never claimed did not cost anything — a pre-gate runs at claim time, a spawn can burn tokens before the claim lands, and core enumerating which steps are ALLOWED to have cost would be core holding an opinion about how a relay measures its own work, the same opinion `--source` and `unit` exist to avoid holding. What was wrong was the SILENCE. So the rows land, the ledger stays the relay's to write, and every unclaimed target is named on the outcome for the caller to warn about.
"NEVER CLAIMED" IS `attempt == 0`, the same predicate D2's own exemption uses (missingUsage, D5's second half): `attempt` counts claims and nothing else — it is not bumped on failure, and `step resolve --as retry` moves `attempt_base` rather than zeroing it — so a zero attempt is exactly "no worker ever held this step", for every status at once. It is one field already on the row rather than an event scan, and it cannot disagree with the discrepancy probe that reads it three lines away.
It covers the SUPERSEDED case the same evidence names, and covers it correctly. The loop's supersede sweep takes `pending` instances only, so a swept step is a never-claimed step and warns here by the same clause; a `fix-loop` resolution supersedes a step that HAD claimed and HAD spent, and that one does not warn — its usage is real, and back-filling it is D2's own documented resolution. Warning on status alone would have fired on the legitimate path and stayed quiet on neither.
A BAG ON A LIVE OR PENDING STEP IS DROPPED, NOT REFUSED. A reaped executor's transcript carries usage and a complete model trace, so its back-fill row carries a bag, and the step is pending for retry when the back-fill runs. The refusal this replaced failed the whole batch, wrote nothing, and left the dispatch unclosable. Now the usage row lands, the bag is dropped, and the step is named on the outcome. A bag that is not a JSON object still refuses the batch whatever the step's status, because the shape is a property of the input and not of the step.
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) ClaimStepRendered ¶
func (e *Engine) ClaimStepRendered( conn *sql.DB, stepID int, opts ClaimOptions, templatePath, executor string, ) (*ClaimResult, *RenderResult, error)
ClaimStepRendered is ClaimStepWithGates for `step claim --render`: the claim and the packet render as ONE saga (DKT-804).
The render used to run strictly AFTER the claim's commit, and a render-time refusal — an unpinned packet file, a template that does not parse — landed on a step already `claimed` with no token ever delivered. RUN-56 stranded eight steps this way in one dispatch: the lease was held, every recovery verb requires the token nobody received, and the only exits were the full TTL or an operator-gated `step reap`.
So the render is VALIDATED FIRST, pre-claim, on the same rule that places the metadata-bag validation before `Begin()`: a refusal must write NOTHING — no lease, no attempt, no event — and leave the step exactly as claimable as it was. The render cannot simply move inside the claim's transaction, because it reads through the connection while the claim's transaction would hold the pool's single connection (the same deadlock loadTTLConfig's placement avoids).
The AUTHORITATIVE render then runs post-claim, so the returned packet reports the step as its claimant will see it — `context.step` claimed, this claim's `attempt`, the merged metadata bag. Every failure the preflight can catch is deterministic over the run's pins and the template's bytes, so a post-claim refusal requires the filesystem to change between two reads milliseconds apart. That residual race does not roll the claim back — the claim is COMMITTED, and the `step-claimed` event it wrote is the budget floor's accrual (a SUM over those events), so un-taking the lease means un-writing a ledger entry the log is append-only about. It raises IncompleteClaimError instead (DKT-1564): the token goes back to the claimant with the failure, so ending the lease costs it one of its own verbs rather than the full TTL or an operator-gated reap. Every deterministic failure still refuses before anything is written.
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`. The nil-runner form keeps every existing caller and every test written before pre-gates 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, skip IntegrationSkip, nowMS int64, ) (*CloseOutcome, error)
CloseDispatch closes the open manifest of a run.
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 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.
DKT-1284: BEFORE ANY OF THAT, it verifies every write-class step's own recorded commit reached the shared branch (dispatch_integration.go) — ancestor first, patch-equivalent (`git cherry`) when a cherry-pick minted a new sha for identical content, refusing CONFLICT with the unintegrated list otherwise. skip.Reason, when non-empty, is the operator's override: the check does not run at all, and the reason rides on the close event instead (the "record the override, don't silently honor it" pattern this codebase already applies to trust changes). Every later close of the run honors that record, so on a bound run the override requires the run's conductor capability in skip.Token; a close without it requires none.
DKT-2758: every CONFLICT it returns is recorded as one `dispatch-close-refused` event, in its own transaction that commits although the close did not, naming the dispatch when one was open.
func (*Engine) CompleteStep ¶
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) DecideStepWith ¶
DecideStepWith is `step approve` and `step reject` — the human-gate verbs.
NO LEASE TOKEN: a human gate is never claimed, so there is no lease to authorize against. What the verb requires instead is the RUN's conductor capability (DKT-2465, conductor.go) — the authority used to be repository access alone, which under a harness every executor shares, so an executor could approve the very gate its own commit is guarded on. The record carries WHO ruled and from where (DKT-2450) — the same two claim-level fields a trust grant carries, on the event.
func (*Engine) DriveRunLifecycles ¶
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: next 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 ¶
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) ExtendDispatch ¶
ExtendDispatch appends rows the engine minted or readied since the open to the run's OPEN manifest (DKT-2071).
It exists because the manifest is frozen at open and a fix round is minted at the RECORD TIME of the step whose routing chose `fix-loop` — never at `next` — so every loop round started in a later dispatch by construction, and a relay paid a whole dispatch boundary (the remainder of its wave plus the bookkeeping) for a row the engine had already readied. The same applies to a held-cluster gate, an `on_fail` route, and the chain tail a `--limit` cut.
ONE OPEN MANIFEST, STILL. This is an APPEND to the manifest the relay is already reconciling, not a second dispatch: `next`'s open-dispatch refusal and the single-open index are both untouched, so relay drift still stalls loudly. The appended rows are byte-hashed with the same canonicalRowBytes the open used, so `dispatch verify` and the spawn guard compare them exactly as they compare an opened row.
IT DRIVES VOTE AND ACTION LIFECYCLES to quiescence before computing its rows, exactly as OpenDispatch does. `step record` and `step fail` drive them after their own writes, but `step approve`, `step reject` and `step resolve` ready work mid-wave without driving it. An undriven vote step appended here would carry no proposal to cast on, and an undriven action step would ride the manifest as a ready row no relay can execute.
An EXPIRED manifest is refused rather than extended: extending one would push a lapsed manifest's expiry back out and resurrect a batch the TTL had already given up on, which is the wedge `dispatch abandon` exists to avoid.
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 ("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: 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?
It allows iff BOTH:
(a) the proposed rows byte-match the open dispatch, and (b) no unacknowledged write reaps exist.
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 this verb owns BY NAME: unacknowledged reaps are 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.
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.
THE 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, 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) IntegrateSteps ¶
func (e *Engine) IntegrateSteps( conn *sql.DB, stepIDs []int, by Attribution, token string, nowMS int64, ) ([]StepIntegration, error)
IntegrateSteps cherry-picks each named step's recorded writer chain onto the shared checkout and annotates the step with the resulting HEAD. It returns the steps it completed, also on a failure part way through.
func (*Engine) NextSteps ¶
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 ("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.
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.
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 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) ReconcileDispatch ¶
func (e *Engine) ReconcileDispatch( conn *sql.DB, runID int, rows []BackfillRow, source string, onDuplicate string, acceptMissingUsage bool, skip IntegrationSkip, nowMS int64, ) (*ReconcileOutcome, error)
ReconcileDispatch performs back-fill, then verify, then close, refusing at whichever stage fails (DKT-580).
ON FAILURE IT RETURNS BOTH the partial outcome and a *StageError. The outcome is deliberately not discarded: after a verify-stage refusal the back-fill HAS landed and is a fact the operator needs — re-running the whole pipeline would hit the ledger's (step, attempt, unit) key on the rows that are already in — so a caller that reports the partial work reports the truth. The error is always non-nil when a stage failed; nothing about the partial outcome should be read as success. A refused integration override is the one exception: it is ruled on before any stage runs, so it returns the conductor refusal itself, not a *StageError, with no stage recorded.
AN EMPTY BATCH IS REFUSED, at the back-fill stage, by BackfillUsage's own rule. That is the intended behaviour rather than a skip-to-verify: a wave whose usage journal produced no rows is a measurement that went missing, and closing quietly over it is exactly the silent lie this verb exists to make harder.
func (*Engine) ResolveStepWith ¶
func (e *Engine) ResolveStepWith( conn *sql.DB, stepID int, opts ResolveOptions, ) (*ResolveOutcome, error)
ResolveStepWith is every `step resolve` shape — the `waiting-human` resolutions, `--batch` (DKT-546), `--drop-interposed` (DKT-861), and `--worktree` (DKT-1034) — and reports the outcome the step row does not carry.
`--batch` is the resolution plus one run-scoped grant per failed completion gate, so later steps in the SAME run failing the same gate with the same failure signature (gate name + exit + reason) auto-pass at routing instead of re-asking the operator; the grant dies with the run. `--drop-interposed` is the operator's EXPLICIT acknowledgment that override-pass's generic pass skips the step(s) the threshold interposes; without it, resolveStep refuses such an override-pass BEFORE anything commits, because the DKT-470 warning used to arrive beside a mutation already decided (RUN-61's verify-tribunal, skipped under the operator who had chosen override-pass precisely to reach it).
func (*Engine) ResumeSaga ¶
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) ResumeStrandedSagas ¶
ResumeStrandedSagas finishes the sagas a dead detached run left behind. A child killed mid-saga leaves a recorded step in a gate stage with no lock held and nothing else to advance it; the next scheduling call does. It runs only where a launcher is installed (the real binary), so a store that never detaches behaves exactly as before. Held and action sagas have their own resume paths and are left alone. Each step is resumed under its saga lock, so a live child and the sweep never both run a gate.
func (*Engine) RunActionStep ¶
RunActionStep enters an action step's saga ENGINE-SIDE — no claim, no token, no worker artifact.
It is the other half of `claim` refusing an action step. Action steps are the engine's deterministic half: 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) RunDetachedPreGates ¶
func (e *Engine) RunDetachedPreGates( conn *sql.DB, stepID int, targetSHA string, nowMS int64, ) (*DetachedPreGateRun, error)
RunDetachedPreGates is the detached child's whole work: hold the (step, target) lock, confirm the measurement is still wanted, reconstruct the target, run every pre-gate under its entry's own timeout, and record the rows keyed to the target. `step pregate` is its only production caller.
Every outcome short of a measurement is a quiet decline rather than an error: a second child finding the lock held, a result already complete, a step that moved on, a target that moved. Only a target that cannot be reconstructed is an error, because a run that was asked for and could not measure should say so to whoever asked.
func (*Engine) RunDetachedSaga ¶
RunDetachedSaga is the detached child's whole work: hold the step's saga lock, resume the saga, then drive the run's engine-side lifecycles. A child finding the lock held exits without running.
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 — with `Stage` excluded from that comparison (DKT-19, below).
THE LOAD-BEARING PROPERTY 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 (`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 (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 — 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").
func (*Engine) WaiveStaleTargets ¶
func (e *Engine) WaiveStaleTargets( conn *sql.DB, runID int, instances []string, targetSHA, note string, by Attribution, token string, nowMS int64, ) ([]WaivedTarget, error)
WaiveStaleTargets records one waiver per named step instance, all for one target sha, in ONE transaction — the operator's ruling either covers every row they listed or none of them. Each waiver logs its own `stale-target-waived` event, so the feed shows exactly what standing precedent was minted and by which verb.
The instances are the strings the advisory itself printed (`review@1#0`), matched verbatim by staleTargets — which is also why nothing here checks them against live steps: a fanout sibling minted by a later round is a NEW instance with a new signature, and the operator waives what warned, not what might.
A waiver is a conductor ruling: on a run bound to a conductor capability, token must be that capability, checked inside the writing transaction.
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
// Holders names the steps that made a claim refusal (DKT-4264). Empty for
// every error that is not a scope or headroom refusal.
Holders []Holder
}
Error is an activation failure carrying its taxonomy code.
type ErrorCode ¶
type ErrorCode string
ErrorCode classifies an activation failure so the CLI can map it onto the error taxonomy 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. CodeConflict ErrorCode = "CONFLICT" // CodeGone is GONE: history that `events prune` removed. Two product paths // return it: an `events list --since` cursor below the retained minimum, // and the recorded issue body and scope read-back (recordedIssue in // context.go, reached by step context and step render) when the prune // removed the step's claim event. CodeGone ErrorCode = "GONE" // CodeAuth is AUTH_ERROR (exit 5): a caller presenting a token that is not // the run's conductor capability on one of the operator verbs (DKT-2465). // The same code the lease refusals use for "you do not hold this", and // for the same reason: the answer to a caller with no capability never // says more than that. CodeAuth ErrorCode = "AUTH_ERROR" )
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 the shape. A trust
// event has no run 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.
Step string `json:"step,omitempty"`
// StepID is `STEP-N`: the joinable id, the addition to the shape.
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; the
// 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 the public event shape, field for field:
event { seq, at_ms, kind, run?, step?, data }
plus `step_id`, which is an ADDITION to that shape. 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.
type EventPage ¶
type EventPage struct {
Events []Event
// Total counts MATCHING events before the slice, so `truncated` is
// computable rather than guessed.
Total int
}
EventPage is one `--since` answer, with the truncation contract every list verb carries.
func ListEvents ¶
func ListEvents(conn *sql.DB, q EventQuery) (*EventPage, error)
ListEvents serves `events list --since`.
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: 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, 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.
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.
RunID int
// Limit applies AFTER ordering. 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. "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
// Kind filters to one event kind; empty is every kind. StepID filters to
// one step's events by its joinable id; zero is every step. Both are
// predicates in the WHERE clause, so they apply BEFORE Limit and Tail and
// Total counts only the matches (DKT-3293): a lookup by kind or step
// covers the whole trail instead of searching the oldest page.
Kind string
StepID 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.
//
// IT IS IGNORED WHEN RunID IS SET (DKT-583). A run belongs to exactly ONE
// project, so `--run` is ALREADY a project scope — a narrower one. Anding
// the invoking project's scope onto it cannot select a meaningful subset:
// it either changes nothing (the run is this project's) or empties the page
// entirely (the run is a neighbor's), and the second case answered
// `{"ok":true,"events":null,"total":0}` for a run with hundreds of events.
// A successful empty feed is the WORST available answer — a consumer
// polling it waits forever on a run it can see in `run report`, which
// resolves a run's project independent of the cwd. `--run` now does the
// same, which is the disposition DKT-583 prefers.
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.
RepoRoot string
// Identity is the project identity trust entries bind to. 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.
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,
// which is forbidden 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. Swapping this one constructor is what replaced the stub runner.
func (*ExecActionRunner) Run ¶
func (r *ExecActionRunner) Run( ctx context.Context, a ActionSpec, sc StepContext, ) (ActionResult, error)
Run computes one action step.
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 — and
// the containment boundary.
RepoRoot string
// Identity is the project identity trust entries bind to. 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.
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 —
// one is forbidden, 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 PROMISED CONSTRUCTOR SWAP: NewEngine names it instead of PassThroughRunner, and the saga does not change around it.
func (*ExecRunner) Execute ¶
func (r *ExecRunner) Execute(_ context.Context, g GateSpec, sc StepContext) (GateExecution, error)
Execute is Run's full-fidelity form: it returns every row to record rather than collapsing to one.
func (*ExecRunner) Run ¶
func (r *ExecRunner) Run(ctx context.Context, g GateSpec, sc StepContext) (GateResult, error)
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, 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.
func (*ExecRunner) TrustRunner ¶
func (r *ExecRunner) TrustRunner() *ExecRunner
TrustRunner reports the runner itself, so *ExecRunner satisfies the interface its own wrappers use.
type ExecutorLedger ¶
type ExecutorLedger struct {
Runs int `json:"runs"`
Executors []ExecutorLedgerRow `json:"executors"`
Voters []VoterLedgerRow `json:"voters"`
}
ExecutorLedger is the whole document: the runs read and one row per identity, each list ordered by its name (a total key).
func LoadExecutorLedger ¶
func LoadExecutorLedger(conn *sql.DB, opts ExecutorLedgerOptions) (*ExecutorLedger, error)
LoadExecutorLedger builds the document over every run the options admit. IT WRITES NOTHING.
type ExecutorLedgerOptions ¶
type ExecutorLedgerOptions struct {
// ProjectID scopes the runs to one project; 0 reads every project in the
// store, the way `events list --all-projects` does.
ProjectID int
// SinceRun admits runs whose id is at least this; 0 sets no floor.
SinceRun int
// SinceMS admits runs created at or after this instant; 0 sets no floor.
SinceMS int64
}
ExecutorLedgerOptions bounds the window the ledger reads.
type ExecutorLedgerRow ¶
type ExecutorLedgerRow struct {
Executor string `json:"executor"`
// Runs is how many runs in the window expanded a step carrying the hint.
Runs int `json:"runs"`
Steps int `json:"steps"`
// FixLoopRoutes counts steps carrying the hint whose recorded routing is
// `fix-loop` — the step's own threshold or on_fail sent the issue into a
// fix round.
FixLoopRoutes int `json:"fix_loop_routes"`
// OverridePasses counts `step resolve --as override-pass` rulings on steps
// carrying the hint: the times an operator passed what the step's own
// gates would not.
OverridePasses int `json:"override_passes"`
// Reaps counts the `lease-reaped` events on the hint's steps; ForcedReaps
// is the subset a relay declared dead with `step reap`.
Reaps int `json:"reaps"`
ForcedReaps int `json:"forced_reaps"`
// UniqueClusters and CorroboratedClusters count, over every `aggregate`
// round whose step declares `source_field`, the clusters a step carrying
// the hint contributed a member to: unique when the cluster had one
// member, corroborated when it had more. HeldClusters is the subset whose
// spread tripped `hold_spread`.
UniqueClusters int `json:"unique_clusters"`
CorroboratedClusters int `json:"corroborated_clusters"`
HeldClusters int `json:"held_clusters"`
}
ExecutorLedgerRow is one executor hint's record across the window.
type Extension ¶
type Extension struct {
Dispatch string `json:"dispatch"`
Run string `json:"run"`
// ExtendedSeq is the event seq the appended rows were computed at — the
// same fact `opened_seq` records for the open, read before this call's own
// event so it names the log position rather than the append.
ExtendedSeq int64 `json:"extended_seq"`
// ExpiresMS is the manifest's expiry AFTER the extension, grown by the
// appended rows' own staged lease sum.
ExpiresMS int64 `json:"expires_ms"`
Rows []model.StepRow `json:"rows"`
// Reaped names the step instances whose leases this extend reaped, the
// same channel OpenDispatch reports its own reaps on.
Reaped []string `json:"reaped,omitempty"`
// ReapHold is the guidance for the hold THIS EXTEND left behind, carried
// for OpenDispatch.ReapHold's reason: an extend runs the same shared reap,
// and reaping a BOUNDED class inserts an unacknowledged `reap_acks` row
// that denies the next `guard spawn --active`. A relay that extends and
// then launches the appended rows would meet that denial with no warning
// from the verb that created it, and the engine's own advice names
// `dispatch open --ack-reap` — a verb this relay cannot reach without the
// dispatch boundary the extend exists to avoid.
ReapHold string `json:"reap_hold,omitempty"`
}
Extension is `dispatch extend`'s answer: the open manifest's identity and the rows this call APPENDED, never the whole grown list.
The appended rows alone are what the caller can act on — they are the batch `guard spawn --rows` will admit as a subset of the stored manifest — and a response carrying the grown manifest would invite a relay to re-spawn rows it already launched.
type FailedGate ¶
type FailedGate struct {
Gate string `json:"gate"`
Verdict string `json:"verdict"`
Exit *int `json:"exit"`
// Reason explains an `unmatched` verdict or a timeout, verbatim from the
// recorded row.
Reason string `json:"reason,omitempty"`
}
FailedGate is one completion gate whose routing attempt did not pass, in the shape a caller needs to SAY SO: the name, the verdict word, and the exit code the process left behind.
Exit is a pointer for db.GateResultRow's reason exactly — an `unmatched` gate never ran, and rendering `exit 0` for a process that does not exist reads as a pass.
func FailedGates ¶
func FailedGates(conn *sql.DB, stepID int) ([]FailedGate, error)
FailedGates returns the completion gates that did not pass, sorted by name.
It is the READ SIDE of the routing decision (DKT-982): a step whose gate failed parks, and until this existed the verb that parked it could name nothing — `step record` printed a success-shaped line with no gate on it, and the executor reading that line reported the run green. The engine knew at print time; it had no accessor to say it with.
Empty means every completion gate passed, or none ran.
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: 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.
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 the runner applies at the gate: a report assembled from several reads could disagree with itself mid-render.
type ForceReapOptions ¶
type ForceReapOptions struct {
// Reason is `--reason`, REQUIRED: the assertion that the holder is gone.
Reason string
// By is who asserted it and from where. REQUIRED: an empty field refuses
// the reap before anything is written (see Attribution).
By Attribution
// Token is the run's conductor capability (DKT-2465, conductor.go):
// required on a bound run, ignored on an unbound one.
Token string
NowMS int64
}
ForceReapOptions are `step reap`'s inputs.
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 and a flaky command records each attempt individually.
type GateOutcome ¶
type GateOutcome string
GateOutcome is a gate's decided-or-not verdict, in the three states a caller ever needs to branch on. It collapses `model.ProposalStatusClosed` (a proposal retired without a tally) into "open" — a closed proposal answered no vote and a gate status probe exists to say which way a gate was decided, not to add a fourth state nothing above this asks for.
const ( GateOutcomeApproved GateOutcome = "approved" GateOutcomeRejected GateOutcome = "rejected" GateOutcomeOpen GateOutcome = "open" )
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"`
// StubReason is the resolving entry's recorded reason for being a stub —
// why the decision was made and which issue tracks replacing it (DKT-607).
// It rides here so the documented decision is discoverable from the
// preflight itself, not only from a tribunal transcript or the trust file.
// Empty when the entry recorded none, which the renderer calls out.
StubReason string `json:"stub_reason,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"`
// Warning is the matcher's warning for a matched gate, verbatim: today an
// entry with an absolute argv[0] and no content pin, which runs whatever
// that file holds when the gate fires.
Warning string `json:"warning,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 the runner 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.
Declared files are checked under the invocation's exec root: no step has a worktree yet at activation, so that is the tree a gate would run in. A step worktree that later differs is caught by the spawn-time check, not here.
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 the `gate result` shape.
`Stub` is the field that makes the pass-through window safe. Every result the pass-through 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: an input to the step rather than a
// judgment of it, and excluded from the saga's verdict.
Pre bool
// Stub is 1 ONLY for results the pass-through produced. NOTHING THIS
// RUNNER PRODUCES SETS IT — the field survives so a migrated pass-through
// 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
// TargetSHA keys a PRE-gate row measured by a detached run to the commit
// it measured, so a claim reuses it for that target only. The
// runner never sets it: the detached phase stamps its rows after the
// measurement, exactly as the claim path stamps Pre. Empty everywhere
// else.
TargetSHA string
}
GateResultRow is one result this runner produced, in the `gate result` shape plus `reason` 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 this encoding 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. PassThroughRunner and the real runner both implement it. The saga is written against this interface, so swapping runners 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`.
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.
Commands []string
// CommandHashes are the SHA-256s activation stored alongside each command,
// positionally aligned with Commands.
//
// The real runner re-verifies each one against its stored command BEFORE
// spawning. The activation snapshot already closes "the issue body cannot
// inject"; this closes the narrower "the stored row cannot be swapped". A
// direct database write is outside the 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 the workflow's two gate spellings.
type GateStatusResult ¶
type GateStatusResult struct {
StepStatus string
// Proposal is the display id of the vote this gate opened, empty on a
// human gate and on a vote gate whose proposal has not opened yet.
Proposal string
Outcome GateOutcome
Tally *GateTally
Seats []GateSeat
MissingSeats []string
Target *GateTarget
}
GateStatusResult is `gate status`'s one-call envelope.
func GateStatus ¶
GateStatus reads STEP-N's decision state: step status, outcome, and — for a vote step with an open proposal — the tally and every declared seat's cast state, all from ONE StepView plus the two proposal reads a seat roster needs.
stepID must name a `type="human"` or `type="vote"` step — the same restriction `guard gate` applies, because every other step kind has no decision to report status for.
type GateTally ¶
GateTally is a vote gate's score against its threshold. Nil on a human gate and on a vote gate whose proposal has not opened yet — a gate with no proposal has nothing to tally.
type GateTarget ¶
GateTarget is the git ref a gate judges — the same resolved pair StepView.TargetSHA/TargetWorktree carries, present only when the step's packet named one.
type GateVerdict ¶
type GateVerdict struct {
Gate string `json:"gate"`
Status string `json:"status"`
// Reason says why, naming the paths and steps involved, so an operator can
// tell a `make` entry (retarget it to name the script) from a pin that was
// never recorded (the run activated before gate scripts were pinned), or
// an edited copy from a swept work root.
Reason string `json:"reason"`
}
GateVerdict is one finding about a declared gate's script: the run does not pin it, it could not be checked, or a copy of it where a gate runs (the invoking exec root, a step's recorded work root) differs from the pin or is absent. Status names which.
type GitPick ¶
type GitPick struct {
// Before is the checkout's HEAD ahead of the pick.
Before string
// Tip is its HEAD after, the commit a `step annotate --integrated-sha`
// names.
Tip string
// Picked lists the commits the pick added, oldest first.
Picked []string
}
GitPick is what one range pick did to the shared checkout.
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 ¶
GuardGate answers `docket guard gate --step NAME [--run RUN-N]`: does a PASSED gate step of that name exist?
"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 vote machinery, 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.
WHICH RUNS ANSWER is the caller's choice, and the two forms ask different questions:
runID names ONE run, and only that run's gate can answer. An approval is a decision about one run's change, so another run's approval says nothing about this one. This is the form for a caller that knows which run it is acting under — an executor's brief carries its step id, and `step show` resolves the run from it. The named run is exempt from project scoping (naming a run is naming intent, as `guard record` reads it); a run that does not exist is a NOT_FOUND refusal rather than a denial or an allow, because a typo in a hook must not read as either; and a run that has ended denies outright, since its gates authorized work that is over.
runID 0 answers over EVERY active run in scope, allowing on the first approved gate of that name. The reading is cross-run by construction: one run's approval opens the gate for every caller in the project until that run finishes, including a caller working under a second run whose own gate is still undecided. The form exists for callers with no run context — an operator session's hook cannot know which run a git write belongs to — and a hook that has a run should scope instead.
projectID scopes the unscoped 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 ¶
GuardRecord answers `docket guard record`: is there no unreconciled dispatch?
"Unreconciled" is an open dispatch OR any discrepancy — THE SAME TWO PROBES `next` USES, 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.
`--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 GuardSpawnActive ¶
func GuardSpawnActive( conn *sql.DB, projectID, decidingVote int, usageJoin bool, nowMS int64, ) (*GuardVerdict, error)
GuardSpawnActive answers `docket guard spawn --active`: may every active run in the project accept a spawn right now?
DKT-1287: docket-spawn-guard-hook.sh resolved the `run status` list and asked `guard spawn` about `runs[0]` alone, so with two concurrent active runs the OLDER run's reap half went unasked — a hold on it would not have denied the hook at all. This answers (b), the reap half, over EVERY non-terminal run in the project (guardRunScope's own scope), denying if ANY would deny and naming which.
IT ANSWERS ONLY THE REAP HALF. GuardSpawn's (a), the row comparison, is a fact about ONE run's open dispatch and ONE proposed batch — there is no reading of "these rows against every active run" that means anything, and a relay spawning against a specific run's manifest already has that run's id to pass via `--run`. `--ack-reap` is the same kind of per-run act and stays on the `--run` path for the same reason.
`--deciding-vote` IS NOT: the proposal names its own run. A proposal is linked to issues, and an issue's steps name the runs it serves (decidingVoteRuns), so the carve-out admits the hold of every served run and a run the proposal does not serve denies exactly as it would without the flag. Without this a hook launching a panel had to ask twice — `--active` first, then `--run RUN-N --deciding-vote` with the run parsed off the denial.
Runs are checked in guardRunScope's own oldest-first order, so the FIRST denial found names the OLDEST run that would deny.
`--usage-join` (DKT-3289) admits the hold of every run: the launch claims no step on any of them, so no run's headroom is at stake.
func GuardStop ¶
GuardStop answers `docket guard stop`: is the machine done working?
The base rule: 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, and a `gated` step suspended on its triage panel while that panel's proposal is open.
A `pending` step waiting only on its predecessors, or waiting because the RUN ITSELF is `waiting-human` (paused), does not block on its own (DKT-1845): a paused run is the run-level version of the same wait-on-a-person exemption a `waiting-human` STEP already gets, and a step that never started is not work a stop interferes with. The same holds for a step held because its ISSUE is parked on a person (CondIssueParked): that condition precedes CondPredecessors, so a parked step's successors report the park rather than their unfinished predecessor. Any other unreadiness — headroom, 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. A `claimed`/`running` step still denies even while its run is paused — `run pause` honors in-flight completes rather than killing them, so the machine may still have a live worker to interrupt.)
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 ¶
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 HeldStep ¶
type HeldStep struct {
Step string `json:"step"`
Instance string `json:"instance"`
Issue string `json:"issue"`
Reason string `json:"reason"`
}
HeldStep is a step parked by a hold (DKT-4273), with the reason the holder gave. A manifest lists these beside the rows it withheld, so a reader sees that a missing row is held on purpose and why.
type HoldOptions ¶
type HoldOptions struct {
// Reason is `--reason`, REQUIRED: it becomes the step's park reason.
Reason string
// By is who held it and from where (see Attribution).
By Attribution
// Token is the run's conductor capability; required on a bound run.
Token string
NowMS int64
}
HoldOptions are `step hold`'s inputs.
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 Holder ¶
Holder names one step that holds what a refused claim needed, with the status it holds it in.
type IncompleteClaimError ¶
type IncompleteClaimError struct {
// Result is the claim as transaction A committed it. Context carries the
// bundle that transaction recorded, which is what the step's inputs are
// bound to; it lacks only what the failed stage would have added.
Result *ClaimResult
Err error
}
IncompleteClaimError is a claim whose LEASE COMMITTED and whose remaining work then failed (DKT-1564).
Everything after transaction A — the pre-gate phase, the context transaction, the authoritative render — runs as the winning claimant, on a lease that is already durable and already accrued. A bare error from there hands the caller a refusal while the engine holds a lease in the caller's name: the token was never issued, so `complete`, `fail`, `heartbeat` and `release` are all closed to it, and the step sits until its TTL or an operator-gated reap. RUN-90 lost two steps and two ack-reap panels to exactly that.
So the token travels WITH the failure. The caller is the claimant, it can end the lease it now provably holds, and the failure is still reported — the two facts are both true and neither is worth suppressing.
It is raised ONLY where the committed lease is still the caller's. A claim lost during the pre-gate phase (the refresh guard matching zero rows) reports an ordinary CONFLICT: that token is dead, and offering it would be a lie.
func (*IncompleteClaimError) Error ¶
func (e *IncompleteClaimError) Error() string
func (*IncompleteClaimError) Unwrap ¶
func (e *IncompleteClaimError) Unwrap() error
type IntegratedRange ¶
type IntegratedRange struct {
// Known is false when git could not answer; the caller refuses rather than
// record an integration nobody verified.
Known bool
// Carried reports that the tip holds a patch-equivalent of every commit of
// the writer's chain, or holds the chain itself.
Carried bool
// From is the commit the integrated range starts after: the first parent of
// the earliest integrated commit. The re-pinned diff runs From..tip, so a
// multi-commit writer's diff covers every integrated commit, not the last.
From string
// Missing names the writer commits (short ids) the tip carries no
// equivalent of, when Carried is false.
Missing []string
}
IntegratedRange is what IntegrationRangeFn establishes about a commit that is said to carry a writer's work (DKT-4269).
type IntegrationAnnotation ¶
type IntegrationAnnotation struct {
// IntegratedSHA is the verified commit, as recorded.
IntegratedSHA string `json:"integrated_sha"`
// ResolvedFrom is the head the step's previous `issue.diff` record named —
// the commit the landed content diverged from.
ResolvedFrom string `json:"resolved_from,omitempty"`
// Repin is the `issue.diff` re-record this annotation performed, in the
// shape the `--worktree` re-pin reports; Unchanged when the step's record
// already named this sha at this body.
Repin *IssueDiffRepin `json:"issue_diff_repin,omitempty"`
}
IntegrationAnnotation is what one `--integrated-sha` annotation did.
type IntegrationCheck ¶
type IntegrationCheck struct {
// Status is "verified" (every write-class step's commit reached the
// shared branch) or "skipped" (--skip-integration-check named a reason).
Status string `json:"status"`
// Reason is the operator's stated reason for skipping, present only when
// Status is "skipped".
Reason string `json:"reason,omitempty"`
// Checked names every write-class commit this close accepted and how:
// asked of git this pass, honored from a prior close, or vouched for by
// this close's skip reason. A skipped close asks git nothing, but still
// records what its reason covered — that record is what lets the next
// close honor it (integrationCandidatesTx).
Checked []CheckedIntegration `json:"checked,omitempty"`
}
IntegrationCheck is what a close's verification did, riding on CloseOutcome and the close event (AC3).
type IntegrationSkip ¶
type IntegrationSkip struct {
// Conditions is `--no-conditions`, the waiver for unfiled seat conditions
// (DKT-4263). It rides here because both are close-time overrides that the
// same conductor token authorizes.
Conditions ConditionsWaiver
Reason string
Token string
}
IntegrationSkip is `dispatch close --skip-integration-check`. The zero value runs the integration check. A non-empty Reason skips it, and Token is the run's conductor capability that authorizes the skip on a bound run.
type IssueDiffRepin ¶
type IssueDiffRepin struct {
// Worktree is the checkout the diff and target were recomputed from.
Worktree string `json:"worktree"`
// FromSHA is the target the step's previous issue.diff record named — the
// stale one — or "" when the step had recorded no diff at all.
FromSHA string `json:"from_sha,omitempty"`
// ToSHA is the re-pinned target: Worktree's HEAD at re-pin time.
ToSHA string `json:"to_sha"`
// Artifact is the `ARTIFACT-N` the re-pin recorded, and Supersedes the
// step's previous issue.diff artifact it revises. Both empty on an
// Unchanged re-pin, which records nothing.
Artifact string `json:"artifact,omitempty"`
Supersedes string `json:"supersedes,omitempty"`
// Unchanged reports that the step's newest issue.diff already describes
// Worktree's tree byte for byte, at the same head, so nothing was recorded
// and no event was written: the resolution proceeds and the caller says
// so, rather than refusing a resolution whose only fault is redundancy.
Unchanged bool `json:"unchanged,omitempty"`
// contains filtered or unexported fields
}
IssueDiffRepin is what one re-pin did, as the verb reports it.
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 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
// Stalled reports that the refusal was non-convergence rather than the
// `max_fix_loops` bound: budget remained, but the loop was not moving.
Stalled bool
// 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 loop rule 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.
WHICH step routed decides WHICH CLUSTER enters (cluster scoping, DKT-544): the serving `loop = true` bodies, their `after_loop` roots' downstream, and any per-cluster round bound are all read for the TRIGGERING step's name — a `fix-loop` from `verify` and one from `reconcile` produce identical effects exactly when the same bodies serve both, which is always true in a workflow that declares no `serves`. The counter stays the ISSUE's either way: every cluster's entries share one ordinal sequence and one global ceiling.
func EnterLoopAuthorized ¶
func EnterLoopAuthorized( tx *sql.Tx, step *db.Step, def *workflow.Definition, note string, 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.
`note` is the resolution's `-m` text — the operator's steering for the round they just paid for (DKT-725). It is stamped onto the round's freshly instantiated rows as their entering routing record, which is the ONE channel that reliably reaches a new round's rendered packet: issue comments are not a context source at all, and a mid-run `description` edit never renders either, because `body_snapshot` is frozen at activation. Before this stamp, the note died on the SUPERSEDED park's row — RUN-51's fix@9 rendered zero bytes of the judge-converged remedy the operator recorded when authorizing it.
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
// and the boundary for "reaps this relay has not yet seen".
OpenedSeq int64 `json:"opened_seq"`
// ExpiresMS is not in the wire shape and is emitted alongside it, because
// both open refusals must 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"`
// Total is the ready set's size BEFORE `--limit` cut it, and Truncated
// says whether the cut dropped anything. They ride BESIDE the wire shape
// for StaleTargets' reason: a relay reading only `rows` cannot tell a run
// whose remaining work fits from one the cap is metering out, and RUN-95's
// conductor read a post-cut count as the run's whole offer twice against a
// 930-row ready set. Rows are untouched, so hashing and `dispatch verify`
// are unchanged.
Total int `json:"total"`
Truncated bool `json:"truncated"`
// Limits is the effective per-class `[limits] max` from the scheduler's
// merged limits, for every class present in the manifest's rows. Without
// it a relay inferred each class's concurrency from the largest same-stage
// count in the manifest, which a chain-deep manifest carrying few issues
// per stage under-certifies.
Limits map[string]int `json:"limits,omitempty"`
// LimitSources names what set each Limits entry, keyed by the same
// classes: "workflow", "override", or "auto" (ClassCapSource).
LimitSources map[string]string `json:"limit_sources,omitempty"`
// StaleTargets rides BESIDE the wire 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 wire
// 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"`
// Reaped names the step instances whose leases THIS open reaped — the
// same fact `next` reports in ReadySteps.Reaped, on the wire because a
// relay that only ever opens dispatches never calls `next`. OpenedSeq is
// the boundary for reaps the relay has not yet seen; a reap the open
// itself performs lands AFTER that boundary, and --ack-reap applied to the
// same open cannot cover it. Without this a relay learned of its own
// open's reap only when `guard spawn --active` denied the launch it had
// just composed (RUN-95: two of four launches, about nine minutes each to
// convene the panel, join its usage, and relaunch).
Reaped []string `json:"reaped,omitempty"`
// ReapHold is the guidance for every unacknowledged bounded-class reap
// still holding this run's headroom once the open committed — the reaps
// above included — with each seq and the flag that clears it. It is the
// text `guard spawn` will deny the next launch with, handed to the relay
// BEFORE it composes that launch, so the panel or the acknowledgment can
// come first. Empty whenever nothing is held.
ReapHold string `json:"reap_hold,omitempty"`
// RetryEligible lists the gap-only parks whose issue's depends_on or blocks
// prerequisites are all done and one moved after the park (DKT-4257), so the conductor settles them
// advisor-first in this loop pass instead of waiting for an operator to ask.
// Beside the wire shape for StaleTargets' reason: it is a live-derived fact,
// never a row field. The open also records one park-retry-eligible event
// per park.
RetryEligible []RetryEligible `json:"retry_eligible,omitempty"`
// Held lists the run's held steps with the holder's reason (DKT-4273). A
// held step is parked, so it is never a row; this says that a withheld row is
// held on purpose, ready or staged, so the conductor keeps no exclusion list
// of its own. Live-derived like RetryEligible.
Held []HeldStep `json:"held,omitempty"`
}
Manifest is the `dispatch` wire shape: `{dispatch, run, opened_seq, rows}`.
type MemberCount ¶
MemberCount is one point of ClusterComplementarity's distribution: how many clusters this round reduced from exactly this many members.
type MoveRunOptions ¶
type MoveRunOptions struct {
RunID int
// Verb names the CLI verb for the refusal message: pause, resume, abandon.
Verb string
// To is the target status; From is the closed set of statuses the
// transition may leave.
To model.RunStatus
From []model.RunStatus
// Reason is `--reason`, required by the CLI on an abandon.
Reason string
// Under is the authority a PAUSE or an ABANDON is exercised under
// (DKT-1899), required on exactly those two. A RESUME carries none: it
// returns a run to work rather than disposing of anything, so there is no
// resolution for an authority to answer for.
Under Authority
// Token is the run's conductor capability (DKT-2465, conductor.go):
// required on a bound run, ignored on an unbound one.
Token string
NowMS int64
}
MoveRunOptions are the lifecycle verbs' inputs — one options entry rather than a seven-positional signature, for the reason DecideOptions gives.
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 (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 the 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.
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 schema-less behavior — which is what makes the survival suite a re-run of the schema-less 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 PacketGate ¶
type PacketGate struct {
Name string
// Pre marks a gate that runs at claim rather than at record.
Pre bool
// Source is a fence gate's source; its commands are the fenced lines,
// each matched when the gate runs, so no single argv is rendered.
Source string
// Matched and Argv are the trust store's answer for a named gate.
Matched bool
Argv []string
// Reason is why a named gate is unmatched.
Reason string
}
PacketGate is one declared gate as a packet renders it (DKT-3292).
type PassThroughRunner ¶
type PassThroughRunner struct{}
PassThroughRunner is the stub 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 ¶
func (PassThroughRunner) Run(_ context.Context, g GateSpec, _ StepContext) (GateResult, error)
Run returns `{verdict: "pass", exit: 0, stub: true}`. It never spawns anything: "no subprocess ever executes inside a transaction" holds trivially here, and it still holds for the real runner because the saga's gate stage is its own transaction-free stage.
type PinEpoch ¶
type PinEpoch struct {
// Epoch numbers from 1, so a zero on a step means "no epoch", never
// "the first one".
Epoch int `json:"epoch"`
// FromSeq is the event seq this agreement took effect at — the
// `run-activated` event for epoch 1, the repin's first event for the rest.
// Zero for epoch 1 on a run whose activation event has been pruned away.
FromSeq int64 `json:"from_seq"`
AtMS int64 `json:"at_ms,omitempty"`
// Origin is `activation` or `repin`.
Origin string `json:"origin"`
// Reason is the operator's `--reason`, verbatim, on a repin epoch.
Reason string `json:"reason,omitempty"`
// Changes is what MOVED entering this epoch, one entry per changed ref,
// carrying both hashes exactly as the event recorded them. Empty on epoch 1:
// nothing moved, the run froze.
Changes []RepinChange `json:"changes,omitempty"`
}
PinEpoch is criterion 2's timeline: one segment of a run's life during which its pin agreement did not move.
Epoch 1 is ACTIVATION — the agreement the run froze. Every later epoch is one `run repin`, which is the only thing in the engine that rewrites a `pins` row (the two statements in repin.go are the whole set; activation's own writes are `INSERT OR IGNORE`, so re-activation ADDS refs and never moves one).
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"`
// References is the CLOSURE check, kept beside the per-pin check rather
// than mixed into it: a conductor reading this needs to know which of the
// two failed, because they have different remedies and only one of them is
// about the filesystem. Empty (never nil), in ref order.
References []ReferenceVerdict `json:"references"`
// Changed, Missing and Unpinned are the counts a caller branches on without
// re-walking the lists.
Changed int `json:"changed"`
Missing int `json:"missing"`
Unpinned int `json:"unpinned"`
// Gates lists the declared gates whose script this run does not pin
// (GateUnpinned) or could not check (GateUnchecked), those whose copy
// under the invoking exec root differs from the pin
// (GateExecutedCopyChanged) or is absent (GateExecutedCopyAbsent), and
// those whose copy under a work root a step recorded differs from the pin
// (GateStepCopyChanged) or is absent, or whose work root is gone
// (GateStepCopyAbsent), by gate name. Sound() reads only
// GateExecutedCopyChanged: a `make <target>` entry names no script file,
// an absent copy cannot run, and a step may legitimately edit a gate
// script, so Sound() does not read GateStepCopyChanged or
// GateStepCopyAbsent. Empty (never nil) only when every declared gate was
// checked, found pinned, and found matching where it would execute, in
// gate-name order.
Gates []GateVerdict `json:"gates"`
// UnpinnedGates counts only the GateUnpinned rows.
UnpinnedGates int `json:"unpinned_gates"`
// ExecutedCopyChanged counts the GateExecutedCopyChanged rows, the one
// gate status Sound() reads.
ExecutedCopyChanged int `json:"executed_copy_changed"`
}
PinReport is `docket run verify-pins`.
func VerifyPins ¶
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: 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.
The tree is the RUN's: refs resolve against runConfigRoots, so a run whose pins live under its checkout's `.docket/config` verifies the same from any cwd, including one outside every git checkout.
func (*PinReport) Sound ¶
Sound reports whether every pin still matches, the pin set is closed, and no gate run from the invoking checkout would execute a script other than the pinned one. A run can fail any of these alone: RUN-59 had all 30 pins matching disk and four judge steps that could not be claimed. It does not read the step work-root rows (GateStepCopyChanged, GateStepCopyAbsent): they report copies a step may legitimately edit, never a blocking finding.
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 the pin check 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: both resolve refs against runConfigRoots, the run's recorded exec root, falling back to the invoking roots when none is recorded.
It is the PER-PIN half only. `verify-pins` also reports refs the pinned bytes reference and the pin set does not hold (DKT-821), and that question has no per-pin verdict to render here — it is the verb's answer, not a drift line — so this advisory keeps to the pins and keeps to a hash-per-pin of work on every `run status`.
type PinnedWorkflowStaleness ¶
type PinnedWorkflowStaleness struct {
// Ref is the pin's ref verbatim — `name@version`, what the run froze.
Ref string `json:"ref"`
Name string `json:"name"`
// PinnedVersion is the version this run is expanded from.
PinnedVersion int `json:"pinned_version"`
// CurrentVersion is the highest registered version of this name that STILL
// BINDS — the version a run activated now would pin. Zero when no version
// of the name binds any more (every one retired, or the name is gone from
// this project's registry), which is a fact worth publishing rather than
// rounding to the pinned version.
CurrentVersion int `json:"current_version"`
// Behind is THE STALENESS COUNT: how many binding versions are registered
// STRICTLY ABOVE the pinned one.
//
// A count of rows rather than `current - pinned`, and never negative. The
// subtraction would go negative the moment a higher version is retired
// while a run holds it, and would over-count where a name's versions are
// not contiguous — a registry with @1 and @8 and nothing between is one
// version of advance, not seven.
Behind int `json:"behind"`
}
PinnedWorkflowStaleness is criterion 1: one pinned workflow, and how far the registry has moved past it.
It covers WORKFLOW pins only. A `schema` pin is also a registered `name@version` and could carry the same diff, but the issue asks about workflows and a schema's version moves for different reasons; a FILE pin (a contract, a fragment, `policy.toml`) has no version at all — its "11 versions stale" in the retro means corpus git commits, which live outside this store and which core cannot count. `verify-pins` is the verb that answers the file question, by hash.
type PolicyAssignment ¶
type PolicyAssignment struct {
Model string `json:"model,omitempty"`
Effort string `json:"effort,omitempty"`
Variant string `json:"variant,omitempty"`
}
PolicyAssignment is the {model, effort, variant} triple ResolveExecutor and ResolveSeat compute (DKT-1282 AC1) — `next --run`'s wire shape for it.
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"`
// Stub and StubEntry are the flags `docket step gates` carries, so a
// consumer judging from a `pass` here can tell a placeholder or migrated
// row from a measurement. StubEntry describes the authorizing trust entry:
// it is false on a row no entry authorized, so read the verdict first.
Stub bool `json:"s3_migrated,omitempty"`
// StubEntry is never omitted: an absent key would read the same as a
// docket too old to carry the flag.
StubEntry bool `json:"stub"`
Reason string `json:"reason,omitempty"`
}
PreGateResult is a gate result as it rides in the context bundle.
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.
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 ProjectRegistryAudit ¶
type ProjectRegistryAudit struct {
ProjectID int `json:"project_id"`
Project string `json:"project"`
Prefix string `json:"prefix,omitempty"`
Identity string `json:"identity,omitempty"`
// Compared is how many distinct registered names were classified. It is
// carried so "no findings" is distinguishable from "nothing registered
// here" — two clean-looking results with nothing in common.
Compared int `json:"compared"`
Behind []RegistryLag `json:"behind"`
Orphaned []RegistryOrphan `json:"orphaned"`
}
ProjectRegistryAudit is one project's verdict.
func (ProjectRegistryAudit) Clean ¶
func (p ProjectRegistryAudit) Clean() bool
Clean reports whether this project matched the corpus on every name.
type PruneQuery ¶
type PruneQuery struct {
// Before deletes events with `seq < Before` — STRICTLY LESS, 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. It is the
// preferred form — 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. Zero is every run.
RunID int
// DryRun computes the answer and deletes nothing.
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. 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. 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.
LiveRuns []string `json:"-"`
}
PruneResult is what a prune did, or would have done.
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, 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" // CondIssueParked is R2b: no step of the issue is parked `waiting-human`. // A park is the ISSUE's — the operator is being asked about that issue, // and nothing else on it moves until they rule — and only the issue's: // the run stays `active` and every other issue's rows keep scheduling // (reconcileRun parks the run only once no unparked work remains). // RUN-90 measured the run-level alternative: eleven parks, each a single // issue's verify, each stopping all 45 issues — 1372 of 1656 dispatched // rows never launched. CondIssueParked ReadyCondition = "the issue is parked on an operator decision" // 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. 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. CondBudget ReadyCondition = "no budget headroom" // CondPreGatePending is the pre-gate hold: a // detached run is measuring this step's pre-gates against the target its // claim would resolve, and still holds the (step, target) in-flight lock. // The claim never waits on that run, so a claim admitted now would run // the gate inside its own budget and record the cut — the row the // detached run exists to replace. Evaluated after every clause but the // budget (R7): it applies only to a step whose place in the run admits // it, so a step still behind its predecessors keeps reporting // CondPredecessors (and stays stageable) whatever the lock says, and a // CondBudget refusal still means every other condition held, which the // claim's cost override relies on. A stored manifest row whose step is // held verifies as matched (dispatch verify): the hold defers the claim // without changing the offer. It lifts // when the lock does — the run records its rows, or the child dies and // the kernel drops the flock — and a run that never launched holds // nothing. CondPreGatePending ReadyCondition = "a detached pre-gate run for its target is in flight" )
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.
// 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.
// "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 that 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
// Limits is the effective max the scheduler enforces for each class the
// offer's Steps carry, and LimitSources names what set each one:
// "workflow", "override", or "auto" (ClassCapSource). A class with no
// positive max is unbounded and absent from both, the rule Manifest.Limits
// follows.
Limits map[string]int
LimitSources map[string]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 ReconcileOutcome ¶
type ReconcileOutcome struct {
Run string `json:"run"`
Backfill *BackfillOutcome `json:"backfill,omitempty"`
Verify *VerifyResult `json:"verify,omitempty"`
Close *CloseOutcome `json:"close,omitempty"`
}
ReconcileOutcome is what all three stages did, in the order they ran.
A stage that never ran is absent (`omitempty`), so a reader can tell "verify passed with nothing to say" from "verify never happened" — the distinction the per-stage errors exist to preserve.
type ReferenceVerdict ¶
type ReferenceVerdict struct {
// Status is always PinUnpinnedReference; it is carried per row so a
// consumer branches on one field name across both lists.
Status string `json:"status"`
Ref string `json:"ref"`
// IncludedBy is the closure file(s) whose `packet_includes` name the ref —
// the contract an operator actually edits. Empty when a step's own `packet`
// entry is what reaches it.
IncludedBy []string `json:"included_by,omitempty"`
// RequiredBy is the pending step(s) (and unexpanded phases) that can still
// open the ref — the claims that will refuse.
RequiredBy []string `json:"required_by"`
// Path is where the file sits on disk, when it does. Present-but-unpinned
// is the RUN-59 case and its remedy differs from absent-and-unpinned, the
// same distinction DKT-818 drew in the claim-time refusal.
Path string `json:"path,omitempty"`
}
ReferenceVerdict is one ref the run's own pinned bytes reach that the run does not pin (DKT-821).
It is deliberately NOT a PinVerdict. A pin verdict answers "do these bytes still match", and there are no pinned bytes here to match; conflating the two would also hand every consumer of `Pins` — repin's dispositions, the `run status` drift advisory — a row with no pinned hash to reason about.
type RefreshedBody ¶
type RefreshedBody struct {
Run string `json:"run"`
Issue string `json:"issue"`
// FromSHA256 and ToSHA256 name the superseded snapshot and the refreshed
// one. The shas rather than the bodies in THIS answer: a description runs
// to kilobytes, and the digest is what an operator reading the verb's
// output compares. The event is the other case — it carries the superseded
// text itself, because that text is the only record of the body an attempt
// claimed before the refresh was handed (see recordedIssueBody).
FromSHA256 string `json:"from_sha256"`
ToSHA256 string `json:"to_sha256"`
// Steps names the non-terminal instances the refresh reaches, in id order:
// exactly the steps that will render the new body.
Steps []string `json:"steps"`
}
RefreshedBody reports what a refresh did, as the verb answers with it.
func RefreshIssueBodyInRun ¶
func RefreshIssueBodyInRun( conn *sql.DB, runID, issueID int, reason string, by Attribution, token string, nowMS int64, ) (*RefreshedBody, error)
RefreshIssueBodyInRun re-reads `issues.description` and overwrites ONE run-issue's `body_snapshot` and `body_sha256` (DKT-2291).
The unit is (run, issue) for the reason it is there: that is where the snapshot LIVES, one row per bound issue per run, read by every step of that issue. A per-step verb would let two steps of one issue state two different requests inside one dispatch.
On a bound run it requires the conductor capability as `token`, as the scope refresh does: `issue edit --description` is ungated, so without this check an executor could amend its own issue and then make that amendment reach its own packets.
type RefreshedFiles ¶
type RefreshedFiles struct {
Run string `json:"run"`
Issue string `json:"issue"`
// From and To are the frozen list and the live one that replaced it, both
// in path order, the order activation freezes them in.
From []string `json:"from"`
To []string `json:"to"`
// Steps names the non-terminal instances the refresh reaches, in id order.
Steps []string `json:"steps"`
}
RefreshedFiles reports what a refresh did, as the verb answers with it.
func RefreshIssueFilesInRun ¶
func RefreshIssueFilesInRun( conn *sql.DB, runID, issueID int, reason string, by Attribution, token string, nowMS int64, ) (*RefreshedFiles, error)
RefreshIssueFilesInRun re-reads the issue's attached paths and overwrites the `files` key of ONE run-issue's activation snapshot.
On a bound run it requires the conductor capability as `token`: `issue file add|remove` is ungated, so without the check an executor could change its own issue's attachments and then make that change reach its own packets.
type RefreshedScope ¶
type RefreshedScope struct {
Run string `json:"run"`
Issue string `json:"issue"`
// From and To are the frozen scope and the live one it was replaced with,
// in DECLARED ORDER — the author's order, which the snapshot echoes back
// verbatim (issue_scope.go) and which this must not sort.
From []string `json:"from"`
To []string `json:"to"`
// Steps names the non-terminal instances the refresh reaches, in id order:
// exactly the steps that will render the new scope and record their diffs
// over it.
Steps []string `json:"steps"`
}
RefreshedScope reports what a refresh did, as the verb answers with it.
func RefreshIssueScopeInRun ¶
func RefreshIssueScopeInRun( conn *sql.DB, runID, issueID int, reason string, by Attribution, token string, nowMS int64, ) (*RefreshedScope, error)
RefreshIssueScopeInRun re-reads `issues.scope_globs` and overwrites the `scope` key of ONE run-issue's activation snapshot (DKT-869).
The unit is (run, issue) rather than a single step because that is where the snapshot LIVES: one blob per bound issue per run, read by every step of that issue. A `step refresh-scope STEP-N` would name one step and silently move its siblings' scope too, which is a blast radius the argument no longer matches — and it would let two steps of one issue disagree about scope inside one dispatch, which is the drift the quiescence rule exists to prevent.
On a bound run it requires the conductor capability (DKT-2465) as `token`: `issue edit --scope` is ungated, so without this check an executor could widen its own issue and then make that widen reach its own packets.
type Registration ¶
type Registration struct {
// Kind is `schema` or `workflow`. The pinned-only files do not appear here;
// they are counted (the report'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`. The 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 — the report's wire row.
type RegistryAudit ¶
type RegistryAudit struct {
// Roots are the instance-config roots the one scan walked, in precedence
// order, so a reader can tell WHERE current was read from — and can see
// when a root they expected is missing.
Roots []string `json:"roots"`
Projects []ProjectRegistryAudit `json:"projects"`
// BehindTotal and OrphanedTotal are store-wide counts of the FINDINGS, not
// of the projects carrying them.
BehindTotal int `json:"behind_total"`
OrphanedTotal int `json:"orphaned_total"`
}
RegistryAudit is the whole store's verdict, plus where "current" was read from.
func AuditRegistries ¶
func AuditRegistries( conn *sql.DB, index *CorpusIndex, opts RegistryAuditOptions, ) (*RegistryAudit, error)
AuditRegistries compares every project's registered workflow and schema rows against ONE scan of the shared corpus.
The index is passed in rather than scanned here so the CALLER owns the two refusals that precede any verdict — no root exists, and a root that would not scan — and can word them for an operator. A report that silently classified against an empty index would name every registration in the store.
type RegistryAuditOptions ¶
type RegistryAuditOptions struct {
// ProjectID audits ONE project; zero audits every project in the store,
// which is the whole point of the verb and therefore the default.
ProjectID int
}
RegistryAuditOptions narrows the audit.
type RegistryLag ¶
type RegistryLag struct {
Kind string `json:"kind"`
Name string `json:"name"`
// RegisteredVersion is the highest version this project holds a row for,
// retired versions INCLUDED: the question is what the registry contains,
// not what would bind, and a retired row is still a registration.
RegisteredVersion int `json:"registered_version"`
CurrentVersion int `json:"current_version"`
// CurrentPath is the corpus file declaring the current version, so the
// operator can read the definition they are behind without a second search.
CurrentPath string `json:"current_path,omitempty"`
}
RegistryLag is one registered name whose highest registered version is BEHIND what the corpus now declares.
type RegistryOrphan ¶
type RegistryOrphan struct {
Kind string `json:"kind"`
Name string `json:"name"`
// Versions are every registered version of the stranded name, ascending.
// A rename typically strands several at once, and `workflow deprecate`
// takes them one at a time.
Versions []int `json:"versions"`
// Retired is true when EVERY version listed is already deprecated — the
// orphan an operator has finished with. It is reported rather than filtered
// out, for `workflow list --orphans`' reason: hiding an already-retired
// orphan would leave a cleanup pass unable to see its own work. It reads
// `deprecated_at_ms` on both registries: `workflow deprecate` for one,
// `schema deprecate` (v36) for the other.
Retired bool `json:"retired"`
}
RegistryOrphan is one registered name that NO file in any scanned root declares any more — the state a rename leaves behind, since registering the new name never retires the old one.
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 — 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 — and the resolved contract verifies against its pin like any other entry, while a hint whose contract does not exist refuses loudly naming the exact path. Since DKT-581, activation pins the packet CLOSURE rather than the whole config tree — entries substituted with the declared executor and fanout hints — so a resolved hint outside the workflow's own declarations resolves only if the corpus declares it somewhere in the bound definition (the shipped corpus declares label-resolved executors as `when`-gated steps, which the closure covers) or the operator pinned its contract with `--pin`.
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"`
// Dropped marks the DKT-582 disposition: the ref no longer resolves at all
// and nothing non-terminal reads it, so the pin was retired rather than
// moved. NewSHA256 is "" on exactly these, and the recorded event carries a
// null `new_sha256` — "there are no bytes now" said in the trail, which is
// a different fact from "the bytes are these".
Dropped bool `json:"dropped,omitempty"`
// Added marks the DKT-805 disposition: the adopted bytes' packet closure
// reaches a file ref the run never pinned, so a pin was created rather than
// moved. OldSHA256 is "" on exactly these, and the recorded event carries a
// null `old_sha256` — "there were no pinned bytes before" said in the
// trail, the mirror of the drop's signature.
Added bool `json:"added,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; a parallel pin-history table would be a second source of the same fact.
DROPPING A REF THAT NO LONGER RESOLVES (DKT-582). "Adopt current disk bytes" has nothing to say about a ref with no current bytes, so repin refused the whole set on one NOT_FOUND — and corpus commits that DELETE a contract are as ordinary as commits that edit one. RUN-42 died on `contracts/test-infra.md` and `contracts/pr-comment-author.md` being deleted; RUN-36 died the same way after surviving two earlier repins. Neither run had a step left that would ever open either file.
So there is a second disposition, and its guard is the pending packet closure (pending_closure.go): a NOT_FOUND FILE pin that NO non-terminal step can reach may be RETIRED — the row deleted, a `run-repinned` event recorded with a null `new_sha256`. The properties above are unchanged by it. Completed steps' provenance still is not rewritten (the old sha is in the event, and their own rows are untouched), and the run stays whole rather than partially recovered, because a ref nothing left to run can read is not part of the agreement the remaining steps work under.
It is OPT-IN — `--drop REF` or `--drop-unresolvable` — for the same reason repin itself is a separate verb: deciding a pinned file is gone for good is a person's decision, and an engine that made it silently would delete the pin that was about to explain a refusal. A NOT_FOUND ref a pending step DOES read still refuses, naming the steps, however the flags are spelled.
ADOPTED BYTES BRING THEIR OWN CLOSURE (DKT-805). "Adopt current disk bytes" used to re-hash the run's EXISTING pin set and nothing more — but the pin set is the packet closure of the bytes the run froze (DKT-581), and the adopted bytes can have a DIFFERENT closure. A corpus edit that makes a contract include a new fragment left repin reporting full success while every step reading that contract became unrenderable: the fragment was not pinned, so render refused (VALIDATION_ERROR) with "start a new run" as its remedy — the disposition repin exists to avoid. RUN-56 lost a whole 18-row dispatch to exactly that, the day after an operator-approved repin reported "Repinned 10 pin(s) (0 dropped, 19 already matched)".
So a repin that adopts changed bytes also walks the pending packet closure those bytes reach (pending_closure.go — the same walk the drop guard uses) and PINS every reachable file ref the run does not already hold, at its current disk hash, in the same transaction. Adding a pin can never rewrite completed provenance: no recorded step ever read an unpinned ref (render refuses on exactly that), so there is no history for the new row to contradict — the same argument RA3's re-activation additions rest on. Each added ref records its own `run-repinned` event with a null `old_sha256` and `added: true`, the mirror of the drop's signature: "there were no pinned bytes before" is a different fact from "the bytes were these".
A newly-required ref with NO bytes on disk refuses the whole set up front, naming the ref and the steps that read it — pinning it is impossible and repinning around it would trade the CONFLICT for a render-time refusal, which is the exact failure this closure walk exists to close. The dispositions are the missing-refusal's: restore the file, or abandon the run and re-plan.
type RepinOptions ¶
type RepinOptions struct {
// Reason is --reason, recorded verbatim on every event.
Reason string
// Drop names refs to retire individually. A named ref that still resolves,
// or that this run does not pin, is a refusal rather than a no-op: it means
// the operator and the run disagree about what is wrong.
Drop []string
// DropUnresolvable applies the same disposition to EVERY currently drifted
// file pin that no longer resolves and that no non-terminal step reads,
// without naming each. Pins that resolve to different bytes are untouched
// by it — those are repin's ordinary business.
DropUnresolvable bool
// Token is the run's conductor capability. A repin moves the agreement
// every remaining packet renders from, so on a bound run only the
// conductor may do it.
Token string
}
RepinOptions carries the operator's dispositions for pins that "adopt the current disk bytes" cannot fix on its own (DKT-582).
DROPPING IS OPT-IN, ALWAYS. A ref that stopped resolving is either a deletion the run can survive or a deletion that wedges it, and only the pending closure can tell the two apart — so the verb refuses by default and these fields are how a person says "retire it", never how the engine decides to.
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"`
// Dropped is one entry per pin RETIRED because its ref no longer resolves
// and no non-terminal step reads it — empty (never nil) when none were.
Dropped []RepinChange `json:"dropped"`
// Added is one entry per pin CREATED because the adopted bytes' packet
// closure reaches a ref the run never pinned (DKT-805) — empty (never nil)
// when the adopted closure was already covered.
Added []RepinChange `json:"added"`
// Unchanged counts the pins that already matched disk.
Unchanged int `json:"unchanged"`
}
RepinOutcome reports what RepinRun did.
func RepinRun ¶
RepinRun re-pins a run's drifted pins to what their refs resolve to now, presenting token as the run's conductor capability.
func RepinRunWith ¶
func RepinRunWith( conn *sql.DB, runID int, opts RepinOptions, nowMS int64, ) (*RepinOutcome, error)
RepinRunWith is RepinRun with the DKT-582 dispositions.
type RepoPaths ¶
type RepoPaths struct {
// ExecRoot is the cwd every gate and action runs in, and the containment
// boundary for their binaries.
ExecRoot string
// Identity is what trust entries bind to — the project, not the
// checkout.
Identity string
// LockPath is the tree-mutex lockfile serializing this project's
// tree-declaring gates.
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, so a gate's binding check and its cwd could not 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 ResolveOptions ¶
type ResolveOptions struct {
// As is `--as`: one of resolveValues.
As string
// Note is `-m`/`--note`, the steering channel (DKT-247/DKT-725).
Note string
// Batch is `--batch` (DKT-546); override-pass only.
Batch bool
// DropInterposed is `--drop-interposed` (DKT-861); override-pass only.
DropInterposed bool
// Worktree is `--worktree` (DKT-1034): RE-PIN the step's recorded
// `issue.diff` to this checkout's tree before resolving — see
// IssueDiffRepin. override-pass and rerun-gates only.
Worktree string
// By is who is ruling and from where. REQUIRED: an empty field refuses the
// resolution before anything is written (see Attribution).
By Attribution
// Under is the authority the resolution is made under (DKT-1899).
// REQUIRED: the zero value refuses it before anything is written.
Under Authority
// Token is the run's conductor capability (DKT-2465, conductor.go):
// required on a bound run, ignored on an unbound one.
Token string
NowMS int64
}
ResolveOptions are `step resolve`'s inputs — the CompleteOptions/ClaimOptions shape, adopted here when the third positional variant arrived (DKT-1034): a fourth dimension on `(as, note, batch, dropInterposed)` is how call sites end up passing the wrong bool.
type ResolveOutcome ¶
type ResolveOutcome struct {
// Repin is the issue.diff re-pin `--worktree` performed, nil without the
// flag.
Repin *IssueDiffRepin `json:"issue_diff_repin,omitempty"`
}
ResolveOutcome is what a resolution reports beyond its error: the facts the verb established that the step row does not carry.
type RetryEligible ¶
type RetryEligible struct {
Step string `json:"step"`
Instance string `json:"instance"`
Issue string `json:"issue"`
ParkClass string `json:"park_class"`
DependsOn []string `json:"depends_on"`
// contains filtered or unexported fields
}
RetryEligible is a step parked gap-only whose issue's depends_on or blocks prerequisites are all done, at least one of them linked or changed after the park (DKT-4257). The park waited on a gap another issue was filed to close; once those issues are done the park is a retry candidate, settled advisor-first, instead of sitting until an operator or a conductor with a long memory asks about it.
func RetryEligibleParks ¶
func RetryEligibleParks(conn *sql.DB, runID int) ([]RetryEligible, error)
RetryEligibleParks lists the run's gap-only parks whose issue has at least one blocking prerequisite (depends_on, or the inverse blocks) and no prerequisite that is not done, where at least one prerequisite was added or last changed after the park, in step order. A prerequisite that was already met before the park cannot be what the park waits on. A pure read: `run status` renders it, and `dispatch open` renders it and records the event.
type RoutingLintReport ¶
type RoutingLintReport struct {
// Label is the configured security label; empty means the lint is off.
Label string `json:"label"`
// LexiconTerms is the number of configured lexicon terms.
LexiconTerms int `json:"lexicon_terms"`
// LabelledIssues is the number of bound issues carrying the label whose
// scope the overlap check can compare (a glob with a literal prefix): the
// only issues an overlap can be found against.
LabelledIssues int `json:"labelled_issues"`
// Linted is the number of bound issues without the label that were checked.
Linted int `json:"linted"`
// Unscoped is the number of linted issues whose scope held no glob with a
// literal prefix, so the overlap check could not apply to them.
Unscoped int `json:"unscoped"`
}
RoutingLintReport states what a dry run's lint was able to check. It is present on every dry run, so "no warnings" is distinguishable from "nothing was checkable".
func (RoutingLintReport) CheckedNothing ¶
func (r RoutingLintReport) CheckedNothing() bool
CheckedNothing reports a lint that was off, or had no lexicon and nothing to compare scopes against (no comparable labelled issue, or no linted issue with a comparable scope): its empty result carries no information.
type RoutingWarning ¶
type RoutingWarning struct {
Issue string `json:"issue"`
// Label is the security label the issue lacks.
Label string `json:"label"`
// Overlaps lists the security-labelled issues whose scope this issue's scope
// overlaps.
Overlaps []string `json:"overlaps,omitempty"`
// Terms lists the lexicon terms found in the issue's title, scope or body.
Terms []string `json:"terms,omitempty"`
// Reason is the one-line statement of the above.
Reason string `json:"reason"`
}
RoutingWarning is one bound issue the lint flagged.
type RowMismatch ¶
RowMismatch is the verify 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 verify must show "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: 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 ¶
GetRunBudget is the read form.
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, the meaning the flag has always carried and the one `budgetSnapshot.unlimited()` enforces. Raising and lowering are the same operation — the verb exists to "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 — a line that
// exists because the cap'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 the 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: 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.
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"`
// VoteUsageNote states, on any run whose panels cast, that the seats'
// measured spend (the report's `vote_usage` section) is EXCLUDED from
// `reported` and `spend` — and why (DKT-584).
//
// It is a note rather than a fold because `Reported` must remain exactly
// the rows enforcement's own snapshot sums (the step usage_ledger): the
// report publishing a "reported" no decision was made against is the
// two-sources-of-truth failure RunFloorTx's export exists to prevent. The
// seats' DECLARED cost reaches the budget through the floor instead — a
// vote step's expected_cost accrues at materialization — so the panel is
// not invisible; its measured spend is simply accounted in its own
// section, and this line says so instead of leaving the omission silent.
VoteUsageNote string `json:"vote_usage_note,omitempty"`
}
RunBudgetReport is 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, 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.
type RunFact ¶
type RunFact struct {
Run string `json:"run"`
Kind string `json:"kind"`
Seq int64 `json:"seq"`
Proposal string `json:"proposal,omitempty"`
Voter string `json:"voter,omitempty"`
Step string `json:"step,omitempty"`
Cause string `json:"cause,omitempty"`
Reason string `json:"reason"`
}
RunFact is what `run fact add` recorded.
func RecordRunFact ¶
func RecordRunFact(conn *sql.DB, opts RunFactOptions) (*RunFact, error)
RecordRunFact validates and records one conductor-observed fact.
type RunFactOptions ¶
type RunFactOptions struct {
RunID int
Kind string
Reason string
// ProposalID and Voter name the re-seated seat (vote-reseated).
ProposalID int
Voter string
// StepID and Cause name the deferred step (step-deferred).
StepID int
Cause string
// Token is the run's conductor capability, from DOCKET_TOKEN or stdin.
Token string
By Attribution
NowMS int64
}
RunFactOptions are `run fact add`'s inputs.
type RunNote ¶
type RunNote struct {
ID int `json:"id"`
Text string `json:"text"`
// Authority is the authority the writer recorded the note under, operator
// or conductor, and RunNoteAuthorityUnknown for a note whose row stores
// neither. It never comes from Text, which may claim any authority.
Authority string `json:"authority"`
// RecordedAtMS dates the note, so a reader of a packet can tell a note
// recorded before dispatch from one added mid-run.
RecordedAtMS int64 `json:"recorded_at_ms"`
}
RunNote is one recorded note, in the ONE shape it has everywhere: the verb's answer, the `run note list` row, and the bundle's `notes` element are all this, so a consumer parses one shape wherever a note appears.
func AddRunNote ¶
func AddRunNote(conn *sql.DB, runID int, token string, by Attribution, under Authority, text string, nowMS int64) (*RunNote, error)
AddRunNote records one note against a run, event-logged, in one transaction.
It refuses an empty note (a note with no content steers nothing), one over the cap, an unknown run, and a TERMINAL run: a run that is done or abandoned renders no more packets, so a note against it would be a statement nobody will ever be told, recorded as if somebody would. A `planning` run is fine — the motivating note is written BEFORE the first dispatch, and the verb must not force a dispatcher to activate first.
One trailing newline is dropped, so a note fed from a file renders exactly as the same words passed inline; nothing else about the text is touched.
A note renders in every later packet as the dispatcher's word, so on a run bound to a conductor capability only its holder may write one: `token` is checked inside the transaction that writes the note, by the same matrix the operator verbs use. An unbound run accepts a note without a token.
`under` says whose ruling the note carries, and the run-note-added event records it as `authority`. The token proves the writer holds the run; it cannot tell an operator's decision relayed by the conductor from the conductor's own, so the writer must state which. ParseRunNoteAuthority's two-value set is enforced here as well as at the CLI, so it binds every writer.
`by` says who wrote the note and from where, and the event records it as `actor` and `cwd`, as every ruling does. A note with either field empty is refused as a validation error before anything is written.
type RunNoteRuling ¶
type RunNoteRuling struct {
Note int `json:"note"`
Authority string `json:"authority"`
Actor string `json:"actor"`
Cwd string `json:"cwd"`
}
RunNoteRuling is one run note and its attribution, read from its `run-note-added` event: the note id that joins it to `run note list`, the authority it was recorded under, and the actor and cwd the writer resolved. A note event recorded without attribution keeps its row with those fields empty, because nothing can supply them after the fact.
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"`
// PinnedWorkflows is DKT-594's first half: per pinned workflow, how many
// registered versions the corpus has advanced since this run froze.
//
// It rides in the report rather than in `verify-pins` because the question
// is not about DRIFT. A pinned `ui-change@8` whose file is byte-identical to
// what the registry holds is perfectly sound and can still be five versions
// behind, and `verify-pins` — which compares hashes at one ref — is right to
// call it `ok`. What a post-mortem reader needs before trusting a finding is
// the other number, and until this section it existed in no read verb: every
// analyst on RUN-32 recovered it from git by hand.
PinnedWorkflows []PinnedWorkflowStaleness `json:"pinned_workflows,omitempty"`
// PinEpochs is DKT-594's second half: the run's pin-agreement timeline,
// PRESENT ONLY ON A RUN THAT REPINNED.
//
// A run whose agreement never moved has one epoch, every step ran under it,
// and the `pins` table already says what it was — so the section is absent
// and each step's `pin_epoch` is absent with it, rather than a column of 1s
// on every report in the store. Where it IS present it is what RUN-39's
// analysts assembled by hand from event seqs and step ids.
PinEpochs []PinEpoch `json:"pin_epochs,omitempty"`
// Steps is the count by EFFECTIVE status, computed at read.
Steps []model.StatusCount `json:"steps,omitempty"`
// Attempts is the other half: per-step attempts, ordered by instance.
Attempts []StepAttempt `json:"attempts,omitempty"`
// Issues is the 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 the per-actor event counts.
//
// It publishes the traceability rule'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"`
// Authorities is DKT-1899: the run's resolutions and run notes counted by
// the authority they were made under. Actors answers WHAT CAUSED a transition;
// this answers, for the human ones, WHAT ENTITLED it — the distinction the
// conductor policy routes on and that only a free-text note used to carry.
Authorities []AuthorityCount `json:"authorities,omitempty"`
// NoteRulings is every run note the run recorded, in event order, with who
// wrote it and from where. Authorities counts the notes; this names their
// writers.
NoteRulings []RunNoteRuling `json:"note_rulings,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 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"`
// Complementarity is DKT-2452: per `aggregate` step, how many of its
// clusters are UNIQUE (one member) versus CORROBORATED (more than one),
// the distribution of member counts across clusters, and how many
// upstream artifacts fed the round that produced them. It is the number
// Anthropic's fanout-width case for multi-agent review depends on ("only
// 12 vulnerabilities in common") and that this run document had no way to
// show: `aggregate` has always written `members` on every cluster, and
// nothing before this read it back.
//
// It reads `members` (engine-owned), never the author's own
// `member_ids` linkage key: `members`' length already equals
// `member_ids`'s wherever both exist (findings-cluster@3 declares them in
// the same arrival order), and `members` is ALSO defined on a
// carried-forward standing finding, which has a scalar `severity` and no
// `member_ids` at all. Reading the author's key here would make core's
// generic rollup depend on one workflow's own vocabulary; reading
// `members` keeps it something every `aggregate` step answers, whatever
// field it aggregates.
Complementarity []ClusterComplementarity `json:"complementarity,omitempty"`
// SourceAttribution is DKT-2462: per `aggregate` step that declares
// `source_field`, one row per DISTINCT value that field's arrays name
// across the round's clusters — the step ref that produced a member — with
// how many clusters that source contributed to and, when the ref resolves
// to a step in this run, the executor hint that step declared.
//
// It reads `params.source_field` (aggregate.go's ParamSourceField), never
// a corpus-owned name such as `member_sources`: core groups by whatever
// key the workflow names, the same way `route_at` takes its floor as an
// opaque value rather than assuming a field. Absent the param, this is
// nil, matching Recorded's absent-parameter convention.
SourceAttribution []ClusterSourceAttribution `json:"source_attribution,omitempty"`
// Metadata is 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"`
// SilentVoteSeats is the identity behind VoteUsageCoverage.Silent
// (DKT-733): each cast that reported no spend — which seat, on which
// proposal, seated via which path. The count alone told an operator that
// seats went silent on a run and nothing said WHICH, so `vote
// backfill-usage` — the verb that exists to close exactly this gap
// (DKT-115) — could not be aimed without spelunking proposals by hand.
//
// `omitempty`: a run whose every seat reported carries no key, because the
// coverage line already says so and an empty list would restate it.
SilentVoteSeats []SilentVoteSeat `json:"silent_vote_seats,omitempty"`
// Findings is DKT-2451: every structured finding the run's panels
// recorded, one row per entry, with the evidence it cited — or the
// statement that it cited none.
//
// It exists because the record could not tell an ASSERTED finding from a
// REPRODUCED one: `step_inputs` says what a judge step was served, and
// nothing said what a cast relied on. A cast may now cite
// `artifact:ARTIFACT-N` and `gate:<name>` references the engine resolved
// against this run when the cast recorded (ValidateCastEvidence), and this
// section is where the two kinds of finding become distinguishable: an
// entry with no citation carries `unsupported`, set out loud rather than
// left to be inferred from an absent list.
//
// Casts on a proposal that is SealedOpen (DKT-2447) are withheld here as
// they are in every other read verb; they appear once the tally closes.
// `omitempty`: a run whose panels recorded no structured findings carries
// no key.
Findings []CastFinding `json:"findings,omitempty"`
// 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"`
// MissingUsage is D2 asked WITHOUT its grace: every claimed step that
// reached a terminal status and has no ledger row, however recently.
// `next` and `dispatch close` give such a step `dispatch.grace` to be
// back-filled, so a relay that launches its usage join beside the close
// rather than ahead of it is never refused for the lag — and a run's last
// wave has no later close to refuse at all. This is where that wave's
// silence shows: the done report reads it the way it reads
// `silent_vote_seats`, and both must be empty before a run is called
// finished. `omitempty`: a fully billed run carries no key.
MissingUsage []Discrepancy `json:"missing_usage,omitempty"`
}
RunReport is the whole document.
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 ¶
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 ¶
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 — dormant — 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 ¶
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) ClassCaps ¶
ClassCaps returns the effective max and its source for every class this snapshot bounds: the same caps R5 enforces. A class with no positive max is unbounded and has no entry, the rule rowClassLimits follows, so every report of class caps lists the same classes.
func (*Scheduler) ClassHolders ¶
ClassHolders names the steps occupying this step's class headroom: the claimed, running, and gated steps classInFlight counts, so a CondHeadroom refusal says which launches still hold the slots (DKT-4264). A gated holder is the usual answer — its worker finished and its gates still run. The unacknowledged reaps classInFlight also counts follow, as their reaped steps, so a refusal with no live holder still names what to wait on.
func (*Scheduler) Expired ¶
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. The spec 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 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 ¶
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 ¶
IssueAwaitingDecision reports whether ANY step of one issue is an undecided declared gate whose turn has come.
func (*Scheduler) LabelsFor ¶
LabelsFor is the frozen labels of the issue a step belongs to, for rendering a `next` row. It answers nothing about readiness.
func (*Scheduler) LoopHoldReason ¶
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 ¶
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) ScopeHolders ¶
ScopeHolders names the claimed or running steps that make CondScope refuse this step (DKT-4264).
func (*Scheduler) SizeFor ¶
SizeFor is the frozen size of the issue a step belongs to, for rendering a `next` row. It answers nothing about readiness.
func (*Scheduler) SortSteps ¶
SortSteps orders a ready set by PRIORITY THEN AGE ("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 ¶
Steps returns the loaded step set, for callers that render alongside the readiness answer.
func (*Scheduler) UnacknowledgedReaps ¶
UnacknowledgedReaps exposes the snapshot's open reaps for the REFUSAL MESSAGE.
A headroom denial with nothing running is otherwise baffling, 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 ¶
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 ¶
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 SilentVoteSeat ¶
type SilentVoteSeat struct {
Proposal string `json:"proposal"`
Voter string `json:"voter"`
Role string `json:"role,omitempty"`
Path string `json:"path"`
}
SilentVoteSeat is one cast that reported no spend, with the seating path that minted its proposal (DKT-733). The proposal id is the argument `vote backfill-usage` takes, so each row is an aimable backfill, not just a name.
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 SourceWarning ¶
type SourceWarning struct {
Workflow string `json:"workflow"`
// State is the model.WorkflowSourceState value, so a consumer branches on
// the verdict rather than on prose.
State string `json:"state"`
SourcePath string `json:"source_path,omitempty"`
RegisteredSHA256 string `json:"registered_sha256"`
CurrentSHA256 string `json:"current_sha256,omitempty"`
Reason string `json:"reason"`
}
SourceWarning is one BOUND workflow whose registered source file no longer answers for the registered bytes, on the terms activation warns rather than refuses about (see checkBoundSources).
It travels on the result for ScopeWarnings' reason: the engine holds no output dependency, and the verb picks the channel — stderr in human mode, an array in JSON.
type SpawnOptions ¶
type SpawnOptions struct {
// Rows is the JSON array a relay is about to spawn, 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,
// the second proposes spawning nothing.
Rows []byte
// AckSeqs are `--ack-reap SEQ`, repeatable. 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
// UsageJoin is `--usage-join` (DKT-3289): the launch claims no step — a
// read-only usage join over finished transcripts — so it consumes no
// write-class headroom and a reap hold has nothing to protect from it.
// Like DecidingVote it relaxes the REAP half only and is event-logged;
// unlike it, nothing names a proposal, so the engine enforces the one
// thing it can check: a launch that proposes rows is dispatch-bearing
// and is denied under the hold.
UsageJoin bool
NowMS int64
}
SpawnOptions are `guard spawn`'s inputs beyond the run.
type StageError ¶
type StageError struct {
// Stage is one of StageBackfill, StageVerify, StageClose.
Stage string
// Err is the stage's own refusal, unmodified.
Err error
// Verify and Mismatch are set ONLY on a verify-stage row mismatch, which
// is the one refusal a caller cannot render from the error text alone:
// the verify diagnostic is the differing BYTES of both renderings plus
// DKT-243's per-row verdict block, and those live on the result, not in a
// message.
// Carrying them here is what lets the reconcile path print the identical
// refusal `dispatch verify` prints rather than a summary of it.
Verify *VerifyResult
Mismatch *RowMismatch
}
StageError names WHICH stage of a reconcile refused.
It wraps rather than replaces: Unwrap reaches the stage's own error, so CodeOf still finds the taxonomy code the standalone verb would have exited with and errors.Is still reaches any db sentinel underneath. A reconcile therefore exits with the SAME code for the same failure — the message gains a stage name and nothing else changes.
func (*StageError) Error ¶
func (e *StageError) Error() string
func (*StageError) Unwrap ¶
func (e *StageError) Unwrap() error
type StaleTarget ¶
type StaleTarget struct {
Instance string `json:"instance"`
Issue string `json:"issue"`
TargetSHA string `json:"target_sha"`
Reason string `json:"reason"`
// Absent distinguishes DKT-742's harder case machine-readably: the
// recorded target does not resolve as a commit object from the shared
// checkout AT ALL (`git cat-file -e <sha>^{commit}` fails), as opposed to
// resolving but sitting off HEAD's history. A consumer planning a wave
// reads it as "no seat can reconstruct this tree", not merely "confirm
// the tree before reviewing". `omitempty` keeps every divergence-shaped
// advisory byte-identical to what it always was.
Absent bool `json:"absent,omitempty"`
}
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 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 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"`
// ParkReason is the ENGINE's text for why this step parked, kept apart
// from Routing so neither overwrites the other (DKT-1898).
//
// On a RESOLVED park, Routing is the resolution — its routing and the
// resolver's note — and this is still the question that park asked. Reading
// the two together is what the report could not do: 328 park events across
// the store, and the engine's own text survived on 3 of them.
//
// Empty on a step that never parked, and on one parked before the column
// existed.
ParkReason string `json:"park_reason,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"`
// Ruling is the LAST operator ruling recorded on this step — an approve,
// a reject, a resolve, or a forced reap — and who made it, from where
// (DKT-2450). `Routing` says what was decided; this says by whom. Absent
// on a step nobody ruled on, and on one whose last ruling predates
// attribution.
Ruling *StepRuling `json:"ruling,omitempty"`
// PinEpoch is WHICH PIN AGREEMENT this step's recorded work ran under
// (DKT-594), indexing RunReport.PinEpochs.
//
// Absent unless the run actually repinned, and absent on a step that has not
// run — see PinEpochs and stepReportsAnEpoch. On a run whose agreement moved
// mid-flight it is the field that says which bytes a completed step
// consumed: `pins` holds only the CURRENT agreement, completed steps' rows
// are never rewritten, and correlating the two was the hand-join RUN-39's
// post-mortem performed against event seqs (5375/5376 vs STEP-1350/1353).
PinEpoch int `json:"pin_epoch,omitempty"`
// Metadata is THIS STEP'S WHOLE BAG, verbatim (DKT-868) — the detail behind
// RunReport.Metadata exactly as StepUsage is the detail behind
// Budget.Reported.
//
// The rollup answers "which values did this key take, and how often". It
// cannot answer "which values did two keys take TOGETHER on one step",
// because grouping by key is precisely what discards the pairing. Any bag
// whose keys are a REQUEST and its RESOLUTION — the shape the corpus
// actually writes — is therefore unaggregatable from the report: RUN-51's
// rollup showed one key with no `low` value and its partner with one, a
// mismatch on exactly one step that no reader could name. Recovering it
// meant `step show` per step, and the audit that motivated this ran ~90 of
// them across 19 runs.
//
// It rides on the ATTEMPT ROW rather than in a section of its own so the
// bag arrives already joined to the four facts that make it interpretable:
// effective status, routing, attempt count and issue. That is what closes
// the other half of the gap — a step that FAILED or was reaped carries only
// what its dispatcher recorded at claim (`step claim --metadata`, DKT-592),
// and in a rollup that half-bag is indistinguishable from a completed
// step's, so drift concentrated in failures reads as no drift at all.
//
// CORE READS NO KEY HERE, as everywhere. It
// publishes the bag; what a pair of keys MEANS — a tier, a variant, a desk
// — stays the workflow author's business, and the consumer does the
// comparison core must not learn how to make.
Metadata map[string]any `json:"metadata,omitempty"`
// MetadataUnreadable marks a step whose stored bag exists but does not
// decode, so an absent `metadata` is never silently read as "the dispatcher
// recorded nothing" — the exact ambiguity DKT-868 is about. It mirrors
// model.Vote's field of the same name and the same purpose.
//
// The bag is NOT re-validated here: a read verb that refused because one
// row held odd bytes would be useless during exactly the run an operator
// wants to inspect, which is why db.MetadataRollup skips such a row
// too. This row says so out loud rather than skipping silently, because a
// per-step row IS the row — there are no other rows to carry the fact.
MetadataUnreadable bool `json:"metadata_unreadable,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
// StepID is the step's numeric id, exported to the child as DOCKET_STEP —
// rendered `STEP-N` — so a gate can look up its OWN inputs (DKT-1186).
//
// Instance is the step's `name@k#i` identity, which is what a human reads
// and what a workflow declares; it is not what the read verbs take.
// `docket step context` and `docket step artifacts` both take `STEP-N`, so
// a gate holding only DOCKET_ISSUE had to list the issue's steps and pick
// itself out by an instance-name convention before it could ask the engine
// anything. The numeric id is carried rather than the rendered string
// because every other id in this struct is numeric and the rendering
// belongs at the one place that builds the environment.
//
// Zero means unknown, and the variable is then UNSET in the child rather
// than set to `STEP-0`: absence is the same encoding Scope and Base use,
// and an id that names no row is worse than no id at all.
StepID 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
// Base is the sha of the step's base commit — the commit WorkRoot was
// created from — exported to the child as DOCKET_GATE_BASE (DKT-992) so a
// gate can scan exactly the step's committed range (base..HEAD of the
// tree it runs in). The saga fills it, for worktree-recorded steps only,
// with the same fork-point resolution the diff stage uses (runDiffBase /
// worktreeForkPoint); it stays "" — the var unset — for a shared-checkout
// step and on the pre-claim path, where no committed range belongs to the
// step being gated. Empty means unset, never an invented value: executors
// commit before `step record`, so a gate that falls back to scanning the
// working tree scans nothing, and before this field it had no way to
// learn the range and guessed (`git diff HEAD~1`, wrong for multi-commit
// steps) or measured the clean tree (always empty).
Base string
// CacheRoot is a scratch directory the caller destroys together with the
// tree in WorkRoot, or "" when the tree outlives the spawn (DKT-1166).
//
// Only the pre-claim path fills it, and only when it RECONSTRUCTED the
// tree under review: a reconstruction is deleted within the minute, and a
// linter result cache keyed by package content but carrying absolute
// source paths outlives it, so a later run over the same content re-opens
// a path that is gone, finds no `//nolint` there, and re-emits an issue
// the source suppressed. exec.EnvPolicy.CacheRoot documents the mechanism
// and names the variables it redirects.
CacheRoot string
// Deadline is the wall-clock point past which this gate may not run, or
// zero when the entry's own timeout alone bounds it. Only the pre-claim
// path fills it, from claimPreGateBudget: the claim's pre-gate phase is
// bounded as a whole so `docket step claim` returns inside the executor's
// tool timeout, and each gate's timeout is clamped to what remains. A
// gate that finds nothing remaining records `skipped`; one the clamp cuts
// off records its timeout with a reason naming the budget beside the
// entry's own timeout, so a reader does not conclude the entry changed.
Deadline time.Time
// PinRoot is the RUN's recorded exec root, the checkout its gate scripts
// were pinned under at activation, and FilePins is the run's file pins,
// keyed by ref (an absolute, symlink-resolved path for a gate script).
// The runner compares the copy of each relatively named gate script under
// the gate's cwd with the pin for the same path under PinRoot before every
// spawn. PinRoot is never the invoking checkout: under a saga resumed from
// another checkout that root holds no pins, and every script would run as
// unpinned. Both are empty when no run is in hand, and every relatively
// named script then runs as unpinned, with a note saying so.
PinRoot string
FilePins map[string]string
// Worktree is the writer's recorded worktree, kept when WorkRoot is
// replaced by an engine-owned checkout (DKT-4250): a binary inside the
// writer's own tree is still refused as the executor's file.
Worktree 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 the real runner needs the identity and the working directory, not the closure.
type StepIntegration ¶
type StepIntegration struct {
// Step is the step, STEP-N.
Step string `json:"step"`
// Picked lists the commits added to the shared branch, oldest first.
Picked []string `json:"picked"`
// IntegratedSHA is the shared checkout's HEAD after the pick, the commit the
// step was annotated with.
IntegratedSHA string `json:"integrated_sha"`
// Annotation is the verified annotation the pick ended in.
Annotation *IntegrationAnnotation `json:"annotation"`
}
StepIntegration is what integrating one step did.
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.
// On a waiting-human run, Scheduler.Expired is false, so this field stays unset.
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"`
// PriorAttemptEnd is StepRow's field of the same name (DKT-1279), on the
// inventory row too: how the step's MOST RECENT claim ended, "reaped" or
// "failed", never a tally the breakdown above already gives.
PriorAttemptEnd string `json:"prior_attempt_end,omitempty"`
// ParkClass is why a `waiting-human` row parked, as a closed enum
// (DKT-1900). It belongs on the INVENTORY row because `step list` is where
// a conductor scanning a run for parked work decides which parks it can
// route itself and which need a panel — a question the reason sentence
// beside it cannot be asked programmatically. Empty on every row that is
// not parked.
ParkClass db.ParkClass `json:"park_class,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 ¶
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 ¶
RunStepList answers `docket step list --run RUN-N`: every step of one run, with its EFFECTIVE status 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 StepRuling ¶
type StepRuling struct {
// Event is the ruling's kind: `step-approved`, `step-rejected`,
// `step-resolved`, or `lease-reaped` for a `step reap`. An expiry reap
// shares that kind but is the scheduler's act and never appears here.
Event string `json:"event"`
Actor string `json:"actor"`
Cwd string `json:"cwd"`
}
StepRuling is one operator ruling on a step and its attribution (DKT-2450): the event kind that recorded it, and the claim-level actor and cwd the verb resolved at its call site — see Attribution.
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.
Routing string
SagaStage string
// ParkReason is the ENGINE's text for why this step parked, unchanged by
// the resolution that answers it (DKT-1898). Routing carries the LATEST
// decision and its author's note; on a resolved park those are the
// resolver's words, and this is still the engine's.
//
// Empty on a step that never parked, and on one parked before the column
// existed — that history was never captured and is not reconstructed here.
ParkReason string
// ParkClass is that park's ROUTABLE form (DKT-1900): the closed-enum value
// a conductor or panel decides on, where ParkReason is the prose it shows
// the person it escalates to. Empty exactly where ParkReason is.
ParkClass db.ParkClass
// Authority is what entitled the step's latest ruling (operator,
// standing-grant, or conductor); AuthorityRef names the standing grant and
// is set only beside standing-grant. Both empty on a step never ruled on.
Authority string
AuthorityRef string
// Owner and ExpiresMS are the stored lease's facts, reported whenever the
// effective status still counts that lease: always for a LIVE lease, and
// for a LAPSED one only while the run is not active. Scheduler.Expired —
// the clause EffectiveStatus and Row.LeaseExpired both read — is suspended
// off an active run, so a lapsed-but-unreaped claim on a `waiting-human`
// run still renders `claimed` there, and this view names its owner and
// expiry to match. On an ACTIVE run the effective status reads the lapsed
// lease as gone, and so do these fields: neither is reported, matching the
// v6 `lease` object's rule that a field which is not a fact does not
// appear.
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
// TargetSHA and TargetWorktree are the SAME resolved target ref the step's
// context bundle carries (Context.TargetSHA / TargetWorktree, DKT-24),
// answered without assembling the bundle (DKT-1056).
//
// A vote panel is seated from this read: the wave asks `step show` for the
// gate's row first and probes the (up to 1MiB) bundle only when the row
// names a target, so a target the bundle knows and this view does not is a
// panel reading its own HEAD instead of the judged tree.
//
// BOTH ARE EMPTY, NEVER APPROXIMATE, when the step's inputs resolve no
// `issue.diff` round record. There is no fallback to the shared HEAD here
// and there must not be one — the caller can tell "no target" from "this
// target" only if absence is reported as absence.
TargetSHA string
TargetWorktree string
}
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`.
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 schemas close.
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. "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.
func EvaluateThresholdOverDiff ¶
func EvaluateThresholdOverDiff( instance string, threshold map[string]string, order []string, payloads []map[string]any, schema OrderResolver, facts *DiffFacts, ) (ThresholdResult, error)
EvaluateThresholdOverDiff is EvaluateThreshold with the reserved `diff.*` family available (DKT-2063).
`facts` is non-nil exactly for a step whose completion measured a tree, and a `diff.*` predicate then evaluates over that measurement instead of over the payload set. Passing nil is EvaluateThreshold's behavior in full: `diff.lines` is an ordinary field name nothing declares, and every existing caller keeps the evaluation it had.
type TrustGrant ¶
type TrustGrant struct {
Name string
ArgvSHA256 string
// Argv0SHA256 is the content pin on argv[0]: the SHA-256 of the bytes the
// grant approved, empty when argv[0] is a bare name with no pin. Without it
// a re-pin of a changed script records the same argv hash as the original
// add, so the trail could not show which bytes were approved.
Argv0SHA256 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
// StubReason is the operator's recorded why-and-what-tracks-it for a stub
// (DKT-607). It rides in the event so the grant's trail carries the
// documented decision, not only the fact of hollowness; empty when the
// grant recorded none (every pre-DKT-607 stub) and always empty on a
// non-stub grant.
StubReason string
// 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.
//
// IT IS REQUIRED (DKT-595): RecordTrustEvent refuses an empty one. The
// literal "unknown" — the resolver's last fallback when neither a git
// identity nor an OS user exists — DOES count as supplied: it is the
// resolver's honest report of an anonymous environment, the same value
// every other authored row carries there. What is refused is the empty
// string, which no resolver produces and which can only mean the writer
// never filled the field.
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.
//
// IT IS REQUIRED (DKT-595), same as Actor: RecordTrustEvent refuses an
// empty one, and a caller whose working directory cannot be read must
// refuse the whole trust change rather than degrade the field.
Cwd string
}
TrustGrant is what a trust entry AUTHORIZES: the gate name, the argv hash, the repo binding, and the flags.
IT HAS NO ARGV FIELD, and that absence is load-bearing. The argv itself is forbidden 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 TrustProbeGate ¶
type TrustProbeGate struct {
Name string `json:"name"`
Stub bool `json:"stub"`
// Exit is nil when the entry was never run — an empty argv, or the probe
// was interrupted before reaching it. A nil exit counts as a FAILURE, the
// same rule gate-probe.js held: a vacuous pass must be impossible.
Exit *int `json:"exit"`
LogTail string `json:"log_tail"`
// Warning is the trust lookup's warning for an entry that still ran, such
// as an absolute argv[0] with no content pin. It is kept apart from
// LogTail so the gate's own output is never mixed with engine text.
Warning string `json:"warning,omitempty"`
}
TrustProbeGate is one roster entry's probe result.
type TrustProbeResult ¶
type TrustProbeResult struct {
Head string `json:"head"`
Gates []TrustProbeGate `json:"gates"`
Skipped []TrustProbeSkip `json:"skipped"`
Failed []string `json:"failed"`
Passed bool `json:"passed"`
}
TrustProbeResult is `docket trust probe`'s wire shape.
func ProbeTrust ¶
func ProbeTrust( ctx context.Context, repoRoot string, entries []trust.Entry, isAction func(name string) bool, ) (*TrustProbeResult, error)
ProbeTrust runs every non-action entry of `entries` once, in one throwaway detached worktree of clean HEAD, and reports one row per gate.
isAction classifies a roster entry BY NAME (DKT-1283 AC2/AC5) — built from ActionNames() by the caller so this function stays testable without a filesystem scan. repoRoot is the ORIGINAL checkout: it is the containment boundary exec.Resolve refuses a binary inside of and the value DOCKET_REPO carries, exactly as gate_exec.go's spawnMatched holds it fixed across a step's own worktree — only the working DIRECTORY moves to the throwaway checkout.
THE WORKTREE IS REMOVED UNCONDITIONALLY (AC4): on a normal return, on any error, and when ctx is canceled — a caller that wires ctx to signal.NotifyContext gets removal on SIGINT/SIGTERM too, because a canceled context makes this function return rather than killing the process out from under its own defer.
type TrustProbeSkip ¶
TrustProbeSkip is one roster entry the probe never ran because it is a declared engine action (DKT-1283 AC2) — an engine ACTION is fed a JSON bundle on stdin at record time, so a stdin-less probe can only fail it.
type UnclaimedTarget ¶
type UnclaimedTarget struct {
Step string `json:"step"`
Instance string `json:"instance"`
Status string `json:"status"`
}
UnclaimedTarget names one back-filled step that NO WORKER EVER CLAIMED, so the advisory can be rendered by the caller and read by a JSON consumer (DKT-993).
`Status` is on the row because it is the whole diagnostic: `pending` says the row landed on a step that has not run YET, `superseded` says it landed on one the loop swept before it ever could, and `skipped`/`failed-routed` say it landed on one that was terminated unrun. An operator reading "STEP-3146 (design-qa@1) is pending" knows immediately that the relay's journal mis-joined; a bare step id does not carry that.
type UnexpandedIssue ¶
type UnexpandedIssue struct {
Issue string `json:"issue"`
Status string `json:"status"`
BlockedBy [][]BlockerHop `json:"blocked_by"`
// BlockedByTruncated marks a list cut at maxBlockerChains.
BlockedByTruncated bool `json:"blocked_by_truncated,omitempty"`
}
UnexpandedIssue is a bound roster issue that holds no steps yet, with the unmet depends_on chains keeping it from getting any.
Each chain runs from the issue's direct blocker down to the blocker that is itself unblocked — the root an operator releases or finishes to move the issue. A conductor's close-out question reads it to state which held issues wait behind which, instead of diffing the roster against the step list by hand (DKT-4255).
func UnexpandedIssues ¶
func UnexpandedIssues(conn *sql.DB, runID int) ([]UnexpandedIssue, error)
UnexpandedIssues lists the run's bound issues that hold no step, in issue order. It is a pure read.
type UnfiledCondition ¶
UnfiledCondition is one sealed vote with approve-with-concerns casts and no linked tribunal issue.
type UnintegratedStep ¶
type UnintegratedStep struct {
Step string `json:"step"`
Instance string `json:"instance"`
SHA string `json:"sha"`
Worktree string `json:"worktree,omitempty"`
// How is "unintegrated" (git ran and found no ancestry and no patch
// match) or "cherry-error" (the patch probe itself could not run —
// AC1's "a cherry error counts as unintegrated"). "ancestor" and
// "patch-equivalent" never appear here: either one clears the step.
How string `json:"how"`
}
UnintegratedStep is one write-class step whose recorded commit did not reach the shared branch — dispatch close's refusal, one row per finding (AC1).
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.
type VoteBackfillRow ¶
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 means "nothing to route yet".
Verdict string
// Score is the weighted score the existing tally computed, when there is
// one.
Score *float64
// Required is the proposal's `required_voters` — the denominator a ballot
// count is read against (DKT-895). Carried because the proposal read below
// already has it; nothing recomputes quorum from it.
Required int
}
VoteOutcome is what a vote step's phase-4 read observed.
func ReadStepVoteOutcome ¶
ReadStepVoteOutcome is ReadVoteOutcome asked of the STEP ROW rather than the pinned spec: the same single read, keyed off `step.Kind`.
It exists for callers that have a step and no spec, and must not grow a second read of proposals to compensate (DKT-726). `step resolve` is the motivating one — a resolution is offered on a vote step whatever its status (R11), so the refusal it needs to compute has to be decidable before the pinned definition is even loaded, and for a MATERIALIZED step the definition never declares the minted name at all, so `workflow.StepByName` returns nil there and the spec-keyed reader could never be called.
The MINTED KIND is the authority for what a step is — the same fact the `resolvable` test and the parked-vote refusal above it already key off. A step whose kind is not `vote` has no proposal by construction, and reports nothing.
func ReadVoteOutcome ¶
ReadVoteOutcome reads the decided outcome: 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.
type VoterLedgerRow ¶
type VoterLedgerRow struct {
Voter string `json:"voter"`
// Runs is how many runs in the window the name cast in.
Runs int `json:"runs"`
Casts int `json:"casts"`
// The casts by verdict, in the tally's own vocabulary.
Approve int `json:"approve"`
ApproveWithConcerns int `json:"approve_with_concerns"`
Reject int `json:"reject"`
// FixLoopRoutes, OverridePasses, and HeldClusters count the casts this
// name made on a vote step that then routed `fix-loop`, that an operator
// resolved `override-pass`, or that was a materialized held-cluster
// ballot. Each is a fact about the panel the seat sat on, never about
// how the seat voted; the verdict columns carry that.
FixLoopRoutes int `json:"fix_loop_routes"`
OverridePasses int `json:"override_passes"`
HeldClusters int `json:"held_clusters"`
}
VoterLedgerRow is one voter name's record across the window. Reaps and cluster columns do not apply to a seat — a vote step is never claimed and produces no cluster — so the row carries neither rather than an honest zero that reads as a measurement.
type WaivedTarget ¶
type WaivedTarget struct {
ID int `json:"id"`
Instance string `json:"instance"`
Target string `json:"target_sha"`
}
WaivedTarget names one recorded waiver, as the verb reports it.
type WorkflowOriginIndex ¶
type WorkflowOriginIndex struct {
// contains filtered or unexported fields
}
WorkflowOriginIndex answers, for a registered workflow name, whether a file in the instance-config roots still declares it.
It is built LAZILY. Activation constructs one per activation and consults it only on a binding refusal, so the ordinary path pays nothing: reading and parsing every workflow file in the corpus a second time, on every activation, to answer a question nobody asked would be a real cost for a diagnostic.
func ScanWorkflowOrigins ¶
func ScanWorkflowOrigins() (*WorkflowOriginIndex, error)
ScanWorkflowOrigins scans the instance-config roots for the read verbs (`workflow list --orphans`).
The error is the SCAN's own refusal — a root that is a regular file, a dangling symlink — surfaced rather than swallowed, because those are the states in which every registration would otherwise be reported orphaned on the strength of having looked nowhere.
func (*WorkflowOriginIndex) Err ¶
func (i *WorkflowOriginIndex) Err() error
Err is the failure that stopped the build, if any.
func (*WorkflowOriginIndex) Orphaned ¶
func (i *WorkflowOriginIndex) Orphaned(name string) bool
Orphaned is Status reduced to the one bit the refusal annotations need.
func (*WorkflowOriginIndex) Roots ¶
func (i *WorkflowOriginIndex) Roots() []string
Roots are the canonicalized roots that were scanned, in precedence order.
func (*WorkflowOriginIndex) Scanned ¶
func (i *WorkflowOriginIndex) Scanned() bool
Scanned reports whether any instance-config root existed to look in. It asks nothing of the files themselves — a build failure is Err's to report — so the two refusals a caller owes an operator stay separate facts.
A false answer means every verdict this index gives is `unchecked`, and a caller whose whole purpose is orphan detection should say so rather than render an empty result that reads as "nothing is orphaned".
func (*WorkflowOriginIndex) Status ¶
func (i *WorkflowOriginIndex) Status(name string) *model.WorkflowOriginStatus
Status is the verdict for one registered workflow NAME.
A nil index is `unchecked` rather than a panic: the annotation call sites sit on refusal paths, where the only thing worse than no annotation is a crash while reporting someone else's error.
Source Files
¶
- action.go
- action_exec.go
- action_names.go
- activate.go
- after_fired.go
- aggregate.go
- annotate.go
- annotate_integration.go
- artifact_hash.go
- artifacts_read.go
- autoregister.go
- backfill.go
- batch_override.go
- budget.go
- claim.go
- conductor.go
- config_change.go
- context.go
- definitions.go
- diff_elide.go
- diff_repin.go
- dispatch.go
- dispatch_chain.go
- dispatch_conditions.go
- dispatch_integration.go
- dispatch_reconcile.go
- doctor.go
- drive.go
- engine.go
- event.go
- events_prune.go
- events_read.go
- evidence.go
- executor_ledger.go
- fence_report.go
- gate.go
- gate_exec.go
- gate_fingerprint.go
- gate_preflight.go
- gate_script_pins.go
- gate_status.go
- guard.go
- guard_dispatch.go
- held.go
- hold.go
- human.go
- integrate_git.go
- integration_range.go
- linked.go
- lookahead.go
- loop.go
- metadata.go
- next.go
- note.go
- order.go
- orphan_registration.go
- packet.go
- packet_closure.go
- packet_size.go
- passfloor.go
- payload_validation.go
- pending_closure.go
- pin_staleness.go
- pinref.go
- policy.go
- policy_pin.go
- policy_resolve.go
- pregate.go
- pregate_detached.go
- pregate_scratch.go
- ready.go
- reap_ack.go
- reconcile.go
- recorded_head.go
- refresh_body.go
- refresh_dispatch.go
- refresh_files.go
- registry_audit.go
- render.go
- repin.go
- repopaths.go
- report.go
- retry_eligible.go
- routing_lint.go
- run_budget.go
- run_fact.go
- run_lifecycle.go
- saga.go
- saga_detached.go
- saga_resume.go
- schema_pins.go
- schema_references.go
- scope.go
- scope_refresh.go
- scope_snapshot.go
- source_drift.go
- stage.go
- step_comment.go
- step_integrate.go
- store.go
- strand.go
- threshold.go
- treelock.go
- triage.go
- trust_probe.go
- unexpanded.go
- verify_pins.go
- vote.go
- vote_cast.go
- vote_panel.go
- vote_record.go
- waive.go