AFK Pipeline
Substantial eligible development routed to AFK uses a GitHub-issue label to trigger a pipeline that implements the change, verifies it in a real browser, opens a draft PR carrying committed QA Evidence, and — only when the requester asked for one — publishes a visual recap. A human reviews and merges. In a registered repo on the AFK lane you never implement product code inline — your deliverable is the brief, and the brief is the contract: the pipeline agents get no follow-up questions.
Preamble (run FIRST)
cd "$(git rev-parse --show-toplevel)" 2>/dev/null
_R=.agents/afk-pipeline/REGISTRY.md
[ -f "$_R" ] && echo "REGISTRY: $_R" || echo "REGISTRY: missing"
REGISTRY: missing→ either you are not in the consumer's home repo (ask where the registry lives), or the pipeline was never adopted here (offer reference/installation.md).- Otherwise Read the registry file in full now — it maps each repo to trigger labels, default lane, clone path, and quirks. Do not route from memory of it.
Contract
- Every substantial eligible development task routed to AFK in a registered repo goes through an Agent Brief — no inline implementation, no direct pushes, no merges.
- Phases are fail-safe ON: Pipeline Flags only reduce work; absent flags mean the full pipeline (implement → advisory second-model review → verify → draft PR → recap) on
engine: claude. Recap is opt-in at authoring time (v2.9.3): every brief stampsrecap: offunless the requester explicitly asked for a recap — the runtime fail-safe staysrecap: on, so the line is mandatory, never omitted. - Implement↔verify retry loop (v2.13.0): on a behavioral verify fail the wrapper re-enters implement with a bounded feedback envelope, up to
retries:extra attempts (default 2, allowed 0–3, values >3 clamp to 3). A clean first pass surfaces nothing; a run that passes after a retry adds a Convergence section on the PR and one completion-comment clause; budget/deadline/indeterminate terminals get aStopped:blocked comment with per-attempt rows. Behavior change: defaultretries: 2applies after consumer regen — stampretries: 0to restore single-attempt. Underverify: offretries are inert (one attempt). Proof-only:RETRY_PROOF_VERDICTS(JSON map of attempt → verdict override, incl. indeterminate) may be set as a repo Actions variable for seeded Tier-1 proofs; the generated workflow never sets it — remove the variable after the proof. - Recorded browser evidence is experimental and opt-in (v2.10.0): every new brief stamps
recording: offunless the requester explicitly asks for video proof.recording: onis v1-hosted-only (cloud or Vercel Sandbox), requires verify, uploads the full WebM as a 30-day Actions artifact, and commits only a small GIF preview to the PR. Before recommending it, read reference/experimental-recording.md. - The implementation engine is a brief-stamped contract (v2.9.0):
engine: claudeby default;engine: codexruns the four agent phases (implement / review-fix / verify / write-pr) on Codex when the dispatcher has confirmed the target lane can run Codex;engine: cursorruns implement only on the Cursor Agent CLI, then keeps review-fix / verify / write-pr on Claude because Cursor sessions are non-resumable and structured-output retry needs resume. Cursor v1 is cloud-lane only (agent:implement), requires the repo secretCURSOR_API_KEY, defaults to modelgrok-4.5-xhigh, and honors aCURSOR_MODELenv override.engine: grok(v2.12.0, implemented) runs implement only on the Grok Build CLI (native xAI; custom sandcastle provider at.sandcastle/grok/provider.ts, modelgrok-4.6viaGROK_MODELvar, efforthighviaGROK_EFFORT— the model default), then keeps review-fix / verify / write-pr on Claude; Grok headless sessions ARE resumable (-s/-r, proven 2026-07-12), so codex-parity (all four phases) is a permitted later expansion, but v1 mirrors Cursor's smaller runner surface. Grok v1 is cloud-lane only (agent:implement): auth is theGROK_AUTH_B64repo secret — a base64 of the dispatch host's~/.grok/auth.json(X Premium+ OAuth), re-cut at dispatch likeCODEX_AUTH_B64— materialized to the runner and bind-mounted into the per-phase container; the image installs the grok CLI only for grok-stamped runs (INSTALL_GROKbuild-arg). The pre-implement auth smoke is a real tiny chat call, NOT a models listing (the models endpoint answers without the chat entitlement while chat 403s — observed 2026-07-12). Consumers on older parsers fail-safe anengine: grokstamp back toengine: claude; regen to v2.12.0 + setGROK_AUTH_B64before stamping grok. Recap stays Claude. The second-model review defaults to the opposite vendor for Codex (engine: codex+ absentreview-engineresolves toreview-engine: claude); Cursor and Grok follow the Claude default review contract. Explicitreview-engineoverrides are honored and visible; no phase silently swaps engines when a requested engine is unavailable. - Repos with a Convex backend (
convexDirin.sandcastle/config/pipeline.json) get a Convex integrity gate, always-on and not flag-controlled: the pipeline regenerates_generatedwith real codegen against a keyless anonymous local backend; the schema/types not validating fails the RUN, while divergent committed files are self-healed (v2.5.1) — the gate commits the canonical codegen output itself (visible[convex-gate]commit in the PR) instead of discarding the whole implement run over machine output (a full andyChat slice was lost to this on 2026-07-07). Hand-editing_generatedis forbidden everywhere. Companion stack policy: merging a Convex change must trigger an automatic backend release (Vercel-coupled build or a main-push deploy workflow) — a repo where merged backend code waits for a manualconvex deployis drift. - Skip decisions key on predicted diff shape — the files and surfaces the change will actually touch — never on how the task is framed.
- Labeling starts a paid, unattended run. Show the complete brief and flags before dispatch, then follow the active workspace authority contract — including any dispatch gate it defines (e.g. a contract that requires the user's explicit go-ahead immediately before any cloud or sandbox dispatch: show brief + flags + lane, then wait for it). Beyond the contract's own gates, do not add extra confirmation rounds.
- No failed run loses committed work (v2.4.0): any failure or timeout-cancel after the branch exists salvage-pushes the agent branch (uncommitted debris included as an explicit WIP commit) and the failure comment names it. A salvaged branch is unreviewed-for-merge by definition; a successful retry force-replaces it. The one class this cannot cover: a crash inside the implement phase before the agent commits. Vercel Sandbox lane limitation (pre-existing, spec §9.3 — not a retry-loop regression): workspace sync-out runs only after a phase succeeds, so in-microVM commits of the crashing phase itself are lost; completed phases' synced commits (including the wrapper's host-side evidence-relocation commits) are always salvageable. Upstream sync-on-failure is the eventual fix (skills#74 territory).
- Three lanes (v2.5.0, rearchitected v2.6.0), selected per task by trigger label:
agent:implement(cloud — hosted VM with the agent in per-phase Docker),agent:implement-local(self-hosted runner, agent in per-phase Docker),agent:implement-sandbox(hosted runner + Vercel Sandbox microVM). The non-local runner is per-repo config (v2.7.0): pipeline.jsoncloudRunsOn=ubuntu-latest(GitHub, default) or a Blacksmith tag likeblacksmith-4vcpu-ubuntu-2404(needs the Blacksmith GitHub App on the repo's org; a tag with no backing runner queues forever — first run after flipping validates). Local-lane containers are CPU-capped (v2.7.0,dockerCpus, default 3) so concurrent runs can't starve the runner host; RAM is still the real ceiling — the registry's routing policy (max concurrent local runs, overflow lane) is the primary defense, the cap is the backstop.engine: codexuses the dedicated AFK Codex home (~/.codex-afk) on the local lane with a host lock, cloud-lane CODEX_AUTH_B64 seed materialization on the runner, and sandbox-lane CODEX_AUTH_B64 delivery via a per-command in-VM seed after sandbox creation. Codex-stamped briefs never overflow to a codex-incapable lane: queue them or re-stampengine: claude — override ignored: <reason>before labeling.engine: cursoris cloud-lane only in v1; local and sandbox labels fail fast, while the shared Docker image carries the CLI solely for the hosted cloud lane. The sandbox lane rides sandcastle's own isolated-provider machinery: the AGENT runs inside a per-phase Firecracker microVM and the workspace syncs in/out via git bundle/format-patch, so push, salvage, artifacts, and step outputs stay host-side on all three lanes..sandcastle/vercel/provider.tsis a thin hardened stand-in for sandcastle's stocksandboxes/vercel(stock gaps as of 0.12.0: exec ignoresstdin— which is how agent providers deliver the PROMPT — no transport-fault tolerance, nopersistent: false; collapse it once upstreamed). Per-repo provisioning:sandbox: {teamId, projectId}in pipeline.json (the repo's Vercel team + itsafk-sandboxproject), the team-scopedVERCEL_SANDBOX_TOKENrepo secret, and the@vercel/sandboxdevDependency. Unprovisioned label use fails loudly at the first agent phase.
The loop
- Route. Find the target repo's registry row. No row → not an AFK repo: do normal work, or offer installation. Done when: row in hand.
- Mini-grill. Interrogate the request until the brief is writable: goal, constraints, acceptance criteria, blast radius, and what could hide inside it. One question at a time, with your recommended answer. Never paper over ambiguity: an unclear or underspecified request means the issue does not get written yet — ask until it's settled. Escalate to a full
grill-with-docssession (target = this repo) when the task reshapes domain language, carries a trade-off worth an ADR, or introduces a new integration, credential, or external service. Done when: every acceptance criterion is checkable by an agent with no context (browser or shell), and you can predict the diff shape. - Flags. Apply reference/phase-rubric.md to the predicted diff shape. Also read
~/.hermes/profiles/matias/.agents/engine-override.json: absent, malformed, unsupported lane, carve-out, or incapable target means off and the brief still stampsengine: claude — override ignored: <reason>; a valid codex override on a capable lane stampsengine: codex; a valid cursor override stampsengine: cursoronly for the cloud lane withCURSOR_API_KEYconfigured. Codex-stamped briefs never overflow to a codex-incapable lane, and Cursor-stamped briefs never move to docker or sandbox. Done when: each flag has a one-line reason. - Brief. Write it per reference/brief-template.md,
### Pipelinesection included, and show the user the full body plus your flag reasoning. Every new brief stampsengine:. Before a cloud Grok dispatch, runnode <skill>/scripts/seed-grok-auth.mjs --repo <owner>/<repo> --canonical <encrypted-auth-path>; it pins the auth home, proves a real chat, copies the opaque refreshed file to the encrypted store, seedsGROK_AUTH_B64, and reports the secret timestamp without printing credentials. Done when: the helper exits 0, then and only then may the label land. When dispatching a cloud/sandbox codex run, refresh the target repo secret from the mini's current auth before any label lands, as its own command, and confirm it exited 0:base64 < "$HOME/.codex-afk/auth.json" | gh secret set CODEX_AUTH_B64 -R <owner>/<repo>(gh secret setreads stdin — it has NO--body-fileflag; the flag error is silent doom if compounded). Never chain a seed refresh and label: a failed refresh must leave the run undispatched. Codex quota runbook: ChatGPT usage limits can exhaust mid-run. Diagnose on the dispatch host withecho "Reply OK" | codex exec --json -m <model>; aturn.failedusage-limit message is the signature. Wait for reset or re-stamp Claude; never burn retries on a quota-dead account. - Authority. Record that the user saw the full brief and flags, then apply the active workspace authority contract. Stop only for an unresolved judgment, permanent-loss, or human-only gate, or a dispatch gate the contract defines (e.g. required user confirmation before a cloud or sandbox dispatch); otherwise create and label without an extra approval round. Done when: the routing decision and any gate disposition are durable.
- Trigger. Create the issue, add the repo's trigger label (lane per registry default). Watch by polling
gh run view <id> --json statusin a loop — nevergh run watch(unbounded output). - Deliver. When the draft PR opens, hand over PR + QA Evidence + recap links (when a recap was requested), plus the live deploy-preview URL when the repo has a PR-preview integration (e.g. Vercel comments it on the PR) — it's the fastest human review surface, ahead of screenshots. If the diff touches the pipeline itself, say plainly: pipeline changes execute from the default branch, so they can't self-prove — their first validating run is the first run after merge.
Output format
End a triggered task with: issue URL, run URL, flag set with reasons, and a status line — WAITING_ON: review or BLOCKED: <one line of evidence>.
Watching a local-lane run
The agent's output streams to the GitHub Actions job log (repo → Actions → agent-implement run, or gh run watch -R <owner>/<repo>), plus the committed evidence dir and the PR recap. Docker Desktop is only for liveness/CPU/kill: sandcastle boots the container detached with a keepalive PID 1 and drives all work via docker exec, so the container's own Logs tab is empty by design. Container names are sandcastle-<uuid> — hardcoded upstream in @ai-hero/sandcastle (no name option; never docker rename a live one, sandcastle addresses it by that name). Identify containers by IMAGE: docker ps --format '{{.Names}} {{.Image}} {{.RunningFor}}' — the per-repo image (sandcastle-<project>) tells you whose run it is.