Skip to content

Engine improvements: fix loops, votes, packet pins, gates, and registry - #38

Open
erikreinert wants to merge 199 commits into
mainfrom
feature/engine-improvements
Open

erikreinert wants to merge 199 commits into
mainfrom
feature/engine-improvements

Conversation

@erikreinert

@erikreinert erikreinert commented Sep 1, 2026 •

Copy link
Copy Markdown
Member

Changes

Workflow grammar — when predicates gain or and contains_any; steps can declare a severity floor and stall limit; cross-issue linked input form; max_fix_loops counting documented.

Fix loops — entry scoped to per-trigger clusters; loop parks when the routing verdict repeats or the fix commit didn't move; steering notes carry into the next round; run-scoped batch gate-override covers repeated identical failures (DKT-546).

Votes — approved votes route on concerned casts; vote records exposed; retry refused once a proposal has decided; silent seats named with their path; weighted score separated from ballot count; vote-step spend counted against the run budget.

Claims and budget — claims can declare a variant-scaled cost; a forced reap no longer spends the attempt budget; routing metadata recorded at claim; run report pairs requested and resolved tier; aggregate builtin gets a routing floor; warning when usage is backfilled against an unclaimed step.

Packet pins — pin only the closure a run's workflows reach; check the pin set is closed; validate a packet before taking a lease; repin drops gone files and adds newly required ones; registered workflow source drift detected on disk and shown in run reports.

Gates — gate processes get the step's base commit; stub gate results marked with a reason and tracking issue; override-pass refused when it would skip a gate; failed gate named when a step parks; advisory pre-gate verdicts marked; gates named as make targets.

Registry and scope — cross-project workflow/schema registration and a drift audit; orphaned registrations surfaced; --deprecated hides retired versions and show NAME skips them (DKT-616); a widened scope can reach an active run, with a warning when it can't; unresolvable stale targets flagged and waivable.

CLI fixes — events follow --tail starts at the newest N; events list finds a run in any project; next --run returns the full ready set; dispatch close does backfill and verify in one step; trust changes refuse a missing actor/cwd; loop redirects no longer bind an unrelated step's output; operator pause survives a step record; in-repo docket skill copy retired.

@erikreinert erikreinert self-assigned this Sep 1, 2026
@erikreinert
erikreinert force-pushed the feature/engine-improvements branch 2 times, most recently from 583fae1 to 30eb937 Compare September 1, 2026 23:39
56 commits hardening the run engine and the CLI around it.

Workflow grammar — `when` predicates gain `or` and `contains_any`; steps can
declare a severity floor and stall limit; cross-issue linked input form;
`max_fix_loops` counting documented.

Fix loops — entry scoped to per-trigger clusters; loop parks when the routing
verdict repeats or the fix commit didn't move; steering notes carry into the
next round; run-scoped batch gate-override covers repeated identical failures
(DKT-546).

Votes — approved votes route on concerned casts; vote records exposed; retry
refused once a proposal has decided; silent seats named with their path;
weighted score separated from ballot count; vote-step spend counted against
the run budget.

Claims and budget — claims can declare a variant-scaled cost; a forced reap no
longer spends the attempt budget; routing metadata recorded at claim; run
report pairs requested and resolved tier; aggregate builtin gets a routing
floor; warning when usage is backfilled against an unclaimed step.

Packet pins — pin only the closure a run's workflows reach; check the pin set
is closed; validate a packet before taking a lease; repin drops gone files and
adds newly required ones; registered workflow source drift detected on disk
and shown in run reports.

Gates — gate processes get the step's base commit; stub gate results marked
with a reason and tracking issue; override-pass refused when it would skip a
gate; failed gate named when a step parks; advisory pre-gate verdicts marked;
gates named as make targets.

Registry and scope — cross-project workflow/schema registration and a drift
audit; orphaned registrations surfaced; `--deprecated` hides retired versions
and `show NAME` skips them (DKT-616); a widened scope can reach an active run,
with a warning when it can't; unresolvable stale targets flagged and waivable.

CLI fixes — `events follow --tail` starts at the newest N; `events list` finds
a run in any project; `next --run` returns the full ready set; dispatch close
does backfill and verify in one step; trust changes refuse a missing
actor/cwd; loop redirects no longer bind an unrelated step's output; operator
pause survives a step record; in-repo docket skill copy retired.
@erikreinert
erikreinert force-pushed the feature/engine-improvements branch from 30eb937 to 10b6f6a Compare September 1, 2026 23:47
…ainst built dist

Removes the standalone test/qa jobs — go build/test already run inside
build-shell/build via the vorpal pipeline, and qa.sh now runs against
the actual docket-dist-* artifact from the build job instead of a
throwaway `go build` binary, so e2e exercises what actually ships.
repo-gates is renamed to gates to match its new position as the first,
fast-fail job; secret-scan.sh's comment is updated to match.
…breaks

- a tree comparison on a moving branch tip read any later commit on the
  same paths as a divergence, so clean cherry-pick integrations warned
  "integration diverged" on every review row
- a disproved ancestry now asks first whether HEAD carries the target's
  patch, via git cherry refined by zero-context patch-id comparison,
  including a squashed integration's combined diff
- the tree probe remains as a fallback and may only acquit; the advisory
  says "diverged" only when the patch probe measured the patch missing
…olve

- step resolve --worktree, with override-pass or rerun-gates, recomputes the
  recorded issue.diff and target sha from the named checkout and records it
  as a new artifact superseding the step's previous one
- no resolution re-recorded the diff before, so after an out-of-band patch
  every downstream review packet still rendered the pre-patch commit and a
  full fix round ran to report nothing needed fixing
- the re-pin is event-logged as issue-diff-repinned with both shas and rides
  the resolve output as issue_diff_repin
- an empty diff over a recorded change is refused rather than pinned
- extract computeIssueDiff from the routing stage so the re-pin and step
  record share one diff implementation
- fold the resolve variants into ResolveStepWith on a ResolveOptions struct
- a type-filtered listing of eleven documents was 83KB of JSON because every row carried its full body
- rows now carry id, type, status, title, author, timestamps, and body_bytes; bodies stay in doc show
- --with-body restores the previous full-document output for bulk callers
- issue list, next, plan and board now drop description from each JSON row and carry description_bytes in its place
- a filtered listing of a few dozen described issues ran to tens of kilobytes to answer a question that needs ids, titles and status
- --with-body restores the previous full-row shape on all four verbs; issue show is unchanged
- the previous-round pass matched on emitted kind alone, so a re-synthesis
  received the prior round's whole raw review fanout beside the digest of it
- a re-entry step now reads only its own name at the prior ordinal plus the
  standing set the loop body declares it acted on
- one security-change round-2 synthesis carried a 263KB packet of four raw
  judge payloads it was also carrying as the reconciled aggregate
- a vote panel is seated from step show and only probes the context bundle when the row names a target
- the bundle carried target_sha and target_worktree but the step view did not, so every panel read its own HEAD instead of the judged tree
- the view now resolves the same pair in the same transaction, emitting both keys only when the step consumes issue.diff and a round record exists
- steps with no target emit neither key rather than an empty or approximated value
(cherry picked from commit f01b09faa828b3229e413832023f7ffd261c726f)
…ng gap

- derive the reserved-label set (selected by no workflow, declined by
  every workflow) from the live corpus instead of hand-listing it, so
  the sweep tracks corpus changes automatically
- fail loudly, naming the label, when one is declined by only some
  workflows and selected by none (an orphaned exclusion) instead of
  silently reporting it as an unrouted gap
- apply the non-vacuity floor to the routable vocabulary only, after
  reserved labels are set aside, across all three sweep tests
- ParseRelationType no longer accepts related_to/related-to; it backs
  the wire format and the workflow-spec vocabulary, both of which are
  meant to stay exactly the documented set
- move the typo tolerance to a new parseRelationTypeArg helper shared
  by link add and link remove, where it only ever affects a human
  typing at a terminal
- add tests pinning the parser, the JSON decode, and the CLI helper
  so a future alias can't widen the wire format silently again
- new run_notes table (schema v26): append-only, run-scoped, capped
  at 16 KiB per note, dead with the run
- docket run note add RUN-N --text|--file records one; refuses an
  empty or oversized note, an unknown run, or a done/abandoned run
- docket run note list RUN-N renders them back, human and --json
- context assembly reads notes as a sixth source alongside the
  existing five, so step context/claim/render all carry them
- the default packet template renders each note verbatim as its own
  section right after the request body, so an operator ruling made
  before dispatch reaches every step of the run instead of getting
  rediscovered per step
- documented in the engine spec and reliability delta
…g it

- a gap file's leading header block (Severity/Priority/Kind/Labels
  lines right after the title, ending at the first line that isn't
  one) now sets the filed issue's priority, kind, and labels instead
  of every gap landing at priority none, kind task, no labels
- Severity maps to priority (blocker->critical, high->high,
  medium->medium, else none); an explicit Priority line wins either
  way it's ordered against Severity
- unrecognized keys are skipped without ending the block, so a
  Home: line composes with the header; invalid values fall back to
  the default rather than refusing the whole issue
- the body is stored verbatim, header included
- documented in step complete/record's help text
A gate step that ends up skipped (routing resolved elsewhere, an
on_fail=skip rejection, a false `when`, or a quorum miss) used to leave
its downstream `after` successors ready to run anyway, since a skipped
predecessor still counts as terminal. Successors had no way to say
"only run me if that predecessor actually fired."

Add a step-level `after_fired` list: naming a predecessor there means
this step is skipped in the same transaction the moment that
predecessor is skipped, cascading transitively through the graph.
Every `after_fired` entry must also appear in `after`, so the step
still waits for the predecessor to reach a terminal state before the
skip (or the run) is decided.
- the claim wrote step_inputs but no read verb used it, so step context
  and step show re-resolved a handed-out step over the run's current
  artifacts and reported inputs and a target sha the worker never saw
- a claimed step now reads back the bindings its claim recorded; a
  pending or never-claimed step still resolves live
- a re-claim clears the last attempt's bindings before recording its own
- the claim records its bindings before pre-gates run so a read that
  lands mid-claim finds them
- step context --live keeps the current-state resolution reachable
…e with it

- golangci-lint and staticcheck cache issues by package content but store the absolute source path and re-open it to find a suppressing nolint comment
- a pre-gate reconstruction is deleted within the minute, so a later run over the same content replayed a stale entry, could not find the nolint, and failed a clean tree
- point GOLANGCI_LINT_CACHE and STATICCHECK_CACHE at a scratch sibling of the reconstruction that is removed with it; durable trees keep their shared caches
…ays blocked

- Expansion gated on every issue in every earlier topological level, so an
  unrelated phase-1 sibling left at todo by abandon-issue stalled a whole
  chain whose only real predecessor was done
- An issue now expands once its own depends_on predecessors are done,
  matching how the planner reads the same graph
- Activation reports every issue it left unexpanded with the predecessors
  holding it, on the summary line and as blocked_issues in the JSON envelope
…'s domain

- a workflow may declare `[match] domain_paths`; it binds nothing and is read only by an activation lint
- an issue whose whole scope lies inside another workflow's domain, lacking only that workflow's labels, is named in the activation report
- exactly-one-match refuses zero and several matches but cannot see one wrong match; a mis-labelled TUI test issue bound the baseline pipeline and silently lost the UI gates
- warns and never refuses, stays quiet on partial overlap, kind mismatch, or a firing unless_labels
- a gate needing an earlier step's artifact had to list the issue's steps
  and pick itself out by an instance-name convention, which broke silently
- exports the step reference on both completion gates and pre-gates, and
  leaves the variable absent rather than STEP-0 when no step is known
…s reaped

- failed_attempts/reaped_claims tally every ending a step ever had and go
  ambiguous once a history mixes both; a router needs only the last one
- a session killed mid-wave had ten leases reaped and an on_failure policy
  escalated all ten as if they had failed
- schema v27 adds steps.last_claim_end, stamped by the fail and reap paths,
  surfaced as prior_attempt_end on next, dispatch open, step show and list
- next.go also makes IssueStepList return an empty slice instead of nil so
  a stepless issue serializes "steps": [] rather than null
- a nil slice marshals as "steps": null and forces every JSON consumer
  to guard against it
`docket trust probe [--run RUN-N] --json` runs every non-action trust-roster
entry once against a throwaway worktree of clean HEAD, using the same
resolve/spawn/timeout path gates use at record time, and removes the worktree
unconditionally including on interrupt. `docket trust list --json=v2` items
now carry `class: "gate"|"action"`, resolved from the workflow corpus's own
`action = "<name>"` declarations, so a caller can tell the two apart without
grepping workflow TOMLs itself.

This replaces the gate-probe.js workflow script's equivalent, which spent one
agent per gate and could not honor per-entry timeouts.
`docket next --run` executor rows now carry model/effort/variant resolved
from the run's pinned policy.toml, and vote rows carry the same per voter.
Resolution walks the attempt- and round-based escalation ladder, redirects
around [security]-forbidden models via [escalation.fallback], and clamps to
[security].ceiling on a sensitive row -- a direct port of wave.js's and
tribunal.js's resolve()/resolveSeat(), verified against
tests/wave-escalation-walk.test.sh's own table and structural assertions. A
run with no pinned policy.toml leaves every row exactly as it was before this
feature existed.

policy.toml was already pinned per run (packet_closure.go's policyPinRef);
this reads those bytes back instead of leaving routing to a second,
independently-maintained parser downstream.
`dispatch close` now verifies, for every write-class step recorded in the
run, that its own recorded commit is an ancestor of the shared branch's HEAD
or patch-equivalent to a commit on it (a cherry-pick mints a new sha for
identical content), refusing CONFLICT with the step, sha, and worktree
otherwise. A cherry error counts as unintegrated rather than being assumed
equivalent. `--skip-integration-check REASON` is the operator override, and
the close event records integration: verified|skipped plus the checked shas
either way.

Reuses the same ancestor/patch-equivalence probes the stale-target advisory
already shells out with, rather than a second implementation of `git cherry`
plumbing.
- Replaces the six read-only probes a conductor cleared by hand before
  the first dispatch of an attach with one read-only verb
- Every check runs; a missing --run or --source reads as SKIP and
  leaves the report unclean rather than silently checking fewer things
- Straggler worktrees are reported but never move the clean bit
- config set refuses vote.rule.<name>.roster and .weighting by name,
  pointing at the vote-step field that replaced each; stored rows are
  ignored
- SetConfigTx writes inside a caller's transaction and returns the prior
  value at that scope
- A write to any vote.rule key records a config-changed event in the same
  transaction, carrying key, scope, prior, new value and writer
- Attribute the issue-body-refreshed kind so the closed-set tests pass
- A package failing early in a long go test run lost its failing test
  names to the fixed 20-line tail, so the verifier could not tell which
  test failed
- validateOnExhausted emits V41 five ways, but RuleIDs and the test
  table both omitted it, so the set-equality check could not see the gap
- Cluster rounds and the issue loop count agree unless another cluster
  spent an ordinal first, so a second case runs one to tell them apart
- The span tripwire still pinned v34 after the loop-history columns
  bumped the schema, failing the db suite; the amendment records why
  v35 was needed
- Treat `--- `/`+++ ` as file headers only between a `diff --git` line
  and its first hunk; inside a hunk they are content and count toward
  diff.lines, so a removed `-- banner` no longer under-sizes the change
- Derive diff.empty from the ledger's own record-or-drop test over the
  in-scope portion, so rename-, mode-, and binary-only diffs the ledger
  keeps as real changes read as non-empty and route review
- When a retry computes an empty diff and the guard keeps the earlier
  recorded change, measure diff.* from that recorded body instead of the
  empty one, so a retry cannot skip review over an unreviewed change
… time

- Move the diff.lines, diff.files, and diff.empty names into the workflow
  package so validator and evaluator share one spelling
- V45 refuses a diff.* predicate on a step that holds no tree (action,
  type, fanout, or holds_tree = false), where no measurement exists and
  the predicate would silently never match
- V46 refuses a non-integer literal under an ordered operator on
  diff.lines or diff.files, moving the refusal from a parked run to the
  author's terminal
- Exempt exactly the three reserved names from V21a on payload-declaring
  steps: no schema can declare them, and the refusal told authors to add
  diff.lines to a payload schema; lookalikes such as diff.bogus are still
  refused
- Add the reserved diff.* family to the payloads and thresholds TDD:
  measurement, absent-record rule, the V45/V46 lint, and the V21a
  exemption
- Add the on_exhausted row to the engine spec's step key table, the three
  loop-history facts a refused entry writes, and the step row wire shape
  that carries them
…peat

- The byte-identical guard compared the newest recorded body against the
  computed one, and "no record yet" read as an empty body, so a first
  empty diff matched it and was dropped despite the stated rule that a
  genuine first "nothing changed" records
- Report whether a record exists separately from its body and suppress
  only a repeat of an actual record
- V46 now covers == and != on diff.lines and diff.files, since the engine
  parses a count literal as an integer whatever the operator
- V47 refuses an ordered operator or a non-boolean literal on diff.empty,
  so no registered definition reaches a record-time refusal
- Describe the three reserved fields and their register-time rules in the
  engine spec threshold grammar
- Record V47, V46 under equality, and the first-empty-diff record rule in
  the payloads and thresholds TDD
- A fanout step expands to executor rows, and each sibling that holds the
  tree measures and routes on its own recorded diff, so refusing the
  predicate there was stricter than the engine
- Refuse it only where no measurement exists: action and type steps, and
  executor or fanout steps declaring holds_tree = false
… parks

- The attempts-exhausted park stored the worker's fail note verbatim, so an
  empty note parked a step with no reason and `step show` omitted the field
  as if the step had never parked; a refused fix loop on a rejection stored
  `<reject note>; <bound reason>`.
- Every reader documents park_reason as the engine's own text. Both sites
  now write `attempts-exhausted: <n> of <max>` or
  `fix-loop-exhausted: <bound reason>`; a spent triage panel's sentence is
  prefixed with the class.
- The caller's note stays where it was: the routing record, the step-failed
  or step-rejected event, and the trail comment.
- SetStepRoutingWithParkReasonTx takes the park text apart from the routing
  reason; SetStepRoutingTx delegates with the two equal, so the engine-text
  sites are unchanged.
- A step whose triage panel had already ruled lost the worker's fail note
  when its budget ran out again: the panel's sentence replaced it in the
  routing record, the step-failed event and the trail. It is now appended,
  and the park reason carries the budget count ahead of it.
- A park whose reason is blank, such as a failed gate that printed
  nothing, stored an empty park_reason that `step show` omits. The write
  now falls back to the park class word.
- Tests pin the park_reason wire key in raw run-report and step-show
  JSON, the exact composed formats, and the spent-panel path.
- `docket schema deprecate <name>@<version>` retires a schema version the
  way `workflow deprecate` retires a workflow: a timestamp on the row,
  never a delete. Runs that pinned it keep validating against it; new
  `payload` references are refused by register, lint, and activation's
  auto-registration, naming the schema and the restore remedy.
- Refuses a version a workflow still in service names as payload,
  listing the referencers, with no override; refuses the builtin.
  `--restore`, `--project`, and `--all-projects` mirror the workflow verb,
  with `in-use` as its own per-project outcome.
- `schema list` hides retired versions unless `--deprecated`; `schema
  show NAME` resolves the highest version still in service; `registry
  audit` reports a fully retired orphan schema as retired.
- Store migration v36 adds `schemas.deprecated_at_ms`, NULL meaning in
  service, with the column-probing rewind guard and no back-fill.
- `workflow list` and `workflow lint` take `--project <ref>` with the
  write verbs' ref forms and error, so another project's registry can be
  listed and linted against from any checkout, including a project whose
  checkout is missing from this machine.
- Reports each project's verdict (new, unchanged, conflict, invalid) in
  the per-project report register --all-projects writes, so a store-wide
  sweep can be checked before it runs
- A repo-relative path under an instance-config root was stored verbatim,
  so verify-pins, pin show, and resume could never resolve it
- Files under a root now pin by their config-relative ref from any cwd
- Absolute paths outside every root keep their form; relative ones are
  refused with VALIDATION_ERROR before anything is written
- Template pin lookup matches the normalized ref as well as the typed path
- The id remap probed the whole store, so re-importing a project's own
  export remapped every row and failed on the first label mapping's
  foreign key
- An id held by this project's own row is now the same row and is skipped;
  only ids held by another project are remapped
- Labels match by project and name, so mappings land on the existing label
- Issue-label and issue-file mappings whose rows are missing are skipped
  instead of aborting the import
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant