0018 — The Astra lane is queue-driven, and its launcher is a rung

The failure

An Astra job existed only because some session hand-typed:

nohup codex exec -m gpt-6-astra --approve-for-me -C <dir> "$(cat brief)" \
    </dev/null > /tmp/astra/<name>.log 2>&1 &

There was no launchd job, no cron, no loop. So the moment a one-shot run exited, the lane sat at zero until a human or a session happened to remember.

On 2026-09-10 it sat at zero for 11.5 hours. The 15:00Z run had finished normally — clean Stop hooks, artifact delivered, 215k tokens — and simply nothing launched the next brief. Nobody was at fault and nothing was broken; that is the point.

If a fact has to be remembered to stay true, it will be false. A lane whose supervisor is a Claude session's memory is not supervised. This is the same lesson as ADR-0002 (a decision that lived only in a conversation), ADR-0004 (a decision that lived only in memory) and the catalog's own founding story (four detectors nearly retired because nothing on disk said they existed). This is that lesson applied to running work rather than to written knowledge.

The decision, in three parts

1. The Astra lane has a queue, and it is on disk. ~/.claude/jobs/astra/ with queue/ running/ done/ failed/ receipts/. Briefs are launched in filename order, so authors sequence them (01-…, 02-…) and fixing a typo does not send a brief to the back.

2. Sessions ENQUEUE. Sessions do not LAUNCH.

astra-relaunch --enqueue /tmp/astra/brief-deck.md --workdir ~/wt-design --name 01-deck.md

This is the part that changes how systems relate, and therefore the part that required this ADR. Before it, "the queue" was a session's intention, held in a context window that ends. After it, the queue is a directory that outlives every session that writes to it.

3. The launcher is a rung, in puller's shape. astra-relaunch, a launchd StartInterval job (300s): read disk, decide, launch at most one run within a hard cap, write one line, exit. It arms no loop, holds no GATE, never types into a pane, never messages a session, and must never appear in the nudge roster — operator/SKILL.md §0b orders a second looper killed on sight and whoever found one here would be right. Per ADR-0003 it reaches nothing sideways or down: it consumes files and emits a log line, and the Systems tab consumes that.

Why a rung and not an agent

A live session watching for "the lane went to zero" would burn a full-price context to produce "still running" — which is the answer for roughly two hours out of every two hours and five minutes. It would also be a second looper, and it would itself be a thing that has to stay alive to stay true, which is the failure one level up. The tick calls no model, reads one ps, one directory and one ledger, and is done in well under a second.

What this rung may NOT do

Liveness is derived from the process table, never from what the job wrote

The observable is ps -axo pid,lstart,command — the kernel's answer about the present. run.json supplies a pid and a start time and is never believed on its own: the lane counts as occupied only when the process table agrees the pid exists, its argv carries that run's --wrap <runid> sentinel, and its start time matches the record, so a recycled pid cannot inherit a dead claim.

This cannot go stale because it is not remembered. A snapshot the kernel produces on demand describes the present by construction; the failure mode "a job believes the state file it wrote last tick" has no purchase, because the state file is only ever used to look something up in that snapshot.

A second check holds the lane for any foreign -m gpt-6-astra process — a session still hand-typing, or fable-decide's rung-2 Astra call. Deliberately over-cautious: holding costs one 300s tick; double-launching costs an account.

A ps that fails is UNREAD (exit 4), never "the lane is empty." For the same reason, "there is no brief queued" (exit 0, IDLE) and "I could not read the queue" (exit 4, UNREAD) have different exit codes and different log words. A broken read rendering as nothing-to-do is the most repeated entry in this fleet's memory.

Inventory of the superseded surface (ADR-0004 §2)

Surface Disposition
Hand-typed nohup codex exec -m gpt-6-astra … & in sessions Superseded. Replaced by astra-relaunch --enqueue. Still possible — nothing forbids a shell — and safe: a hand-launched run is detected as a foreign run and holds the lane, so it cannot be double-fired against.
/tmp/astra/brief-*.{txt,md} as an informal brief store Retained as the run-log location (/tmp/astra/<name>.log), so lane-status's existing Astra view stays true. Briefs themselves now live in ~/.claude/jobs/astra/, which macOS does not reap.
lane-status's pgrep -f "codex exec" liveness check Not fixed here, and not copied. It matches any command line containing those words, including a Bash tool call that mentions them. astra-relaunch matches the model string instead. Flagged for a follow-up to lane-status; it is a display, not a control.
A session's memory as the lane's supervisor Removed. That is the whole ADR.

Fencing (ADR-0004 §3)

Enforcement candidate, not yet implemented: a guard-bash.sh rule in the style of rule 13 flagging codex exec -m gpt-6-astra outside astra-relaunch and fable-decide, pointing at --enqueue. Deliberately not shipped in this effort: the hook runs on every Bash call in every session on the machine, and a new rule there has a blast radius this change does not need. Until it exists, this ADR is the authority — exactly as ADR-0002's last row records for send-keys.

Liveness proof (ADR-0004, the inert-mechanism clause)

SYSTEMS.md → astra-relaunch, two signals: the launchd job is loaded, and ~/.claude/logs/astra-relaunch.log has matched the literal astra-relaunch OK within 1200s (four ticks). UNREAD and BREAKER lines are written but do not match, so the row goes stale exactly when the rung stops being able to decide.

The signal keys on the launcher deciding, not on a run being live: an empty queue is healthy, and a signal that went red on a quiet lane would page for the one thing nobody should be paged about.

Consequences