Repository navigation
internals oneshot
User guide: Oneshot.
Phase behaviour comes from the prompts plan.md, implement.md, and review.md. This page covers only what the engine enforces.
Code: internal/oneshot (orchestration), cmd/steiner (cmd_oneshot.go, phase_driver.go, runtime_build.go), and internal/interactive (phase_control.go). Model configuration is in configuration.
-
NewOrchestratorcallsRequireModels(preflight.go) before any run state exists. Each phase needs a model reference from itsoneshotassignment, falling back todefault_model. The advisor uses its own assignment, falling back todefault_model. A blank reference returnsconfig.ErrNoModelConfigured. Nothing falls back to the active orchestrator model. -
buildRuntimeWithRootsruns the same check throughpreflightRuntimeModels, so an empty config fails before the worktree, lock, or manifest exists. Errors wrap asoneshot <phase> phase model: %woroneshot advisor model: %w. -
runOneshotTaskandrunOneshotResumethen callRequireSubAgents(fails when sub-agents are disabled) andRequireProviders(rejectsclaude_subscriptionin the default, advisor, oneshot, and sub-agent aliases).newPhaseRunneralso rejectsclaude_subscriptionin its provider factory withErrClaudeSubscription.
The run ID is 8 random bytes, hex-encoded (generateRunID). The slug comes from SlugFromTask: lowercased, runs of non-alphanumerics become -, capped at 48 bytes, and run when empty.
| Item | Value |
|---|---|
| Branch | oneshot/<slug>-<id> |
| Worktree | <project>/.steiner/worktrees/oneshot-<id> |
| State dir |
<project>/.steiner/oneshot/<id> (holds run.json and run.lock) |
| Planning dir | <worktree>/.steiner/plans/oneshot-<id> |
ProvisionWorktree (worktree.go):
- The start point is the
origin/maincommit when it resolves, otherwiseHEAD. Its SHA is stored asworktree_base. - An existing
refs/heads/<branch>is checked out. Otherwise the branch is created with-b. - A stale worktree path is pruned and removed first.
-
ensureSteinerProjectDirwrites.steiner/.gitignorecontaining*.
Orchestrator.Run order: provision the worktree, acquire the run lock (Release runs on return, via defer), derive the interrupt context, create the planning dir, write the manifest with every phase pending, run each phase, then finalizeRun. Concurrent runs never share a branch, worktree, manifest, or lock because run IDs are unique.
acquireRunLock (lock.go) creates run.lock exclusively. It holds a LockRecord with owner, acquired_at, updated_at, and generation.
- If the file exists and its mtime is older than
defaultLockStaleAfter(15 minutes), the caller re-reads it and checks that owner,updated_at, and mtime still match. If they match, the stale file is removed and recreated. Three attempts are made, thenerrLockHeldis returned. -
startPhaseHeartbeatrefreshes the lock everydefaultLockStaleAfter/3(5 minutes). It also refreshes at phase start and after a phase succeeds. -
HeartbeatandReleasecheck that owner andacquired_atstill match. A mismatch returnserrLockLost. A failed heartbeat cancels the running phase. A lost lock makesReleasea no-op, andfinalizePhaseFailureskips its manifest write, so a foreign lock is never modified.
Each phase runtime is built by newPhaseRunner through buildRuntimeWithRoots, with the main checkout as projectRoot and the run worktree as workDir (phaseParams, cmd_oneshot.go). buildRuntimeSandbox (runtime_build_sandbox.go) passes projectRoot to sandbox.New as the writable root and workDir as the --chdir target. Sandboxed commands therefore start in the worktree, and the main checkout is the sandbox root.
Phases run in order: plan, implement, review. runPhase builds a fresh runner for each phase:
- A new
PhaseRunnerandprompt.StaticContextCache, in workflow modeprompt.DelegatedChildWorkflowMode(). -
WorkDiris the worktree.ProjectRootstays the main checkout, which holds the manifest.ProjectAgentsPathis<worktree>/AGENTS.md. - Approvals:
worktreeAutoApproverapproves onlymutatepath requests inside the worktree (tool.PathWithinRoot). Every other approval is denied. - Advisor: enabled, with
MaxUsesPerRunper phase: plan 4, implement 3, review 4. - Cache identity:
SessionIDandPromptCacheKeyare the run ID. Oneagent.NewCacheBaselineStore()perRunorResumecall is shared by all phases. - Seed: one user message from
phaseSeedConversationwith the task, phase, run ID, branch, worktree, and the pathsoverview.mdandplan.yamlin the planning dir. - Phase prompt: a core static source (
ContextSourcePhasePrompt) that bypasses byte budgets. Source order (source_plan.go): preamble, agents, project context, skills, phase prompt, session date, conversation.
CheckBoundary (boundary.go) runs after RunPhase returns. It fails when a required artifact is missing or when git status --porcelain=v1 --untracked-files=all lists any entry outside .steiner/. Required artifacts: plan needs overview.md and plan.yaml; implement adds execution.md; review adds review.md and still requires execution.md. Commits are not checked.
runPhaseOnDriver (cmd/steiner/phase_driver.go) hosts each phase on an agent.ConversationDriver:
- The runtime's
Supervisoris the driver'sBackgroundand completion source. - The phase prompt is submitted.
WaitQuiescentwaits until the driver is idle with no sub-agent pending. Then the boundary check runs. -
MaxTokensPerEpisodeislimits.max_tokens.ErrEpisodeBudgetExhaustedfails the phase. - On any failure (budget, run error, cancellation, heartbeat loss, wait error), the supervisor shuts down with
CancelCauseSystemand then the driver closes. The driver loop is joined beforeRunPhasereturns.
phaseDriverEvents forwards conversation-state events to the session sink when RegisterControl is set (interactive runs). Headless runs drop them.
Each phase creates its own driver. The driver's input is the phase's fresh seed message (phaseConversation, internal/oneshot/orchestrator.go), which is the only message in PhaseRunInput.Conversation. The driver starts with no history and an empty ConversationLineage. PhaseRunInput has no LastMainRequest field, so ConversationDriver.LastMainRequest() starts at zero. Within the phase, the driver carries the last successful main request across episodes (ConversationDriver.LastMainRequest()). A compaction that runs before the phase's first successful main call sees a zero value and treats the cache as cold. RunResult.LastMainRequest is returned for every phase, but no code in internal/oneshot reads it, so the value does not reach the next phase.
runPhaseOnDriver returns the phase result as follows:
- Success: the driver snapshot's conversation and lineage, the last assistant reply, the token total and stop reason from the phase's run record, and
LastMainRequest. - Failure: a
RunResultwith onlyLastMainRequestset. Conversation, reply, lineage, token count, and stop reason are empty. The returned error is chosen in this order:ErrEpisodeBudgetExhausted(wrapped asrun phase), the run record's error,ctx.Err(), then the wait error.
Phase runners build compaction fallback providers through the wrapped providerFactory set in newPhaseRunner (cmd/steiner/cmd_oneshot.go). A claude_subscription fallback model therefore fails to build with oneshot.ErrClaudeSubscription. That attempt reruns on main with status skipped (context management internals). RequireProviders does not check the compaction fallback reference, so this wrapper is the only check on that path.
newPhaseSession creates a session titled <phase> phase: <task> before the phase runs, records its ID in phase_session_ids, and writes the manifest. PhaseRunInput.Session carries the ID and a Save callback for that session.
Each driver snapshot saves the session with its outstanding sub-agent ledger (sub_agent_ledger, internal/session/session.go), so a crash mid-phase leaves the ledger on disk. After the phase, saveResult saves once more with the last stored ledger. It does nothing when the phase recorded no conversation lineage.
Each phase attempt has one session. Resume starts the interrupted phase with a new session and leaves the old one on disk. Compaction history is archived per phase session. phaseHistoryArchive (cmd/steiner/cmd_oneshot.go) keys each archive by the phase session ID. Archive storage and write rules are in Compaction history archive. The phase session archive is not a run-level archive.
run.json is written with fsatomic.WriteFile at mode 0600. created_at is set once and updated_at on every write. The Manifest struct (manifest.go) defines these keys:
| Key | Notes |
|---|---|
run_id, slug, task, branch, worktree_path
|
Set when the run is created. |
worktree_base |
Start commit SHA. Resume fills it when blank. GenerateFinalReport requires it. |
model_snapshot |
default_model and phase_models (phase to model reference). |
current_phase |
Current phase name. |
phase_statuses |
Phase to status (below). |
phase_session_ids |
Phase to session ID. |
commit_milestones |
{phase, commit, message, recorded_at} entries. No code path writes them. |
report_path |
Location of final-report.json. |
closeout_url, closeout_provider, closeout_state, closeout_note
|
Closeout result (see Closeout). |
created_at, updated_at
|
Timestamps. |
Phase status values: pending, running, done, failed, skipped. Resume treats done and skipped as complete. No code path writes skipped.
Orchestrator.Resume (resume.go) serves steiner oneshot --resume <id>:
- Reads
run.json. Blankbranchorworktree_pathis filled from the run identity. - Acquires the run lock before checking the worktree. Stale locks are reclaimed as under Run lock.
- If
worktree_baseis blank, it is set to the current start point and saved to the manifest. -
ensureResumeWorktree: an existing worktree is checked for the expected branch (verifyWorktree). A missing worktree is re-provisioned atworktree_base. -
resumeFromManifestrestarts the first phase that is neitherdonenorskipped, with a new session. When no phase is incomplete andcloseout_stateisfailed, onlyfinalizeRunruns.
Before restarting, reportOrphanedWorktrees loads the interrupted phase's session (when the session store implements Load), reads its SubAgentLedger, and emits an orphaned phase indicator for each sub-agent entry with a WorktreePath.
ListResumableRuns labels a run whose resume phase is closeout as retry closeout, or retry closeout (stale lock) when the lock is stale.
The engine never removes worktrees. CleanupWorktree (worktree.go) has no non-test caller.
Resume runs no artifact check before the restarted phase. CheckBoundary runs only after the phase returns, so a resumed implement phase must still write execution.md before it finishes.
- The run context comes from
InterruptFactory. The default issignal.NotifyContext(os.Interrupt). The TUI usescontext.WithCancel. -
finalizePhaseFailuremarks the phasefailedand writes the manifest, skipped when the lock was lost. It emits the indicatorcancelledfor lock loss, heartbeat failure, andRunPhaseerrors. It emitsboundaryfor runner construction, session creation or save, andCheckBoundaryfailures. - The run lock is released on return, and the worktree stays in place for inspection or resume.
- A failure report (
tryFailureReport) is written whenRunreturns an error and the manifest has a run ID (reportFailureIfNeeded).Resumeapplies the same condition in adeferblock.
finalizeRun writes final-report.json to the planning dir (mode 0600) through WriteFinalReport (report.go), then runs closeout. Fields: run_id, slug, task_summary, branch, worktree_path, planning_path, report_path, current_phase, status (success or failure), completion, phase_sessions, git (branch, commit, short_commit, dirty, files_changed), changes_made, files_changed, validation, review_outcome, review_fix_iterations, assumptions, risks, unresolved_issues, failure (only when the run did not pass: attempted, blocked, changes_left_behind, left_behind_summary), and generated_at. List fields hold at most 8 items. Text fields hold at most 240 runes.
Closeout (closeout.go) runs on the host, not in the sandbox.
- Skipped unless
oneshot.auto_pris true and the report'scompletionis passing. The state isskippedwith a note. - Remote:
branch.<name>.remotewhen set, elseorigin. The provider comes from the remote host:github.comorgithub.*is GitHub,gitlab.comorgitlab.*is GitLab,dev.azure.comorvisualstudio.comis Azure DevOps. Any other host sets stateunsupported. - Target branch: the branch's upstream (
@{u}), elserefs/remotes/<remote>/main, elsemaster. If none resolve, closeout fails. - Title: the first non-blank line of
overview.mdwhen it is an H1, elsetask_summary, elseslug, elseoneshot closeout. - Body (
assembleCloseoutBody):overview.mdminus its leading H1 and the blank lines after it, then a---separator andreview.md. The separator and review are added only when the trimmed review is non-empty. Without a leading H1, the whole trimmed overview is used. Bodies over 60,000 runes are cut at a line boundary with a note naming the planning dir. The user-facing title and body rule is in Oneshot. - GitHub: pushes the branch, then runs
gh pr create(requiresghonPATH). - GitLab: pushes with
merge_requestpush options. It returns no URL, socloseout_urlstays empty and the note says the merge request was created through push options. - Azure DevOps: pushes, then runs
az repos pr create(requiresazonPATH). - States:
created(withcloseout_urlandcloseout_provider),unsupported,skipped, orfailed. On error the manifest recordscloseout_state: failedand the error incloseout_note. The run stays resumable (see Resume).
Interactive runs use routeControlToOneshot:
- Phase events are multiplexed onto the session sink (
output.NewMultiSink). - Phase divider:
Phase: <name>in the scrollback, with no timestamp. - Status bar:
phase · <name>while the run is active. - Sidebar:
Oneshot - <phase>while the run is active. - Indicator states emitted by the engine (
events.go):starting,boundary,completed,cancelled,orphaned.phaseIndicatorRunningis defined but nothing emits it. - Control routing: one
interactive.PhaseRouterper run is set withSession.SetActivePhaseControland passed to the orchestrator asRegisterControl(router.Register). While it is set, the session driver is detached from the steer queue (DetachSteers). Release reattaches it (AttachSteers). Between phases the router has no target: steers wait in the queue, and cancel requests report that no phase is active. - The orchestrator runs under
Session.RunBackground, andCancelBackgroundcancels it. On quit,runQuitSequencecancels background runs first, which takes the cancellation path above.awaitRunsthen joins them. - Steering-only composer: see Steering input for command and input behavior.
-
OneshotFinishedEventis emitted from the run goroutine on success and on error. Its handler clearsoneshotRunningand the phase chrome fields.
Integration and resume tests use real git worktrees (integration_test.go, resume_test.go, worktree_test.go).
Get started
Using Steiner
- How to work with steiner
- Interactive mode
- CLI reference
- Skills
- Sub-agent delegation
- Oneshot
- Context management
- Advisor
- Security audit
Tools and integrations
Safety
Usage and cost
Reference
Internals and maintenance