Sandboxed.sh Missions
Delegate coding/automation tasks to isolated containerised missions via the mcp_sandboxed_assistant_* MCP. Each mission runs in a chosen workspace (a fresh container with a known set of init scripts and pre-installed tools) and executes the prompt autonomously via a configured agent. Intermediate tool calls stay in the mission's own context.
A conversational start_mission is a worker of this chat. Hermes stamps origin_session_id, enrolls the mission, and the terminal webhook folds the result back here. End the turn after dispatch — do not poll, and do not invent a cron just to wait. Controller/cron ticks are different: they pass project and report on the next tick / project route.
When dispatching against a project roadmap, also pass the declared track,
its acceptance_criteria, and a stable retry-safe idempotency_key. The
server reserves the track owner and links the mission as one durable intent;
reusing the key returns the original launch instead of duplicating work.
Mission completion alone does not satisfy the track. Accepted criterion
evidence at the governed artifact version must be recorded separately.
For long workspace commands, use start_workspace_job, or pass both
mission_id and idempotency_key to workspace_bash. Both return a durable
job ID immediately through the same admission path; retry the same submission
with the same key. Without that pair, workspace_bash is a short diagnostic
(60 seconds by default, maximum 120) that is killed at timeout. Shell command
text is not used to infer durability. Consume the job completion callback;
do not keep an agent polling or launch another build to inspect the first.
This is not the same as delegating to a CLI coding agent (Claude Code, Codex, OpenCode) via the terminal tool. The MCP runs an entire conversation loop inside the container; the CLI agents are interactive programs you spawn in a single terminal() call. Use this skill for isolated multi-step research/coding, or work that needs a specific pre-baked workspace (e.g. tailscale-ubuntu, minecraft, dgx-spark).
When to use
- User says "lance une mission", "sandboxed", "spawn a worker", "delegate to a coder in a clean room".
- Task is non-trivial: a project to scaffold, a refactor, a test suite, a multi-file code review.
- You want isolation: the work must not touch the host's working state, dependencies, or secrets.
- The task benefits from a specific workspace's pre-installed tools (Python/uv, Node/bun, gh CLI, Tailscale, etc.).
When NOT to use:
- Single trivial edit →
patch/write_fileis faster and cheaper. - Task needs back-and-forth with the user → subagents can't use
clarify. - You need a long-running daemon → use
cronjoborterminal(background=true)instead. Missions terminate when the agent emits its final message. - Task is purely a tool call with no reasoning →
execute_codeis more direct.
The MCP surface
| Tool | Purpose |
|---|---|
mcp_sandboxed_assistant_list_workspaces |
List all workspaces (containers) you can target. |
mcp_sandboxed_assistant_start_mission |
Launch a new mission: pick workspace + agent + prompt. |
mcp_sandboxed_assistant_get_mission |
Fetch a mission's current status and metadata. |
mcp_sandboxed_assistant_get_mission_events |
Read transcript/trace of what the agent did. |
mcp_sandboxed_assistant_list_missions |
List recent missions (optionally filtered by status). |
mcp_sandboxed_assistant_list_active_missions |
Only the in-flight ones (pending/active/blocked/awaiting-user). |
mcp_sandboxed_assistant_send_message_to_mission |
Resume a mission with a follow-up prompt (for awaiting_user missions). |
mcp_sandboxed_assistant_cancel_mission |
Stop a mission. Returns "not found" if the mission is already gone — that's normal. |
Terminology: backend, workspace, host, and remote node are different layers
When a user asks which “servers/backends” sandboxed.sh uses, answer by layer instead of conflating them:
- Control-plane host — runs sandboxed.sh production and orchestrates mission state.
- Workspace — container/host execution environment selected by
workspace_id; examples include project workspaces and the dedicateddgx-sparkworkspace. - LLM backend — the
backendfield (codex,claudecode,opencode,gemini,grok); this selects the agent/model transport, not a physical machine. - Remote node/build worker — extra compute reached through sandboxed-node or
/api/remote-build; current documented general runners arebabylon,nippur, andashur, while the Lean build fleet also includesdgx-spark. - Adjacent runner — e.g. a GitHub Actions self-hosted runner. This is not automatically a sandboxed.sh remote node even if it runs on the same machine.
For inventory questions, report all SSH-reachable machines separately from the subset registered as sandboxed.sh compute. Date any “currently deployed” claim unless live state was checked via GET /api/remote-nodes and GET /api/health/fleet.
Production DGX routing incident rule. If disk-sentinel reports only dgx-spark unreachable, check systemctl is-enabled tailscaled, systemctl is-active tailscaled, and tailscale ping -c 2 100.77.4.93 on agent-core before changing keys or topology. The expected recovery is systemctl enable --now tailscaled, followed by strict-host-key SSH and a silent sentinel run. Do not recreate an old-agent tunnel: old-agent is compute-only.
Picking the agent
The agent field is one of the agent names registered on the platform. The two universal agents are build and plan. Some setups also offer ana (audit) and paloma (coordinator), but availability varies per installation and workspace — do NOT assume all four are available. The right one depends on the work:
build— general implementation agent. Use for "build this project", "scaffold this repo", "implement feature X". Default for coding tasks.plan— research/planning agent. Use for "investigate options", "draft an architecture", "compare libraries", "write a draft", "research and summarize". No code (or minimal code). Default for research/article/analysis tasks.ana— analytical/audit agent. May not be available — check first or usebuildif you just need code review.paloma— coordinator/operator agent. May not be available.
Pitfall — agent availability and naming vary by deployment.
agentis a registered platform profile andbackendselects the execution backend; they are logically separate, but current deployments may register backend-aligned agent IDs such ascodexin addition to profiles such asbuildandplan. Do not hard-code a universal agent list. Use the live tool schema/catalog or a previously verified route. For Codex, always setbackend="codex"and the exact OpenAI model ID inmodel_override; use the deployment's accepted agent profile (codex,build, orplan) and verify the created mission records the requestedagent,backend,model_override, andmodel_effort. A mission merely reachingpendingis not enough—re-read it after startup and require execution evidence before calling the route healthy.
The backend parameter (opencode, claudecode, codex, gemini, grok) selects the LLM backend, not the agent — this is the underlying model the agent uses.
User-mandated model runs: bound, verify, then independently gate
When the user explicitly requests a particular non-default model, honor the request with the exact backend/model identifier and verify the created mission records that identifier plus an active status shortly after dispatch. Treat that worker as a bounded implementation or investigation lane, not as the sole merge or architecture authority:
- Give it a concrete scope, reproduction/validation criteria, and explicit no-go boundaries (for example: no merge, no legal attestation, no secrets).
- Require it to distinguish source evidence from assumptions and return verifiable handles for every external change.
- Independently verify pushed commits, tests, PR state, and any production claim before reporting success.
- Route material architecture decisions or merge readiness through the normal stronger/latest-head review gate unless the user explicitly waives it.
This keeps a user-requested model useful without converting its self-report into an unverified operational decision.
Codex /goal objective length limit
Codex goal mode has a strict objective-size limit: if the prompt starts with /goal and the objective is too long, the mission can immediately fail with:
codex thread/goal/set failed: goal objective must be at most 4000 characters
Recovery pattern:
- Keep
/goalitself short (under ~3500 characters to leave margin). - Put only the durable objective, constraints, and final-output schema in the
/goaltext. - Move bulky context into linked repo docs, a branch issue, project files, or a follow-up non-goal prompt if the platform supports it.
- Relaunch a fresh mission with the shortened
/goal; do not keep retrying the same oversized objective.
This limit is separate from model context size. A model can handle the information, but the Codex goal API rejects overlong goal objectives before execution starts.
Backend tiering for cost optimisation (multi-provider routing)
The opencode backend is the universal router — it can target ANY provider model via model_override, including non-OpenAI/Anthropic providers that the other backends can't reach (Z.AI GLM, Minimax, Kimi/Moonshot, Cerebras, Spark, Virtuals). The other backends (claudecode, codex, gemini, grok) are locked to their respective providers.
This enables a cost-tiering strategy when the user has "unlimited" quota on alternative providers:
| Tier | Backends | Use for |
|---|---|---|
| Premium (subscription-limited) | codex (approved GPT-5.6 Terra/Sol routes), claudecode (current approved Claude route) |
Critical code, hard proofs, deep reasoning — tokens are scarce, use them where they matter |
| Alternative (often unlimited) | opencode + model_override=glm-5.2 / MiniMax-M3 / moonshotai-kimi-k2-7-code |
Research, article drafting, investigation, documentation — tasks that benefit from a model but don't need frontier-tier reasoning |
How to dispatch on an alternative provider:
mcp_sandboxed_assistant_start_mission(
agent="plan", # research/planning agent
backend="opencode", # the universal router
model_override="glm-5.2", # or "MiniMax-M3", "moonshotai-kimi-k2-7-code", etc.
...
)
Discovering available providers and their model IDs:
# GET /api/ai/providers (JWT-authenticated) returns:
# - id (UUID), name, provider_type, use_for_backends[]
# - Models live in the static catalog: GET /api/providers → {providers: [...]}
# Provider types seen (June 2026): anthropic, openai, google, xai, cerebras,
# zai, minimax, kimi, spark
Fable 5.1 via claudecode only. Virtuals is deprecated and no longer available. For demanding reasoning and long-horizon agentic work, launch via backend="claudecode" with the exact catalog ID model_override="claude-fable-5-1". The older claude-fable-5 remains available only for explicit compatibility. Do NOT use opencode + virtuals/claude-fable-5-1 — Virtuals has been removed from the provider catalog.
Model ID gotchas:
- Z.AI:
glm-5.2,glm-5.1,glm-5-turbo(lowercase, hyphenated) - Minimax:
MiniMax-M3,MiniMax-M2.7(camelCase as in catalog) - Kimi:
moonshotai-kimi-k2-7-code(not barekimi-k2.7) - If
model_overridefails, the error message lists valid IDs — copy from there
User-mandated model that produces no output: do not silently replace it with another model and present the substitute as its opinion. Retry once with a sharply bounded, direct-output prompt (for example: no tools/web, explicit word cap and output schema). If the same model again reaches an idle/stall timeout without content, report that it was successfully addressed but returned no usable answer, then synthesize only from workers that actually produced output. Preserve the distinction between provider failure and research conclusions.
The routing mental model: opencode is a superset of the provider surface. If a provider's use_for_backends includes opencode, you can route to it. If it only lists a specialised backend (e.g. codex for OpenAI), you MUST use that backend and cannot reach it via opencode.
Pitfall —
model_overrideneeds the exact catalog ID. To override which LLM a backend uses, passmodel_override— but it must match the provider's catalog ID exactly. Anthropic usesclaude-<family>-<ver>with hyphens, not the shorthand leaderboard name:claude-opus-4-8✅, NOTopus-4.8❌ (the leaderboard/website may call it "opus-4.8", but the API catalog does not). If you getModel 'X' not found in <provider> catalog, the error message lists every valid ID — copy the exact string from there. Other providers (OpenAI, etc.) generally accept their public model names directly.
Pitfall —
model_effortcaps differ per backend and may change by model/backend version. Themodel_effortfield acceptslow/medium/high/xhigh/max, but not all backends honor all values. Historicallycodexrejected"max"; recentcodex+gpt-5.5missions have accepted"xhigh"and moved toactivesuccessfully. If a user explicitly requestscodex+xhigh, pass it through and verify the mission status immediately withget_mission/list_active_missions. If launch fails with an effort-related error, drop one level ("max"→"xhigh"→"high") until accepted. When no model/effort is specified,"high"remains the safest cross-backend default.
Pitfall —
opencodebackend can crash on non-standardmodel_overridevalues withProviderModelNotFoundError. Seen in the Beal campaign (June 2026): launchingopencodewithmodel_override="glm-5.2"ormodel_override="MiniMax-M3"(passed as if they were model IDs) returnedawaiting_userstatus withProviderModelNotFoundError. Theopencoderouting treats the override as a provider identifier for some configurations, not a model name, even though the samemodel_overrideis documented to work as a model name in other contexts. Workaround: use an approved reliable route for critical work (for Verity:codex+gpt-5.6-terra), and always verify the dispatch succeeded by checkingget_missionstatus within 30s. If it is terminal before repo work, do not treat it as a model result or burn a cache build on a doomed mission.
Pitfall — a shared OpenCode adapter crash is an infrastructure incident, not three independent bad-model results. A launch can pass creation, run the one-time OpenCode database migration, then terminate before any repository command with
fn3 is not a function(observed identically forglm-5.2andMiniMax-M3). When two or more alternative-provider scopes fail at that same initialization boundary: (1) classify them as failed transport attempts with no proof/review evidence; (2) stop further OpenCode launches rather than serially burning surplus quota; (3) start exactly one boundedsandboxed-sh-devrepair mission on a reliable route, requiring an alternative-override regression test and a PR; and (4) keep critical work moving only on approved reliable routes, respecting Codex OAuth launch staggering. After the fix is merged/deployed, validate with one narrow no-push alternative-provider probe before relaunching deferred workers.
Picking the workspace
list_workspaces returns the full set. The right choice depends on what tools the task needs. Key workspace properties to check:
init_scripts— e.g.base,uv-python,bun-mcp,github-cli,tailscale,browser-x11. These are the only things pre-installed.mcps— additional MCPs the agent gets in-container (e.g.google-calendar,orchestrator).env_vars— secrets/tokens injected at start (e.g.GH_TOKEN,GOOGLE_OAUTH_CREDENTIALS).tailscale_mode— if you need network reach through the tailnet.status—readyis what you want;pendingmeans the container hasn't been started yet and may take minutes to initialise.
Secret-safe workspace discovery. Treat a workspace record as potentially secret-bearing, even for a read-only REST lookup: some control-plane responses can include
env_varsvalues rather than just their names. For selection, reduce the response at the source to onlyid,name,status,init_scripts,skills, and an array of environment-variable names. Never print or retain theenv_varsmap. If an inspection nevertheless exposes a credential-like value, stop expanding that output, do not copy it into prompts/reports/other calls, and report only the exposure class and endpoint to the security owner for normal rotation/remediation.
Backend-specific workspace-visibility canary
A mission record carrying workspace_id / workspace_name does not prove that its selected backend sees the intended workspace mount or project checkout. This matters for MCP canaries: one backend can reach the project while another starts in an isolated empty mission directory.
Before declaring a workspace-dependent canary successful, require all of:
- the expected checkout/project path exists in that mission's execution scope;
- its Git ref and project pin (
lean-toolchain, when relevant) can be read there; - an actual registered MCP tool resolves a file from that checkout, not merely that its tool name is callable.
If the route sees only an ephemeral mission directory and MCP calls report project/file-not-found, classify it as a workspace-mount/configuration defect, not a Lean or MCP semantic failure. Keep the canary read-only and retain only concise path/tool-result evidence. When authorized, dispatch at most one bounded no-deploy control-plane repair on a reliable route; require a regression covering backend mount/project-path propagation and prohibit it from modifying the target workspace, checkouts, credentials, or permissions. Re-run the canary only after the repair is independently verified.
Stored GitHub auth may not appear in workspace env_vars
A workspace inventory with no GH_TOKEN / GITHUB_TOKEN does not by itself prove authenticated GitHub access is unavailable: it may have valid stored gh and git credentials. Before recording an auth blocker for an existing-branch repair, run this secret-safe probe in the same workspace and retain only booleans:
# Do not print login, token, credential-bearing remote, or git config.
env -u GH_TOKEN -u GITHUB_TOKEN gh api user --jq .login >/dev/null
gh repo view <owner>/<repo> --json nameWithOwner >/dev/null
git ls-remote https://github.com/<owner>/<repo>.git HEAD >/dev/null
echo 'github_api_capability=ok; git_read_capability=ok'
This proves authenticated API/read transport in that execution scope and prevents a false external-auth blocker. It does not prove normal push transport; require the worker to re-check that immediately before a guarded non-force push. It also does not waive any controller policy that explicitly requires propagation of a named environment secret: record that distinction rather than inventing a secret name or exposing stored credentials.
Known workspace init-script matrix (June 2026)
| Workspace | Base | Python | gh CLI | Tailscale | Browser | Notable extra |
|---|---|---|---|---|---|---|
assistant |
ubuntu-noble | uv | yes | no | no | google-calendar MCP, bun-mcp, bitwarden-secrets skill |
misc |
ubuntu-noble | uv | yes | no | no | orchestrator MCP, bitwarden-secrets |
dumbcontracts |
ubuntu-noble | uv | yes | no | no | ORACLE env (large), 32G memory limit, vercel-cli |
minecraft |
ubuntu-noble | uv | yes | no | x11 | minecraft-shard, deployment-management, vercel-cli |
dgx-spark |
tailscale-ubuntu | uv | yes | yes | no | Tailscale exit node, dgx-spark skill |
sandboxed-sh-dev |
browser-tailscale | uv | yes | yes | x11 | design-taste-frontend skill, shared_network=true |
host |
(host machine) | (host Python) | yes | no | no | Path: /root. Use for "skip the container, run on my actual host." |
Critical pitfall —
assistantworkspace has NO coding-agent CLI installed. Missions inassistantcannot invokeopencode,codex, orclaude(the Anthropic CLI), AND the workspace lackscurl,wget,npm, andbunglobally. Thebun-mcpinit script only enablesbunfor MCP server processes — not for shell-level installs. Errors surface as one of:
OpenCode CLI 'opencode' not found and neither curl nor wget is available in the workspace.Codex CLI 'codex' not found and neither npm nor bun is available in the workspace.Claude Code CLI 'claude' not found and neither npm nor bun is available in the workspace.Any Python script that does
import requestsand hits an external URL still works (Python ships its own HTTP), but shell-level network calls and CLI invocations fail. If your task needs ANY coding-agent CLI or shell-level network tools, pick a different workspace or run onhost.
Workaround when the agent's workspace is too stripped-down. Pivot to
host(workspace_id00000000-0000-0000-0000-000000000000, path/root). The host has the full user shell,curl/wget/gh/uv/python3all pre-installed, and access to secrets in/var/lib/hermes-assistant/.env. The trade-off: you lose the per-task container isolation and you may have to be more explicit about cleanup.
Pitfall — codex backend has a single-use OAuth refresh token. Launching multiple codex missions simultaneously (parallel batch in one turn) causes an auth race: each mission's container tries to refresh the same OAuth token concurrently, and since the refresh token is single-use, all but one fail with
auth_error/refresh token already used. This is fatal for those missions — the token is consumed and the mission cannot recover. Never launch a second mission onto a worktree that already has a live occupant —create_missionreturnsworkspace_occupied. Stagger remaining sequential Codex launches, or useclaudecode/ kimi / glm. The other backends (claudecode,opencode) do not have this OAuth race and can be batch-dispatched freely. If a Codex mission dies withauth_error/refresh_token_invalidatedon launch,create_missionwill reject furtherbackend=codexwithcodex_oauth_invalidateduntil the token is repaired — re-dispatch onclaudecode(or kimi/glm) on the existing worktree writer. Do NOT retry on Codex. A newly accepted Codex mission can showpending/activefor a few seconds and then flip tofailed/acknowledgedwith an OAuth error such asrefresh_token_invalidated, so for watchdog/anti-stall dispatches do a second verification read (orlist_active_missions) shortly after launch before updating trackers or reporting the launched ID. If an automatic/manual fallback mission appears tagged likeredispatch-after-codex-auth, track and report the fallback mission ID as the live worker, and keep the failed Codex ID only as a failed attempt. Fallback metadata may not exactly mirror the original (backend=claudecode,intent=implement,github_pr=nullare all plausible), so identify it by project/title/tags/workspace as well as intent. Auto-redispatch may lag behind the first failed-mission read: after seeing Codexrefresh_token_invalidated, wait briefly and read exact mission + active list again before marking the workstream blocked or patching trackers toblocked-external.
Library skills — the mission agent's knowledge base
Each workspace exposes a set of library skills (visible as skills: [...] in list_workspaces output). These live at /root/.sandboxed-sh/library/skill/<name>/SKILL.md and are git-versioned — the entire library/ directory is a repo (Th0rgal/sandboxed-library, branch main).
The mission agent reads library skills, NOT Hermes skills. Your Hermes skills (oraxen-pr-review, release-oraxen, etc.) are only read by you (Paloma). The agent inside the container reads the library skills attached to its workspace. This has a direct operational consequence:
- When a mission repeatedly fails at the same step (wrong build flag, missing asset, wrong path, wrong API endpoint), the root cause is often an under-specified or outdated library skill, not a bad mission prompt. Patch the library skill at
/root/.sandboxed-sh/library/skill/<name>/SKILL.mdandgit commitfor a durable fix that propagates to all future missions in that workspace. - Patching only your Hermes skill does NOT help the mission agent — it never sees it. Enriching the Hermes skill helps you write better prompts, but the failing agent still starts from the stale library skill.
- Always
git add + commitlibrary-skill changes so they survive server rebuilds and container re-provisioning. Uncommitted edits tolibrary/can be lost on the next deploy.
Concrete example (Oraxen jar builds). Every mission produced a stripped demo jar (no textures) because the library skill
oraxenwas missing the-Poraxen_compiled=falseflag and thecore/src/main/pack/path migration (v1.217+). Fixing the Hermesoraxen-pr-reviewskill helped Paloma write more explicit prompts, but missions kept failing until the library skilloraxenitself was enriched and committed. Lesson: patch the layer the failing agent actually reads.
The launch pattern
mcp_sandboxed_assistant_start_mission(
title="<short human title>",
prompt="<full self-contained prompt>",
workspace_id="<from list_workspaces>",
agent="build", # one of the platform agent names
backend="opencode", # or claudecode / codex / gemini / grok
model_effort="medium", # low/medium/high/xhigh/max
)
Conversational launches are workers of this chat. Hermes stamps
origin_session_id and enrolls the mission so the terminal webhook
folds the result back here. End the turn after dispatch. Do not poll
and do not create a cronjob just to wait. Prefer
delegate_task(backend="mission") when you want an explicit handle;
start_mission from a desktop/API conversation is equivalent.
Prompt structure (template):
You are <doing X> for <person/repo>. Goal: <one-line outcome>.
## Context
- <inputs the agent has no way to know without this section>
- URLs, IDs, file paths, repo owner, etc.
## Tech stack
- <language, deps, what tools to use>
## Architecture / approach
- <high-level shape of the solution>
## Deliverables
1. <concrete artifact 1>
2. <concrete artifact 2>
3. ...
## Constraints / gotchas
- <what NOT to do>
- <known library/tool quirks>
## Workflow
1. <step 1>
2. <step 2>
3. Verify: <how to know it worked>
## Workspace
- <which init scripts the agent can rely on, and which NOT>
- <any pre-existing env vars to use>
Self-containment is the single biggest factor in mission success. The mission agent has no access to your context — every fact, every file path, every constraint the agent needs to make a correct decision must be in the prompt. If you find yourself wanting to say "see above" or "you know what I mean", rewrite the prompt.
Monitoring and recovery
How the result comes back
Conversational launch (desktop / API / TUI): start_mission is a worker of this chat. Hermes stamps origin_session_id and enrolls the mission. Confirm pending/active, then end the turn. The terminal webhook folds the result back here (ledger), or appends a [Mission callback] and wakes this session. Do not verify PALOMA_WEBHOOK_FORWARD_URL, do not start fleet-heartbeat, and do not create a cronjob to poll.
Controller launch (cron tick with deliver: project:<slug>): pass project, track (a key from get_situation; unknown keys are absorbed as unplanned items), intent, and a stable idempotency_key. One writer per track: 409 track_owned names the holder — attach to it or dispatch read-only (writer=false). Do not wait. Report on the next tick or the project route. Never stamp a cron_* session as origin.
On callback: inspect get_mission / get_mission_digest plus artifacts through the direct source before reporting. Mission self-report is not success. Notify Thomas for a user-launched completion, failure, blocker, decision, PR opened/merged, or useful research result. Stale duplicate ACKs may stay silent.
Do not block a Hermes turn with sleep until complete. Missions can run for hours; a sleeping parent can be compacted or killed.
- Status values:
pending→active→awaiting_user(the agent has a question for you, usesend_message_to_mission) → terminal (completed/acknowledged/interrupted/failed). There is nocancelledstatus in the enum.completedis a real terminal state produced by background-job reconciliation and explicit completion transitions — treat it as done, do not keep monitoring it. - If a mission is
awaiting_user, the agent is blocked OR has finished and is waiting for ack — the two are indistinguishable from status alone; read the last transcript event to tell them apart (see the pitfall in "Surveying a fleet" below). - To close a finished
awaiting_usermission: do NOT usecancel_mission— it returns 404 on non-active missions (the cancel handler needs a running control actor that doesn't exist post-completion). UsePOST /api/control/missions/:id/status {"status": "acknowledged"}via the REST API. For bulk cleanup, loop/statuscalls with a JWT +ThreadPoolExecutor. Only usecancel_mission(or resume→cancel) onactive/pendingmissions you want to kill mid-flight.
Non-interrupting Ask fallback when sandbox isolation is unavailable
When using /ask as a side-channel, prefer sandbox: true for extra isolation. If it returns 400 Sandbox mode requires a git workspace (no isolated worktree could be created), this means Ask could not create an isolated worktree for that mission; it does not mean the side-channel is broken. If the user asked for non-interrupting inspection, retry /ask without sandbox only with a strict read-only prompt: no file modification, no task execution/reruns, no commits/pushes, no secret/env printing.
Circuit-breaker-safe sequence: do not probe sandbox:true on several missions in parallel. A predictable sandbox_unavailable response counts as an MCP failure; three parallel failures can trip the sandboxed-assistant circuit breaker and block the immediate unsandboxed fallback. Probe one representative mission first. If sandbox creation fails, switch that mission to a strict read-only unsandboxed Ask, then inspect additional missions sequentially or in batches of at most two. If the breaker already tripped, use the authenticated REST mission/events endpoints for read-only reconciliation during cooldown rather than retrying MCP into the cooldown. For long histories, page events with since_seq; a mission whose first page looks stale may have thousands of later executable events.
Mission PR dispatch: verify the PR exists, not just that the mission launched
When the user asks for a PR link from a mission, do not answer from launch status. Read get_mission history/status and only provide a PR URL if the mission returned one or if you independently verify it in GitHub. If the mission failed before producing a PR (e.g. terminal_reason=infinite_loop), say no PR exists yet, then relaunch with a shorter/bounded prompt or different backend. Long Claude Code website/update prompts can loop; retrying with a compact gate + final-output schema on a different backend is the durable recovery pattern.
Mission PR completion is not merge-readiness
A mission can finish as acknowledged with a self-report like "PR opened, builds passed" while the PR is still not merge-ready: later CI jobs may still be queued, OCR/advisory jobs may post retryable failures, or a latest-head Codex review may contain unresolved threads. Before telling Thomas the workstream is done, verify the PR live:
gh pr view <N> --json state,isDraft,headRefOid,mergeStateStatus,statusCheckRollup,reviews,comments.- Check latest-head Codex specifically: identify the current
headRefOid, then inspect review threads for unresolved/non-outdated comments authored by Codex on that commit. A top-level "no major issues" comment is useful, but active review threads win. - Treat queued/in-progress required CI as pending, not green; if only long Foundry/Lean jobs remain, say that precisely.
- If a real blocker exists, resume the same mission with a bounded follow-up containing the exact PR, current head SHA, thread URL/path/line, and requested validation loop. Do not start a duplicate mission unless the original is unrecoverable.
Merge authority, independence, and provenance
For repositories/campaigns Thomas asks Hermes to manage, autonomous guarded merge is authorized. Keep the roles explicit:
- Give implementation writers
no merge; this limits that mission and does not revoke the campaign-level merge grant. - Give the dedicated integrator the exact repository/PR scope and immutable head, set
writer=trueso it holds the exclusive PR writer lease, then setrequest_merge_authority=true. Never supplymay_mergeormerge_authority_sourcefrom model-controlled arguments. Theassistant-mcpserver derives the grant itself only when the canonicalowner/repository#numbertarget matches its operator-configuredHERMES_MERGE_AUTHORITY_REPOSITORIES; it signs the receipt withHERMES_MERGE_AUTHORITY_SOURCE. An absent/mismatched grant—or a merge request withoutwriter=true—must fail closed. Require--match-head-commitor an equivalent API precondition. - Treat only required
SUCCESSchecks as green.NEUTRAL,SKIPPED,ACTION_REQUIRED, missing checks, and a bot declining to run are not green. If no CI exists, sayNO_CI_CONFIGUREDand require clean local gate reproduction plus a separate exact-head review. - If a reviewer pushes a fix, it is now a writer for that head. A different read-only reviewer (or a GitHub Codex review received after that push on the exact head) must certify it before merge.
- Resolve ordinary branch conflicts with a normal merge from the current base, then rerun all exact-head gates. Never rebase or force-push under this standing authorization.
- Persist a merge receipt: executing mission ID/role, controller origin when available, authority source, PR/head, CI classification, reviewer/head/time, thread count, merge method, and merge commit. GitHub's
mergedByis the shared account identity, not actor provenance.
Terminal push versus the current PR head
A worker can correctly finish a normal push, local full/trust validation, and an @codex review request, while repository automation appends a derived commit before the controller's next read. The worker's reported SHA is then no longer the governed artifact.
Before calling the PR ready, waiting on CI, or dispatching a replacement:
- Independently compare the worker-reported pushed SHA with the live
headRefOidand inspect the current workflow'shead_sha. - If they differ, record the worker as terminal after push, never as an active owner. Re-evaluate CI, exact-head review, and unresolved technical threads on the live head only.
- If the live-head workflow is terminal
action_requiredwith no child jobs, classify it as an external platform/approval gate. Do not launch a polling or repair worker and do not use the old-head Codex request as current-head review evidence. - After the owner/platform starts the exact-head workflow, re-run current-head CI and review/thread reconciliation before considering merge or downstream stack progression.
This avoids two false moves: a duplicate repair for a branch that already advanced, and a claim that an earlier clean review covers an automation-derived head.
This pattern prevents a finished mission's optimistic self-report from masking post-PR review findings.
Safety-audit → existing-branch repair handoff
For PRs that implement retention, garbage collection, storage lifecycle, or any code that could later delete operator data, a read-only audit finding is a dispatch trigger — but it is not enough by itself to authorize a fix:
- Pin the audit to the immutable PR head and independently inspect the cited current-source lines before acting. Confirm the executable control-flow claim; an audit can correctly spot one defect while over-reading a nearby predicate or documentation comment.
- Re-read the exact audit mission and global active/pending list. If it ended
awaiting_userwithturn_complete, ACK it so it cannot remain a phantom evidence owner. - If the confirmed finding is current-head actionable and no scoped writer exists, launch exactly one existing-branch repair with no-merge/no-force-push/no-new-PR/no-deploy/no-real-cleanup authority. Pin the before-SHA and require a normal guarded push only after tests pass.
- Test both dry-run and opt-in execution semantics. Protection for live processes/scopes and active missions must be checked before candidate enumeration and before deletion, not only in a later orphan sweep. Regression fixtures must show caches, worktrees, images, unattributed paths, and nested build caches stay retained unless an explicitly authorized policy says otherwise.
- After any pushed repair, restart the normal final-head CI/review/thread gates. Never infer that a dry-run-only implementation is safe to merge merely because its ordinary CI is green.
Use it deliberately for long-running missions that have parked themselves with a background waiter or vague "standing by" note. Ask for a bounded, safe status report instead of a broad "what's up?" Example:
Please report current progress in exact numbers: total work items identified at start,
completed now, currently running, queued/not started, failed, current phase, ETA, and
any blockers. Do not print secrets or credential values.
Then read get_mission, not only get_mission_events(view='transcript'): event pagination can return [] while get_mission.history contains the newly appended user message and the agent's latest response. Trust concrete counts from the mission only as a self-report until independently verified from repo artifacts / PRs / logs.
Distinguish a verified blocker from unfinished reconnaissance
For implementation watchdogs, do not promote a repository gap into an “exact reproducible blocker” merely because a stalled worker found empty artifact directories, .gitkeep placeholders, or documentation saying a target is blocked. Those facts prove only that the artifact is not checked in; they do not prove it cannot be generated.
Before reporting a technical blocker, require evidence that the worker attempted the relevant build/generation/execution path and captured:
- the exact failing command and key output;
- source revision and relevant tool/interface versions;
- the smallest prerequisite that would make that command advance;
- confirmation that no branch, commit, test log, or PR artifact already exists.
If the worker stalled during read-only reconnaissance and never invoked the pipeline, classify the failure as a transient stall, not a technical blocker. After checking for an active successor and live GitHub artifacts, launch exactly one bounded replacement when authorized. Its prompt should explicitly require an immediate pipeline
…(truncated)