Loop Synchronous Dispatch + Verify Seam — Design Note
Status: shipped — T459 (beam-search forks) and T463 (non-code verify gate) are wired into the loop's syncDispatchTask path, see src/engine/autonomyLoop.ts:728-739,823,866.
Source cross-checked (every symbol verified against live source): src/engine/autonomyLoop.ts (run() ~L461-823; forkBeamSearchCandidates ~L389; verifyNonCodeResult ~L422), src/launchBg.ts (launchBackgroundRun L82, returns BgLaunch — detached/fire-and-forget), src/orchestrator/Orchestrator.ts (dispatch L32 returns Promise<Result[]> in-process; judge L?), src/orchestrator/buildAdapters.ts (buildDefaultAdapters L260, availableBuildWorkerIds L762), src/engine/beamSearch.ts (runBeamSearchFork L197, resolveBeamWidth L92, BEAM_WIDTH_CAP=5 L44, provisionWorktree/captureDiff/removeWorktree, DiffJudge.decide), src/engine/nonCodeVerifyGate.ts (runNonCodeVerifyGate L203, isNonCodeTask L94, GateVerdict L130), src/engine/harnessStatusDecider.ts (StatusSignals.diffGatePassed/nonCodeGatePassed, decideLoopStatus), src/engine/WorkflowEngine.ts (DiffJudge L466, isolate -> step.winnerOutput diff), src/engine/implementTaskWorkflow.ts (the standing design->implement fleet def), src/judges/DiffJudge.ts, src/tasks/manifestReader.ts (ManifestTask L7 — has NO fork/candidates field), docs/design/autonomy-loop.md (§2 STATUS, §5 beam-search, §8 verify gate).
1. The problem
AutonomyLoop.run() dispatches every task with launchBackgroundRun(prompt, cwd) (src/engine/autonomyLoop.ts ~L793, ~L607). That call spawns a detached npx tsx child, returns a BgLaunch ({runId, logPath, pid}) immediately, and the loop moves on. The loop never sees the task's Result, never sees its diff, and never blocks on completion. This is correct for fire-and-forget throughput (the operator pays ~0; runs land in .local/runs/<id>/ and are harvested later), but it has a structural consequence:
Two already-built features are dead code because both operate on a dispatched task's OUTPUT, which the async path never captures:
- T459 beam-search (
src/engine/beamSearch.ts+AutonomyLoop.forkBeamSearchCandidates): runs N candidate approaches in worktrees, applies + typechecks each diff viaDiffJudge, keeps the winner, removes losers. It needs to RUN candidates in-process and judge their diffs. The module is complete and unit-correct; it has zero call sites inrun(). - T463 non-code gate (
src/engine/nonCodeVerifyGate.ts+AutonomyLoop.verifyNonCodeResult): runs depth-check -> Adversarial/Llm judge -> intent-alignment over a task's RESULT text. It needs the dispatched task'sResult. Complete and unit-correct; zero call sites inrun().
Update 2026-09-22: both are now called from
syncDispatchTask, whichrun()invokes whensyncVerifyis on —forkBeamSearchCandidatesatsrc/engine/autonomyLoop.ts:866,verifyNonCodeResultatsrc/engine/autonomyLoop.ts:823. Neither is dead code any more.
Independently, the harness STATUS decider (src/engine/harnessStatusDecider.ts) exposes diffGatePassed and nonCodeGatePassed signals that NOTHING populates today — because the async loop has no result to gate on. docs/design/autonomy-loop.md §2/§8 lock done = the verify gate passes, but the loop currently flips no task to done via a gate at all; it dispatches and forgets.
Update 2026-09-22:
syncDispatchTasknow populates both signals —diffGatePassedfrombeamResult.winner !== null(src/engine/autonomyLoop.ts:867) andnonCodeGatePassedfromverifyNonCodeResult(...).passed(:832) — and feeds them intodecideLoopStatus(src/engine/harnessStatusDecider.ts:242), so the sync-verify path does drivedonethrough a gate.
The fix is not "add a call site." It is a missing execution mode: the loop needs a path that runs a task synchronously, in-process, capturing its Result + diff, so the gates and beam-search have something to operate on. That path is the subject of this note.
2. Two dispatch modes (locked)
The loop keeps BOTH dispatch modes and chooses per-run:
- Async-background (default, unchanged).
launchBackgroundRun— detached child, fire-and-forget, operator pays ~0. This stays the default forcyberine loopso existing behavior and throughput are untouched. In this mode no gate runs; status ismore/blocked/stuckfrom frontier + defer + circuit-breaker signals exactly as today. - Synchronous-verify (new, opt-in). The loop runs the task in-process, awaits its
Result(and diff for code tasks), feeds it through the verify gate (DiffJudge for code,runNonCodeVerifyGatefor non-code), and only then setsdoneviadecideLoopStatus({ diffGatePassed | nonCodeGatePassed, ... }). Beam-search forks are available only in this mode (they are inherently synchronous).
Opt-in surface (additive, backward compatible):
- CLI flag
cyberine loop --verifyenables synchronous-verify mode. - Env
CYBERINE_LOOP_SYNC_VERIFY=1is the equivalent for unattended/cron callers. - When neither is set,
run()behaves byte-for-byte as today (async-bg, no gate). Every new branch inrun()MUST be guarded on this mode flag.
Rationale: synchronous verify costs real wall-clock and tokens per task (it runs the work in-process and judges it), so it cannot be the default for a fire-and-forget fleet driver. But it is mandatory for the autonomy STATUS contract and for beam-search. Making it an explicit mode preserves both use-cases without a flag-day migration.
3. The synchronous execution path (locked)
The loop already has two in-process primitives to build on; do NOT invent a third runner:
Orchestrator.dispatch(task, signal, onProgress) -> Promise<Result[]>(src/orchestrator/Orchestrator.ts:32) runs all agents in-process and returns their Results. Agents come frombuildDefaultAdapters()(src/orchestrator/buildAdapters.ts:260), the same pool the one-shotcli.tspath uses. This is the result-capturing path for NON-CODE tasks (the gate needsResult.output).WorkflowEngineisolate-step path (src/engine/WorkflowEngine.ts,DiffJudgeat L466) runs a step in a detached worktree, captures its diff intostep.winnerOutput, and gates withDiffJudge.implementTaskWorkflow.tsis the standing design->implement fleet def that already does exactly this. This is the result-capturing path for CODE tasks (the gate needs a diff togit apply+tsc).
Synchronous-verify dispatch in run() therefore branches by task type:
- Classify the task:
isNonCodeTask(task)(src/engine/nonCodeVerifyGate.ts:94) decides the branch. - Code task -> run via the isolate+capture path (reuse
implementTaskWorkflow/ WorkflowEngine isolate step, or a thin wrapper that provisions one worktree, runs the worker,captureDiff), producing a diff. RunDiffJudge.decide(task, [resultWithDiff]); setdiffGatePassed = verdict.winner !== null(i.e. the diff applied + typechecked). - Non-code task -> run via
Orchestrator.dispatch(task); take the winningResult; callthis.verifyNonCodeResult({ manifestTask, result, goal }); setnonCodeGatePassed = verdict.passed. - Status -> feed the populated signal into the existing
decideLoopStatus(...)so the harness (not model prose) ownsdone. On gate-fail the task is NOT flipped todone; it stays runnable (retry next iteration) or, after the circuit-breaker threshold,stuck.
This reuses the verified DiffJudge gate that WorkflowEngine already runs; it does not add a parallel gate.
4. Closing T463 (non-code gate) — the smaller half
T463 is wired by step 3 above and needs no new module — runNonCodeVerifyGate and verifyNonCodeResult already exist. The only new code is, inside the synchronous-verify branch of run():
- after
Orchestrator.dispatchreturns, pick the winningResult(the loop can reuseOrchestrator.judgewith a default judge, or take the first non-error result for a single-agent dispatch), if (isNonCodeTask(task)) { const v = await this.verifyNonCodeResult({ manifestTask: task, result, goal: this.goalText }); signals.nonCodeGatePassed = v.passed; },- thread
signalsintodecideLoopStatus.
The GOAL string passed as goal MUST be the verbatim GOAL (autonomy-loop.md §9: never summarized) so the intent-alignment assertion is judged against the real goal. this.goalText already exists in run() (the brain.boot goal).
5. Closing T459 (beam-search) — needs a fork trigger
forkBeamSearchCandidates(candidates, taskPrompt, repoRoot, agents?) is ready, but it requires two inputs the loop cannot currently produce:
candidates: BeamCandidate[]— the competing approaches at a fork. The manifest has no way to express "this task is a correctness fork with these N approaches" (ManifestTaskatsrc/tasks/manifestReader.ts:7hasid/status/title/deps/exec/model/blocker/do/area/line— no fork field).agents: Agent[]— the exploration pool, built frombuildDefaultAdapters(), narrowed to a cheap tier viarouteTask(beam-search already importsrouteTask;pickExplorationAgentselects the cheap one).
This note locks the fork trigger as a new optional manifest field, parsed by manifestReader:
## T999 [todo] <title>
fork:
- <approach A descriptor>
- <approach B descriptor>fork:is an optional multi-line list (>=2 entries). Its presence marks the task as a bucket-1 correctness/quality fork (autonomy-loop.md §4). Each entry becomes aBeamCandidate(the descriptor is injected into that candidate's worker prompt as the approach to take).- A task WITHOUT
fork:dispatches normally (single candidate) — full backward compatibility; no existing task changes shape. - In synchronous-verify mode, when
task.forkis presentrun()routes throughforkBeamSearchCandidates(candidates, prompt, repoRoot, agents)instead of single dispatch. The winner's diff is the task's result for the DiffJudge gate (step 2). Per §5 of autonomy-loop.md, losers are auto-removed immediately (runBeamSearchForkalready removes losers; the CALLER mustremoveWorktreethe winner after integrating —run()integrates the winning diff then removes its worktree). - Beam width is bounded by
resolveBeamWidth()(envCYBERINE_BEAM_WIDTH, hard capBEAM_WIDTH_CAP=5); sequential forks stay bounded because each fork judges-then-keeps-winner (no N^k tree held).
Fork detection is manifest-explicit by design, NOT a heuristic: a heuristic that guesses "this is a fork" would silently triple the cost of ordinary tasks. The author opts a task into beam-search by writing fork:. (A future heuristic auto-fork can layer on top, gated separately; out of scope here.)
6. Backward compatibility (locked)
- Default
cyberine loop(no--verify, noCYBERINE_LOOP_SYNC_VERIFY) is unchanged: async-bg dispatch, no gate, no beam-search. Every new branch guards on the mode flag. ManifestTask.forkis optional; absent on every current task.manifestReaderadds one optional field;runnable/statusWriterare untouched.- The new capsule/status fields reuse
StatusSignals(already defined). No new judge, no new runner — DiffJudge, Orchestrator, WorkflowEngine, runBeamSearchFork, runNonCodeVerifyGate are all reused. - No barrel/index files; match the
[autonomy-loop]stdout narration style.
7. Verification plan
npm run typecheck (tsc --noEmit) is the standing gate and must stay clean — verifiable offline now.
Behavior that needs a live gateway + provider keys (the T404 blocker — gateway not up in a headless bg session):
- A code task with
fork:spawns N worktrees, DiffJudge picks a winner, losers are removed, winner diff applies + typechecks, task flips todoneviadiffGatePassed. - A non-code task runs
Orchestrator.dispatch, the result passes (or fails)runNonCodeVerifyGate, andnonCodeGatePasseddrives the status — a thin result is rejected, not accepted. --verifyoff: identical behavior to today (regression check viacyberine loop --dry-rundiff of dispatched ids).
Offline-checkable without a gateway: typecheck; cyberine loop --verify --dry-run prints the per-task mode decision (sync vs async, fork vs single) without launching workers; a selftest: smoke that constructs an AutonomyLoop with a stub Agent returning a fixed diff/result and asserts the gate signal + status (no network).
8. Task mapping + sequencing
- New task T467 (or fold into T459) — fork-trigger manifest field.
manifestReaderparses optionalfork:list intoManifestTask.fork?: string[]; document the syntax in USAGE.md. Prereq for T459 wiring. exec: agent, model: sonnet. - T459 re-do — wire beam-search. Add the synchronous-verify mode plumbing (§2) + route
fork:tasks throughforkBeamSearchCandidates, integrate winner diff, gate via DiffJudge ->diffGatePassed. Depends on T457 (done) + the fork-field task. model: sonnet. - T463 re-do — wire non-code gate. In synchronous-verify mode, capture the dispatched Result and route non-code tasks through
verifyNonCodeResult->nonCodeGatePassed->decideLoopStatus. Depends on T457 (done) + shares the synchronous-verify mode plumbing with T459 (do T459's mode scaffold first, or build the mode scaffold as a shared step both consume). model: opus. - T466 eval (already blocked on T459) becomes runnable once T459's beam-search is live.
Because T459 and T463 share the synchronous-verify mode scaffold (§2/§3), build the scaffold ONCE (as part of T459) and have T463 consume it — do NOT fan them out as two independent worktrees that each re-add the mode branch (guaranteed merge conflict on run()). Sequence: fork-field -> T459 (scaffold + beam) -> T463 (non-code gate on the scaffold) -> T466 eval.
9. Cross-check statement
Every symbol cited was verified against live source on 2026-06-27. Confirmed present: launchBackgroundRun (src/launchBg.ts:82, returns detached BgLaunch); Orchestrator.dispatch (src/orchestrator/Orchestrator.ts:32, returns in-process Result[]); buildDefaultAdapters (src/orchestrator/buildAdapters.ts:260); runBeamSearchFork/resolveBeamWidth/BEAM_WIDTH_CAP=5 (src/engine/beamSearch.ts:197/92/44); runNonCodeVerifyGate/isNonCodeTask/GateVerdict (src/engine/nonCodeVerifyGate.ts:203/94/130); AutonomyLoop.forkBeamSearchCandidates/verifyNonCodeResult (src/engine/autonomyLoop.ts:389/422) — both confirmed defined with zero call sites in run(); StatusSignals.diffGatePassed/nonCodeGatePassed + decideLoopStatus (src/engine/harnessStatusDecider.ts); DiffJudge used by WorkflowEngine (src/engine/WorkflowEngine.ts:466) and implementTaskWorkflow.ts (the standing isolate->winnerOutput gate pattern). Confirmed ABSENT: any fork/candidates field on ManifestTask (src/tasks/manifestReader.ts:7) and any synchronous dispatch in run() (only launchBackgroundRun at ~L607/793). The locked decisions in docs/design/autonomy-loop.md (§2 status, §5 beam-search bound, §8 per-task-type gate, §9 verbatim GOAL) are implemented by this seam without deviation.
Update 2026-09-22: §5/§8 of this note (the
fork:manifest field and sync-verify mode) are now implemented (T474/T459/T463).ManifestTask.fork?: string[]exists (src/tasks/manifestReader.ts:42) andrun()has the synchronoussyncDispatchTaskpath (src/engine/autonomyLoop.ts:728), guarded bysyncVerify.
Autonomy Loop — T455 Design Note
Status: design (T455). Locks the architecture for the AUTONOMY epic (T456-T466). No product code changed. Implementation tasks must treat the contracts here as fixed. Source cross-checked:...
Code-Context 3-Act Architecture — T440 Design
Status: design (T440). Locks the architecture for the CODE-CONTEXT epic (T441-T454). No product code changed. Implementation tasks must treat the contracts here as fixed. Source cross-checked:...