# AFK (unattended) mode — Autonomous Pipeline

> **Trigger phrase:** when a user prompt includes the phrase **"overnight mode"** alongside a `next-session-*.md` path (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`](../glossary.md)); it dispatches each ticket as its own session and hands itself over: [`running-a-chief.md`](running-a-chief.md).

## The 11 phases (+ step 0: queue lock)

0. **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 the `release-token: <token>` line the acquire
   prints and keep the token for Phase 11**: `release` (and `heartbeat`)
   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 is `QUEUE_LOCK_SESSION_SCOPE` if you
   set it, else `CLAUDE_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, set
   `QUEUE_LOCK_SESSION_SCOPE` or 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 set
   `QUEUE_LOCK_TAKEOVER=1` to 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_SECONDS` tunes 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 by
   `QUEUE_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.mjs` rewrites
   `settings.json`; `--check` reports whether it is wired, `--off` removes
   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: `status`
   reports FRESH and `acquire` refuses (`QUEUE_LOCK_TAKEOVER=1` still
   forces). `heartbeat` and `release` now emit the same `AGING` warning
   `status` always 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 manual `queue-lock.sh heartbeat <handover-path>
   <token>` on long sessions is still the lever.
1. **Plan:** `superpowers:writing-plans` on the active brief; commit to `<plans-root>/YYYY-MM-DD-<slug>.md` where `<plans-root>` resolves as:
   - `$HANDOVER_DIR/plans/` when `HANDOVER_DIR` is 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.
2. **Worktree:** `/clean_garden feat/himmel-<N>-<slug>`.
3. **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, run `bash 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 a `next-session-N.md` snapshot at the partial-completion point, file followup tickets for remaining tasks, and exit Phase 3). Operator-triggered via `/stop` (soft) or `/stop --hard` (also `TaskStop`s in-flight). `/stop --reset` clears the marker.
4. **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-check` runs the critic panel (default: the paid codex critic, as there is no free anchor; `CR_PROFILE=none` skips it) and **you, the orchestrating session, adjudicate its `[<slug>-N]` candidates inline** (`agreed|disproved|conflict|unaddressed`); no `pr-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 with `CR_CLAUDE_AGENTS=1` (launching shell / `.env`; default OFF) does `/pr-check` also dispatch the upstream `pr-review-toolkit:code-reviewer`. Keep that to ONE holistic `code-reviewer` for 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-check` applies the docs charter inline (or via ONE `code-reviewer` under `CR_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`](../../.claude/commands/pr-check.md) (steps 2.5, 3.0, 3.5).
5. **Automated review gate (`/pr-check` clears the marker)** — aggregate the adjudicated findings into Critical/Important/Minor counts by file; `/pr-check` clears the pre-push review-owed marker only on a clean result, and without a clean run `gh pr create` is 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 the `CR_CLAUDE_AGENTS=1` branch of `/pr-check` step 3.5, where the applicable subset of `pr-review-toolkit` agents (`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 ultra` sits ABOVE the matrix (eval — ADOPT):** it is the MOST expensive lane — more than a default `/pr-check` run or the opt-in agent fan-out — so it is the top/last-resort escalation for the biggest/riskiest PRs, run AFTER `/pr-check` is clean, never as a cheaper first reach. It is operator-triggered + billed; the agent cannot launch it — record an `ultra` pass as a one-action operator step.
6. **Fix batch:** dispatch fix subagent(s) for ALL Critical + Important findings. Run tests after each batch.
7. **Automated re-review:** re-run `/pr-check` on the fixed head (same matrix; the Claude agents again only under `CR_CLAUDE_AGENTS=1`). Loop fix → re-review until 0 Critical remain.
8. **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 first `git commit` of any shippable change, after you have genuinely run tests + `/pr-check`, write the gate trailers directly into that commit body:
   ```text
   Platforms tested: <linux|macos>   # only when the diff touches shell/scripts
   Security reviewed: <pr-review-toolkit|manual|claude-code-security-review>   # only for non-docs code
   ```
   Then the push passes the `platforms-tested` / security-reviewed gates first-try with no amend. **Do NOT commit first, hit the gate, then `git commit --amend` to add the trailer**: the auto-mode classifier flags that reactive amend as security-gate circumvention and HARD-blocks it (uncleable, even with operator approval).
9. **Merge** — `gh pr merge --squash` once automated review is clean. **Default to a PLAIN squash merge — do NOT add `--admin`**. `--admin` exists to bypass *branch protection*; this repo has none (the protection API returns 403 on a free private repo, and `reviewDecision` is empty), so `--admin` bypasses 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 `--admin` if the repo *actually* has branch protection that blocks the plain merge, and then only with `GH_ADMIN_MERGE_OK=1` set in the launching shell (the `scripts/handover/pr-merge.sh` helper encodes this plain-first/admin-fallback logic). **If the merge is blocked for any reason, load the `himmel-ops:stuck-playbook` skill** (§ "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 is `bash "$HIMMEL_REPO/scripts/handover/merge-on-green.sh"` (the anchored spelling; the PROJECT and worker-profile relative `bash scripts/handover/merge-on-green.sh` standing 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 when `ARMAUTOMERGE=1` is opted in (launching shell), the PR's own repo is private, `check-ci.sh` returns 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-rule `Bash(bash "$HIMMEL_REPO/scripts/handover/merge-on-green.sh":*)` and launch/arm the dependency chain with `ARMAUTOMERGE=1`. Public propagation stays a separate one-go operator/bridge step.
10. **Jira:** invoke the CLI as `node scripts/jira/dist/index.js transition <KEY> "<state>"` (NOT the global `jira` shim; it points at an unrelated, often-broken `jira-cli` package). This runs autonomously **provided the specific standing allow-rule `Bash(node scripts/jira/dist/index.js:*)` is present**. A generic `Bash(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).
11. **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 as `BLOCKED resolve <pr> <thread ids>`, which verifies it against the head and resolves it), update `status.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 says `queue-lock: token read from <path>` on stderr, and it exists only when a session scope was resolvable at acquire time (`QUEUE_LOCK_SESSION_SCOPE` or `CLAUDE_CODE_SESSION_ID`; see Phase 0); only when that is gone too does `QUEUE_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](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-fable-5)
and
[Opus-5.5](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-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":

```bash
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"
fi
```

A 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`](../internals/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
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`](../internals/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:

```bash
SUITE_LOCK_WAIT=7200 bash scripts/ci/run-shell-tests.sh scripts
```

It queues for up to that many seconds and prints a repeating line every
`SUITE_LOCK_WAIT_INTERVAL` seconds (default 60) naming the **current** holder:

```text
WAITING: queued behind the machine lock of scan root "scripts" — holder
pid=3221699 host=overlord8 held=2h14m (8040s); this run has waited 5m00s
(300s) of its 7200s budget. lock: /tmp/himmel-shell-suite-scripts.lock
```

On 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's `description:` frontmatter; the doc carries a regen snippet).
- **`CLAUDE.md` quality:** if the diff changes any `CLAUDE.md` (root or
  subtree), run `/claude-md-audit` before `gh pr create`. It runs
  the `claude-md-improver` rubric 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-check` automated review matrix adds ~0 Claude subagent dispatches (cross-model critic calls + inline adjudication); only the opt-in `CR_CLAUDE_AGENTS=1` fan-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, `low` effort ≈ `xhigh` on prior models, which strengthens the raise-effort-before-tier rule of the Subagent delegation & escalation policy (root `CLAUDE.md`). For mechanical subagent work, raise effort before raising the model tier; reach for Opus-class only with a concrete reason. Default effort `high`; `max` is 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 `--squash` merge is blocked by *real* branch protection (not the no-op case that was removed) and `--admin` would be needed but is not authorized (`GH_ADMIN_MERGE_OK` unset). 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:

1. **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-check` on the branch, not a bare
   `clear-cr-marker.sh` recheck**. The exhausted critic's `avail` row is
   already on the ledger, so re-running only `clear-cr-marker.sh` reads that
   same stale row and refuses again; `/pr-check` re-invokes the panel and
   records a FRESH row (`ok` once the subscription usage budget has actually reset, or a new
   `unavailable` if it has not) before the marker is re-checked. Enabling
   `CR_FLOOR_FALLBACK=claude-only` is 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-running
   `check-ci.sh <pr>` once a stale review refreshes.
2. **Continue with other queue items.** A park on one branch never idles the
   rest of the queue while independent work is ready to ship.
3. **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.
4. **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`](../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 --amend` to add a `Security 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.
- **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 is `Bash(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.

What this means for an autonomous run, and how to stay unblocked:

1. **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.
2. **Never `git commit --amend` to 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; see [`stuck-playbook.md`](../internals/stuck-playbook.md) § missing attestation trailer). Do not amend.
3. **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.
4. **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 (see `stuck-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](https://x.com/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:

1. **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.

2. **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 subdir `CLAUDE.md` tree scopes context to the subtree.
     The shared root `CLAUDE.md` is 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?

3. **Handoff drift:** context lost between agent A and agent B.
   - *Mitigation:* **ADDRESSED.** `next-session-N.md` snapshots carry decisions
     forward; `/handover-flush` consolidates across `handover/*`
     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.

4. **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 in `scripts/himmel-run/src/run.ts`). `proper-lockfile` v4 then creates the lock artifact at `<target>.lock` (so `<tDir>/.lock.lock` here) as a sibling **directory**, not a file, as verified by the `mkdirSync(lockPath)` line in the `'recovers from stale lockfile'` test at `scripts/himmel-run/tests/lock.test.ts`. To clean a stale lock: `rm -rf <target>.lock` (or `rmdir <target>.lock` since the dir is empty in v4). POSIX `rm` without `-r` refuses directories; that is the failure mode if you treat the artifact as a file.
- **`lastIndexOf('--')` not `indexOf('--')`** 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_URL` and 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 graphql` uses `-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.ts` hard-codes `--project HIMTEST` and asserts issue keys match `/^HIMTEST-\d+$/`
- Followups were filed as separate tickets
