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: src/engine/LoopController.ts, src/engine/WorkflowEngine.ts, src/engine/worktreeSandbox.ts, src/adapters/LoopAgentAdapter.ts, src/judges/DiffJudge.ts, src/tasks/manifestReader.ts, src/tasks/runnable.ts, src/tasks/statusWriter.ts, src/store/crashResume.ts, src/launchBg.ts, src/core/progress.ts, src/core/router.ts, .local/tasks/ACTIVE.md. Brainstorm sources: .local/research/autonomy-brainstorm.md and .local/research/autonomy-brainstorm-vi.md (§11 locked decisions). Shared substrate: docs/design/code-context-3act.md (ContextBrain port T441, CARRY capsule T450 — reused, not redefined here).
1. The core problem
The agent's working thread lives inside the context window. When the window ends (task done) or clears (compaction), the thread dies. Two visible symptoms: the agent finishes and waits for a human to read the result and type the next prompt (continuation pain); or the agent clears context and either cannot resume or resumes wrong (survival pain). These are the same bug at different timescales.
The fix is one principle in two halves. The harness owns STATUS — done, blocked, stuck, and continue are decided by observable signals the harness checks, never by the model asserting them in prose. The durable thread lives in an external resume capsule — context is scratch, not memory; the capsule is what the agent re-reads after any clear.
Cyberine is already most of the way there. The verify gate (DiffJudge), the frontier (the manifest + computeRunnable), a working autonomous loop (src/engine/LoopController.ts, today isolated inside the Evolve subsystem), and approximately 80% of the capsule (run.json + events.jsonl + winnerOutput) all exist. The work is to generalize the one loop, make it trust the gate instead of the model's prose, give deferred decisions a first-class structure, and write the resume capsule consistently. The AUTONOMY epic is wiring and completing, not building from scratch.
2. The STATUS model (locked)
The harness owns every terminal status. The model never decides its own status in prose — it only provides the raw output the harness then evaluates.
Done — the verify gate passes. For code tasks: DiffJudge (src/judges/DiffJudge.ts) runs git apply + tsc --noEmit + the task's optional verify_cmd; all three must be clean. For non-code tasks: the judge verdict and intent-alignment check pass (see §8). "Done" is never the model writing "I've completed this task." It is an externally observable gate clearing.
More work — the frontier (computeRunnable over the manifest in src/tasks/runnable.ts) is non-empty after the current task passes. The loop continues without interruption.
Blocked — the worker emits a first-class structured defer slot: {status: 'blocked', reason: string, decision_needed: string, best_guess?: string}. This is a first-class result type, not an error. The worker must emit this explicitly; the harness never infers it from model prose.
Stuck — a circuit breaker derived from artifacts, not from the model's self-assessment. Three observable signals: (a) no new git commit in N consecutive iterations; (b) identical tsc --noEmit error text across consecutive runs; (c) identical tool-call sequences across rounds. src/adapters/LoopAgentAdapter.ts already emits a stuck ProgressEvent (with stuckReason: 'identical-tool-calls' or 'no-progress', lines 1406 and 1422) but src/engine/WorkflowEngine.ts currently never consumes it — T457 closes that gap. On stuck detection the task is aborted with the reason recorded; it is not silently retried.
Update 2026-09-22: T457 has landed —
WorkflowEnginenow consumes the stuckProgressEvent: it aborts the worker via itsAbortControllerand records a durableworker:stuckevent, seesrc/engine/WorkflowEngine.ts:4220-4234.
Two safety invariants that are not negotiable for unattended runs: budget and iteration caps are checked before each LLM call (post-flight checking burns tokens without preventing the overage); reversibility is classified before the run starts and enforced by the settings deny floor and PreToolUse hooks — the model never self-classifies an irreversible action mid-run.
3. The defer-and-report flow (no mid-loop pause)
An unattended A-Z run never idles waiting for a human response. There is no mid-loop pause state that stops the run.
When a worker encounters a fork that requires a human decision, the task is deferred: it is not executed, it is marked needs-human with its question and best-guess recorded in the capsule, and the loop skips it and continues with other runnable tasks. The run proceeds at full speed through everything it can complete autonomously.
At the end of the A-Z run, the harness emits one consolidated decisions-needed report: every deferred item with its question, its best-guess (if the model could form one), and the task waiting on it. The human reads this one report when they return — not a stream of mid-run interruptions.
When the human provides answers, the loop resumes exactly the deferred tasks with the answers injected into the resume capsule — not a full re-run of the entire backlog. The injected answers are threaded through the deferred_tasks field in the CARRY capsule (see §9).
"Ask the human" occurs at exactly two moments in the unattended lifecycle: (1) the kickoff interview when the GOAL is ambiguous (resolves done-criteria before the loop begins; see §7); (2) the aggregated end-of-run decisions report. For an unattended kickoff where no human is present, the default is to proceed on reasonable assumptions and fold all unanswered kickoff questions into the final report — the loop does not wait.
4. The 3-bucket decision table (locked)
Most "which option?" forks are resolved by the loop itself — the human is the last resort, not the first. Each fork is classified into exactly one of three buckets:
| Fork type | Resolved by | Human in the loop? |
|---|---|---|
| Correctness / quality — which implementation is right or better | Generate the candidate options as competing worktree candidates; DiffJudge (src/judges/DiffJudge.ts) runs git apply + tsc --noEmit and picks the winner. This reuses the existing compete+judge+worktree engine in src/engine/WorkflowEngine.ts; the new element is generating the OPTIONS at a fork as candidates. | No — the judge decides |
| Scope / intent / taste — how far to build, A-Z vs A-Y, which variant | Check against the top-level GOAL's done-criteria first: "MVP" resolves toward the narrower option; "full" toward the wider. If the GOAL is silent, defer to the decisions report. | Rare — only when the GOAL is silent |
| Non-codeable — credentials, irreversible external action, account or env choice, business or legal rule | Defer + report. There is no implementation path through these. | Yes — but this is the small residual set |
A well-stated GOAL with tight done-criteria absorbs most of the scope/intent bucket, leaving the human-decision set as nearly pure non-codeable items. This is the reason the kickoff interview exists: it is not for picking options, it is for stating done-criteria precisely enough that the loop self-resolves later.
5. The beam-search bound on speculative options (locked)
Speculative options as competing worktree candidates carry a real cost: compute multiplies per option, and sequential forks risk N^k blow-up. The brake is beam search.
Beam-search discipline: judge after each fork. Keep only the winning candidate. Then continue from the winner. Never hold the full tree of N candidates across K sequential forks simultaneously. The beam width is narrow-by-default (typically 2-3 competing approaches at a fork) with cheap-model routing for exploration work.
Each candidate runs in an isolated worktree off HEAD via src/engine/worktreeSandbox.ts. After DiffJudge selects the winner, losing worktrees are auto-removed immediately — they hold no value once the judge has decided. The disk cost is bounded per-fork, not per-run.
The judge resolves correctness and quality forks reliably. The judge is structurally blind to pure intent and scope forks — it can determine which diff is valid TypeScript, not which feature set matches the user's intent. That is why the GOAL-criteria tie-breaker (not more worktrees) is what resolves scope/intent forks. Disk is not the limiting trade-off here; the judge's blindness to intent is.
6. The mode list (one engine, many scopes)
One generalized driver replaces the Evolve-only src/engine/LoopController.ts. Evolve's diff-mining approach becomes one pluggable task-source inside this engine rather than a separate loop. The entry point is cyberine loop with these modes:
cyberine loop— run all runnable tasks in the manifest frontier until the GOAL is met or the frontier is empty.cyberine loop <ids>— run a specific subset: e.g.cyberine loop T440 T447.cyberine loop --area <tag>— run all tasks with a matching area tag: e.g.--area code-context.cyberine loop "<NL goal>"— interview if ambiguous, write a dependency-ordered backlog to.local/tasks/ACTIVE.md, then drive it viacomputeRunnable(see §7).cyberine loop --test-debug-fix— run until the verify gate is green; this mode drives a tight edit-compile-fix cycle rather than consuming a task backlog.
All modes share the same STATUS model (§2), the same defer-and-report flow (§3), and the same resume capsule (§9). The scope argument changes which tasks feed the frontier; the loop mechanics are identical.
7. The GOAL done-criteria and stop anchor (locked)
The loop stops when the GOAL's done-criteria are met — not when the frontier is momentarily empty. A temporarily empty frontier (all remaining tasks blocked on deferred decisions) does not terminate the run; it triggers the end-of-run decisions report and waits for answers. A GOAL with done-criteria met terminates the run even if the manifest still has tasks tagged backlog.
This is the AutoGPT-trap brake: when the loop is allowed to refill its own backlog (research tasks, NL-goal mode), it may only add tasks that move toward the stated GOAL. A research task that enriches context without advancing the goal is out of scope. The done-criteria are the definition of "toward" — without them, backlog-refill becomes unbounded drift.
The kickoff interview for cyberine loop "<NL goal>" is the primary mechanism for setting done-criteria before the loop starts. The interview is batched (one round, all clarifying questions together). If no human is present at kickoff, the loop proceeds on reasonable assumptions and records what it assumed in the final report.
8. Per-task-type verify gate (locked)
The gate that produces the Done status differs by task type.
Code tasks — DiffJudge (src/judges/DiffJudge.ts): git apply (the diff must apply cleanly), tsc --noEmit (the repo must typecheck clean), and the task's optional verify_cmd (if defined in the manifest). All three must pass. "Done" is never the model writing that it finished; it is these three commands passing.
Non-code tasks (research, plan, design, doc) — a quality pipeline: research as deep as the task requires -> judge (AdversarialJudge or LlmJudge from src/judges/) -> re-reason against the judge's critique -> the strongest available model synthesizes the output and makes an explicit intent-alignment assertion ("given the GOAL, this output is complete because...") -> if intent alignment is still unclear, a short clarification exchange. The gate passes on judge verdict + intent-alignment, never on the model self-declaring done.
The non-code pipeline is necessary because there is no compile check to anchor on. The judge plays the same role tsc --noEmit plays for code — it is the external signal that makes "done" observable rather than claimed.
9. Resume capsule and shared substrate (locked)
The AUTONOMY loop depends on and extends the brain substrate defined in docs/design/code-context-3act.md. These pieces are consumed here, not redefined:
ContextBrain port (src/core/contextBrain.ts, T441) — the boot / recall / carry interface. The loop calls brain.boot() at the start of each iteration to reconstruct ground truth from artifacts, and brain.carry(capsule) after each task passes the verify gate.
CARRY capsule schema (docs/design/code-context-3act.md §6) — the capsule written to .local/runs/<runId>/context.md. The autonomy loop extends the base capsule with these additional fields:
current_step— the exact next action (not "continue working"), written before any context-heavy operation so a mid-op clear cannot orphan the work.key_decisions— locked choices and rationale; prevents re-derivation on resume.verify_cmd— how to confirm the next step before beginning.deferred_tasks— list of deferred items:{task_id, question, best_guess}. When the human provides answers, the harness injects them here and the loop resumes those tasks reading from this field.open_questions— unanswered kickoff questions, folded in when the human was absent at kickoff.
The GOAL field is never summarized — it is copied verbatim into every capsule and every worker handoff. A summarized GOAL drifts across re-summarizations and breaks the scope/intent tie-breaker in §4.
Capsule scope now, ContextQ-compatible later. The resume capsule is LocalBrain-only for this epic (writes to .local/runs/<runId>/context.md, read by LocalBrain.boot() in the next iteration). The schema is kept push-compatible with ContextQ's bulk-upsert format so the cloud wire-up costs only an adapter swap. No ContextQ connection is made in T456-T466.
10. Model routing (locked)
Per-task model routing is the primary economy lever; budget caps are the safety backstop.
Each task in the manifest carries a model: tier field (set by the nf-tasks manifest at .local/tasks/ACTIVE.md). The router (src/core/router.ts) maps that tier to a concrete model at dispatch time. Mechanical or exploratory work (refactoring, spec tasks with clear outputs, beam-search candidate generation) routes to cheaper models. Design, ambiguity resolution, intent-guessing, and the final synthesis in the non-code verify pipeline route to premium models.
Budget caps (per-task token ceiling and whole-loop cost ceiling) are checked before each LLM call. On breach: the task is marked blocked, the reason is recorded, and the item surfaces in the decisions report. The loop does not silently retry with a higher cap.
11. Cross-check statement
Every claim against source was verified directly. Confirmed: src/engine/LoopController.ts exists (the Evolve-only autonomous loop, the generalization target for T456). src/engine/WorkflowEngine.ts exists; confirmed it has no stuck-event handler — grep for 'stuck' or case.*stuck returns no matches, confirming the stuck ProgressEvent from LoopAgentAdapter is dropped today. src/adapters/LoopAgentAdapter.ts lines 1406 and 1422: stuckReason: 'identical-tool-calls' and stuckReason: 'no-progress' emitted as ProgressEvent. src/core/progress.ts line 52: stuckReason field on ProgressEvent. src/core/router.ts lines 36-49: documents the expected stuck-handling contract that a loop consumer must implement (this is the spec T457 builds to). src/tasks/manifestReader.ts, src/tasks/runnable.ts, src/tasks/statusWriter.ts (manifest parse, frontier computation, status-flip — all confirmed present). src/engine/worktreeSandbox.ts (worktree isolation for speculative candidates). src/judges/DiffJudge.ts (the code verify gate). src/store/crashResume.ts (crash-resume path). src/launchBg.ts (background dispatch for capsule offload). .local/tasks/ACTIVE.md: T455-T466 confirmed under the EPIC AUTONOMY header with matching descriptions. Brainstorm sources read in full: .local/research/autonomy-brainstorm.md (§1-§11, English) and .local/research/autonomy-brainstorm-vi.md (§1-§11, Vietnamese) — both carry identical §11 locked decisions; this note implements those decisions without deviation. The shared substrate from docs/design/code-context-3act.md is referenced for the ContextBrain port interface (§4 of that doc), the CARRY capsule schema (§6 of that doc), and the LocalBrain/ContextQBrain adapter split — none of those contracts are redefined here.
Update 2026-09-22: the stuck-event handler this cross-check found absent has since been added —
src/engine/WorkflowEngine.ts:4220now handlesevent.type === 'stuck'(T457).
Persona — Cyberine
Cyberine ships a single built-in persona, Cyberine, that gives the chat planner a consistent voice and identity. The persona is optional: set CYBERINEPERSONA=none to disable it and run with bare...
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.