AFK (unattended) mode — Autonomous Pipeline
Trigger phrase: when a user prompt includes the phrase “overnight mode” alongside a
next-session-*.mdpath (e.g.load handovers/<USER_SLUG>/himmel/epics/HIMMEL-XX-…/next-session-N.md overnight mode), execute this pipeline end-to-end without pausing for confirmation between phases.
When to use it
Use AFK (unattended) mode when a single ticket has a complete brief + plan, the work is well-scoped, and you want to ship without sitting at the keyboard. Proven on the himmel-run runner + himmel-jira plugin, shipped in one overnight session.
Do not use AFK (unattended) mode when:
- The brief is ambiguous or the spec is incomplete.
- The work touches multiple independent subsystems (split into separate tickets first).
- Destructive ops outside the worktree are required.
For work that will need mid-flight decisions or will outlive one context window, run a chief instead (glossary.md); it dispatches each ticket as its own session and hands itself over: running-a-chief.md.
The 11 phases (+ step 0: queue lock)
-
Queue lock: before Phase 1, run
bash scripts/handover/queue-lock.sh acquire <handover-path>. rc=0 (fresh acquire or an automatic stale takeover) proceeds straight to Phase 1. Capture therelease-token: <token>line the acquire prints and keep the token for Phase 11:release(andheartbeat) refuse without it, so a late or wrong-session release can never rm a live lock. The token ALSO persists per session (under$XDG_RUNTIME_DIR/himmel-queue-lock/, dir 0700 / file 0600), so a Phase 11 release with no token on argv reads it back and names the file on stderr; an argv token still wins. That persistence is conditional: it needs a per-session scope, which isQUEUE_LOCK_SESSION_SCOPEif you set it, elseCLAUDE_CODE_SESSION_ID. With neither set nothing is written and nothing is recalled. That is deliberate, because the only remaining candidate (the POSIX session id) is shared by every process in one terminal and would let a sibling session recall your token. Under Claude Code the scope is always present; outside it, setQUEUE_LOCK_SESSION_SCOPEor expect no recovery file. Either way keep capturing the printed token: the file is a safety net for a mid-run autocompact eating the transcript line, not a replacement for carrying the token forward. rc=2 means the queue is owned by a LIVE session elsewhere right now (the exact 2026-07-10 00:51 double-fire shape). Do NOT proceed on this queue: load the coordination file, pick a different queue, or stop with a clear message to the operator. Do not setQUEUE_LOCK_TAKEOVER=1to push past a FRESH lock unless you have independently confirmed the other holder is gone (the operator said so, or the machine it names is verifiably offline): a live double-acquire is the failure this step exists to prevent. Release the lock at wrap (Phase 11, with the captured token) or via/stop; an un-released lock is covered by the TTL (default 6 h, sized to cover the 3-4 h overnight budget;QUEUE_LOCK_TTL_SECONDStunes it — TTL sizing is an open operator question on the queue-lock design) so a crashed session never strands the queue. A live worker does not have to remember to heartbeat. A worker acquires here and releases at Phase 11 and calls nothing in between, so after one TTL its lock used to go silently STALE and takeable. The lock now self-heartbeats from a PostToolUse hook (queue-lock.sh self-heartbeat) that refreshes the locks THIS session acquired, throttled byQUEUE_LOCK_SELF_HEARTBEAT_SECONDS(default 300), and records the session’s transcript path in the lock. Operator: wire the self-heartbeat hook VM-first (wire-session-telemetry-hooks.mjsrewritessettings.json;--checkreports whether it is wired,--offremoves it); never from a worker. Even un-refreshed, a lock whose holder’s transcript on the SAME host was written within the TTL is LIVE regardless of heartbeat age:statusreports FRESH andacquirerefuses (QUEUE_LOCK_TAKEOVER=1still forces).heartbeatandreleasenow emit the sameAGINGwarningstatusalways did (heartbeat age past half the TTL), so a worker that outran its margin unseen finds out at its next refresh or at wrap. Without the hook, a manualqueue-lock.sh heartbeat <handover-path> <token>on long sessions is still the lever. -
Plan:
superpowers:writing-planson the active brief; commit to<plans-root>/YYYY-MM-DD-<slug>.mdwhere<plans-root>resolves as:$HANDOVER_DIR/plans/whenHANDOVER_DIRis set (Mode B: plans live with handover state in<state-repo>/handovers/plans/).<repo>/docs/superpowers/plans/otherwise (Mode A default, for backwards-compat; this path is gitignored, so Mode-A plans stay local and are never committed to the public repo).
The Mode B path collocates the plan with the per-ticket session notes (
<state-root>/<repo>/{epics,standalones}/<TICKET>/next-session-N.md), so a reviewer auditing the work can grep one root for plan + decisions + outcomes. Mode A keeps plans inside the code repo for solo-operator no-env-var setups. -
Worktree:
/clean_garden feat/himmel-<N>-<slug>. -
Impl:
superpowers:subagent-driven-development. Sequential per task: implementer subagent → spec-compliance reviewer → code-quality reviewer → fix-on-finding → next task. Verifier cadence (Fable-5 scaffolding): every 3 completed tasks, dispatch one fresh-context verifier subagent that re-checks the accumulated branch diff against the Phase-1 plan. Fresh-context verification beats self-critique, and the per-task reviewers see one task at a time; this is the layer that catches cross-task drift before Phase 4. Stop-check: before EACH subagent dispatch in this loop, runbash scripts/afk/stop-marker.sh check(silent; rc=0 = stop marker present, rc=1 = clear). When rc=0, finish the in-flight subagent if one is running, then halt the loop gracefully (write anext-session-N.mdsnapshot at the partial-completion point, file followup tickets for remaining tasks, and exit Phase 3). Operator-triggered via/stop(soft) or/stop --hard(alsoTaskStops in-flight)./stop --resetclears the marker. -
Final review: one holistic pass over the full diff vs main, run as ONE
/pr-check: the cross-model automated review matrix is the gate, not a hand-dispatched reviewer set./pr-checkruns the critic panel (default: the paid codex critic, as there is no free anchor;CR_PROFILE=noneskips it) and you, the orchestrating session, adjudicate its[<slug>-N]candidates inline (agreed|disproved|conflict|unaddressed); nopr-review-toolkit:*Claude agents are dispatched. CodeRabbit is NOT part of the matrix: it is the GitHub App and reviews the PR once it exists (its threads are handled after the PR is open). Optional, opt-in Claude pass: only withCR_CLAUDE_AGENTS=1(launching shell /.env; default OFF) does/pr-checkalso dispatch the upstreampr-review-toolkit:code-reviewer. Keep that to ONE holisticcode-reviewerfor this phase; the full per-agent matrix is the same opt-in branch, never the default. Never zero automated review (2026-06-13): a docs-only diff still gets the docs-audit lane:/pr-checkapplies the docs charter inline (or via ONEcode-reviewerunderCR_CLAUDE_AGENTS=1), scoped to repo-claim accuracy (hooks/gates/flags/paths/commands vs actual code), dead links, stale file/flag/ticket refs, example correctness, internal consistency; NOT prose nitpicks (CLAUDE.md→/claude-md-audit). Review is never skipped outright. Full semantics:.claude/commands/pr-check.md(steps 2.5, 3.0, 3.5). -
Automated review gate (
/pr-checkclears the marker) — aggregate the adjudicated findings into Critical/Important/Minor counts by file;/pr-checkclears the pre-push review-owed marker only on a clean result, and without a clean rungh pr createis blocked by the PreToolUse hook. Cross-model floor behaviour (CR_REQUIRE_CROSS_MODEL,CR_FLOOR_FALLBACK=claude-only) and the park path when it is exhausted are in § Park protocol. The parallel per-agent Claude heavy automated review is NOT the default (it dispatched a Claude subagent per reviewer every round and was retired; an overnight run that followed the old text of this doc did exactly that). It exists only as theCR_CLAUDE_AGENTS=1branch of/pr-checkstep 3.5, where the applicable subset ofpr-review-toolkitagents (code-reviewer,pr-test-analyzer,comment-analyzer,silent-failure-hunter,type-design-analyzer) runs in parallel and docs-only or test-free PRs skip the no-op reviewers./code-review ultrasits ABOVE the matrix (eval — ADOPT): it is the MOST expensive lane — more than a default/pr-checkrun or the opt-in agent fan-out — so it is the top/last-resort escalation for the biggest/riskiest PRs, run AFTER/pr-checkis clean, never as a cheaper first reach. It is operator-triggered + billed; the agent cannot launch it — record anultrapass as a one-action operator step. -
Fix batch: dispatch fix subagent(s) for ALL Critical + Important findings. Run tests after each batch.
-
Automated re-review: re-run
/pr-checkon the fixed head (same matrix; the Claude agents again only underCR_CLAUDE_AGENTS=1). Loop fix → re-review until 0 Critical remain. -
PR open + push:
gh pr create --body-file <path>. Write the body to a file first to avoid shell-interpretation surprises (heredocs,$()substitutions, multi-line content with quotes). Attestation trailers go in the FIRST commit, never a reactive amend; see “Auto-mode classifier & attestation” below. Before the firstgit commitof any shippable change, after you have genuinely run tests +/pr-check, write the gate trailers directly into that commit body:Platforms tested: <linux|macos> # only when the diff touches shell/scriptsSecurity reviewed: <pr-review-toolkit|manual|claude-code-security-review> # only for non-docs codeThen the push passes the
platforms-tested/ security-reviewed gates first-try with no amend. Do NOT commit first, hit the gate, thengit commit --amendto add the trailer: the auto-mode classifier flags that reactive amend as security-gate circumvention and HARD-blocks it (uncleable, even with operator approval). -
Merge —
gh pr merge --squashonce automated review is clean. Default to a PLAIN squash merge — do NOT add--admin.--adminexists to bypass branch protection; this repo has none (the protection API returns 403 on a free private repo, andreviewDecisionis empty), so--adminbypasses nothing useful yet it trips the auto-mode classifier’s “bypassing the approval gate = destructive op outside the worktree” HARD-veto (see § Block-only criteria) — which is exactly what stalled an earlier run. Only reach for--adminif the repo actually has branch protection that blocks the plain merge, and then only withGH_ADMIN_MERGE_OK=1set in the launching shell (thescripts/handover/pr-merge.shhelper encodes this plain-first/admin-fallback logic). If the merge is blocked for any reason, load thehimmel-ops:stuck-playbookskill (§ “a PR merge was blocked”), then — rather than guessing or routing around the block — record the merge as a one-action operator step in the Phase 11 handover. Retrying a blocked merge via a different command path is flagged as evasion and hardens the block. Armed dependency chains that auto-merge on green: a self-run merge worker is denied by the classifier (“Merge Without Review”), so an armed/overnight dependency chain stops at a green, review-clean PR by default. To let a dependency chain merge unattended — PRIVATE repo only — the sanctioned path isbash "$HIMMEL_REPO/scripts/handover/merge-on-green.sh"(the anchored spelling; the PROJECT and worker-profile relativebash scripts/handover/merge-on-green.shstanding allow-rules were retired — a user-scope settings rule may still admit the relative spelling on a given station, but the project and worker-profile registries no longer pre-approve it, so a station with no user-scope rule of its own loses the relative fallback and must type the anchored or absolute spelling): it merges ONLY whenARMAUTOMERGE=1is opted in (launching shell), the PR’s own repo is private,check-ci.shreturns 0 (green + all threads resolved + no changes-requested), and the certified head SHA is still the PR head (--match-head-commit), logging every attempt (certified SHA + gate verdict). It is not a blanket--dangerously-skip-permissions(every other guard stays live). Activation is two operator steps (the agent cannot self-modify settings): add the specific standing allow-ruleBash(bash "$HIMMEL_REPO/scripts/handover/merge-on-green.sh":*)and launch/arm the dependency chain withARMAUTOMERGE=1. Public propagation stays a separate one-go operator/bridge step. -
Jira: invoke the CLI as
node scripts/jira/dist/index.js transition <KEY> "<state>"(NOT the globaljirashim; it points at an unrelated, often-brokenjira-clipackage). This runs autonomously provided the specific standing allow-ruleBash(node scripts/jira/dist/index.js:*)is present. A genericBash(node *)is NOT enough to authorize an external write (see § Auto-mode classifier). If the rule is missing, a transition is classifier-blocked; do NOT retry via a different path (evasion); record it as an operator action in Phase 11 and have the operator add the rule once (it then works for all future runs). -
Handover — write
next-session-<N+1>.md(or whichever increment) in the right dir, file followup tickets for non-blocking findings, record any classifier-blocked Jira transitions as explicit operator actions (a classifier-blocked review-thread resolve is never an operator action: it goes to the chief asBLOCKED resolve <pr> <thread ids>, which verifies it against the head and resolves it), updatestatus.md+roadmap.md, commit + push (trailers in the first commit per Phase 8). Release the step-0 queue lock (bash scripts/handover/queue-lock.sh release <handover-path> <release-token>— the token captured from the step-0 acquire output; if the transcript line was lost to an autocompact, drop the token argument and the persisted per-session copy is used instead — it saysqueue-lock: token read from <path>on stderr, and it exists only when a session scope was resolvable at acquire time (QUEUE_LOCK_SESSION_SCOPEorCLAUDE_CODE_SESSION_ID; see Phase 0); only when that is gone too doesQUEUE_LOCK_FORCE_RELEASE=1 bash scripts/handover/queue-lock.sh release <handover-path>force-release loudly and log the override — that log is the audit trail for genuine takeovers, so a routine wrap should never land in it) as part of this phase — the TTL is a safety net for a crashed session, not a substitute for releasing on a clean wrap.
Launch preamble
Any prompt that launches an overnight/armed run carries the standing
instructions below. Delivery paths: the
arm-resume.sh / schedule-resume.sh relaunch prompt names this section
explicitly; the next-session ## Overnight Mode Trigger block points here;
and manual “overnight mode” invocations reach it because executing the
pipeline requires reading this doc. This section is the SINGLE copy of the
text; the next-session templates deliberately do not inline it (the
previous 4-copy duplication silently drifted: the seeded _templates/ hop
dropped the preamble from essentially every autonomously emitted session
for ~2 months). It is model-neutral by design: overnight
impl runs on Sonnet (§ Budget), the parent session may be Fable-5 or
Opus-class, and no reliable model-identity seam exists at
launch time. Snippets are selected, not pasted, from the official
Fable-5
and
Opus-5.5
prompting guides:
When you have enough information to act, act. Do not re-derive facts already established in the conversation, re-litigate a decision the user has already made, or narrate options you will not pursue in user-facing messages. If you are weighing a choice, give a recommendation, not an exhaustive survey. This does not apply to thinking blocks.
You have ample context remaining. Do not stop, summarize, or suggest a new session on account of context limits. Continue the work.
Match the length of written documents to what the task needs: cover the substance, but do not pad with filler sections, redundant summaries, or boilerplate.
The first counters overplanning on long autonomous turns; the second
counters context-budget anxiety (Fable 5 occasionally wraps up early when
it believes context is short, and the same instruction is directly
relevant to known Opus 1M autocompact behaviour); the third counters
written-deliverable padding (Opus 5 pads documents by default, and this
harness writes next-session-N.md, status.md, roadmap.md, specs and
morning reports on every run). The guides’ grounded-progress-claims and
early-stopping snippets are NOT duplicated here: the attestation
discipline (§ Auto-mode classifier) and the block-only criteria already
cover them.
“Ample context remaining” is now checkable, not a reassurance to take on
faith. bash scripts/context-fill.sh prints this session’s
own context-window fill; --percent gives a bare integer. It exits 3 (STALE)
or 4 (UNKNOWN) with an EMPTY stdout rather than guessing, so a wrap check must
branch on the exit status. A bare [ "$(…)" -ge 65 ] turns a missing reading
into a false comparison that reads exactly like “below threshold”:
if fill=$(bash scripts/context-fill.sh --percent); then [ "$fill" -ge 65 ] && echo "wrap: ${fill}% of the context window used"else echo "fill is STALE or UNKNOWN — arm a successor on other evidence"fiA chief deciding whether to arm a successor for a worker should ask the worker
for this number instead of an estimate. It measures context-window FILL, not
the <total_tokens> spend budget, which is a different, much larger
allowance and was routinely conflated with fill before this ticket.
Safety-classifier reroute is not a failure. ~5% of Fable sessions trip a (trigger-happy) safety classifier and route to Opus with a notification (observed on Opus 4.8, community-measured, @PawelHuryn). This is a different layer from the auto-mode classifier below: nothing was blocked, and the run should simply continue. Do not halt, retry, or escalate to the operator over a reroute notice.
A zero is not evidence without a positive control. Any count feeding a ruling (an unresolved-thread sweep, a grep count, a diff stat) first proves its instrument on a known-match pattern; read a tool’s header contract before invoking it. A wrong repo slug, an MSYS-mangled path, or a bare tool run against the wrong input can all render as a clean “0” indistinguishable from a genuine pass.
A read outside the working directory can still hang an unattended worker. Auto mode does not cover this class: a Read/Grep/Glob
target outside cwd/additionalDirectories prompts interactively regardless
of mode, and nobody is there to answer it. Pre-listing the known-safe
outside paths in permissions.additionalDirectories closes the common
cases; queue-lock.sh status’s idle-warn line flags the rest (“possibly
stuck on a prompt” once a FRESH lock’s heartbeat exceeds
QUEUE_LOCK_IDLE_WARN_SECONDS, default 45m) so a stalled worker is visible
well before the hours-scale TTL/aging thresholds fire. See
environment-gotchas.md for the
operator recovery.
A top-level armed session self-exits once its successor is armed
(macOS/Linux; HIMMEL_ARMED_RELAUNCH=1 marks it). A session launched by
arm-resume.sh’s crontab or at body carries HIMMEL_ARMED_RELAUNCH=1 in
its environment. When such a session’s work is done and, if the plan calls
for one, a successor is armed, it runs kill -TERM "$CLAUDE_PID". SIGTERM
runs SessionEnd hooks, so this is the clean shutdown path; never kill -9.
This does not apply to a subagent (only the top-level session carries the
grant), and Windows sessions are not covered.
Operator-Gated Wrap
If your only remaining dependency is an operator action, wrap at once — do
not idle-wait. Append a RESUME BRIEF to this file (worktree, branch, base SHA,
dirty paths, the verbatim operator block, the ordered remaining steps), release
the queue lock, message the chief OPERATOR-GATED, wrapped, safe to close,
and end the turn. Never idle-wait for the operator — an idle worker holds the
queue lock, burns its prompt cache and emits no signal, so it is
indistinguishable from a crashed one. The chief resumes it from that brief
(bash scripts/handover/worker-resume-brief.sh <this file> regenerates the
skeleton from git) instead of waking a cold session to ask.
“End the turn” means the session actually ends — the harness exits. A
message announcing that you are closing is not closing: a worker that says
“safe to close” and stays resident holds its RAM, keeps its queue lock, and
still reads as live in ListAgents. If you cannot end your own session,
say exactly that in your final message so the chief can list you for the
operator instead of assuming you are gone.
Suite evidence — scope the run, and never wait invisibly
A worker opens its PR on the SCOPED suite covering its diff, and reports the full run afterwards as a PR comment. This is the default, not a time-pressure concession; it was re-derived per-worker-session per-shift for months before being written down here.
Why it has to be the default: scripts/ci/run-shell-tests.sh takes a
machine-wide advisory lock keyed by scan root, so exactly
one full-tree run happens at a time on the box. On the 2026-08-29 shift six
concurrent workers each needed that run; the holder was measured at 2h20m and
still going: genuinely alive, just slow, because seven sessions were
loading the machine. Six workers × a serialized 2h+ run is a wall the parallel
dispatch pattern cannot cross.
Because the lock is keyed by scan root, a scoped run takes a different lock and does not queue behind a full-tree one at all:
bash scripts/ci/run-shell-tests.sh scripts/<subtree-that-EXERCISES-your-change>Scope by what the change is exercised BY, not by where the diff lives. A
scoped run covers the suites under that scan root, which is not the same set
as “every test that touches my change”. Edit scripts/lib/foo.sh and run only
scripts/lib and you have missed every scripts/hooks suite that sources it;
the diff’s own directory is the wrong default whenever the changed code is
shared. Pick the root that contains the suites which actually exercise the
change (grep for who sources or invokes it), and when that spans several
roots, run each; several scoped runs still finish in minutes and still take
different locks.
So scoped evidence is narrower than a full run, deliberately. That is the
trade the default makes: it covers what the change is exercised by, in
minutes, instead of covering everything in hours behind a fleet-wide queue.
It is not a claim that the full run would have found nothing. That is
exactly why the full run is still reported afterwards as a PR comment rather
than skipped, and why a worker that cannot identify the suites exercising its
change should run the full tree (with SUITE_LOCK_WAIT, below) instead of
guessing at a subtree.
Run several scoped roots as separate calls, so your inbox is read between
them. A worker blocked in one long foreground command cannot read
a halt from the chief until it returns. bash scripts/handover/chief-kit/run-suite.sh
runs exactly one scan root per call and prints
CHECKPOINT <i>/<n> <root> rc=<rc> next=<root|none>; type each call as its own
Bash command and follow next=. --plan lists the roots. Do not chain the
calls with && or a loop: the call boundary is the checkpoint. The window is
still one root, so a root that is itself long (scripts/hooks) leaves a halt
unread for that run’s length; see the delivery limitation in
retask-channel.md.
When you do need the full tree, never hand-roll a retry loop. An rc=2 refusal used to send workers into their own invisible wait, and a queued worker is then indistinguishable from a parked one: both present as a worker that has stopped reporting, and the remedies are opposite (a queued run needs patience, a parked worker needs a nudge). Getting it backwards is how a dispatching parent nudges a healthy worker and creates a second-writer race on its branch; a near-miss of exactly that shape occurred on the same shift.
Use the runner’s own wait instead, so the waiting shows up in the log the dispatching parent is already polling:
SUITE_LOCK_WAIT=7200 bash scripts/ci/run-shell-tests.sh scriptsIt queues for up to that many seconds and prints a repeating line every
SUITE_LOCK_WAIT_INTERVAL seconds (default 60) naming the current holder:
WAITING: queued behind the machine lock of scan root "scripts" — holderpid=3221699 host=overlord8 held=2h14m (8040s); this run has waited 5m00s(300s) of its 7200s budget. lock: /tmp/himmel-shell-suite-scripts.lockOn acquiring it says so (ACQUIRED: … after waiting …); if the budget runs
out it emits the full REFUSED verdict and still exits 2. Default is 0
(refuse on sight, the historical behaviour), so this is opt-in per run.
For a dispatching parent: a worker reporting a WAITING: line is queued, and the
holder pid + elapsed are right there: no ps, no reading
/tmp/himmel-shell-suite-*.lock/owner by hand. A worker reporting nothing at all
is parked. That distinction is the point of the knob; a worker that hand-rolls
its own sleep loop throws it away.
A worker that ends its turn on a background job never wakes. The harness
backgrounds any Bash call that runs past ~120 s, and a worker whose turn ends
waiting on that job gets no wake-up; it sits “working” forever while the
chief reads its silence as progress. The brief therefore names the scoped
suites the worker runs (each under the 120 s budget or explicitly foreground),
and forbids “I’ll wait for the notification”: waiting is one foreground
blocking command or a foreground until … done loop, never a turn that ends
on a background task.
Base verification — arming on a fence, and syncing across workers
Chief ruling 66 (prompted by #2080 merging while its own after-report suite
was still running, and a dependent worker arming minutes later on that
unverified base): arm a worker on a fence only once every merged PR that
touched it carries its after-report SUMMARY.
scripts/chief/base-status.sh <fence-path>... lists the merged PRs on a
fence that are still PENDING (no after-report comment yet) or RED (a red
one); arm-resume.sh reads the handover’s own fence: frontmatter key and
calls it before arming, refusing (rc=21) on a non-clean fence, or when the
certification itself fails (missing script, gh query error, truncated
merged-PR list), because “cannot verify” refuses exactly like “verified
dirty”, unless --provisional-base-ok is passed, which WARNS loudly and
prints the warning text for the worker’s first message instead of refusing.
scripts/handover/merge-on-green.sh records its own local
after-report-pending marker at merge time (best-effort, mirrors the
cr-pending marker dir), and scripts/ci/run-shell-tests.sh --pr <N> (or
SUITE_REPORT_PR=N) posts the SUMMARY block as a PR comment from the RUNNER
PROCESS itself, so a closed parent session can never lose the report.
How workers sync, since there is no shared live state beyond the above:
direct messages between sessions (SendMessage/ListAgents), the
handover/ledger files each worker already writes, and PR comments themselves
(the after-report SUMMARY, MERGED/marker audit lines). Workers coordinate by
reading each other’s durable artifacts, not by a shared process. A red
after-report routes to the chief, which hands it to the current owner of
that fence as a follow-up PR + ticket, never to whichever worker happens to
notice first. A dependent worker holds its fence conclusions (does not
build further on that fence, does not report it clean) until the fix lands
and a fresh after-report replaces the red one.
Artifacts to keep in sync
The pipeline ships code, but some repo artifacts mirror that code and must be
updated in the same PR or they silently rot. Owner: the executing agent,
as the first action of Phase 8 (before gh pr create): diff the change set
against this list and commit any sync edit onto the branch. The Phase-4/5 /pr-check
automated review (its adjudicated panel findings; comment-analyzer under CR_CLAUDE_AGENTS=1) is the backstop, not the primary
catch.
docs/commands-catalog.md: if the diff adds, renames, or removes a command under.claude/commands/, update the matching row (verbatim from the command’sdescription:frontmatter; the doc carries a regen snippet).CLAUDE.mdquality: if the diff changes anyCLAUDE.md(root or subtree), run/claude-md-auditbeforegh pr create. It runs theclaude-md-improverrubric audit against the changed file(s) in the worktree and surfaces findings to fix in-branch; lean-invoke, not a hook (an LLM audit per commit is too heavy), and audit-only (no auto-edits).
This list grows as new mirrored artifacts appear; add a row when you create one.
Morning review — one report, then drill into PRs
A /afk-shift run ends by writing a consolidated morning report:
one artifact, overnight-report-YYYY-MM-DD.md, generated by
scripts/afk/morning-report.sh and placed under the handover root
(resolved via scripts/lib/handover-path.sh / handover_root_ensure,
which creates the Mode A inline dir on demand; a broken HANDOVER_DIR
fails closed with exit 2 instead of falling back to a hardcoded
./handovers/). One row per dispatched ticket (ticket, branch,
PR link, status (done / blocked / partial), one-line outcome), with
anything needing a human decision grouped in a “Decisions needed” block at
the top and those tickets sorted first in the table.
The morning-review flow is therefore read one report, then drill into
PRs, not per-ticket discovery across N branches/PRs/reports. Rationale
(Amdahl’s Law applied to multi-agent work): human review is the serial
fraction that caps fanout speedup, so the mandatory human checkpoint is
batched into a single entry point. Work the decisions block first, then
review PRs in report order (/pr-check). Merging stays human; only the
review surface is batched.
Standing operator actions. The report is regenerated every
run, so a one-off note elsewhere won’t resurface. Durable manual actions,
especially single-session-only ones (e.g. a history rewrite), go in
<handover-root>/operator-actions.md; morning-report.sh appends them verbatim
as a ## Standing operator actions section to every report until you delete
them. Use plain bullets (not headings, which would compete with the report’s H2s);
absent or blank ⇒ no section. The run-end command relies on the default path
(<dirname report>/operator-actions.md), so no --actions flag is needed. With no
overnight run, just read the file directly; it’s plain markdown.
Budget (informational)
- Subagent dispatches: ~50-60 per overnight run. Per phase 3, impl is ~3 subagents per task (implementer + 2 reviewers); a 16-task plan = ~48 just for impl. The default
/pr-checkautomated review matrix adds ~0 Claude subagent dispatches (cross-model critic calls + inline adjudication); only the opt-inCR_CLAUDE_AGENTS=1fan-out adds ~6-12 over 1-2 rounds. Plus fix-batch dispatches (5-10). - Wall time: ~3-4 hours.
- Inference cost: ~$10-20 on Sonnet. Bump to Opus for the planning / final-review phases if extra rigor is wanted; Sonnet is enough for impl + per-task reviews.
- Effort tiering (Fable-5): on Fable 5,
loweffort ≈xhighon prior models, which strengthens the raise-effort-before-tier rule of the Subagent delegation & escalation policy (rootCLAUDE.md). For mechanical subagent work, raise effort before raising the model tier; reach for Opus-class only with a concrete reason. Default efforthigh;maxis a re-verification tax (2-3× time for the same answer, community-measured); reserve it for capability-critical single calls, not pipeline phases.
These are estimates from the reference run. The operator decides when to stop; there are no auto-pause guardrails.
Block-only criteria
Block for human input ONLY when:
- An ambiguous design decision is not covered by the spec.
- Ticket creation needs a human judgment call on Jira project or priority.
- A destructive op outside the worktree is required (e.g. force-push to main, drop a remote branch the operator owns).
- A plain Phase-9
--squashmerge is blocked by real branch protection (not the no-op case that was removed) and--adminwould be needed but is not authorized (GH_ADMIN_MERGE_OKunset). Bypassing a genuine approval gate is exactly the kind of “destructive op outside the worktree” AFK (unattended) mode should NOT do unattended. Defer the merge to the operator.
Everything else proceeds without confirmation.
Park protocol
Every controllable hang class in an armed/overnight run resolves to one of two policies: fallback (an opt-in knob lets the gate accept a weaker-but- verified floor and keep going) or park (the gate stays closed and the item is set aside). Park is never a retry-loop: a worker that hits a park-policy gate does this, and only this:
- Write the pending item into the handover’s Pending block: the PR#, the
head SHA, which gate blocked it and why (its refusal reason), and the exact
command to resume once the blocking condition clears. For a cross-model
floor exhaustion, that is
/pr-checkon the branch, not a bareclear-cr-marker.shrecheck. The exhausted critic’savailrow is already on the ledger, so re-running onlyclear-cr-marker.shreads that same stale row and refuses again;/pr-checkre-invokes the panel and records a FRESH row (okonce the subscription usage budget has actually reset, or a newunavailableif it has not) before the marker is re-checked. EnablingCR_FLOOR_FALLBACK=claude-onlyis the other resume path; it ships on the Claude-only floor without waiting for the reset at all. For other park classes, the resume command is the gate itself, e.g. re-runningcheck-ci.sh <pr>once a stale review refreshes. - Continue with other queue items. A park on one branch never idles the rest of the queue while independent work is ready to ship.
- Never re-run the same blocked gate in a loop hoping it resolves on its own. Park records the item once and moves on; it does not poll.
- At wrap, the worker pends for cold restart: a fresh arm/handover once the blocking condition is confirmed cleared, not an in-worker retry.
The five hang classes
| # | Hang class | Disposition |
|---|---|---|
| 1 | Cross-model floor exhaustion: CR_REQUIRE_CROSS_MODEL set, every non-Claude critic down | CR_FLOOR_FALLBACK=claude-only (opt-in) when the exhaustion is VERIFIED quota/rate-limit; otherwise park the branch and continue |
| 2 | Plugin-tool permission wall | Operator allow-rules close it; park the blocked item if hit |
| 3 | CodeRabbit rate-limit vs. stale review | Neither blocks: CodeRabbit is best effort, so a rate-limited, absent or stale review is a NOTE from check-ci.sh, never a park. The gate that remains is CI green + zero unresolved threads + the panel; an unresolved CodeRabbit thread still blocks like any other reviewer’s |
| 4 | Live-worker rc=10 at arm | Wrap discipline (finish/reconcile the worker before arming) closes it in-worker; park if it cannot be resolved before the worker ends |
| 5 | ARMAUTOMERGE unset | Pre-arm WARN only (arm-resume.sh); the run still deliberately stops at every green PR instead of merging; not itself a park |
Telegram relaunch — always PLAIN, never --channels
When an overnight/armed session is scheduled to relaunch (via the
handover-arm-resume skill / scripts/handover/arm-resume.sh), the relaunch
is PLAIN, with no --channels. The always-on bun Telegram bridge
owns the single getUpdates slot and reaches Telegram
independent of any session, so a --channels relaunch is actively harmful: it
becomes a 2nd getUpdates consumer (409 Conflict) and its
--dangerously-load-development-channels prompt hangs the unattended
launch. arm-resume.sh enforces this: it refuses --channels (rc=5) while
the bridge is live. Keep the bun bridge up; relaunch plain. Full rationale:
docs/internals/telegram-bridge.md.
Auto-mode classifier & attestation
Claude Code runs a model-based auto-mode classifier on every tool call in autonomous/auto-accept sessions. It is a SEPARATE layer from the .claude/settings.json permission allow-list. Crucially, a generic catch-all allow-rule does NOT authorize a semantically-risky action; only a SPECIFIC, tool-named rule does. Observed: a Jira write invoked as node scripts/jira/dist/index.js … matched only the generic Bash(node *) and was blocked, whereas the same write invoked as jira … matched the specific Bash(jira *) and ran. The classifier sorts actions into two tiers:
- HARD vetoes — uncleable by any setting OR by operator approval (this is the prompt-injection defense that makes unattended overnight runs safe):
- Security-gate circumvention: e.g.
git commit --amendto add aSecurity reviewed:/Platforms tested:trailer after a push gate already failed. The reactive amend reads as “fabricate an attestation to bypass the gate.” - Self-modification: editing
.claude/settings.json(or via the update-config skill) to widen your own allow-rules. An agent cannot escalate its own permissions.
- Security-gate circumvention: e.g.
- Authorization-gated, cleared by a SPECIFIC, tool-named standing allow-rule (operator-authored once):
- External-system writes: Jira transitions, posting comments, etc. A broad
Bash(node *)does NOT authorize these; a specific rule naming the exact CLI does. For himmel Jira, the rule isBash(node scripts/jira/dist/index.js:*); with it present, transitions run autonomously (no defer). Retrying a blocked write via a different command path is flagged as evasion and hardens the block, so the fix is the standing rule, not retrying.
- External-system writes: Jira transitions, posting comments, etc. A broad
What this means for an autonomous run, and how to stay unblocked:
- Attestation: write trailers into the FIRST commit (Phase 8), never a reactive amend. This avoids the gate-circumvention veto entirely: the push passes the deterministic git hook first-try and there is no amend for the classifier to flag. This is the single most important rule for shipping autonomously.
- Never
git commit --amendto satisfy a just-failed push gate. If you somehow committed without a required trailer, the recovery is to author a clean replacement commit message before the first push of that branch, not an amend after a failed push. If the branch is already pushed, recut it: a fresh branch from main with ONE new commit carrying the trailers, a new PR, and the old one closed (a PR-body trailer clears the pre-push gate but fails the chief’s ready-check, which reads only the first commit; seestuck-playbook.md§ missing attestation trailer). Do not amend. - Self-modification is operator-only by design. The agent cannot add allow-rules to its own settings, nor edit
CLAUDE.md/.claude/settings.json, even in a worktree PR branch: the commit of such a change is classified as self-modification and HARD-blocked when the change is agent-inferred. The unblock is explicit user specification: when the operator states the exact allow-rule string (or exact rule text) verbatim, the intent bar is met and the agent may commit it. Pure operational docs (this file) are NOT governing config and commit freely. - External writes (Jira): ensure the specific standing allow-rule exists, then run them. With
Bash(node scripts/jira/dist/index.js:*)in settings, transitions run autonomously — 100%, no defer. If the rule is absent (and you cannot add it — that’s operator self-mod), do NOT probe alternative invocations (flagged as evasion); record the transition as an operator action in the handover and ask the operator to add the rule once. A review-thread resolve is the exception: it goes to the chief, never the operator (seestuck-playbook.md).
These are environment behaviors, not himmel policy, documented here so every operator’s overnight run understands why an agent stalls and what the unblocked path is.
Failure modes to watch (operator layer)
AFK (unattended) mode fans out N parallel sub-agents (Phase 3) and the operator becomes their coordinator. @nyk_builderz named four operator-layer failure modes that nobody screenshots: “ignore the operator layer and your team collapses into a single blurry agent within a month.” How each shows up here, what mitigates it today, and what the operator should check after a shift:
-
Voice blur: sub-agents converge on the same style after enough shared context; the N PRs read cookie-cutter and reviewers stop noticing.
- Mitigation: NONE. Candidate: seed each ticket’s implementer with a per-ticket persona (different altitude/emphasis) so approaches diverge.
- Post-shift check: skim the N PRs side by side. Do they vary in approach and commit cadence, or is every one structurally identical? Identical = blur.
-
Memory pollution: personal/unrelated context bleeds into a shared layer.
- Mitigation: PARTIAL. Per-branch worktree isolation + per-ticket
handover/<TICKET>branches keep one ticket’s state out of another’s; the nested subdirCLAUDE.mdtree scopes context to the subtree. The shared rootCLAUDE.mdis the one shared layer to guard. - Post-shift check: did any PR commit unrelated-ticket context, scratch notes, or personal state into a tracked file?
- Mitigation: PARTIAL. Per-branch worktree isolation + per-ticket
-
Handoff drift: context lost between agent A and agent B.
- Mitigation: ADDRESSED.
next-session-N.mdsnapshots carry decisions forward;/handover-flushconsolidates acrosshandover/*branches; the Phase-3 implementer → spec-compliance reviewer → code-quality reviewer → fix loop hands forward a reviewed diff, not raw intent. - Post-shift check: read the resume snapshot cold. Can you reconstruct why each decision was made, or only what shipped? Only-what = drift.
- Mitigation: ADDRESSED.
-
Coordination overhead: the person coordinating (here, the operator reviewing N PRs) becomes the bottleneck past some size.
- Mitigation: PARTIAL: batch the entry point + enforce a ceiling. The consolidated morning report (§ Morning review) collapses N-PR discovery into one decisions-first artifact, but review depth is still serial: keep an overnight fan-out to ≈6–8 tickets. Beyond that, manual N-PR review degrades to rubber-stamping (defeating the automated review gate); split into multiple shifts instead.
- Post-shift check: if a shift dispatched >8 tickets, assume review fatigue and spot-check rather than pretend-to-deep-review all of them.
Lessons learned
Non-obvious gotchas that surfaced during the proof-of-concept run; carry forward to future overnights:
- Lock target must be a FILE not a directory. Pass a real file path to
lockfile.lock(), e.g.<tDir>/.lock, touched into existence first (see the// B2:comment block inscripts/himmel-run/src/run.ts).proper-lockfilev4 then creates the lock artifact at<target>.lock(so<tDir>/.lock.lockhere) as a sibling directory, not a file, as verified by themkdirSync(lockPath)line in the'recovers from stale lockfile'test atscripts/himmel-run/tests/lock.test.ts. To clean a stale lock:rm -rf <target>.lock(orrmdir <target>.locksince the dir is empty in v4). POSIXrmwithout-rrefuses directories; that is the failure mode if you treat the artifact as a file. lastIndexOf('--')notindexOf('--')in arg parsers: the user can write--inside earlier flag values.- Init flow must collect ALL required env vars upfront. The reference run missed
JIRA_BASE_URLand had to re-init. Enumerate from the runner config. - Skill markdown body should be a single command line. ALL retry/fallback/recovery logic lives in the runner; skills cannot enforce branching deterministically.
gh api graphqluses-F query=@file.gql(capital F), verified during the reference run’s automated review.- Heavy automated review catches 30+ Critical/Important issues on a fresh runner. Budget ~2 rounds of fix-then-review minimum.
Reference run
- Epic: jira-plugin-wrap
- Task: runner + plugin
- PR: #97 (squash
8c482b5), merged 2026-05-23 - 36 commits on feat branch, 113 tests at merge, 2 review rounds, ~30 distinct Critical/Important fixes
- HIMTEST Jira project created mid-flow as a permanent sandbox; do not delete it;
scripts/himmel-run/tests/integration.test.tshard-codes--project HIMTESTand asserts issue keys match/^HIMTEST-\d+$/ - Followups were filed as separate tickets