Skip to content

feat: executor-verifier orchestration — executable acceptance, budget classes, model ladder, isolation - #21

Merged
piotrlaczkowski merged 5 commits into
mainfrom
feat/executor-verifier-orchestration
Aug 27, 2026
Merged

piotrlaczkowski merged 5 commits into
mainfrom
feat/executor-verifier-orchestration

Conversation

@piotrlaczkowski

Copy link
Copy Markdown
Contributor

Adapts the structural ideas from the-open-engine/zeroshot's executor–verifier orchestration, rewritten for this harness's thesis: small local models, a fixed runway, and gates that fail closed.

Nothing is vendored — zeroshot is TS/Rust, this is Go — so what carries over is the topology, not the code. Its central claim, an independent LLM verifier, is deliberately not adopted: our gates ask the filesystem, which is strictly stronger evidence than a second opinion from the same weak model. What was genuinely missing was everything around that verifier.

What's in it

1. Executable acceptance criteria

Task.Acceptance is prose, and pkg/quality has always mined it for runnable commands with a whitelist scan. That loses three things: which condition failed, whether it's blocking, and whether it was checked at all.

plan.Criterion{ID, Text, Verify, Priority} carries all three. quality.VerifyCriteria runs each Verify through the same whitelist and sanitizer every auto-run command already passes — a criterion cannot widen shell scope by one character — and reports three verdicts, never two:

Verdict Meaning
PASSED a command ran here and exited 0
FAILED it ran and did not; a failed must fails the task
UNVERIFIED nothing ran — no command, or one the whitelist refused

The third state is the point. The prose scan's silent third state is exactly how "the harness did not check" becomes "the harness says it is fine". UNVERIFIED never blocks, but it denies the reviewer fast path: disk evidence proves the worker changed something, never that the stated condition is now true.

Acceptance stays the compatibility surface (Normalize synthesizes it from Criteria), so every consumer that predates the field is untouched and a model too small to emit criteria still produces a valid task.

2. Budget classes

The composer already chose phases, team and skills; it never chose how much of the run's budget a request is worth. So "rename a variable" could still walk architect → explore → plan → split → execute → review → test. On a 7–32B model that ceremony is the failure — the runway is gone before the edit lands.

complexity (trivial|simple|standard|critical) × kind (inquiry|task|debug) buys a profile: optional phase breadth, waves, think passes, QA rounds, strict reviewer.

slmcode compose "fix the typo in README.md"      → trivial:task  ·  5 phases · 1 wave
slmcode compose "rotate the production API key"  → critical:task · 12 phases · 4 waves · strict reviewer

What it explicitly does not scale is reviewer count — our reviewers are LLM calls and our gates are not, so a higher class buys more determinism, never more voices.

composer.Classify is a deterministic cheap tier that costs nothing. Only trivial/inquiry skip the composer LLM; critical still pays for it, because "confidently classified" is not "easy" and a change to auth or billing is exactly where a model's judgment about specialists is worth its tokens.

Config is a ceiling for boolean gates — a class may switch one off, never on. The single downgrade is RequireSmoke for a read-only inquiry, which writes nothing and so could never satisfy it: a deadlock, not a safeguard.

3. Role→model map and failure escalation

The whole routing story was fast_model plus an isLightAgent classification: one lever, two models. model_roles generalizes it; model_escalation answers a different question — this task keeps failing — using state Task.AttemptLog was already recording and nothing read back.

A rung is a separately registered agent, identical to its base except the pinned model, so escalation stays out of the hot path and a task that never escalates pays nothing. Every id-classification helper routes through BaseRoleID: an escalated worker that stopped matching isCodingRole would keep running with the wrong token budget, invisibly.

4. Worktree isolation (--isolate worktree, opt-in)

pkg/rewind already snapshots per wave. Two things it can't do: abandon (one operation that cannot half-succeed, covering files nothing predicted) and contain (an out-of-scope write lands somewhere the operator was never using).

The seam is one line — point Config.Root at a throwaway worktree; every ws_* tool already resolves against it. Config.StateDir pins .slmcode to the origin: derived from a throwaway worktree, the memory layers, repair rules and bandit policy would be thrown away with it, turning "it gets better at your repo" into "it starts from zero every run".

In-place stays the default permanently — plenty of workspaces aren't git repos.

5. Issue intake and PR delivery

gh rather than a Go client: already installed, already authenticated, no new token to store or leak. An issue body is untrusted internet text that becomes the run query, so it's framed as a report rather than pasted as instructions, its harness markers are defused, and it's truncated so a long issue can't evict the repo map from the pack.

Delivery is the one thing here that leaves the machine, so it takes a flag and a confirmation showing branch, title and the exact file list; with no TTY it refuses unless --yes. Only files the run's completed tasks named are staged — never git add -A.

6. Studio Live page

Four fixed zones in priority order, and the ordering is the design: command bar → phase rail (~44px) → run-setup disclosure → the stream, which now gets the space. One horizontal PhaseRail replaces four header panels that spent a third of the viewport answering one question; position along the rail is the progress bar, so the two can't disagree. Phase tint is keyed to group (5) rather than phase (15).

The nav sidebar now forces its icon rail below lg — at 224px it took 60% of a 375px screen.


Not in it

context_role_budget (the "declarative context strategy" item). It's already wired config→packer and documented in docs/config.md. Building it would have been duplication, so I didn't.


⚠️ Reviewer note: this supersedes part of 0989f9f

This branch was cut from cbeab27, before 0989f9f fix(studio): make the Live page fit its window and stop it flickering landed. Both address the same complaint. That commit made the existing panels collapsible with persisted state and capped the detail region at ~38vh; this branch restructures the page instead and extracts PhaseRail / RunSetup.

The rebase kept this branch's structure, but the final commit deliberately adopts that commit's infrastructure rather than discarding it:

  • hooks/useMediaQuery.ts (mine) deleted — useUiState already exports one
  • usePersistentState now backs the rail tab, rail width and the disclosure
  • useStickToBottom replaces an unconditional scrollIntoView that yanked the log down every flush
  • ResizeHandle restores the draggable, persisted side-panel width that would otherwise have been lost

If you'd rather keep 0989f9f's approach and drop the restructure, that's a reasonable call — the Live page changes are confined to web/ and the last two commits.


Bugs found by running it, not by reading it

Isolation didn't isolate openHarness builds the orchestrator — and its workspace — from the original root before Config.Root is redirected. A live run edited main.go in the origin. Now rebuilt after.
Interrupt leaked the worktree Teardown was inline past six return failf(...) paths. Now deferred; Ctrl-C prints "isolated run discarded — your checkout is untouched".
BranchName("!!!") → slmcode/ Passes my own branchSafe regex (slash is legal), then git rejects it — a cosmetic input failing at the very last step.
Classifier missed inflections "panics" didn't match "panic".
Double test-suite execution The review-time criteria insurance ran for roles the worker path skips, so a tester task ran the suite twice — once per path, where the per-report command cache couldn't see it.
Language pick contradicted itself Reported during review: a Go repo whose query mentioned Python got python-worker + python-tester next to "verify with go test ./... -count=1". queryLanguageSpecialists beat projectLanguageSpecialists unconditionally. Now the repository wins unless the inventory corroborates the query, which preserves the legitimate case (the web/ tree in a Go project). See pkg/orchestrator/langpick.go.
Preview presented a guess as a decision Also reported during review. Now one collapsed line labelled guess, saying plainly that the composer picks the real pipeline when the run starts.

Verification

  • Full Go suite green; scripts/lint.sh at its zero-issue baseline; -race clean on every touched package
  • 164 new Go tests, 26 new UI tests (79 total, eslint clean)
  • Isolation verified end-to-end against a real local model: worker wrote into the worktree, origin untouched across a 9-minute multi-task run with correction rounds; SIGINT tore down cleanly
  • Live page checked in-browser at 375px and 1440px, light and dark

Not verified: mkdocs build --strict — .venv-docs has a stale interpreter path from a moved directory (pre-existing, unrelated to this branch).

…olation

Five capabilities adapted from the-open-engine/zeroshot's executor–verifier
orchestration, rewritten for this harness's thesis: small local models, a fixed
runway, and gates that fail closed. Nothing is vendored — zeroshot is TS/Rust
and this is Go, so what carries over is the topology, not the code. Its central
claim (an independent LLM verifier) is deliberately NOT adopted: our gates ask
the filesystem, which is strictly stronger evidence than a second opinion from
the same weak model.

── Executable acceptance criteria ────────────────────────────────────────────

plan.Task.Acceptance is prose, and pkg/quality has always mined it for runnable
commands with a whitelist scan. That loses three things: WHICH condition failed,
whether a condition is blocking, and whether it was checked at all.

plan.Criterion{ID, Text, Verify, Priority} carries all three. quality.
VerifyCriteria runs each Verify through the same whitelist and sanitizer every
auto-run command already passes — a criterion cannot widen shell scope by one
character — and reports three verdicts, never two:

  PASSED      a command ran here and exited 0
  FAILED      it ran and did not; a failed `must` fails the task
  UNVERIFIED  nothing ran — no command, or one the whitelist refused

The third state is the point. The prose scan's silent third state is exactly how
"the harness did not check" becomes "the harness says it is fine". UNVERIFIED
never blocks, but it denies the reviewer fast path: disk evidence proves the
worker CHANGED something, never that the stated condition is now true.

Acceptance stays the compatibility surface — Normalize synthesizes it from
Criteria — so every consumer that predates the field is untouched, and a model
too small to emit criteria still produces a valid task.

── Budget classes ────────────────────────────────────────────────────────────

The composer already chose phases, team and skills; it never chose how much of
the run's budget a request is worth, so "rename a variable" could still walk
architect → explore → plan → split → execute → review → test. On a 7-32B model
that ceremony IS the failure: the runway is gone before the edit lands.

complexity (trivial|simple|standard|critical) × kind (inquiry|task|debug) buys a
Profile: optional phase breadth, wave budget, think passes, QA rounds, strict
reviewer. What it explicitly does NOT scale is reviewer COUNT — our reviewers
are LLM calls and our gates are not, so a higher class buys more determinism,
never more voices.

composer.Classify is a deterministic cheap tier that costs nothing and answers
the easy majority; it reports Confident only on a strong signal, and only
trivial/inquiry skip the composer LLM entirely. Critical still pays for it:
"confidently classified" is not "easy", and a change to auth or billing is
exactly where a model's judgment about specialists is worth its tokens.

Config is a ceiling for boolean gates — a class may switch one off, never on.
The single downgrade is RequireSmoke for a read-only inquiry, which writes
nothing and so could never satisfy it: a deadlock, not a safeguard.

── Role→model map and failure escalation ─────────────────────────────────────

The whole routing story was FastModel plus an isLightAgent classification: one
lever, two models. model_roles generalizes it to as many models as an operator
names. model_escalation answers a different question — this TASK keeps failing —
using state Task.AttemptLog was already recording and nothing read back.

A rung is a separately registered agent, identical to its base except the model
it is pinned to, so escalation stays out of the hot path and a task that never
escalates pays nothing. Every id-classification helper goes through BaseRoleID:
an escalated worker that stopped matching isCodingRole would keep running with
the wrong token budget, invisibly.

── Worktree isolation ────────────────────────────────────────────────────────

pkg/rewind already snapshots and restores per wave. Two things it cannot do:
abandon (one operation that cannot half-succeed, covering files nothing
predicted) and contain (an out-of-scope write lands somewhere the operator was
never using).

The seam is one line: point Config.Root at a throwaway git worktree. Every ws_*
tool already resolves against it, so the tool layer is isolated without knowing.
Config.StateDir pins .slmcode to the ORIGIN — derived from a throwaway worktree,
the memory layers, repair rules and bandit policy would be thrown away with it,
turning "it gets better at your repo" into "it starts from zero every run".

In-place stays the default permanently: plenty of workspaces are not git repos.

── Issue intake and pull-request delivery ────────────────────────────────────

`gh` rather than a Go client: already installed, already authenticated, no new
token to store or leak. An issue body is untrusted internet text that becomes
the run query, so it is framed as a report rather than pasted as instructions,
its harness markers are defused, and it is truncated so a long issue cannot
evict the repo map from the pack.

Delivery is the one thing here that leaves the machine, so it takes a flag AND a
confirmation showing branch, title and the exact file list; with no TTY it
refuses unless --yes. Only files the run's completed tasks named are staged,
never `git add -A`.

── Bugs found by running it ──────────────────────────────────────────────────

- Isolation did not isolate: openHarness builds the orchestrator, and its
  workspace, from the original root before Config.Root is redirected. A live run
  edited main.go in the origin. The orchestrator is now rebuilt after.
- Interrupting an isolated run leaked the worktree and its branch: teardown was
  inline past six `return failf(...)` paths. Now deferred.
- vcs.BranchName("!!!") produced "slmcode/", which branchSafe accepts (slash is
  legal) and git then rejects — a cosmetic input failing at the last step.
- The classifier missed inflected forms: "panics" did not match "panic".
- The review-time criteria insurance ran for roles the worker path skips, so a
  tester task executed the project's suite twice, once per path, where the
  per-report command cache could not see the duplication.
Supersedes the collapsible-panels approach: same complaint, structural fix
rather than a cap. The page is now four fixed zones in priority order, and the
ordering IS the design.

  1. Command bar   what you type and the button you press. One row on desktop,
                   wraps below. Never scrolls away.
  2. Phase rail    where the run is, in ~44px.
  3. Run setup     how it is configured, behind a disclosure.
  4. Stream + rail everything left over, which is most of the screen.

── PhaseRail ────────────────────────────────────────────────────────────────

One horizontal track replaces four separate header panels — a metrics grid, a
percentage bar, a wrapped grid of phase chips and a "current stage" card — that
between them spent roughly a third of the viewport answering one question. It
answers it better too: position along the rail IS the progress bar, so the two
cannot disagree. The active phase scrolls itself into view.

Colour carries meaning and nothing else. Phases were tinted from a fifteen-entry
per-phase palette, which reads as decoration because it is — nobody holds
fifteen hues to fifteen phase names. The tint is now the phase's GROUP (five,
and groups are a real concept in pipeline.yaml); STATE is carried by fill and
weight.

── RunSetup ─────────────────────────────────────────────────────────────────

Composition, team, handoff and SLM-fit hints are genuinely useful, and useful at
a different moment than the log: you read them while deciding what to run and
stop the instant the run starts. Three disclosure states, not two:

  preview            collapsed — a deterministic GUESS from the query text
  running            collapsed — you are watching the log
  runtime, finished  open — this is what actually ran

A composition-read error renders OUTSIDE the disclosure. Hiding it behind a
collapsed panel means the one state the user must act on is the one they cannot
see.

── Screen adaptability ──────────────────────────────────────────────────────

The rail is a column at ≥1024px and a full-height overlay below it, seeded from
the breakpoint and reset when the viewport crosses it — an overlay defaulting to
open covered the console the user came for. The nav sidebar now forces its icon
rail below `lg`: at 224px wide it took 60% of a 375px screen, leaving the page
it navigates to to fight for the rest. The user's own collapse preference is
preserved, not overwritten, and applies again when there is room.

LiveFeedback gains a compact mode and docks to the bottom of the stream, where a
reply belongs — next to what you are replying to, not in a header card above it.

── The preview was presenting a guess as a decision ─────────────────────────

Reported: typing a query instantly produced a full "Planned pipeline" panel, and
for a Go repository it named python-worker and python-tester. It is now labelled
"Provisional pipeline · guess", collapsed, and says plainly that the composer
agent picks the real phases when the run starts. The wrong team itself is fixed
in the engine (see langpick.go).
The three surfaces a user meets these through:

- cli.md gains the run flags (--issue, --isolate, --deliver and friends), what
  a budget class buys, and why the provisional pipeline shown while typing is a
  guess rather than a decision.
- config.md gains model_roles, model_escalation and escalate_after.
- concepts.md explains why acceptance is a contract rather than a paragraph,
  and what the third UNVERIFIED verdict is for.
…ng them

This branch was cut before 0989f9f, which reworked the same Live page and added
hooks/useUiState.ts and ui/ResizeHandle. The rebase kept this branch's structure
— four zones, an extracted PhaseRail and RunSetup — but there is no reason for
it to carry its own copies of infrastructure that now exists:

- useMediaQuery is deleted; useUiState already exports one.
- usePersistentState now backs the rail tab, the rail width and the RunSetup
  disclosure, so a layout the user arranged survives a reload.
- useStickToBottom replaces an unconditional scrollIntoView on every event,
  which yanked the log back down mid-flush and made reading anything during a
  run impossible. It follows the stream only while the user is already at the
  bottom.
- ResizeHandle restores the draggable, persisted side-panel width that the
  superseded version had. The stored width is clamped twice — by the handle and
  again in CSS — so a window narrowed after the fact can never leave the stream
  with no room.

RunSetup's tests clear localStorage between cases: jsdom shares it across a
file, so the tests that click the toggle otherwise leak an expanded panel into
every test declared after them.
Left behind by an edit to the compact placeholder. The trailing-whitespace hook
was the only failing check on this branch.
@piotrlaczkowski
piotrlaczkowski merged commit a217c63 into main Aug 27, 2026
7 checks passed
@piotrlaczkowski
piotrlaczkowski deleted the feat/executor-verifier-orchestration branch August 27, 2026 14:32
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