Operator

September 5, 2026 · View on GitHub

Domain: runtime

Overview

The operator is a standalone, long-lived coordination skill (/fab-operator) — NOT a pipeline stage. It runs in a dedicated tmux pane, coordinates agents across multiple repos and multiple tmux sessions on a single tmux server, observes them via fab operator tick-start --diff --quiet per tick (and fab pane map --all-sessions on demand), detects pending questions via the fab pane questions sweep, routes commands and prompt answers via rk mux send when rk is installed (command -v rk-gated: plain for command routing, --answer for answering a waiting agent, --key for key-name input), degrading to raw tmux send-keys behind its own §3 state gate when rk is absent, monitors progress via /loop, and auto-answers agent prompts (escalating strategic ones to the user). Every agent is addressed by the (session, repo, pane) tuple: the pane ID is the primary key (server-global, stable), with repo (the agent's absolute main-worktree root) and session (its tmux session name) as added dimensions. session is a display/context dimension, never a join key — it is reassigned by swap-window/move-window/rename and a monitored agent's session can change mid-lifetime (run-kit's operator-relocation direction moves the window at enrollment), so all correlation rides the pane ID (the identity-key contract in pane-commands.md § fab pane map). There is one operator per tmux server, owning one server-keyed state file that spans every repo it coordinates. This file documents the operator's behavior, its tick lifecycle and state model, and the operator design decisions. For the @rk_pane_agent_state agent-state convention and fab pane primitives the operator builds on, see runtime-agents.md and pane-commands.md. For the Go-side mechanism (server-keyed state path derivation, the fab operator mutation-verb family, the repo JSON field, fab agent --print --repo), see kit-architecture.md → "Operator State File" and pane-commands.md.

Requirements

Coordination Principles & Safety Model

/fab-operator is a standalone, self-contained coordination skill — NOT a pipeline stage. It runs as a long-lived Claude session in a dedicated tmux pane, observing agents via fab pane map, routing commands and prompt answers via rk mux send (plain / --answer, command -v rk-gated; raw tmux send-keys when rk is absent), monitoring progress via /loop, and auto-answering agent prompts.

Principles

Coordinate, don't execute. The operator routes user instructions to the right agent — it never implements work directly. If the target is ambiguous, ask.

Not a lifecycle enforcer. Individual agents self-govern via their own pipeline skills. The operator does not validate stage transitions or enforce pipeline rules.

Context discipline. The operator never reads change artifacts (intakes, specs, tasks). Its context window is reserved for coordination state — pane maps, stage snapshots, monitoring state.

State re-derivation. Before every action, re-query live state via fab pane map (or wt list + fab change list outside tmux). Panes die, stages advance, agents finish — stale state leads to wrong actions.

Context Loading

As of 260611-zc9m the operator loads only three project files: fab/project/config.yaml, fab/project/constitution.md, and fab/project/context.md (optional — skip gracefully if missing). It is a listed exception to the _preamble.md §1 always-load layer — code-quality.md, code-review.md, and both doc indexes serve artifact generation and review, which the operator never does (per its own §1 Context discipline principle), and a long-lived session re-pays every loaded file after each reload (compaction, /clear, or restart). This was a deliberate, verifier-endorsed behavior change (loading-only — no principle, safety-model, or spawn-procedure text changed) (zc9m). Helpers declared in frontmatter: _cli-agents (the generic agent-CLI interaction primitives — spawn composition, pre-send validation, delivery probe, peek, await — plus the per-provider grammar/discovery dictionary; see agent-primitives.md), _cli-fab (fab command reference), and _cli-external (external tool reference for wt, idea, hop, rk (run-kit — including the fab-owned operator escalation send and the pointer to the startup role self-mark), tmux, and /loop — loaded only by operator, not by pipeline skills). The _cli-agents split is agent primitives vs. operator orchestration: the helper owns how to talk to an agent CLI, this skill owns when and whether to (confirmation tiers, retry budgets, repo targeting, enrollment, dependency resolution, autopilot). _cli-external carries only fab-owned content: what each tool is in one line plus the fab-specific integration choreography no tool's own documentation carries (the operator spawning sequence, the escalation rk notify usage and the role self-mark pointer, the fab pane/new-window and /loop notes). Each owned tool's usage knowledge is delegated at use-time to <tool> skill (the toolkit's per-binary usage briefing, shll standards skill) — command -v-gated fail-silent for all four owned binaries (wt, idea, rk, hop), which are separate sibling formulas that may all legitimately be absent (nnda) — and its exhaustive command tree to <tool> help-dump; both are version-locked by construction (embedded in the same binary as the flags they describe). The delegations fail silently on absence, but the worktree entry points that functionally require wt (the spawn sequence below, fab batch new/switch) stop with an install hint instead — see the ### wt Gate preflight probe under Startup. A required version-skew fallback covers an installed binary predating its skill subcommand: the invocation capability-probes <tool> skill (non-zero exit or empty output = the probe) and falls back silently to the shll.ai bundle-page pointer https://shll.ai/<tool>/skill, so operator context loading never breaks on an older binary. It does NOT run fab preflight (change resolution — distinct from the startup wt availability probe under § Orientation). It does NOT load change-specific artifacts.

Orientation

On invocation, runs fab pane map and displays the output, then signals readiness. Outside tmux ($TMUX unset), falls back to wt list + fab change list for status queries only — monitoring is disabled.

Startup role self-mark (swun): after the tmux gate, the operator marks its own tmux window with run-kit's @rk_win_role window option — command -v rk >/dev/null 2>&1 && rk role operator >/dev/null 2>&1 || true — so run-kit's dashboard pins the operator row directly under its sidebar SESSIONS header. rk owns the option contract, the pinned rendering, and the one-operator-per-server radio semantics; fab is only the producer. The step is fail-silent beyond the command -v gate (an installed rk predating the role subcommand degrades to a silent no-op), idempotent (a restarted operator re-marks the same window harmlessly), and has no unmark counterpart — staleness and radio conflicts are rk's to resolve. Owned by fab-operator.md §2 Startup § Role Mark; _cli-external.md § rk carries the pointer.

Startup wt gate (single preflight probe) (nnda): wt create is the operator's first action for any new request (§ Spawning below), and wt ships as a standalone sibling formula that may legitimately be absent — so startup runs one preflight probe (command -v wt >/dev/null 2>&1) with the session-setup steps, not per-call-site gating (the ~3 individual wt call sites stay unmodified). When wt is absent the operator STOPs with the actionable install hint wt is required for operator spawning — install it via: brew install sahil87/tap/wt rather than failing mid-spawn-sequence. This is the "stop with hint" side of the graceful-degradation split — the operator's tool-usage delegations still fail silently on absence (§ Context Loading). See distribution.md § Homebrew Formula.

Safety Model

TierExamplesBehavior
Read-onlyStatus check, pane mapNo confirmation
RecoverableSend /fab-continue, rebaseAnnounce before sending
DestructiveMerge PR, archive, delete worktreeConfirm before executing

An autopilot queue is confirmed separately, at queue start, with text reflecting the active mode ("Confirm upfront (creates PRs — merge after review)." in cherry-pick-ladder; "Confirm upfront (merges PRs on completion)." in merge-auto; "Confirm upfront (creates stacked PRs — merge after review)." in stacked-prs) — see Autopilot below.

Pre-send validation: Before sending keys to any pane, the operator runs the two-step gate the _cli-agents helper owns (pane exists via a refreshed pane map — dead panes swallow keys silently; agent state fits the send intent per the three-state @rk_pane_agent_state read active/waiting/idle/ unknown, the same mode-aware gate rk mux send enforces when rk is installed) — see agent-primitives.md § Pre-send validation — then applies its own policy on the outcome: a gone pane reports "Pane for {change} is gone" and does not send; for routed commands, active, waiting, and unknown all warrant explicit user confirmation before sending, and a confirmed send rides the gated sender when rk is present (waitingrk mux send --answer; active requires rk mux send --force, the deliberate skip-everything override; unknown warns-and-sends on plain send) and degrades to raw tmux send-keys behind the operator's own state read when rk is absent; only idle sends unattended. The §5 auto-answer flow is the unattended --answer consumer — the confirm policy governs routed commands, not detected-prompt answers. Two operator-specific checks follow the gate: the target change must be the pane's active change (else send /fab-switch <change> first) and the pane's git branch must match the change folder name (else send /git-branch). When a send appears to land but the agent never starts working, the operator applies the helper's delivery probe (the printed-prompt trap: literal sentinel → C-u → retype → Enter → confirm via a working indicator) rather than re-sending blind.

Bounded retries: Every automatic action has a bounded retry count. Unbounded retries compound errors.

SituationMax retriesEscalation
Stuck agent nudge1"Appears stuck at {stage}. Manual investigation recommended."
Rebase conflict0Immediately flag to user
Pane death0Report pane gone. Respawn only in autopilot (1 attempt)
Agent exited (pane survives as a shell)0Report gone (pane kept, cwd intact). Respawn only in autopilot (1 attempt): kill the leftover shell pane first (rk mux kill when rk is installed — an uninstrumented/idle pane passes its gate — else tmux kill-pane -t <pane>), then spawn per the spawn sequence
Send to busy agent0Warn user, require explicit confirmation
Cherry-pick conflict0Abort, log, escalate. Do not spawn

Enrollment & Window Markers

The operator maintains a monitored set persisted to the server-keyed operator state file, each entry tracking: change ID, pane, last-known stage, last-known agent state, enrolled-at timestamp, last-transition-at timestamp. Entries are created, updated, and removed exclusively through fab operator enroll / update / remove (the §4 never-hand-write doctrine) — enroll writes the monitored entry and the { branch, repo } pair in branch_map in one command, and remove deletes the entry while retaining the branch_map pair for downstream dependency resolution. The enrollment/removal mechanics below (window-name prefix on enroll, done-marker swap on removal) key on server-global pane IDs, so they are independent of the (session, repo, pane) addressing model described in the next section.

Enrollment triggers: operator sends a command to it, user requests monitoring, operator triggers an automatic action toward it (including autopilot and watch spawns). Read-only actions do not enroll. Spawned agents are always auto-enrolled — the operator MUST NOT ask the user whether to monitor a spawned agent. This constraint is reinforced in both the §1 principles and the "Spawning an Agent" procedural subsection to ensure proximity-based LLM adherence.

Enrollment also applies the » window-name prefix: after fab operator enroll has written the monitored entry, the operator invokes fab pane window-name ensure-prefix <pane> » (U+00BB). The primitive enforces the literal-prefix idempotent guard internally: operator-spawned windows (already »<wt> from the spawn step), reload-restored entries, and re-enrollment after transient removal all no-op through the guard. A non-zero exit — pane vanished between refresh and rename (exit 2) or any other tmux error (exit 3, including tmux not running / socket unreachable) — is logged as "{change}: window rename skipped ({error})." and does not roll back the enrollment. Window markers (» / ) are unchanged by the multi-repo model — they key on server-global pane IDs, which are unique across every repo and session on the server.

The markers mean operator ownership, so only enrollment applies them. An interactive-pane stage dispatch (fab dispatch open, see dispatch.md) takes the identity string fab-{id}-{stage} and deliberately carries neither marker: a pipeline dispatch borrows tmux as a launch surface without joining the monitored set. It usually opens no window at all — when the dispatching agent is itself a tmux pane, its stage worker is a pane split into that agent's own window, so it never reaches the operator's tab bar; a new window named fab-{id}-{stage} is the fallback shape (a dispatcher with no pane to split, or a --server-named socket), and that window is unmarked for the same reason, and pre-marking would make the tab bar claim the operator owns a lifecycle it does not. Such a window is an ordinary unmarked window from the operator's perspective — if the operator genuinely enrolls it, the normal enrollment path adds » through the same idempotent ensure-prefix primitive.

Removal triggers: change completes (its completion delta — review-pr done/skipped when stop_stage is null, or at/past its stop_stage), pane dies, user explicitly stops monitoring. On every removal path the operator invokes fab pane window-name replace-prefix <pane> » › (U+203A, single right guillemet — the done-marker), swapping the active-monitoring » prefix for the trail-preserved . The primitive's literal-prefix guard protects user-retitled windows: if the user retitled the window mid-monitoring so it does not start with », the swap silently no-ops without clobbering the user's name. Exit 2 (pane missing — the window is gone anyway) is treated as a successful removal; other non-zero exits log "{change}: window rename skipped ({error}).". This keeps the tab bar an accurate at-a-glance map: » for currently-tracked, for operator-touched-but-done, and untouched names for windows the operator never marked.

Multi-Repo Monitoring Model

One operator per tmux server. The isolation unit is the tmux server — a single operator spans every session and every repo on that server. A second operator means a second tmux server (tmux -L <label>). "Multiple sessions, same server" share one operator and one state file. There is no --name dimension; the server boundary is the only isolation knob. This matches the server-wide singleton already enforced by the operator window (fab operator switches to the existing window rather than creating a second one).

Server-keyed state file. The monitored set, autopilot queue, branch_map, watches, and notes persist in one server-keyed file$XDG_STATE_HOME/fab/operator/<server-slug>.yaml (fallback ~/.local/state/fab/operator/<server-slug>.yaml), keyed by the tmux socket path so the one operator-per-server owns one file across all the repos it coordinates. The binary derives this path and owns every mutation: the file is read via fab operator state (which persists the empty skeleton when missing; human output opens with an # -prefixed OPEN NOTES header — one line per open note: id · kind · age · first line — omitted when no note is open and absent from --json; default output excludes resolved notes, --all includes them) and mutated only through the fab operator verb families — enroll/update/remove for the monitored set, the watch verbs (add/rm/toggle/update/checked/seen/complete) for watches, the note verbs (add/resolve/update/list) for notes, the autopilot verbs (start/pause/resume/advance/stop) for the queue, branch-map rm for the explicit user-initiated clear, and tick-start for the tick bookkeeping. The operator never computes the path and never hand-writes the file — the doctrine is agents never compute (or hand-write) what the binary can own (see the Full Mediation design decision below); the binary owns the schema, the timestamps, the list-cap pruning, the persisted top-level notes_seq note-id counter, and the atomic write. Note ids are binary-assigned n<N> from notes_seq, never reused after pruning; note text is binary-capped at 500 characters; note resolve prunes resolved notes past a cap of 50 oldest-first, and open notes never auto-expire. The skill defines the term operator state file once, in §4 (under the ### Operator State File heading), and refers to the live file by that term everywhere else (uliv) (the ~9 stale live-file .fab-operator.yaml mentions were replaced; .fab-operator.yaml now appears only in the deliberate legacy not-read/not-migrated context). See kit-architecture.md → "Operator State File" for the derivation mechanism. Old repo-rooted .fab-operator.yaml files from before the server-keyed model are not migrated — they are abandoned in place (the monitored set is re-derivable from live »-prefixed panes).

(session, repo, pane) addressing. Every monitored agent, branch_map value, and watch is repo-qualified. The pane ID is the primary key (server-global, stable); repo (absolute main-worktree root) and session (tmux session name) are added dimensions, not replacements. The recorded session is context, not identity: never re-derive which entry a pane is from a session name or window index — positional keys are reassigned by swap-window/move-window/rename, and a monitored agent's session can change mid-lifetime — re-derive per tick from the tick's fleet snapshot keyed on the pane ID (fab pane map remains the on-demand form). The entry schema:

  • Each monitored entry carries repo (absolute main-worktree root) and session (tmux session name).
  • The branch_map value is { branch, repo }. The repo is required to disambiguate a dependency's branch across repos and to choose same-repo (cherry-pick) vs. cross-repo (ordering-only) dependency resolution.
  • Each watches entry carries target_repo — the repo a watch's spawned changes land in. A watch with no target_repo cannot spawn.

Multi-Repo Coordination: Spawning, Dependencies, Autopilot

Spawning-rules home. The Operator Spawning Rules — the known-change vs new-change worktree/branch naming strategy (known change: probe-and-route the change-folder branch — the branch usually already exists, created by /fab-new in the original checkout, so pass --checkout <change-folder-name> when the branch exists and the positional <change-folder-name> only when it does not; new change from backlog: wt create on the default branch, then /fab-new in the agent — fab-new Step 11 renames the worktree's disposable branch inline, so the operator does NOT send a post-intake /git-branch; that stale step is gone from _cli-external.md and the skill's lockstep sites (w7dp)) — live in _cli-external.md's wt section (only fab-operator declares _cli-external, so only it pays). This is fab-owned choreography and stays even after the _cli-external slim (clix): the routing rule necessarily keeps stating which wt form to use when (existing branch → --checkout; missing → positional) because that decision is fab's — what the slim moved out is the tool-owned wt contract behind it (the create/delete/list command set, the wt create flags/conflict table, the exit-2 new-branch-only semantics), now delegated to wt skill at use-time. fab-operator.md §6 remains the normative step-by-step spawn procedure. The fab agent --print --repo <target-repo> / "never the operator's own config.yaml" repo-targeting rule has a single home — fab-operator.md §6 step 6; _cli-external.md's wt section states only the separate wt-side rule (run wt create in the target repo's directory) and delegates the session-command targeting there.

Repo- and session-targeted spawning. Every spawn flow first establishes which repo the work targets (an existing change's repo, a watch's target_repo, or the repo the user names — defaulting to the operator's launch repo) and which tmux session the new agent window must land in, then runs each step against those, not the operator's own repo or its ambient session:

  1. Derive the target session (§6 step 2's evidence-ordered inference): first establish the candidate set — rk mux sessions --json role: "user" rows when the fail-silent gate + capability probe succeed, else the _rk-* prefix-exclusion fallback (run-kit's reserved-infrastructure namespace — _rk-ctl, _rk-pin-*, _rk-operator) — always minus the operator's own session (never a spawn target) — then decide from the strongest evidence at hand: (a) monitored agents for the target repo, re-verified from the current tick snapshot / fab pane map --all-sessions — never the persisted session field alone (context, not identity); majority across sessions wins, ties to the most recently enrolled entry; when none match the target repo, any monitored agent's session decides only if exactly one candidate results; (b) pane-map repo affinity — the session holding panes whose worktrees belong to the target repo (what decides a cold start, where the monitored set is always empty; same majority rule, a tie no other signal separates falls to the torn path); (c) the §8 "Spawn target session" setting (user-set, or auto-set by an earlier announced inference); (d) structural dominance — exactly one plausible candidate remains after the exclusions (attached state and window count support the announcement, never silently decide). An evidence-backed decision at any tier proceeds without asking: the operator announces the chosen session and its deciding evidence in the spawn output and auto-sets the §8 setting. Only a genuinely torn spawn — zero candidates, or two-plus with no repo affinity separating them — asks the user once (attended, answer kept as the §8 setting) or escalates via the §5 notification path (unattended watch/autopilot tick); an evidence-backed inference is a derivation, not a guess (cx52). The operator MUST pass -t '<session>:' (shell-escaped — see below) on every new-windowthe ambient session is never an implicit target (the operator may run in its own dedicated session, where an untargeted new-window silently misplaces the window; the session analog of the never-rely-on-CWD rule).
  2. Run wt create --non-interactive with the target repo as the working directory, so the worktree lands under $(dirname <target-repo>)/<repo-name>.worktrees/ rather than the operator's repo.
  3. Compose that repo's default-role session command per agent-primitives.md § Spawn composition, in the role-addressed form with the target repo named: fab agent --print --repo <target-repo> (see kit-architecture.md). The operator-specific rule is always pass --repo <target-repo> — never the operator's own config.yaml, since each repo may configure a different provider/interactive command. The printed command is profile-resolvedfab agent --print composes providers.<default.provider>.interactive_command with the default role's {model}/{effort} substituted (or Claude-style flags appended) — so operator-spawned workers launch with a profile (see the "Repo-Targeted Worker Spawns Carry a Profile" Design Decision below). The helper's provider-addressed form (fab agent --provider <name>) serves ad-hoc cross-provider sessions, not operator worker spawns, which must carry the target repo's default-role profile.
  4. Open the agent tab per the helper's "Open it in a pane" mechanics (including the one-prompt / no-&&-chaining rule), targeted and print-verified — tmux new-window -t '<session>:' -P -F '#{session_name} #{pane_id}' -n "»<wt>" -c <worktree-path> "<spawn_cmd> '<command>'; exec \"$SHELL\"" (the trailing ; exec "$SHELL" is the interactive shell fallback the helper owns — see agent-primitives.md § Spawn composition) — with the operator's own »<wt> window-marker name. The session name is shell-escaped before embedding (it can come from the natural-language §8 setting or an arbitrary tmux session name; the single-quoted -t keeps $()/backticks literal, and a name containing a single quote must be escaped, never interpolated raw). -P -F prints the landed session and pane; enrollment consumes both directly (fab operator enroll --pane <pane-id> --session <session-name>), which records repo and session and adds the { branch, repo } pair to branch_map in the same command. A missing -t target errors loudly at spawn (tmux refuses an absent session) — never a silent retry against the ambient session.

All three work paths (existing change, raw text, backlog/Linear) and watch-driven spawns use this same repo- and session-targeted sequence. The skill states the spawn sequence once (szxd), in §6 "Spawning an Agent": the three Working-a-Change walkthroughs that restated it are replaced by a numbered list mapping each form to its initial command (/fab-fff <change> for an existing change — a single parseable command (w7dp): &&-chained slash commands have no chaining semantics, and the change-name override makes a /fab-switch pre-step unnecessary — /fab-new <shell_escaped_description> for raw text, /fab-new <id> for backlog/Linear), after one shared sentence binds every form to the §6 spawn sequence; Autopilot steps 1–2 and Watches step 4 are one-line §6 references. Variant-specific extras are preserved: the shell-escaping requirement for raw text, the idea-lookup pre-step for backlog/Linear, --reuse for autopilot respawns, and the watch-enrollment extras (stop_stage/spawned_by). The backlog/Linear idea-lookup pre-step is command -v idea-gated with a graceful skip (nnda): command -v idea >/dev/null 2>&1 && idea show <id>idea is a standalone sibling formula that may be absent, and when it is, the lookup is skipped silently and /fab-new <id> spawns unchanged (/fab-new resolves the backlog ID from fab/backlog.md itself, so nothing is functionally lost).

Existence-guarded pointer activation at spawn (5xnx). The §6 "Spawning an Agent" sequence is 8 steps (target repo, target session, worktree, pointer activation, dependencies, session command, open tab, enroll): the existence-guarded pointer-activation sits at step 4, between wt create (step 3) and opening the agent tab (step 7) (5xnx). The new step runs fab change switch <change> in the just-created worktree's directory — writing that worktree's own .fab-status.yaml so the worktree is self-describing after the pipeline completes (a bare fab//fab-* run later resolves the change without naming it). It is guarded on fab resolve --folder <change> succeeding (the skill's existing pure-query verb; non-zero exit cleanly signals "no such change"), so the switch fires only when the change folder already exists, and it is fail-soft — a fab change switch failure logs one line and the spawn continues opening the agent tab (the transient <change> override on the embedded pipeline command still resolves the pipeline; activation is an ergonomic enhancement, not a correctness prerequisite). The activation carries zero cross-tab collision risk: each operator worktree is a dedicated, single-change checkout that owns its own per-worktree .fab-status.yaml, so the switch touches only that worktree's pointer — never the operator's own checkout or any other worktree (the very concern the transient-override path protects against — parallel tabs targeting different changes via one shared pointer — does not arise within a single dedicated worktree). The raw-text and backlog/Linear forms skip the switch — the change folder doesn't exist yet at spawn time, so the existence guard fails and no switch is attempted; /fab-new creates and activates the change inside the spawned agent (activation at fab-new Step 10). This makes the spawn path the second consumer-side activation stance alongside §3 Pre-Send Validation item 3 (which already sends /fab-switch <change> first when targeting an existing pane whose active change is wrong) — the spawn path is now consistent with that established stance. The §6 numbered Working-a-Change list's Existing-change item notes the spawn-time activation while keeping the statement that the transient <change> override (not the pointer) is what targets the pipeline; the raw-text and backlog items note that /fab-new owns activation inside the spawned agent. The &&-no-chaining prohibition is unchanged — the fix is a separate spawn-sequence step, not a chained slash command.

Dependency satisfied. A depends_on entry is satisfied when the dependency's pipeline has completed — its completion delta (review-pr done/skipped with a null stop_stage, or at/past its stop_stage) — and, for a same-repo dependency with a null stop_stage, its PR exists (gh pr view <dep-branch> --json url succeeds, so the branch is pushed and stable). Enrollment, a branch_map entry, or a minted branch is not satisfaction — all exist from the moment the dep's agent spawns. An unsatisfied dependency holds the spawn in both tiers (same-repo has a readiness gate before branch lookup; cross-repo is the ordering barrier), re-checked each tick, logging "{change}: waiting on dependency {dep} ({dep.repo}) to complete.". The autopilot loop advances and spawns the next change only once the current one is satisfied (completion delta observed and PR URL collected).

Two-tier dependency resolution. Each depends_on entry is classified by comparing the dependency's repo (from its branch_map { branch, repo } pair, or its monitored entry) against this change's repo:

  • Same-repo dependencycherry-pick: fetch + resolve the base (step 0 below), then git cherry-pick --no-commit origin/{default_branch}..<dep-branch> into the worktree.
  • Cross-repo dependencyordering-only barrier: wait until the dependency is satisfied (above), then spawn. No code is merged.

REQUIRED caveat — cross-repo deps give the dependent agent NO code. A cross-repo depends_on is a pure sequencing constraint; the dependent worktree receives nothing from the dependency. This is correct only for logical dependencies ("don't start the frontend change until the API change merges"), never for code-level ones. Cross-repo branches share no common default-branch base to cherry-pick across, so there is no sound way to make the dependency's code available. For code sharing across repos, the dependency must merge and be consumed as a normal upstream artifact (package, vendored copy), outside the operator's scope.

Step-0 fetch + default-branch resolution (g8st). Same-repo resolution begins, in the target worktree, by refreshing the remote and resolving the repo's actual default branch — never a hardcoded origin/main:

git fetch origin
default_branch=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')
[ -n "$default_branch" ] || default_branch=$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null)
# Literal fallback when both commands fail: probe the just-fetched refs — main when origin/main exists, else master
[ -n "$default_branch" ] || default_branch=$(git rev-parse --verify -q origin/main >/dev/null && echo main || echo master)

origin/{default_branch} is the cherry-pick base (and the merge-auto rebase target — see Autopilot below). Fetching first prevents a stale base even on correctly-defaulted repos; resolving the name makes autopilot usable on repos whose default branch isn't main. (Formerly all three sites — the cherry-pick range, the "Why origin/main as base" rationale block, and the merge-auto rebase — hardcoded origin/main with no fetch step, making autopilot unusable on non-main-default repos and the base stale everywhere else.) The resolution chain matches /git-pr's (see change-lifecycle.md § Git Integration); the operator's literal fallback probes the just-fetched refs rather than assuming, because the fetch has already run.

Ancestor-pruning (git merge-base --is-ancestor) is scoped to the same-repo subset of the dependency set — it is meaningless across repos with no shared history. The origin/{default_branch}..<dep-branch> transitive-closure argument (only direct/leaf deps need cherry-picking) holds only within a repo; cross-repo deps carry no such transitive content.

Repo-spanning autopilot. An autopilot queue may span repos with mixed dependency semantics: implicit --base/depends_on chaining cherry-picks within a repo and degrades to an ordering-only barrier across repo boundaries. Ordered merge tracks per-repo PR sequences — within each repo, base-first in dependency order; across repos, cross-repo barriers are honored (a cross-repo dependent's PR merges only after its barrier dependency reaches its target repo's main). The queue-completion summary annotates each PR with its repo and suggests a per-repo merge order.

CI-failure = halt-dependents-only. During ordered merge, a CI failure halts the failing repo's merge sub-sequence AND any repo whose queued items carry a cross-repo depends_on into the failed chain — transitively over the cross-repo depends_on graph (a repo halts if any of its queued items depends, directly or via another already-halted item, on a PR in the failed chain). Truly independent repos' sub-sequences continue merging. In an armed sequence, every halt path first disarms the remaining armed PRs (auto-merge choreography rule 5 — halt-dependents-only assumes unstarted merges stay unstarted, which armed auto-merge violates). The operator isolates the blast radius to the failure's dependency cone, reports which sub-sequences halted vs. completed, and escalates the failure to the user.

/loop lifecycle (adaptive heartbeat (mmmt); waiting-triggered (ioku); loop-prompt rule + compaction reload (4q3l)): Start when first change enrolled (no loop running), always as the literal /loop 3m "operator tick" — the prompt is the bare text operator tick, never /fab-operator or any slash command (a slash command macro-expands its ~21k-token source into every firing, exhausting the context window in ~10 ticks); the same rule binds the wakeup prompt in /loop's self-paced (dynamic) mode, and §2 Init prints the literal on its ready line so the agent copies rather than composes. The cadence is not fixed — it adapts to whether any monitored agent is blocked on a human: normal 3m when none is, tightened 90s the moment a tick detects any monitored agent in the waiting state (the @rk_pane_agent_state value, the primary trigger (ioku) — a menu/permission/elicitation block is now event-visible in the Agent column rather than requiring capture-based menu detection; capture-based menu detection on an active/unknown pane remains a fallback trigger), relaxing back to 3m when none is. Adapting cadence means re-establishing the single loop at the new interval (e.g. restart /loop 90s "operator tick") — never a second concurrent loop, preserving the one-loop invariant (_cli-external.md § /loop, "one loop at a time"). The tick's loop-lifecycle step (Tick Behavior step 7) performs the adapt. Autopilot has no cadence of its own — a driving queue rides the same single 3m/90s loop; §4 Adaptive cadence is the only cadence owner (_cli-external.md § /loop carries only the one-loop rule and a pointer). The §4 Idle Message reflects the currently-active interval via its existing fab operator time --interval {interval}, so a tightened cadence shows the nearer next-tick time. Stop when no tracked state remains — monitored set, autopilot queue, watches, and in-progress merge sequence (an open merge-sequence coordination note) all empty. The merge-sequence run-condition exists because at merge-all time the autopilot queue is exhausted and the monitored set is typically empty — without it, the loop might not be running to do the choreography's tick-verify work. Compaction survival: the agent cannot /clear itself; when a tick arrives and the Tick Behavior procedure is no longer in context (harness auto-compaction, a session resumed from a summary, or a user /clear), it runs /fab-operator exactly once (reload + §2 Init), treats that tick as consumed (level-triggered deltas re-emit on the next tick-start --diff), and resumes bare operator tick firings — never by putting /fab-operator into the loop prompt (§4 Post-Compaction Reload; the matching §1 principle is "Survive compaction").

Tick snapshot is server-wide. The tick's snapshot runs inside the binary: fab operator tick-start --diff enumerates panes server-wide (the same discovery+resolve row collection behind fab pane map), keyed off the current socket, so the operator sees monitored agents in every session on its server, not just its own. Each snapshot row carries the repo field (the agent's absolute main-worktree root, null when the pane is not in a git repo — see pane-commands.md) and the resolved change ID; the emitted fleet: block (or, on a --quiet quiet tick, the fleet_summary: count block — the quiet-tick design decision below) orders rows first by repo, then by session, and its pr_url field surfaces the change's PR once it ships (r7ju). The frame's presentation (markdown tables + emoji health) is defined by the Frame rendering model below.

Repo-section status frame. The status frame renders one repo section per repo — an anchor line 📂 **{repo-path}** · {session} followed by a markdown table of that repo's changes — then a Watches section. Grouping by repo (not per-row repo/session columns) is chosen for scannability. A pane whose main-worktree root could not be resolved renders under a 📂 **(unresolved repo)** anchor rather than being dropped. At runtime the operator emits this as bare markdown — never wrapped in a ``` fence, which would render the tables as literal text. Example (fenced here only to display the source):

🛰️ **Operator** · 17:32 · tick #47 · **7 tracked**

📂 **~/code/foo** · work

| | ID | Health | Stage | PR |
|:--:|---|:--:|---|---|
| ▶ | `zq9x` | 🟢 | apply → review | |
| | `ab12` | ✅ | hydrate | https://github.com/acme/foo/pull/412 |

📂 **~/code/bar** · side

| | ID | Health | Stage | PR |
|:--:|---|:--:|---|---|
| ▶ | `vh8m` | 🟡 | review · idle 8m | |
| | `ef56` | 🔴 | apply · idle 32m ⚠️ | |

👁️ **Watches**

| Watch | Target | Health | Status |
|---|---|:--:|---|
| `linear-bugs` | ~/code/foo | 🟢 | 2 known · 1 completed · 3m ago |

Frame rendering (markdown-native). The frame is emitted as an assistant message and rendered by the agent harness as GitHub-flavored markdown — ANSI escapes do not survive this path (stripped as literal text and as real ESC bytes; empirically verified), and markdown headings (#/##/###) render as literal text and are unusable. The only channels that render are tables, emoji, bold, italic, code spans, and links. So the frame uses: a header line 🛰️ **Operator** · {HH:MM} · tick #{N} · **{N} tracked**; a 📂 **{repo}** · {session} anchor + change table per repo; a 👁️ **Watches** anchor + table — the full frame, rendered when the tick document carries fleet:. On a quiet tick (the document carries fleet_summary: instead) the frame is one line — 🛰️ **Operator** · {HH:MM} · tick #{N} · **{tracked} tracked** · no change (with · {W} waiting appended only when waiting > 0) — the header-plus-tables form replaced, the Watches table rendered only if the watch pass produced news. Emoji are the sole color channel — health is 🟢 active/healthy · 🟡 waiting/idle/new-items (the @rk_pane_agent_state waiting state joins plain idle here (ioku)) · 🔴 stuck/errored · ✅ complete · ⚪ paused (geometric glyphs like ●◌✗ render monochrome and are not used). Change-table columns: autopilot (own column) · ID (code span) · Health (emoji) · Stage (with ⚠️ trailing on stuck rows) · PR (full pr_url as plain text — not a [#N](url) link, so it is selectable/copyable in a plain xterm; blank until shipped). The 🛰️/📂/👁️ emoji are the prominence/landmark anchors that headings would otherwise provide. Degrades cleanly: strip emoji and the Stage text still names the state; the URL is plain text regardless. The authoritative column spec, frame example, and health-emoji tables live in fab-operator.md §4's ### Status Frame Format subsection (tick step 1 ends "emit the status frame — see Status Frame Format"), stated as one rule: emit bare markdown — no code fence, no headings, no ANSI; channels are tables, emoji, bold, italic, code spans, plain URLs, plus the distinct agent-critical runtime no-fence rule. (The "Why emoji + table, not ANSI" design history lives in the Status Frame design decision below.)

Monitoring tick (on each /loop tick or "any updates?"): the tick opens with fab operator tick-start --diff --quiet (a user status request drops --quiet), whose binary-internal snapshot and diff deliver the detections below as deltas: (the pane-map + state-read + baseline-diff mechanics live in the binary; candidates: carries the sweep population and fleet: or fleet_summary: the frame data):

  1. Stage advance — reported from the stage_advance deltas (consumed-on-read against the binary-owned baseline; the baseline write rides the same --diff mutation).
  2. Pipeline completion — reported from the completion deltas (level-triggered: review-pr done/skipped when stop_stage is null — hydrate and ship are mid-pipeline and never complete an entry by themselves — or past/at stop_stage; re-emitted every tick until removed). A spawn that deliberately parks earlier (a /fab-ff run stops after hydrate) is enrolled with --stop-stage hydrate, otherwise it never completes. Report and remove from monitored set (fab operator remove is the ack that stops the re-emission).
  3. Review failure — reported from the review_fail deltas (stage went from review back to apply). Report rework.
  4. Pane death / pane mismatch / agent exited — reported from the pane_death (pane absent from the snapshot), pane_mismatch (the pane hosts a different change ID than the monitored entry — tmux recycles %N pane IDs across server restarts), and agent_exited (the entry's pane is present and change-matched, its foreground command is a shell, and a lazy process-tree walk rooted at the pane's #{pane_pid} finds no live agent process, including at the root; the delta carries command) deltas, all level-triggered and evaluated in that precedence order per entry. A mismatched pane emits only pane_mismatch; a non-shell pane performs no liveness walk; a shell-fronted pane with positive live-agent evidence takes the clean-join path. On Linux, a wrapped interactive spawn keeps the non-interactive wrapper shell as the foreground process-group leader for the agent's entire life (verified live 2026-09-02), so #{pane_current_command} alone is a universal false-exit signal for wrapped live agents rather than evidence that the agent is gone. Report and remove a dead, mismatched, or exited entry from the monitored set; a mismatched or exited pane is never diffed, swept, or framed as the old agent — an exited pane's stale agent state still reads idle, so excluding it from candidates: keeps the sweep from typing into a bare shell prompt. Its fleet: row falls back to baseline identity fields with null observed fields (agent_state: null), counting under unknown in fleet_summary:, and its frame row renders the baseline identity with the ⏏ shell marker in place of a live stage. pane_death stays load-bearing for unwrapped dispatch pane workers and any pane killed outright; an exited wrapped interactive spawn surfaces as agent_exited while its fallback shell keeps the pane alive.
  5. Auto-nudge — per-tick population is the --diff candidates: block: waiting agents first (primary), then idle agents as the fallback (ioku): a waiting agent is event-visibly blocked on a human, so it is the primary question-detection candidate; idle agents are still swept so a plain-idle agent sitting on a prompt an uninstrumented harness didn't flip to waiting is not missed. For each such agent, run question detection and the answer model (see below). active/unknown () panes are usable, not swept every tick — the capture-based patterns remain applicable to them (uninstrumented, or not yet flipped to waiting), but the per-tick sweep is waiting+idle only (unknown-state and active panes are excluded from candidates: by construction). (No post-intake /git-branch send — fab-new Step 11 creates or renames the branch inline; there is no backlog-spawn nudge step (w7dp). Only a detected branch/change mismatch warrants a /git-branch send, per pre-send validation.)
  6. Stuck detection — for idle agents NOT detected as input-waiting in step 5, check the idle_duration the diff blocks carry (derived from the epoch-based @rk_pane_agent_state idle duration). If idle at a non-terminal stage for >15m, report as potentially stuck. Advisory only — a waiting agent (blocked on a human) is not stuck.

After processing all changes: if the monitored set is empty, stop the loop and report "All monitored changes complete."

Watch tick dedup (known PLUS completed): On the tick's watch pass (skill §7 step 2; the §4 Tick Behavior watch step states the same union rule (uliv) — its earlier "compare against known" summary contradicted §7), new-item detection deduplicates spawns against the union of the watch's known and completed lists. When a watch-spawned change reaches its stop_stage, fab operator watch complete <name> <item-id> moves the item ID from known to completed — but the source item (e.g., a Linear issue) may still match the watch query, and it MUST NOT be respawned. (Dedup against known alone re-enabled spawning at exactly that moment, producing a respawn loop.) Item IDs are added to known via fab operator watch seen <name> <item-id> only after a successful spawn — the binary appends idempotently and enforces the 200-entry cap (oldest pruned first); completed additionally answers "what did this watch produce?". Per-tick check bookkeeping rides fab operator watch checked <name> [--error <msg>] (sets last_checked, sets or clears last_error).

Auto-Nudge

The operator acts as a proxy for the user on routine operational questions.

Question detection — detection is one binary sweep per tick: fab pane questions --panes <ids> over the tick's candidates: block from fab operator tick-start --diff (the waiting-primary + idle-fallback population above, re-expressed as the command's input; a waiting state is itself the strongest question signal, and the sweep's capture confirms what the prompt is). The binary applies the mechanical guards and indicator patterns itself and returns matches: (pane, agent_state, indicator, snippet) plus skipped: with reasons — the full command contract is owned by pane-commands.md § fab pane questions; the capture/state-read mechanics underneath (including the uninstrumented-pane state-writer caveat that makes capture the universal fallback) belong to agent-primitives.md § Peek. Claude Code permission/tool-approval prompts are not mechanized as their own class — in practice covered by the sweep's yes/no, action-word, imperative, and enumerated indicator classes; novel prompt shapes remain operator judgment via an on-demand --panes sweep or manual capture. The sweep's output is detection input only, never a license to send blind — the answer model below, the §3 pre-send gate, and the re-capture-before-send guard remain LLM-side and unchanged. On a match the operator proceeds to the answer model (bottom-most, most recent, indicator reported); no match → normal idle behavior (stuck detection applies).

Answer model — most detected questions are auto-answered. Rule 4 (numbered menus) classifies the prompt before answering: Routine prompts auto-answer, Strategic prompts escalate. Rule 6 escalates when the operator cannot determine what keystrokes to send. Evaluate in order:

  1. Binary yes/no or confirmation prompt → y
  2. [Y/n] or [y/N] prompt → y
  3. Claude Code permission/approval prompt → y
  4. Numbered menu or multi-choice → classify as Routine or Strategic using LLM judgment over the terminal capture, weighing four signals: option text length, semantic distinctness of options, surrounding agent context, and reversibility of the choice. No hardcoded keyword list, no agent-side sentinel/marker protocol. Routine (tool/permission prompts, binary-framed menus, synonymous-option menus) → 1 (first/default option). Strategic (multi-option choices representing materially different directions — scope, PR split, pipeline shape, commit organization, spec/approach) → non-blocking handling (see "Non-Blocking Strategic Handling" below) (mmmt). A Strategic classification never ends the operator's turn. On classification uncertainty, treat as Strategic (asymmetric cost: false-negative strategic commits the queue to an unchosen direction; false-positive strategic costs at most a notification — and, if auto-picked, a reversal at PR review).
  5. Open-ended question where a concrete answer is determinable from visible terminal context → send that answer
  6. Question where the operator cannot determine what keystrokes to send → escalate (left open). Rule-6 escalations are excluded from auto-pick and from the 30m idle auto-default — sending a guess would emit nonsense into the pane.

Non-Blocking Strategic Handling (mmmt): A Strategic classification MUST NOT block the loop. The operator handles the prompt out-of-band within the current tick and proceeds to the next monitored change in the same tick — one strategic question on one change does not freeze the queue. Two branches:

  • Strategic + defensible recommendation → auto-pick-and-notify: the operator picks its recommended option (LLM judgment over the capture, the same four signals rule 4 lists), sends it (after the Re-Capture-Before-Send guard), fires a notification (Notification Send below), and keeps ticking. The PR review stage is the reversal point (§1 "PR review is the safety net").
  • Strategic + no defensible default → leave open and notify: the operator leaves the prompt open for the user, fires a notification, and keeps ticking. The 30m Idle Auto-Default remains the backstop for left-open prompts.

In both branches the operator continues ticking; the user answers asynchronously (responding to the notification's guidance or typing into the pane) and the operator picks up the resolution on a later tick via its normal re-capture/re-detection — no new pickup mechanism is added.

Notification Send (mmmt): a single out-of-band shell send run when the operator auto-picks or leaves open a Strategic prompt. The default channel is rk notify — a run-kit external contract (run-kit Web Push) that is tool-owned: the notify signature and its guarantees live in rk skill, read at use-time, not pinned to a version here. The send is gated on command -v rk and fail-silent per _preamble.md § Run-Kit (rk) Reference (which documents the gate and the fail-silent discipline, not the notify subcommand itself): command -v rk >/dev/null 2>&1 && rk notify "{change}: {summary} ({repo})" --title "Operator: strategic question". rk notify delivers a real background mobile/desktop Web Push and is fail-silent by contract (exits 0 / prints nothing on any error, so it can never stall the loop). When rk is absent, the operator falls back to the first available documented alternative (configurable via the §8 Notify channel setting): ntfy.sh (curl -d … ntfy.sh/<high-entropy-topic> — high-entropy topic REQUIRED, public topics are world-readable, the topic name is the only secret; strongest no-rk fallback), Discord webhook, the PushNotification harness tool (zero-infra personal push, not a shared feed), or Slack MCP (searchable, but an interactively-authed MCP may be absent in headless/cron runs, so not a headless default). All notify sends fail silently — a send that cannot be delivered (server unreachable, no subscriptions, missing curl/tool) logs one line and the loop keeps ticking; it MUST NOT crash or stall the loop. Mirrors the _preamble.md § Run-Kit (rk) Reference "fail silently" discipline.

No cooldown or retry limit — each question is evaluated independently. Worktree isolation and human PR merge provide the safety gate for auto-answered prompts.

Idle Auto-Default on left-open Strategic prompts (scoped to left-open) (mmmt): This is the watchdog for a left-open Strategic prompt — the no-defensible-default branch of Non-Blocking Strategic Handling above. (Auto-picked Strategic prompts are already resolved, so the watchdog has nothing to act on for them.) When rule 4 leaves a prompt open as Strategic, the operator runs a per-prompt real-time idle timer from the moment the left-open log line is written. If the prompt remains idle for 30 minutes, the operator auto-answers and logs using the distinct auto-defaulted format (see Logging below). The timer runs in the background — it does NOT block the loop (mmmt); the operator keeps ticking and fires the auto-default on whatever later tick crosses the 30-minute mark.

  • Threshold: 30 minutes, hardcoded — unchanged (mmmt). No operator-state-file field, no per-change override, no environment variable exposes this value — the §4 operator state file schema is unchanged.
  • Idle clock reset: the timer resets on any terminal-state change in the pane — new content appended by the agent, user keystrokes that alter the prompt display, or the prompt's own redraw. The timer watches pane-idle-ness, not escalation-open-ness. Tick cadence already provides sub-minute resolution — no new polling infrastructure is required.
  • Answer selection (priority order): (1) if the prompt visibly states a default (e.g., (default: 2), Press enter for 2, [2]), send that stated default; (2) otherwise, send 1. This matches rule 4's existing "first/default" semantics for routine menus.
  • Scope (hard exclusion): applies ONLY to left-open rule 4 Strategic prompts. It does NOT apply to auto-picked Strategic prompts (already resolved) and MUST NOT apply to rule 6 ("cannot determine keystrokes") escalations — the operator does not know what the correct keystrokes are, so sending 1 or the stated default would emit nonsense into the pane. Rule-6 escalations remain open pending user action regardless of idle duration.

Re-capture before send: Before delivering an auto-answer via rk mux send <pane> "<text>" --answer (when rk is installed — rk's built-in delivery verification replaces the manual probe on that path, and a probe failure surfaces as staged text + stderr warning + exit 1: re-capture and decide, never blind-resend; key-name answers — bare Enter, arrows, C-c — ride rk mux send --key), the operator runs the pre-send gate (agent-primitives.md § Pre-send validation) and MUST re-capture the terminal (the operator's own 20-line capture: rk-gated rk mux capture --raw -l 20, raw-tmux when rk is absent — the same 20-line window the question-detection sweep captures, though fab pane questions captures internally via fab's own tmux capture-pane, not rk). fab pane questions output is detection input only — it never replaces the pre-send gate or this re-capture-before-send guard (the sweep's batch capture is older than the just-in-time one, so the guard matters more, not less). If output changed since initial capture, abort — the agent is not waiting anymore. Eliminates the race condition between detection and send. When rk is absent the flow degrades without error to the re-capture guard + raw tmux send-keys + the manual delivery probe, and on that path an answer that appears to land without the agent resuming gets the delivery probe instead of a blind re-send.

Logging (five answer-line shapes + a fail-silent notify line) (mmmt):

  • Auto-answer (routine): "{change}: auto-answered '{summary}' → {answer}"
  • Auto-pick strategic (defensible recommendation): "{change}: auto-picked strategic '{summary}' → {answer} · notified"
  • Left-open strategic (no defensible default): "{change}: strategic '{summary}' left open · notified. Please respond."
  • Escalation (rule 6 — cannot determine keystrokes): "{change}: can't determine answer for '{summary}'. Please respond."
  • Auto-default after 30m idle on a left-open strategic prompt (distinct from auto-answered for grep-based after-action review): "{change}: auto-defaulted after 30m idle: '{summary}' → {answer}"
  • Notification send failure (fail-silent — logged, loop continues): "{change}: notify failed ({channel}). Continuing."

Coordination Patterns

The operator understands the full fab pipeline and command vocabulary, and infers the right action from current state rather than following named playbooks. There is no playbook catalog: the skill states the pipeline reference (intake → apply → review → hydrate → ship → review-pr), the setup/pipeline/maintenance command vocabulary, the spawn sequence, dependency resolution, and autopilot — and the operator composes those against whatever the tick snapshot shows. Autopilot is the one named mode, because it is a queue with its own persisted state (below).

Autopilot

An autopilot queue may span repos with mixed dependency semantics, ordered merge tracks per-repo PR sequences, and CI-failure halts dependents only (not the whole sequence) — see "Multi-Repo Coordination" above for the repo-spanning behavior. The mechanics below describe the per-queue loop.

Drives a queue of changes through the full pipeline — spawning agents, monitoring progress, and collecting PRs for review. Three flat merge modes, resolved at queue start by the ladder explicit user instruction / --mode flag > config autopilot.merge_mode > built-in cherry-pick-ladder — the operator resolves a mode-unnamed queue request silently (no "which merge topology?" round-trip), learns the resolved mode from start's printed mode: <name> (<source>) line, and states it inside the existing upfront confirmation so the user vetoes in the same breath; it pauses and asks ONLY on two misfits (resolved merge-auto vs same-repo depends_on entries, which that mode's disabled implicit chaining contradicts; the user's own message conflicting with the resolved mode). When it does ask, the question carries the at-a-glance glyphs (▂▄▆ cherry-pick-ladder · ░▒▓█ merge-auto · ▄▀ stacked-prs), the three compact box diagrams, and a one-line tradeoff per mode; the skill's no-diagrams caveat is status-frame-only — a mode question is an ordinary conversational message where the fenced diagrams render fine. The mode is persisted in the autopilot state block's mode field, so the mode survives /clear; mode is fixed for the queue's lifetime (changing it is stop + a new start), and a pre-existing state block lacking mode reads as the built-in default, never config-resolved. In the default cherry-pick-ladder: all queued changes build on each other via implicit depends_on chaining, PRs are created but NOT merged until the user explicitly requests merging. Confirm the queue upfront before starting. Default confirmation: "Confirm upfront (creates PRs — merge after review)."

Queue ordering: User-provided (exact order given), confidence-based (descending score), or hybrid (partial user constraints, confidence tiebreaker). User-provided ordering implies implicit --base chaining — each queued change after the first gets depends_on: [<nearest-same-repo-predecessor>]: the closest earlier queue entry in the same repo (cherry-picked); when no earlier entry shares the repo, the immediately previous entry as an ordering-only barrier (cross-repo → no code). (reconciled (w7dp): the rule text, worked example, Dependency Declaration paths, merge-auto paragraph, and the spawn-next item to this one semantics — strict queue-previous would silently break same-repo stacking whenever a cross-repo entry sits in between, since a cross-repo dep contributes no code.)

Per-change loop (cherry-pick-ladder, default): Gate check confidence BEFORE anything spawns (below the gate → flag to user; no worktree, no tab, no dispatch) → spawn worktree (--reuse for respawns; chaining per the nearest-same-repo rule above) → resolve dependencies (cherry-pick same-repo depends_on entries into the worktree; cross-repo = ordering-only barrier) → open the agent tab with the pipeline command embedded at spawn (§6 step 7 — /fab-fff <change>; the loop has no separate Dispatch item, so the command reaches the pane exactly once (w7dp)) → monitor → on completion, run fab operator autopilot advance (the binary moves current to completed and promotes the next queue entry) and collect the PR URL — the { branch, repo } pair is already in branch_map, recorded by enroll at spawn → spawn next change (implicit depends_on per the chaining rule, its command likewise embedded at spawn) → report "ab12: PR ready. 1 of 3 complete. Starting cd34.".

Queue completion summary: When all changes in a cherry-pick-ladder or stacked-prs queue complete, the operator displays a summary with all PR links and suggested merge order (base-first). The user can merge individually, or ask the operator to merge all in dependency order. The CI gate between ordered merges is mode-split: in cherry-pick-ladder (and for merge-auto's per-PR merges) merge-all runs the auto-merge choreography — the operator arms each PR with GitHub auto-merge (gh pr merge --auto --squash; the method flag is explicit and REQUIRED — flagless --auto may prompt or take the repo default, unsafe unattended; a user-directed method maps to --merge/--rebase) and verifies the merge on later ticks, a passive tick check instead of a foreground wait; in stacked-prs, or when arming is unavailable, the operator merges each PR itself and foreground-waits for CI before proceeding. The choreography's five MUST rules (skill §6 Auto-Merge Choreography): (1) sequential arming — at most one armed PR per repo-sequence, arm PR_n only after PR_{n-1}'s merge is verified via a timeline event, never arm a PR whose base is another PR's branch; (2) arming-failure shapes — draft PR → gh pr ready first (fab's /git-pr creates drafts, so this is every autopilot PR), "already clean" rejection (no required checks) → merge directly, auto-merge disabled on the repo → the foreground CI-wait choreography; (3) stall rule — the per-tick check on an unmerged armed PR treats a failed required check (gh pr checks) as the CI failure (disarm per rule 5, halt-dependents-only), and after 3 consecutive ticks with no failed check polls gh pr view --json mergeable (mergeable is the field — mergeableState does not exist there), CONFLICTING → disarm and escalate; both shapes are event-less, so the tick MUST poll; (4) persisted sequence — starting a merge sequence writes a kind: coordination note (fab operator note add) recording, per repo-sequence, position and armed PR (one armed PR per repo-sequence in a multi-repo merge-all), updated as it advances and resolved at sequence end, so a restarted operator re-orients from the note (an armed PR outlives the operator); (5) disarm on halt — a halt/escalation runs gh pr merge --disable-auto on the halted sequences' remaining armed PRs (the failing sub-sequence plus its transitive cross-repo dependent cone — independent sub-sequences keep theirs and continue); a user "stop" is global and disarms all. Arming rides the user's single merge-all confirmation (Destructive tier) — no per-PR re-confirmation.

merge-auto mode: Merge-as-you-go — arm each PR on completion via the auto-merge choreography (a one-PR sequence position; all five rules apply) instead of merging and foreground CI-waiting; the autopilot advance and the next change's spawn defer to the tick that verifies the merge (spawning earlier would start the next agent from a stale base), and only then git fetch origin and rebase the next change onto origin/{default_branch} (resolved per the step-0 chain in "Multi-Repo Coordination" above — never a hardcoded origin/main) (g8st). Implicit --base chaining is disabled in this mode — each change rebases onto origin/{default_branch} independently. Confirmation text changes to "Confirm upfront (merges PRs on completion)." Natural language equivalents: "merge as you go", "merge on complete", "merge each when done".

stacked-prs mode: cherry-pick-ladder merge timing (PRs created up front, merged only on explicit user request) with true stacked-PR topology for same-repo chains — the dependent's branch is created off its dependency's branch at the spawn sequence's worktree/branch step (no cherry-pick commit for same-repo deps), and after /git-pr creates the dependent's PR the operator retargets its base to the dependency's branch (gh pr edit <pr> --base <dep-branch>), so each PR diff shows only its own delta. Cross-repo dependencies stay ordering-only barriers. Confirmation text: "Confirm upfront (creates stacked PRs — merge after review)." Natural language equivalents: "stacked PRs", "stack the PRs". Merge-all adds two steps per merge: verify the dependent PR's base auto-retargeted onto the default branch after its base PR merged (retarget explicitly via gh pr edit --base when the merged branch was not deleted), and rebase the next chain branch onto the default branch after a squash merge to drop the already-merged dependency commits (git rebase --onto origin/{default_branch} <merged-dep-branch> <next-branch> + force-push) — a conflict in that rebase escalates, never skips.

Failure matrix:

FailureActionResume?
Confidence below gateFlag to user: run /fab-fff or skipWait for user input
Review fails (rework exhausted)Flag, skip to nextYes
Cherry-pick conflict (cherry-pick-ladder)Escalate, do not spawnNo — queue halts, wait for user input
Rebase conflict mid-queue (merge-auto)Flag, skip to nextYes
Rebase conflict during stacked-prs merge-allEscalate — never skipNo — halt, wait for user input
Agent pane dies1 respawn attempt, then flag and skipYes
Stage timeout (>30 min same stage)Flag regardless of retry stateYes
Total timeout (>2 hr per change)Flag for reviewYes

Interruptibility: "stop after current" (finish active, halt queue), "skip <change>", "pause" (stop new commands, running agents continue), "resume". Interrupts acknowledged immediately and persisted through the matching verb — fab operator autopilot stop once the current change lands (or immediately to abandon the queue), advance --skip, pause, resume. Queue state (queue, current, completed, state, mode) persists in the operator state file, written only by the fab operator autopilot verbs: start --queue [--mode <name>] begins the queue, on exhaustion the binary retains queue/completed/mode with current: null, state: null so the completion summary can still read them, and only stop clears the block to autopilot: null. A pre-existing block lacking mode reads as cherry-pick-ladder; the binary stores/validates/prints the mode only — all merge choreography is operator-side.

Resumability: If the operator session restarts, state is reconstructable from fab pane map --all-sessions. Resume from first non-completed change.

Configuration

SettingDefaultOverride
Loop interval3m"check every {N}m"
Stuck threshold15m"flag agents stuck for more than {N} minutes"
Waiting/menu heartbeat (mmmt; waiting-triggered since ioku)90s"tighten to {N}s when an agent is waiting / on a menu"
Notify channel (mmmt)rk (run-kit Web Push; auto-fallback when rk absent)"notify via ntfy topic {topic}" / "notify via discord {url}" / "notify via push"

All settings are session-scoped — they reset when the operator session restarts. The strategic auto-default threshold stays hardcoded at 30m — mmmt added no setting for it, and the operator-state-file schema (§4) is unchanged by the two new session settings.

Design Constraints

  • Binary-internal tick snapshot: The per-tick observation primitive is the snapshot inside fab operator tick-start --diff (deltas + candidates + fleet blocks); under --quiet (the tick's default) a no-delta tick whose tick count is not a multiple of 10 replaces fleet: with a five-count fleet_summary: (tracked/waiting/idle/active/unknown), every 10th tick emits the full document, and candidates: is always emitted. fab pane map --all-sessions remains the on-demand/manual/startup-orientation surface
  • No change artifacts: Never reads intakes, specs, or tasks — context window reserved for coordination state
  • No persistent audit trail for v1: Per-answer logging is inline only — no file-backed log
  • Hardcoded patterns: The question-detection guards and indicator patterns live in the fab binary (fab pane questions), table-driven-tested; they remain non-configurable via config.yaml by design. Claude Code permission/tool-approval detection (the skill's former class 4) is not mechanized and stays operator judgment

Launcher

The operator is launched via fab operator — a fab-go subcommand (source: src/go/fab/cmd/fab/operator.go).

rk delegation (checked first) (rkop): when a capable run-kit is on PATH, bare fab operator hands the ENTIRE launch to rk operator, exec'ing it as a process replacement (syscall.Exec behind the injectable execOperator seam) with --workers <v> appended to argv iff the flag was supplied — the value rides argv, not the FAB_AGENT_WORKERS= env prefix, because rk composes the env for the agent it launches, and rk validates it (letters/digits/_/-, usage error exit 2; a behavior delta from the fallback's verbatim pass-through). Capability is probed side-effect-free by the injectable rkOperatorPath seam: exec.LookPath("rk") + rk operator --help exiting 0 with --workers in its output (the passed-through flag is the discriminant — the binary is probed, never the version string). rk then owns the whole launch: its own preconditions ($TMUX, fab on PATH — fab's own $TMUX check is skipped on this path), the role-marked singleton (@rk_win_role=operator wins over a merely-named window), launcher resolution via fab agent operator --print, and the provider-agnostic typed kickoff. A failed probe falls through silently to the built-in launcher below (cli-layering delegation rule 2 — absence degrades, never errors); a failure AFTER a passing probe (the exec syscall) is surfaced, never fallen back on. The operator subcommand family is never delegated — choreography stays in fab.

Built-in launcher (the rk-absent fallback). It creates a singleton tmux window named "operator" running the resolved operator-role session command (via internal/agent + internal/spawn) with '/fab-operator', wrapped in the interactive shell fallback (spawn.WithShellFallback applied after the FAB_AGENT_WORKERS= env prefix — see agent-primitives.md § Spawn composition), so the operator's tab survives its own agent's exit as a shell in the same cwd. The singleton check is an exact, server-wide window-name match (pw3k): it enumerates tmux list-windows -a -F '#{window_id}\t#{window_name}' and compares names exactly — never tmux target resolution, whose prefix/glob fallback would let a window like operator-logs falsely satisfy the check (and was session-scoped, missing an operator in another session on the same server). On exact match it selects the window by its server-global window ID (grammar-exempt) with a best-effort switch-client so a cross-session match moves the user's client; absence launches; a tmux enumeration error is surfaced distinctly (with the child's stderr), never conflated with "absent". Requires an active tmux session (ERROR: not inside a tmux session via the central RunE error path, exit 1).

Launch preconditions (fallback path): neither a git repo nor a fab/ project is required (2sdj). The launcher matches the per-tmux-server, cross-repo singleton model — its natural launch point is a neutral parent directory (e.g. ~/code) with no git and no fab/ project. runOperator resolves two things gracefully:

  • Window cwd (tmux new-window -c <dir>): try gitRepoRoot() (git rev-parse --show-toplevel) first, fall back to os.Getwd() on failure. Inside a repo the window opens in the repo root (today's behavior, unchanged); outside any repo it opens in the current directory. The old hard cannot determine repo root error is gone — it errors only when BOTH gitRepoRoot() and os.Getwd() fail (cannot determine working directory, a genuinely broken environment). The git repo root was only ever the -c argument — it is NOT part of the operator state-file path (still socket-keyed under XDG_STATE_HOME, unchanged — no migration).
  • Session command: when a fab/ project resolves (resolve.FabRoot() succeeds), read the operator role's provider interactive_command from that project's fab/project/config.yaml via the in-process internal/agent resolution (see the next paragraph). When resolve.FabRoot() fails — launched with no fab/ project up the tree — fall back to the PROJECT-FREE cascade (config.LoadNoProject()env > system > built-in defaults, so a machine-wide ~/.fab-kit/config.yaml still governs an operator launched from a neutral directory; previously a nil config discarded it) and then to spawn.DefaultSpawnCommand (the {model}/{effort} template claude --permission-mode bypassPermissions -n "$(basename "$(pwd)")" --model {model} --effort {effort}, profile-substituted by spawn.WithProfile) rather than erroring. A fab/-less launch reads the upper tiersFAB_* env vars and the system ~/.fab-kit/config.yaml — and only the project tier is absent, so a machine-wide agent.session DOES reach an operator launched from a neutral directory. (Before the project-free cascade landed, the config load was gated on resolve.FabRoot() succeeding and a fab/-less launch was fully defaulted, silently ignoring the system file.) The resolution chain itself — role → provider → interactive_command with the role's {model, effort} substituted — is the SHARED roleSessionCommand helper that fab agent <role> also walks, so the operator tab and fab agent operator can never compose different commands; only the missing-interactive_command POLICY differs (the operator falls back so it always launches, fab agent errors). A regression test asserts the two paths agree.

Coordinating agent runs on the operator role. The operator launches its agent on a deliberately-chosen profile rather than whatever the session command happened to specify. runOperator resolves the operator role in-process (operatorProfile/operatorSpawnCommand); operator is a first-class role in the fixed roster (internal/agent) and a Tier-1 (session) one, so which provider it lands on is the agent.session knob's call, and the launcher composes that provider's interactive_command with the resolved {model, effort} via spawn.WithProfile(sessionCmd, model, effort). WithProfile operates in one of two modes selected by placeholder presence (see _shared/configuration.md § providers): for a templated interactive_command (one carrying {model}/{effort} — the built-in claude default is templated, as is a non-Claude command) it substitutes the resolved profile into the placeholders (all-or-nothing, empty-value token-drop) — letting the operator launch a non-Claude coordinating agent whose flag grammar lives in the provider config; for a non-templated interactive_command (a plain-form user config) it appends --model/ --effort to the END (last-wins; duplicate --effort accepted by the claude CLI; each flag omitted when empty). Placing the default's placeholders last makes substitution byte-identical to that append. On any failure — an unresolvable fab project or empty resolution — it falls back to the in-process built-in operator default agent.DefaultProfile(agent.RoleOperator). This keeps the operator a non-orchestrator (non-pipeline) consumer of the agent-role system. A fab/-less launch therefore composes a fully-defaulted command: spawn.DefaultSpawnCommand + the operator default {model, effort}.

fab operator is a parent command with two subcommands:

  • fab operator tick-start — Called at the start of each operator tick (step 1 of tick behavior). Derives the server-keyed operator state file path via StatePath() (no repo-root resolution), reads the file into map[string]interface{} using gopkg.in/yaml.v3 (absent file treated as empty), increments tick_count by 1, writes last_tick_at as an RFC3339 UTC timestamp (time.RFC3339), writes the updated map back atomically preserving all other fields (monitored set, autopilot queue, branch_map, watches, notes). Outputs tick: N\nnow: HH:MM to stdout using local time. Write failure → stderr error + exit 1. The flagless form is exactly this bookkeeping and nothing more. With --diff (the tick's normal form), the binary additionally snapshots the fleet internally — the same discovery+resolve row collection fab pane map uses, server-wide, keyed off the current socket, skipped entirely when the monitored set is empty — diffs it against the monitored baseline, and appends three YAML blocks to stdout: deltas: (event kinds completion / pane_death / pane_mismatch / agent_exited / stage_advance / review_fail in two delivery classes — see the Level-Triggered vs Consumed-on-Read Delta Classes design decision), candidates: (monitored agents whose snapshot agent_state is waiting or idle, waiting first, each with pane/change/agent_state/idle_duration — the auto-nudge sweep population), and fleet: (one row per monitored entry — repo, session, stage, display_state, agent_state, idle_duration, pr_url — the status frame's data source). --quiet (valid only with --diff; alone it errors --quiet requires --diff before any state read/write, consuming no tick) replaces the fleet: block with a five-count fleet_summary: mapping (tracked/waiting/idle/active/unknown, pinned key order; never both keys) on a no-delta tick whose post-increment tick_count is not a multiple of the built-in constant 10 (no flag/config knob); a delta tick and every 10th tick emit the full document, candidates: is always emitted, and the empty-monitored short-circuit emits the all-zero summary (or fleet: [] on a 10th tick). The observed-field baseline write (stage/agent, last_transition iff the stage changed) lands in the same atomic mutation as the tick bookkeeping — --diff is the authoritative baseline writer. agent_exited detection first basename-matches the snapshot row's command field (rk rows' command, or the ninth #{pane_current_command} field of the fallback list-panes -F format — see pane-commands.md) against sh bash zsh fish dash ksh tcsh csh nu. Only that shell branch resolves the pane's root PID and walks its process tree. A live agent process at the root or among its descendants suppresses the delta and gives the entry a clean join; a PID-resolution or discovery error supplies no positive evidence and silently emits agent_exited. The agent classifier uses the provider-aware name set documented under fab pane process. Per-entry evaluation order is pane_deathpane_mismatchagent_exited → clean join, and a dead/mismatched/exited entry gets no baseline write, no stage diffs, and no candidates: row (its fleet row falls back to baseline identity with null observed fields).

  • fab operator time — Pure clock query with no file I/O or side effects. Always outputs now: HH:MM (local 24-hour time). With --interval <duration> (Go duration string, e.g. 3m), also outputs next: HH:MM = now + interval. Invalid duration string → stderr error + exit 1.

Usage in tick lifecycle: The agent invokes fab operator tick-start --diff --quiet at step 1 of each tick (a user status request drops --quiet; the binary's every-10th-tick full document is the periodic full refresh, no skill-side counter) and parses its single stdout document: the tick count (tick: N) and current time (now: HH:MM), the deltas: it acts on, the candidates: block that feeds the auto-nudge sweep, and the frame block — fleet: (rendered as the full status frame) or, on a quiet tick, fleet_summary: (rendered as the one-line compact frame) — the diff tick makes no per-tick fab pane map, fab operator state, or fab operator update calls (the watch pass reads state on its own step). Version-skew fallback has two rungs (softest first): if --quiet errors as an unknown flag, drop --quiet for the session (keep --diff) and report once; if tick-start --diff or fab pane questions errors as an unknown flag/command (new skill, older installed binary), the operator falls back to the flagless tick — plus per-pane manual capture-and-scan per agent-primitives.md § Peek — for the session and reports the mismatch once. Between ticks (idle message), the agent runs fab operator time --interval {interval} to obtain both now: and next: values for the idle message line Waiting for next tick. Time: HH:MM · next tick: HH:MM. The idle message is the only other per-tick output — no restated tick document, no echoed candidates:, no per-candidate "no question detected" lines. Separation of concerns: tick-start has side effects (writes YAML state), time is a pure query (no writes).

Design Decisions

Auto-Answer Model with Strategic Escalation (rule 4 classification)

Decision: Detected questions are auto-answered by default via a numbered decision list (items 1-6, evaluated in priority order). Rule 4 (numbered menus) further classifies the prompt as Routine or Strategic before answering. Routine prompts (tool/permission, binary-framed, synonymous-option menus) auto-answer 1. Strategic prompts (multi-option choices representing materially different directions — scope, PR split, pipeline shape, commit organization, spec/approach) escalate to the user. Classification is LLM-judged over four signals in the terminal capture (option text length, semantic distinctness, surrounding agent context, reversibility); no hardcoded keyword list, no agent-side sentinel protocol. Classification uncertainty MUST escalate (asymmetric cost structure: silently committing the queue to an unchosen branch of work is more expensive than an extra user nudge). Why: Worktree isolation and human PR merge are sufficient safety gates for routine operational prompts, but not for prompts that commit the queue to a direction the user never inspected (scope, PR split, spec/approach). A pure all-auto-answer model traded correctness for throughput in the exact scenarios where correctness matters most. Principle-based LLM classification adapts to novel prompt text without maintaining a keyword list or coupling the operator to every skill's surface area. Rejected: Pure all-auto-answer (original model) — loses correctness on strategic prompts. Hardcoded keyword list — brittle, fails on novel prompts, high-maintenance. Agent-side [STRATEGIC] sentinel protocol — couples the operator to every skill and fails on Claude Code native + third-party prompts the operator cannot modify. Introduced by: 260314-007n-redesign-operator-auto-nudge (original model); 260422-hin2-operator-strategic-menu-escalation (Strategic classification + escalate-on-uncertainty)

30-Minute Idle Auto-Default on Strategic Escalations

Decision: When rule 4 escalates a prompt as Strategic, the operator starts a per-prompt real-time idle timer from the escalation log time. If the prompt remains idle for 30 minutes (no terminal-state change in the pane), the operator auto-answers — sending the prompt's stated default if visible (e.g., (default: 2), Press enter for 2, [2]), otherwise option 1. The auto-default logs with a distinct format — "{change}: auto-defaulted after 30m idle: '{summary}' → {answer}" — so after-action review tooling can distinguish confidently-auto-answered decisions from decisions taken because the user never returned. The idle clock resets on any terminal-state change in the pane (new agent output, user keystrokes, prompt redraw). The threshold is hardcoded 30 minutes — no operator-state-file field, no per-change override, no environment variable exposes it. Why: Strategic escalations stall forward progress if the user is asleep, in meetings, or otherwise away from the terminal. Headless autopilot runs (overnight, multi-hour) become unreliable when every strategic escalation halts the pipeline. A 30-minute idle watchdog trades oversight for throughput in exactly the scenario where the alternative is zero throughput. The grep-distinct log format preserves auditability — auto-defaulted entries are recoverable separately from auto-answered entries. Rejected: Configurable threshold (.fab-operator.yaml, per-change override, environment variable) — added surface area for marginal benefit; one threshold serves the single motivating "user is asleep / in a meeting" scenario well. Shorter threshold — risks auto-defaulting while the user is mid-reply. Longer threshold — defeats the feature. Uniform auto-default across all escalation types — conflates rule 4 Strategic (known-good default 1) with rule 6 "cannot determine keystrokes" (auto-defaulting 1 would emit nonsense into the pane). Reusing the auto-answered log line — muddies audit trails by conflating confidently-answered and fell-back decisions. Introduced by: 260422-hin2-operator-strategic-menu-escalation

Re-Capture Before Send

Decision: The operator re-captures terminal output immediately before sending an auto-answer. If the output changed, the send is aborted. Why: Eliminates the race condition between idle check and send. Single-tick grace period was rejected — it adds latency without fully solving the race. Rejected: Single-tick grace period — delays answers by one full monitoring cycle and doesn't guarantee safety. Introduced by: 260314-007n-redesign-operator-auto-nudge

Claude Turn Boundary Guard

Decision: If a Claude Code > prompt cursor (^\s*>\s*$) appears in the last 2 lines of captured output, question detection is skipped. Why: Claude's output often contains question-like phrasing ("Would you like me to...?") that triggers detection. The > cursor indicates the agent is at a normal human-turn boundary, not a blocking prompt. Rejected: Excluding all question-mark lines from Claude — too broad, would miss genuine blocking prompts from Claude. Introduced by: 260314-007n-redesign-operator-auto-nudge

Operator Uses /fab-fff for Autopilot

Decision: Operator4 uses /fab-fff instead of /fab-ff for autopilot gate checks and pipeline invocations. Why: /fab-fff is the more autonomous pipeline variant, fitting for operator-driven autopilot where human interaction is minimized. Rejected: Keeping /fab-ff — its interactive fallback on review failure conflicts with the operator's autonomous mode. Introduced by: 260314-007n-redesign-operator-auto-nudge

Standalone Operator Over Inheritance Chain

Decision: Operator4 is a fully self-contained skill file. Previous iterations (operator1, operator2, operator3) were deleted — their behavior is inlined into operator4. The skill file loads _cli-external.md (operator-only) for external tool references (wt, tmux, /loop). Why: Understanding the operator required reading 4 files in sequence (operator1 -> 2 -> 3 -> 4), mentally applying overrides. The standalone version is readable from a single file plus standard _ files. Dead operator files in the skills directory risked ghost triggers via sync. Rejected: Keeping operator1/2/3 as archived files — git history preserves them; dead files risk agents loading them. Extracting a shared base — adds indirection for a single-consumer pattern. Introduced by: 260315-a2b2-standalone-operator4-rewrite

Watches Are User-Created, Not a Fixed Registry

Decision: Standing monitoring concerns are watches — user-created conversationally ("watch Linear project DEV for new issues, spawn agents, stop at intake"), persisted per-watch in the operator state file, and evaluated on each /loop tick (tick step 3). There is no fixed, built-in set of concerns: a watch's structured fields (enabled, source, query, target_repo, stop_stage, known, completed, last_checked, last_error) carry the machine-readable part, and a free-form instructions field carries everything the operator evaluates as an LLM (trigger conditions, label filters, concurrency limits). Users manage watches conversationally — create, pause/resume (enabled), retarget (target_repo), re-scope (stop_stage), dry-run ("test watch X"), or remove. The loop is the operator's heartbeat and runs while any tracked population is non-empty (monitored set, autopilot queue, watches, or an in-progress merge sequence) — it is not tied to the monitored set alone. Why: Real workflows have multiple concurrent monitoring concerns, but which concerns matter is per-user and per-repo, so a fixed enumeration built into the skill would be wrong for almost everyone. Making a watch a state-file record with a natural-language instructions field lets the user define both the concern and its policy without any skill change, and lets the operator evaluate criteria (age thresholds, label filters, concurrency caps) that no structured schema would anticipate. Splitting structured fields from instructions keeps dedupe and scheduling mechanical (and testable) while leaving judgment to the LLM. Rejected: A fixed set of built-in, toggleable concerns (wrong granularity — users' concerns are not enumerable in advance, and adding one would require a skill release). An all-structured watch schema (cannot express the trigger conditions users actually state). CLI-level branch resolution (fab resolve --search-branches) — fab operates on change folders, not git branches; branch awareness belongs in the operator skill. Introduced by: 260317-yrgo-operator5-branch-fallback

Notes vs Watches vs Not-Operator-State Routing

Decision: Cross-cutting narrative state routes by content class. A passive narrative read on restart/orientation — phase progress and holds, peer scoping agreements, report-back promises, corrections to earlier conclusions, merge-gate dependency waits (checked by operator judgment per tick — no git/GitHub watch source exists today) — is a note. A standing concern a watch source can express today (linear/slack queries with instructions) is a watch; if a git-source watch ships later, merge-gate waits migrate to it as their own change. Anything still true for a different operator next month — process lessons — is not operator state: it routes via an idea backlog entry → a fab change into docs/memory. There is deliberately no lesson note kind — its absence is the guard against notes degrading into a reflexive scratchpad. The operative routing table lives in fab-operator.md § Notes. Why: Notes re-enter operator context on every state read, so the surface must carry only what re-orientation needs; merge-gate waits and phase progress outlive the monitored set and no watch source expresses them today, so without an owned home they were hand-written into the state file in violation of Full Mediation. The operator has no memory write path (a three-file context load, and it may run with no fab/ project at all), so durable process lessons cannot route through the operator — they need the idea → fab change path into docs/memory. A lesson kind would invite exactly the reflexive scratchpad the routing exists to prevent. Rejected: A lesson note kind (durable lessons belong in docs/memory, not in a per-server state file). Teaching depends_on to survive removal (fixes one case by complicating two schemas — a surviving wait is a note). Auto-expiry for open notes (notes are decisions, not a dedupe cache). Introduced by: 260823-s2gw-operator-note-narrative-state

Branch Fallback in Operator, Not CLI

Decision: Branch fallback resolution lives in the operator skill (user-initiated only), not in the fab CLI. When fab resolve fails, the operator scans branch names as a fallback before reporting failure. Why: fab is orthogonal to git — it operates on change folders (filesystem/YAML). Branch name scanning is a coordination concern (finding where a change lives), not a CLI concern. The operator already has the context to decide between read-only (git show) and action (worktree creation) responses. Rejected: fab resolve --search-branches, --branch output mode, automatic fallback in CLI — all rejected because they couple the CLI to git branch semantics. Introduced by: 260317-yrgo-operator5-branch-fallback

Dependency-Aware Agent Spawning (operator7)

Decision: /fab-operator (v7) adds pre-spawn dependency resolution to the operator. When spawning an agent for a change with depends_on entries, the operator cherry-picks dependency content into the worktree before opening the agent tab. Uses git cherry-pick --no-commit origin/{default_branch}..<dep-branch> && git commit -m "operator: cherry-pick <dep> dependency" (the base was a hardcoded origin/main until the fetch-and-resolve fix (g8st)). On conflict: abort, escalate, do not spawn. Why: Without dependency awareness, agents working on dependent changes start from a baseline missing the dependency code, causing build failures, spec divergence, and manual intervention. This defeats the operator's "automate the routine" principle. Rejected: git merge --squash — rejected for unattended sessions where merge machinery introduces risk. Transitive dependency resolution — rejected because leaf dependency branches already carry transitive content via the operator's own cherry-picking when those deps were spawned; origin/{default_branch}..<dep-branch> gives the complete transitive closure. Introduced by: 260324-prtv-operator7-dep-aware-spawning; Updated by: 260612-g8st-git-state-hardening (base = fetched, resolved default branch — see "Operator Git Ops Fetch First and Resolve the Default Branch")

Operator7 Schema Additions

Decision: .fab-operator.yaml gains three new fields: depends_on (list of change IDs per monitored entry), branch (change's branch name per monitored entry), and branch_map (top-level map persisting change ID → branch name after changes leave the monitored set). Redundant deps are pruned via git merge-base --is-ancestor before cherry-picking. The --base autopilot flag implies depends_on. Why: Branch names must persist after dependencies complete (merged/archived) so downstream changes can still cherry-pick from them. Redundant dep pruning prevents duplicate cherry-picks in chains (B's branch already contains A's content). Introduced by: 260324-prtv-operator7-dep-aware-spawning

Operator7 Direct fab-new for Raw Text Spawns

Decision: When spawning agents from raw text descriptions, the operator passes the description directly to /fab-new instead of creating an intermediate backlog entry via idea add. The "From raw text" spawn path now follows the same structure as "From backlog ID": worktree → resolve deps → spawn with /fab-new <description> → enroll → completion. Why: The idea add step created orphaned backlog entries in fab/backlog.md that served no further purpose — the intake's Origin section already captures the raw input for traceability. /fab-new natively accepts natural language descriptions, making the backlog indirection redundant overhead. Rejected: Keeping idea add for backlog traceability — the intake artifact is the real record of a change's origin, not the backlog entry. Introduced by: 260326-13ro-operator7-direct-fab-new-spawn

Pipeline-First Routing Principle (operator7)

Decision: /fab-operator (v7) §1 Principles gains a "Pipeline-first routing" principle requiring the operator to route all new work through /fab-new then a pipeline command (/fab-fff, /fab-ff, /fab-continue). The operator MUST NOT dispatch raw inline implementation instructions to agent panes and MUST NOT use /fab-continue to skip intake for new work. Operational maintenance commands (merge PR, archive, delete worktree, rebase, /git-branch, /fab-switch) are exempt. A reinforcing blockquote in §6 "Working a Change" references the §1 principle. Why: Without an explicit prohibition, an operator (especially after /clear or under time pressure) could shortcut by sending freeform implementation instructions directly to an agent pane — bypassing intake generation, confidence scoring, and the full pipeline. This violates the fab workflow's core value: specification-driven development with traceability (Constitution §II). Introduced by: 260326-u3un-operator-enforce-pipeline-routing

Cherry-Pick-Ladder Autopilot Default (operator7)

Decision: The autopilot queue defaults to cherry-pick-ladder mode. All queued changes after the first implicitly get depends_on: (equivalent to implicit --base chaining) — naming the nearest same-repo predecessor (w7dp) (the closest earlier same-repo queue entry, cherry-picked; no same-repo predecessor → the immediately previous entry as an ordering-only barrier), not blindly the previous queue entry. PRs are created but not merged until the user reviews and explicitly requests merging. Two opt-in modes share the queue: merge-as-you-go behavior is available as the merge-auto mode, and the default's merge timing with a true stacked-PR topology (each same-repo dependent branches off its dependency's branch and PRs against it) is available as stacked-prs. Queue completion produces a summary with all PR links and suggested merge order (base-first). Ordered merge gates each PR per mode — auto-merge arming with tick verification for cherry-pick-ladder/merge-auto, foreground CI wait for stacked-prs and when arming is unavailable. Why: The previous merge-as-you-go default caused two problems: (1) rebase conflicts when rebasing dependent changes onto freshly-merged origin/main re-linearized commits that cherry-pick resolution had already handled, and (2) no opportunity for holistic review of the full change set before any code merged to main. Stack-then-review gives the user full review control over the entire queue. Rejected: Keeping merge-as-you-go as default — too many rebase conflicts and no review control. Available as opt-in for users who want it. Introduced by: 260327-gwg9-operator-base-chaining-default; Updated by: 260612-w7dp-orchestrator-dispatch-review-pr-recovery (chaining reconciled to nearest-same-repo-predecessor — the strict queue-previous rule contradicted the worked example and silently broke same-repo stacking across an intervening cross-repo entry); Updated by: 260820-t6rq-autopilot-merge-modes-stacked-prs (opt-in framing widened to the three-mode model, and the modes renamed — the former stack-then-review default is cherry-pick-ladder, the former merge-on-complete is merge-auto — so each name conveys its mechanism and merge agency); Updated by: 260823-2c92-operator-auto-merge-choreography (the ordered-merge CI gate is mode-split — see "Auto-Merge Choreography: Sequential Arming with Verified-Merge Gating")

Standardized Tmux Tab Naming (operator7)

Decision: All agent tab names in /fab-operator use »<wt> format (right guillemet + worktree name, no space). Replaces the previous fab-<id> naming which was unreliable for new changes where the change ID doesn't exist at spawn time. The worktree name is always available at spawn time and unique across panes, making it a consistent identifier for all three spawn paths (existing change, raw text, backlog). Originally used (zap emoji) as prefix, but switched to » (U+00BB) because the emoji's double-width rendering caused tmux tab bar misalignment and console output formatting issues. Why: The fab-<id> format had two issues: (1) for new changes, the ID doesn't exist until fab-new runs inside the spawned agent, and (2) the raw-text path already used fab-<wt> as a workaround, creating inconsistency. The » prefix makes agent tabs visually distinct from other tmux windows while being single-width for consistent terminal rendering. Rejected: Keeping fab-<id> with worktree fallback — adds conditional logic without benefit since worktree name is always available. emoji — double-width rendering breaks tmux tab alignment. Introduced by: 260328-iqt8-standardize-tmux-tab-naming

» Prefix Extends to Enrolled Windows

Decision: The » convention applies to every monitored window, not just operator-spawned ones. On enrollment the operator invokes fab pane window-name ensure-prefix <pane> » (U+00BB). The primitive's literal-prefix idempotent check makes the step a no-op when the name already starts with », covering operator-spawned, /clear-restored, and re-enrolled entries. The rename runs after the monitored entry is durably written to the operator state file (repo-rooted .fab-operator.yaml at the time of this decision; server-keyed (oy0k)); a non-zero primitive exit logs one line and leaves the enrollment intact. Why: With the original decision, the » prefix was only half-enforced — windows enrolled via direct command dispatch, user request ("watch this pane"), autopilot spawns, and watch spawns kept their original names, so the monitored set split visually into two indistinguishable populations (prefixed vs unprefixed). Extending the convention to every enrollment path makes the tab bar an accurate at-a-glance map of what the operator is currently tracking. Rename-after-YAML-write ordering guarantees that a partial failure leaves a tracked entry without the cosmetic prefix, never a prefix without a tracked entry. Rejected: (1) Inventing a second signal (status-bar marker, pane title) — violates parity with the existing convention, adds surface area. (2) A generic "already-marked" regex (^[»⚡…]) — would silently absorb legacy or user-chosen markers; the literal » check keeps naming sovereignty with the user. Introduced by: 260422-jyyg-operator-prefix-enrolled-windows. Amended by: 260423-rxu3-window-prefix-primitives (extracted the inline tmux shell into fab pane window-name ensure-prefix).

Done-Marker Swap on Removal

Decision: On every removal path (change completes — at/past its stop_stage, or review-pr done/skipped when stop_stage is null — pane dies, user explicitly stops monitoring), the operator invokes fab pane window-name replace-prefix <pane> » › to swap the active-monitoring » prefix for the done-marker (U+203A, single right guillemet). The primitive's literal-prefix guard silently no-ops on user-retitled windows (if the user retitled the window mid-monitoring so it does not start with », the swap is skipped). Exit 2 (pane missing) is treated as successful removal; other non-zero exits log "{change}: window rename skipped ({error})." and the operator continues. Why: Leaving » on removed windows made the tab bar lie about what the operator was currently tracking. The entire purpose of the prefix is at-a-glance coordination — an honest signal means "» = tracking now, = operator touched but done, untouched = untouched." The done-marker was chosen over because already appears in the operator status frame as the stage-done signal (● apply → review ✓); reusing it on window names would create a semantic collision. preserves the guillemet visual family (», double → single), is single-width BMP (consistent with the 260328 / 260416 decisions), and reads as "was-active, now trail-preserved." The literal-prefix guard inside replace-prefix replaces the prior "no restore on removal" rule: no state-storage (original_name) is needed, because the guard protects user-retitled windows inherently — if the current name does not start with », the swap doesn't match and doesn't fire. Rejected: (1) Leave » on removal (260422-jyyg's original rule) — self-defeating signal staleness; tab bar lies about what is tracked. (2) Restore the original name — requires storing original_name with a "user-retitled-mid-monitoring" ambiguity that the guard-based approach side-steps. (3) as the done-marker — collides with the stage-done signal in the operator status frame. (4) Per-project config option for the done-marker character — speculative, no current demand; the character is a skill constant in src/kit/skills/fab-operator.md. Introduced by: 260423-rxu3-window-prefix-primitives

Multi-Repo / Multi-Session Isolation = Tmux Server (one operator per server)

Decision: The operator's isolation unit is the tmux server — exactly one operator per server, spanning every session and repo on it, owning one server-keyed state file at $XDG_STATE_HOME/fab/operator/<server-slug>.yaml (fallback ~/.local/state/...), keyed by the tmux socket path (the binary derives it via StatePath() — see kit-architecture.md). A second operator means a second tmux server (tmux -L <label>). There is no --name dimension — the server boundary is the only isolation knob, matching the server-wide singleton already enforced by the operator window. Old repo-rooted .fab-operator.yaml files are not migrated — abandoned in place (the monitored set is re-derivable from live »-prefixed panes). Every monitored entry, branch_map value ({ branch, repo }), and watch (target_repo) is repo-qualified under the (session, repo, pane) addressing tuple, with pane ID as the server-global primary key. Why: A single operator coordinating multiple repos/sessions is the central value of the operator (one pane of glass). A repo-rooted state file is single-repo-only; a fixed global path would force a machine-wide singleton. Keying the file by the tmux socket scopes one owner across all repos on a server while still allowing a second server to host an independent operator. No migration is acceptable because operators don't survive a binary upgrade anyway. Rejected: Per-repo operators (loses the single pane of glass). A --name operator dimension (redundant with the server boundary). Repo-rooted .fab-operator.yaml (single-repo only). A fixed global state path (machine-wide singleton). Migrating old state files (unnecessary — monitored set is re-derivable). Introduced by: 260607-oy0k-operator-multi-repo-skill

Cross-Repo Dependencies = Ordering-Only (no code merge)

Decision: depends_on resolution is two-tier, split by repo. A same-repo dependency cherry-picks (git cherry-pick --no-commit origin/{default_branch}..<dep-branch> — base fetched and resolved (g8st)). A cross-repo dependency is an ordering-only barrier: the operator waits until the dependency is satisfied (pipeline completed), then spawns — no code is merged. The skill states the REQUIRED caveat that a cross-repo dependency gives the dependent agent no code (pure logical sequencing), correct only for logical deps ("don't start the frontend until the API merges"), never code-level ones. Ancestor-pruning (git merge-base --is-ancestor) is scoped to the same-repo subset only. Why: Cross-repo branches share no common default-branch base, so there is no sound cross-repo cherry-pick; logical sequencing is the only sound cross-repo semantic. Forbidding cross-repo deps would be too restrictive for real multi-repo workflows. Rejected: Forbid cross-repo deps (too restrictive). Full cross-repo code merge (unsound — no shared base). Cross-repo ancestor-pruning (meaningless across repos with no shared history). Introduced by: 260607-oy0k-operator-multi-repo-skill

Repo-Spanning Autopilot CI-Failure = Halt-Dependents-Only

Decision: An autopilot queue may span repos with mixed dependency semantics (within-repo cherry-pick chaining degrades to cross-repo ordering-only barriers); ordered merge tracks per-repo PR sequences. On a CI failure during ordered merge, the operator halts the failing repo's merge sub-sequence AND any repo whose queued items carry a cross-repo depends_on into the failed chain — transitively over the cross-repo depends_on graph. Truly independent repos' sub-sequences continue merging. The completion summary reports halted vs. completed sub-sequences and escalates to the user. Why: Maximizes independent-repo throughput while still respecting cross-repo ordering barriers — the failure's blast radius is isolated to its dependency cone rather than throttling the whole queue. Rejected: Halt-all (conservative, throttles independent repos — the earlier lean, overturned during clarify). Halt-only-failing-repo (ignores cross-repo ordering barriers, would merge a dependent ahead of its failed barrier). Introduced by: 260607-oy0k-operator-multi-repo-skill

Operator Context Loading Trimmed to Three Files (zc9m)

Decision: fab-operator.md §2 Context Loading loads only config.yaml, constitution.md, and context.md (optional), and fab-operator is named in the _preamble.md §1 exception list with its 3-file load. The Operator Spawning Rules moved from _preamble.md § Naming Conventions into _cli-external.md's wt section (single repo-targeting rule — the duplicate fab spawn-command --repo note at the tmux bullet was dropped); fab-operator.md §6 stays the normative spawn procedure. Loading-only change — no §1 principle, safety-model, or spawn-procedure text was altered. Why: The operator loaded all 7 always-load files, but code-quality.md, code-review.md, and both doc indexes are used nowhere in the skill — against its own §1 "Context discipline" principle — and the operator re-pays the whole layer after every /clear in a long-lived session (it had the largest per-invocation context of any skill: 136,967B, now ~123KB effective at startup//clear). The spawning rules served only the operator yet sat in the always-load preamble paid by every skill; _cli-external is loaded exclusively by the operator, making it the natural home. Deliberate behavior change, verifier-endorsed (finding f117; spawning-rules move f040). Rejected: Keeping the 7-file load (pays for files the skill never reads). Leaving the spawning rules in the preamble (every skill pays for operator-only content). Keeping the duplicate repo-targeting rule at the tmux bullet (verbatim drift risk). Introduced by: 260611-zc9m-preamble-context-diet

Status Frame = Markdown Tables + Emoji (ANSI does not render)

Decision: The operator status frame is rendered as markdown — a header line, one 📂 repo-anchor + change table per repo, and a 👁️ Watches table. Health is shown with emoji (🟢🟡🔴✅⚪); IDs are code spans; the header/repo anchors use emoji + bold; the PR column holds the full pr_url as plain text. Earlier specs colored an ANSI-wrapped glyph (\e[32m●\e[0m) and a later iteration broadened ANSI to many fields ("structural color"). Both were non-functional: the frame is an assistant message rendered as markdown by the agent harness, and ANSI escapes are stripped on that path — verified empirically that neither literal \e[ text nor real ESC bytes render, and that markdown headings also render as literal text. Emoji (glyphs, not escapes) are the only surviving color channel; markdown tables give real column alignment and absorb the wide PR URL. Why: The operator never writes to a TTY directly — it emits text that the harness markdown-renders. Color/visual hierarchy therefore must come from channels that survive that render (emoji, tables, bold, italic, code spans), not ANSI or headings. PR URLs are surfaced as plain text (not [#N](url) links) so they are selectable/copyable in a plain xterm, which shows only the link display text, not the target. Rejected: ANSI SGR codes (stripped by the markdown renderer — the bug this fixes). Markdown headings for the header/sections (render as literal ## text). [#N](url) markdown links for PRs (xterm shows only #N, not a copyable URL). A sparse dedicated-then-folded PR cell / footnote block (chose a full-URL column for always-copyable PRs at the cost of table width). Geometric glyphs ●◌✗ for health (render monochrome — no color). Introduced by: PR #387 follow-up (markdown-native operator frame; replaces the merged-but-non-functional "structural color" ANSI spec)

Operator Git Ops Fetch First and Resolve the Default Branch (g8st)

Decision: Every operator git sequence that uses the default branch as a base — the same-repo dependency cherry-pick, the merge-auto rebase, and the stacked-prs merge-all rebase — runs git fetch origin first and resolves the repo's actual default branch instead of hardcoding origin/main: git symbolic-ref --short refs/remotes/origin/HEAD (strip origin/) → gh repo view --json defaultBranchRef -q .defaultBranchRef.name → probe the just-fetched refs (main when origin/main exists, else master). The resolved origin/{default_branch} replaces all former hardcoded sites: the cherry-pick range, the "Why origin/{default_branch} as base" rationale block, and the merge-auto rebase target. Why: The cherry-pick range and rebase hardcoded origin/main with no fetch step — autopilot was unusable on any repo whose default branch isn't main, and even on main-defaulted repos the base could be stale. Fetch-first fixes the staleness; name resolution fixes the portability. The chain matches /git-pr's default-branch resolution (one convention across autonomous git paths — see change-lifecycle.md § Git Integration); the operator's literal fallback probes refs rather than assuming, because the fetch has already run. Rejected: A shared _ helper file for the chain (two consuming skills, three lines each — a helper would add loading cost for every other skill; the chain is inlined per consumer). Keeping hardcoded origin/main with a config override (the resolution is mechanical; no config knob needed). Introduced by: 260612-g8st-git-state-hardening

Autopilot Single Dispatch Point: Gate Before Spawn, Command Embedded at Spawn (w7dp)

Decision: A spawned change's initial pipeline command has exactly one dispatch point — the §6 spawn-sequence open-agent-tab step (step 7) embeds <command> in the tmux new-window invocation. The autopilot per-change loop's separate "Dispatch" item is removed (the loop renumbered 9→8 items, dispatch folded into the open-tab item), and the confidence Gate moved before the tab opens — a below-threshold change gets no worktree, no tab, and no dispatch. The numbered Working-a-Change list's Existing-change item uses the single parseable initial command /fab-fff <change> (the change-name override) — &&-chained slash commands have no chaining semantics and MUST NOT be sent. Why: The #393 (szxd) refactor left both the spawn-embedded command and the loop's separate Gate+Dispatch items live — the command fired twice into the same pane. All three entry forms and the Watches flow already send the initial command via the spawn tab, so keeping spawn-embedding required no new machinery, while gating after spawn would open tabs for work the gate then rejects. The former /fab-switch <change> && /fab-proceed chain was unparseable as a slash command; /fab-fff <change> targets the change directly (no switch pre-step) and picks up from its current stage. Rejected: Bare tab + post-spawn keyed dispatch (adds an idle-detection/ready-state dependency to every spawn). Keeping the loop's Dispatch item with a "skip if already sent" note (preserves the double-dispatch hazard). Keeping the && chain (no chaining semantics — the trailing command lands as literal text). Introduced by: 260612-w7dp-orchestrator-dispatch-review-pr-recovery

Operator Launch Is Git-Optional, fab/-Optional, and Operator-Role-Resolved

Decision: fab operator requires neither a git repo nor a resolvable fab/ project, and it launches its coordinating agent on the operator role. (1) Window cwd: try gitRepoRoot(), fall back to os.Getwd(), error only if both fail — the old cannot determine repo root hard-fail is removed. (2) Session command: the operator provider's interactive_command from the project config when resolve.FabRoot() succeeds, else spawn.DefaultSpawnCommand (non-fatal FabRoot() failure). (3) Profile: resolve the operator role in-process (tykw) (operatorProfile) and consume its full model/effort directly rather than the fab agent -o yaml native model_alias seam; substitute/append {model}/{effort} via spawn.WithProfile; on ANY failure fall back to the in-process agent.DefaultProfile(agent.RoleOperator). spawn.WithProfile is the shared composition helper in internal/spawn. No state-file path change — state stays socket-keyed; no migration for the launcher itself (config-schema restructures ship as their own migrations). Why: The git root was incidental — used ONLY as the tmux window -c <dir>, never in the socket-keyed state path — and forcing one repo root (and one owning fab/ project) contradicted the operator's per-tmux-server, cross-repo singleton design, whose natural home is a neutral parent dir. On the profile: the coordinator deserves its own named role rather than a borrowed one, so the launcher resolves operator directly instead of probing the apply stage to borrow doing. The role's fill is the coordinator's highest-volume, pattern-matching, escalation-disciplined workload, and the built-in fallback keeps the operator working when no project is resolvable. This keeps the operator a non-orchestrator (non-pipeline) consumer of the agent-role system. fab operator is NOT an alias of fab agent operator — it keeps its distinct responsibilities (tmux singleton window management, launching with /fab-operator as the initial prompt, tick-start/time subcommands); only the session-command composition is shared. Conceptually: fab operator ≈ singleton tmux window running $(fab agent --print operator) '/fab-operator'. Rejected: Keeping the hard git-repo (and fab/-project) precondition (contradicts the cross-repo singleton model; the git root was never essential). Keeping the doing borrow / the apply-stage probe (mis-names the coordinator's role — a real operator role exists (tykw)). Reducing fab operator to an alias of fab agent operator (loses its tmux-singleton / initial-prompt / subcommand responsibilities — only the session-command composition is shared). Inline --model/--effort concatenation in runOperator (not reusable, not unit-testable) — hence the shared spawn.WithProfile helper. Introduced by: 260613-2sdj-operator-doing-tier-no-git-dep; Updated by: 260702-tykw-agent-providers-role-tiers (operator role resolved in-process; session command from the operator provider; fab spawn-command/doing-borrow retired), 260806-j9nh-agent-profiles-session-workers (the operator's provider is the agent.session knob's call)

Bare fab operator Delegates the Launch to a Capable rk operator

Decision: Bare fab operator probes for a capable rk (side-effect-free: LookPath + rk operator --help exit 0 carrying --workers — the passed-through flag is the capability discriminant) and, on a pass, execs rk operator (process replacement, --workers on argv, env untouched). Absence or an incapable rk falls through silently to the built-in launcher; a failure after a passing probe is surfaced, never fallen back on. The subcommand family stays in fab. Why: Launching the operator window is tmux substrate (role-marked singleton via @rk_win_role=operator, readiness-gated typed kickoff that works on every provider — a positional kickoff is claude-only), which rk owns under cli-layering rule 1; rule 2's capability-probe/fail-open keeps fab standalone-installable. The probe must be side-effect-free because the delegated action mutates tmux state — a fallback after a partial rk launch could open a duplicate operator window, which is also why post-probe failures surface instead of degrading. Rejected: Porting the role-option probe and typed-kickoff composite into fab (reimplements rk's layer, duplicates the inject machinery); attempt-is-the-probe (safe only for idempotent reads like pane-map enumeration); a config knob to disable delegation (capability presence is the policy, matching every other fab→rk delegation); bare help exit-0 as the capability signal (masks an early rk operator predating --workers). Introduced by: 260904-rkop-delegate-operator-launcher-rk

Repo-Targeted Worker Spawns Carry a Profile

Decision: Operator-spawned workers read the target repo's default-role session command via fab agent --print --repo <target-repo>, which composes providers.<default.provider>.interactive_command with that role's {model}/{effort} — so workers launch with a profile. default is a Tier-1 role, so the target repo's agent.session decides the provider. Why: A worker must run on the repo's own configured provider/profile, not the operator's. fab agent --print injects the profile directly, so no profile-stripping step is needed. Rejected: A raw print path that emits placeholder-only commands and strips empty placeholders (injects no profile — the worker launches profile-less). Introduced by: 260702-tykw-agent-providers-role-tiers

Non-Blocking Strategic Escalation + Adaptive Heartbeat (mmmt)

Decision: A Strategic menu (rule 4) does not park the operator's turn. Strategic handling is non-blocking, split by whether a defensible recommendation exists: (1) defensible recommendation → auto-pick-and-notify — the operator picks its recommended option (LLM judgment over the capture using rule 4's four signals), sends it after the re-capture-before-send guard, fires a notification, and keeps ticking; the PR review stage is the reversal point (§1). (2) no defensible default → leave open and notify — the operator leaves the prompt open, fires a notification, and keeps ticking; the 30m idle auto-default remains the backstop for these left-open prompts (its scope is now narrowed to left-open prompts only — auto-picked ones are already resolved — and its timer runs in the background, not blocking the loop). In both branches the operator picks up the user's async answer on a later tick via the existing §5 re-capture/re-detection path — no new pickup mechanism is added. Paired with an adaptive heartbeat (§4): the single monitoring loop tightens from 3m to 90s the moment any monitored agent is detected menu-waiting and relaxes back to 3m when none is — re-establishing the loop at the new interval, never a second concurrent loop (one-loop invariant preserved); autopilot composes unchanged (at the time with its own 2m cadence; a single loop with no autopilot cadence — see "Compaction Recovery Is a One-Shot Reload" (4q3l)). The notification send is abstracted behind one fail-silent out-of-band shell command, default rk notify (run-kit Web Push; the contract is tool-owned, read at use-time via rk skill) gated on command -v rk, with documented fallbacks (ntfy.sh with a REQUIRED high-entropy topic / Discord webhook / the PushNotification harness tool / Slack MCP); all sends fail silently. Two session-scoped §8 settings were added (Menu-detected heartbeat 90s; Notify channel rk). The 30m strategic auto-default threshold stays hardcoded (no new setting), and the operator-state-file schema is unchanged. Why: The operator's coordination loop is a single-threaded /loop heartbeat serving every monitored change across every repo and session on one server. The old "escalate to user" behavior parked the loop waiting for a human, so a single strategic question on one change froze the entire queue — every other change stopped advancing until the user returned (observed live on the FKF autopilot queue (bmzo)). Non-blocking escalation is the minimal structural fix — it removes the single-question-freezes-everything failure mode without changing the single-loop architecture. Auto-pick-and-notify keeps the queue moving and defers human judgment to PR review (the existing safety net), while the adaptive heartbeat bounds worst-case detection/pickup latency (sub-2-minute) without paying that cadence when idle. Abstracting the send behind one shell command lets the channel evolve (ntfy.sh → rk notify) without touching the escalation logic; rk notify is the default because it delivers a real background push, is fail-silent by contract, routes through infra the user already runs, and avoids a world-readable topic secret. Rejected: Parking every strategic menu (the freeze this change removes). A second concurrent loop or a separate watcher thread for the watchdog (violates the one-loop invariant, adds infrastructure). Hardcoding ntfy.sh as the default channel (world-readable topic — demoted to a fallback). Shortening or making configurable the 30m auto-default threshold (explicitly out of scope this change — stays hardcoded). 60s tightened cadence (more capture-pane churn for marginal latency gain — 90s chosen as the calmer end of the backlog's 60–90s range). Building run-kit Web Push here (it shipped as a separate run-kit change, backlog [xd9r]); the operator only consumes it as the default channel (mmmt). Introduced by: 260615-mmmt-non-blocking-operator-escalation

Operator Keys Off the Richer @rk_pane_agent_state Agent Column (ioku)

Decision: The operator's monitoring keys off the three-state-plus-unknown Agent column that the pane family now reads from the @rk_pane_agent_state tmux pane option (active/waiting/idle/; see runtime-agents.md and pane-commands.md) — the operator's interface keys off the snapshot rows that carry the column (per tick via fab operator tick-start --diff, on demand via fab pane map --all-sessions --json), and the data is richer. Three concrete keyings: (1) the per-tick auto-nudge population is waiting primary + idle fallback (active/unknown panes are usable but not swept every tick); (2) waiting is the primary trigger for the tightened 90s heartbeat (was capture-based menu detection alone; capture stays a fallback for active/unknown panes); (3) the pre-send gate uses the mode-aware semantics the gated sender enforces (plain send refuses active/waiting; --answer permits waiting for detected-prompt answers; unknown warns-and-sends in both non-force modes — enforced by rk mux send when rk is installed, by the operator's own state read + raw tmux send-keys otherwise). The frame health legend gains waiting alongside idle under 🟡. Why: waiting — a mid-turn permission prompt / menu / elicitation block — was invisible to the old Stop-only pipeline (no Stop fires, so the agent read active and the idle-only sweep likely never probed it). Making waiting event-visible in the Agent column lets the operator detect a human-blocked agent directly rather than inferring it from a terminal capture, and it is precisely the signal the 90s cadence exists to serve. Reading the shared convention also covers non-Claude agents (codex/copilot/gemini/opencode) the Claude-only pipeline was blind to — so the operator's question-detection and pre-send gate now work across every instrumented harness. Keeping idle in the swept population (fallback) means a plain-idle agent sitting on a prompt an uninstrumented harness didn't flip to waiting is still caught. Rejected: Sweeping only waiting (would miss idle-but-prompted panes on uninstrumented harnesses). Sweeping every non-idle pane every tick (churns capture on active/unknown panes for marginal gain — they stay capture-applicable, not per-tick swept). A new operator signal beyond the Agent column (the column now carries the signal directly — no second channel needed). Introduced by: 260705-ioku-divest-agent-state-production

Operator Activates the Change Pointer at Spawn for Existing Changes (5xnx)

Decision: The §6 spawn sequence carries an existence-guarded pointer-activation step at position 4 (between wt create at step 3 and opening the agent tab at step 7, in the 8-step sequence). The new step runs fab change switch <change> in the just-created worktree's CWD, writing that worktree's own .fab-status.yaml, guarded on fab resolve --folder <change> succeeding (the existing pure-query verb; non-zero exit = "no such change") so it fires only when the change folder already exists, and fail-soft (log one line and continue opening the tab on a switch failure). The raw-text and backlog/Linear forms skip the switch — the folder doesn't exist yet at spawn time, so the guard fails — and defer activation to /fab-new, which creates and activates the change inside the spawned agent (activation at fab-new Step 10). This is the second consumer-side activation stance alongside §3 Pre-Send Validation item 3 (switch first when sending to an existing pane whose active change is wrong); the spawn path is now consistent with it. The &&-no-chaining prohibition is unchanged — the fix is a separate spawn step, not a chained slash command. Why: After an operator-driven pipeline completed on an existing change, the spawned worktree had no active-change pointer (.fab-status.yaml is gitignored and per-checkout, so a fresh worktree starts pointer-less; the /fab-fff <change> entry form relies on a transient <change> override that never writes the pointer). Any human who later cds in and runs a bare fab//fab-* got No active change (multiple changes exist — use /fab-switch) and had to name the change on every follow-up (the highest-friction case being /fab-archive <change>). Setting the pointer at spawn carries zero cross-tab collision risk — each operator worktree is a dedicated, single-change checkout that owns its own .fab-status.yaml, so the parallel-tabs-share-one-pointer concern the transient override protects against does not arise within a single worktree. The override remains correct for pipeline resolution; the activation merely also makes the worktree self-describing. Rejected: Leave-as-is / rely on the <change> arg (zero new writes, but pushes friction onto every human follow-up — the ergonomic papercut the user flagged). Sequentialized slash commands/fab-switch <change> before /fab-fff, in either form: (a) a &&-joined string sent as one prompt does not work at all — the spawn embeds a single prompt, where && is not a shell operator and Claude reads one leading /command, so the && … tail is absorbed into the first command's argument rather than running as a second command; (b) two separate Enter-terminated sends would work, but a slash-command switch is a full agent round-trip for a one-line symlink write and a post-spawn send regresses the single-dispatch-at-spawn property (w7dp). The synchronous fab change switch CLI verb does the identical write directly — no agent turn, no pane-timing fragility — and setting the pointer is operator coordination state the operator already owns directly (like wt create/window renames), not pipeline work to delegate; hence the chosen fix is the CLI verb in a spawn step, not a slash dispatch. Aborting the spawn on switch failure (over-couples ergonomics to correctness — the transient override still resolves the pipeline). A bespoke test -d folder check instead of fab resolve --folder (duplicates resolution logic and ignores archived state). Cross-references the §6 numbered Working-a-Change list (the Existing-change item notes spawn-time activation; raw-text/backlog items note /fab-new-owned activation) and the &&-no-chaining lineage of the "Autopilot Single Dispatch Point" decision above (w7dp). Introduced by: 260617-5xnx-operator-spawn-activate-pointer

Spawn Target Session Is Inferred Live Per Spawn, Never Persisted-and-Trusted

Decision: §6 spawn-sequence step 2 infers the tmux session every new agent window lands in, live at spawn time: it hard-excludes _rk-*-prefixed sessions and the operator's own session, decides from the strongest evidence at hand (monitored agents for the target repo → pane-map repo affinity → the §8 setting → structural dominance), acts default-and-announce on an evidence-backed inference (announce session + deciding evidence in the spawn output, auto-set the §8 setting), and asks (attended) or §5-escalates (unattended) only when genuinely torn — never guess means never decide without evidence, so autopilot/watch ticks decide on evidence too. The operator MUST pass a shell-escaped -t '<session>:' on every new-window: the ambient session is never an implicit target, the session analog of the never-rely-on-CWD rule. Why: The operator may run in its own dedicated tmux session, distinct from the work session(s); tmux new-window without -t resolves against the ambient session, so every spawn path (worker tabs, watch spawns, autopilot spawns — all through the §6 open-tab step) silently misplaced its window there, an observed real failure (z597). The skill's own doctrine already made the fix's shape inevitable: session is context-not-identity (reassigned by move-window/rename, changeable mid-lifetime) and §1 Re-derive-state forbids trusting stale values, so the session must be derived live at spawn time; repo-scoped evidence handles multi-repo naturally because each repo's work clusters where it already lives. The original 4-rung ladder consulted only the monitored set, so every cold start (monitored set empty by definition) fell to an ask-the-user rung — firing exactly when the answer was obvious from the pane map the operator already fetches (live reproduction: the ask's own prompt computed "fabKit — 13 target-repo worktree panes" as its recommendation, then asked anyway) — and unattended cold-start spawns escalated a human round-trip for the same known answer; a wrong landing is cheap to correct (move-window; the -P -F print confirms it), so evidence-backed default-and-announce replaced ask-first (cx52). Rejected: Deriving from the persisted monitored/branch_map session field (stale-prone, and structurally absent for fresh requests — branch_map carries no session); a config-only default work session, incl. a persistent operator.spawn_session key (a single durable value fights the multi-repo model and goes stale — session names are per-tmux-server and ephemeral; the operator may also run with no resolvable fab/ project); ambient-as-implicit-default (the bug itself); keeping the ask-first cold-start rung (asks exactly when the answer is obvious, and recurs every session because the §8 setting is session-lifetime-scoped); Go-side fixes to the sibling ambient spawns (fab pane open's new-window shape, fab batch new/switch) — deferred to a backlog follow-up; the fab operator launcher itself stays deliberately ambient. Introduced by: 260823-z597-operator-spawn-target-session; evolved by 260904-cx52-operator-spawn-session-inference

Spawn-Target Candidacy Delegates to rk mux sessions, Prefix Rule as Fallback

Decision: The operator sources spawn-target candidacy from run-kit's session-role query — rk mux sessions --json behind the standard fail-silent command -v rk gate plus a capability probe (non-zero exit / no output = an rk predating the verb): role: "user" rows are the candidates, minus the operator's own session, and the rows' attached/windows facts feed structural dominance and the announcement. When rk (or the verb) is absent, the fallback excludes _rk-*-prefixed sessions by name (_rk-ctl the control anchor, _rk-pin-* board pin-sessions, _rk-operator the operator session — run-kit's reserved-infrastructure namespace) plus the operator's own session, before any evidence is weighed. Why: rk owns the substrate and derives the roles from its own reserved constants (PinSessionPrefix, ControlAnchorSessionName, OperatorSessionName in run-kit's internal/tmux); its reserved catch-all keeps the exclusion correct when rk introduces a fourth infrastructure kind, which a hard-coded prefix list cannot. Precedent — fab already consumes run-kit's @rk_pane_agent_state pane-option convention as a data contract. The prefix rule stays as fallback because rk is optional on fab hosts and installed builds may predate the verb. Rejected: A blanket _* hidden-session rule (broader than what rk reserves; rk owns the substrate). A stamped @rk_ses_role option (rejected rk-side — roles are name-derivable; run-kit Constitution II/X). Prefix-only exclusion as the permanent mechanism (drifts when rk adds reserved names; kept only as the rk-absent fallback). Introduced by: 260904-cx52-operator-spawn-session-inference; delegation to rk mux sessions per run-kit 260905-csk9-mux-sessions-role-query

The Spawn Command Prints Its Own Landing via -P -F, Feeding Enrollment (z597)

Decision: The §6 open-tab command carries -P -F '#{session_name} #{pane_id}'; enrollment consumes the printed values directly as fab operator enroll --pane <pane-id> --session <session-name>. Why: The skill never specified how enrollment learns --pane/--session post-spawn (a latent gap), and the print-back verifies placement in the same call — converting silent misplacement into a checked output. fab's own Go spawn path already uses -P -F '#{pane_id}' (internal/pane/create.go), so the idiom is established. Rejected: Post-spawn fab pane map scraping (racy, and still unspecified); a compare-and-escalate branch on the printed session (unreachable once -t is passed — tmux errors loudly on a missing session, so a verification feed is sufficient). Introduced by: 260823-z597-operator-spawn-target-session

_cli-external.md Slimmed to Fab-Owned Content, Tool Usage Delegated to <tool> skill (clix)

Decision: _cli-external.md (the operator-only helper) keeps only fab-owned content — the operator spawning choreography (run wt create in the target repo's directory, fab agent --print --repo <target-repo>, tmux new-window with $SPAWN_CMD, the Operator Spawning Rules incl. the /fab-new Step 11 disposable-branch rename, the do-NOT-send-/git-branch rule, and the --checkout-vs-positional routing decision), the escalation rk notify send (the operator's {change}: {summary} ({repo}) / Operator: strategic question message/title template), the absent-binary discipline (one gated class — all four owned binaries command -v-gated fail-silent for delegations, with wt's functional entry points stopping with an install hint) (nnda), the tmux/fab pane notes, and /loop. Each owned tool's tool-owned usage knowledge — the wt create-contract/flags/conflict table + probe-and-route recipe examples, idea's verb/flags/query/backlog-format content, hop's discovery gist, and rk's notify contract + static context/iframe//proxy/{port}//Visual-Display pointers — is removed and delegated at use-time to <tool> skill (command -v-gated fail-silent for all four owned binaries — wt/idea are standalone sibling formulas that may be absent too (nnda)), a sibling of the pre-existing help-dump delegation, scoped to the four owned binaries (tmux and /loop excluded). A required version-skew fallback capability-probes <tool> skill and falls back silently to https://shll.ai/<tool>/skill on an older binary, composing with the command -v gate for rk/hop. _cli-external.md's description: was rewritten to the fab-owned-plus-skill-delegation model, and every cross-reference in the mirror/sweep class was re-pointed: the _preamble.md § Run-Kit pointer does not claim the "full rk command reference" body lives in _cli-external.md; fab-operator.md's § rk reference now names the retained fab-owned notify usage + rk skill delegation (its § wt references still resolve to retained routing choreography). Why: _cli-external.md hand-authored a per-tool gist that restated each tool's own documentation, so it drifted on every tool release (the wt 2af2 contract change forced lockstep edits across _cli-external.md, fab-operator.md, and their SPEC mirrors). The toolkit skill standard (shll standards skill) makes each binary serve a static, ≤150-line, agent-optimized usage briefing that is version-locked by construction — the prose ships inside the same binary as the flags it describes, so it can never document a capability the installed binary lacks; the bundles were seeded from _cli-external.md's own tool-owned rows (shll [agst] SEED RULE), so the tool side is a superset of what fab deletes. Delegation-at-use-time exactly mirrors the file's own proven help-dump pattern — same § Reference Model, same single command -v-gated fail-silent class for all four owned binaries (nnda), same fail-silent discipline — inventing no new convention, and drops per-operator-session context cost. This is the consumer-side half of shll [agst]; the producer side (all four wt/idea/rk/hop skill bundles) was verified shipped at intake. Rejected: Keeping the gists + adding a sync/drift-guard against the tool repos (preserves the second copy and adds machinery — the standard's whole point is that the binary itself is the offline, version-locked source). Re-inlining a retained gist per tool as the version-skew fallback (backlog allowed either form; retaining gists would defeat the slim, and the retained fab-owned choreography already carries the operator-critical wt semantics, so an old binary is a degraded-mode edge, not a data-loss one). Removing the help-dump delegation (it remains for the exhaustive command tree — skill covers usage knowledge; they are siblings per the standard). Fixing the stale docs/specs/skills.md:126 "_cli-external.md carries no SPEC" exclusion note here (a pre-existing inconsistency outside this change's sweep class — deferred as a deletion candidate). Introduced by: 260718-clix-slim-external-md

Agent Primitives Live in _cli-agents; the Operator Keeps Orchestration Policy

Decision: The operator declares helpers: [_cli-agents, _cli-fab, _cli-external] and references the _cli-agents sections for the generic agent-CLI mechanics — spawn composition and window opening (§6 spawn steps 5–6), the pane-exists/agent-state pre-send gate and the delivery probe (§3, §5 auto-answer sends), and capture/state-read peek (§5 question detection) — instead of restating them. Every operator-specific policy statement stays here verbatim: confirmation tiers, bounded retries, the change-active and branch-alignment pre-send checks, repo targeting (always --repo <target-repo>), pointer activation, enrollment and window markers, dependency resolution, and autopilot. The split line is agent primitives vs. operator orchestration — the helper answers how to talk to an agent CLI, this skill answers when and whether to. Why: The mechanics are generic and have consumers beyond the operator, so keeping them inside a 700-line orchestrator only that orchestrator can load forces every other caller to reinvent them (the printed-prompt/send-keys trap most expensively). Referencing rather than copying keeps one home per procedure — the duplicate-truth failure the sibling/mirror sweep rules exist to prevent — and the operator loses nothing: its policy around each primitive is exactly what makes it the operator. Rejected: Leaving the mechanics inline (no other consumer can reach them); copying them into the helper and keeping the operator's copies (two homes, guaranteed drift); moving operator policy into the helper as well (would make an "agent primitives" helper carry autopilot and enrollment — the boundary that justifies the extraction). Introduced by: 260805-nvad-cli-agents-helper-provider-spawn

Role Mark Is Skill-Side, Not Launcher-Side

Decision: The @rk_win_role operator window mark is produced by the skill's startup step (fab-operator.md §2 Startup § Role Mark), not by the fab operator Go launcher. Why: The skill runs in the window regardless of how it was launched (via fab operator or manually), so the self-mark covers both paths; it also keeps the change Pure Prompt Play (Constitution I) with zero Go surface. Rejected: Marking from the Go launcher — misses manually-started operators and adds a binary change for a one-line producer. Introduced by: 260813-swun-operator-role-self-mark

Role Mark Suppresses Errors Beyond the command -v Gate

Decision: The rk role call carries >/dev/null 2>&1 || true in addition to the command -v rk gate. Why: An installed run-kit may predate the role subcommand (probed true at the time the step shipped) and errors on it; the _preamble.md fail-silent rule must extend to version skew or every startup on an older rk prints an error. Rejected: Bare command -v rk && rk role operator — fails loudly on any rk predating the companion feature. Introduced by: 260813-swun-operator-role-self-mark

Full Mediation of State Writes

Decision: Every operator-state-file mutation — including observed-field updates (fab operator update), watch bookkeeping (watch checked/seen/complete), and the tick mechanics — goes through a fab operator verb; the operator never writes the YAML itself. The doctrine extends to tick mechanics: fab operator tick-start --diff computes the per-tick diff inside the binary and is the authoritative writer of each monitored entry's observed-field baseline (stage/agent, touching last_transition iff the stage changed) in the same atomic mutation as the tick bookkeeping — the diff path involves no per-tick fab operator update stage/agent bookkeeping; update covers non-baseline field edits (e.g. stop_stage). The schema block in fab-operator.md §4 is reference documentation of what the binary maintains, not a format the agent composes. The doctrine's scope extends beyond writes to computation and detection policy: narrative state (operator note) and the tick snapshot/diff (tick-start --diff) already compute binary-side, and fab pane questions is the first detection-policy offload — the auto-nudge sweep's mechanical guards and indicator patterns live in the binary (table-driven-tested) rather than in skill prose applied freehand per pane; the genuinely judgment-requiring steps (the answer model, the delivery guards) stay LLM-side. Why: "Schema cannot drift" holds only if no hand-write path remains; the motivating incident (2026-08-13: a gpt-5.6-luna session hand-edited the YAML with an invented free-form field instead of a §7 watch entry) was a tick-time watch write. "Run a command with flags" is the instruction class weak models follow reliably; whole-file YAML regeneration is the class they fail — and prose-enforced schema fails exactly where the operator is meant to become provider-agnostic. Rejected: Mediating only structural ops (leaves the highest-frequency writes hand-edited); a post-hoc schema validator (detects drift instead of preventing it, and still leaves timestamp/pruning computation on the agent); schema-validated whole-file write on stdin (still makes weak models compose YAML; rejection loops stall the tick loop); leaving detection policy in skill prose (the guard/pattern list is regex matching over captured text — redone in natural-language reasoning every tick, untested, and free to drift from the documented list as the skill file is edited). Introduced by: 260819-m7kq-operator-state-binary-subcommands (scope extended to detection-policy offload by 260823-dckc-pane-questions-sweep)

Tolerant-Read / Typed-Write State IO Posture

Decision: The shared state IO helper (src/go/fab/cmd/fab/operator_state.go) preserves unknown top-level keys on read-modify-write (the tick-start precedent), while the five owned sections (monitored, autopilot, branch_map, watches, notes) are re-marshaled from typed structs on mutation — drift inside an owned section can neither be introduced nor survive a mutation of that section. No strict-decode hard error; the schema stays byte-compatible with the documented §4 shape and no migration file exists. Why: Strict decode would wedge the operator on any pre-existing hand-drifted file (the file is machine-local XDG state re-read every tick); tolerant top-level + typed sections prevents new drift while degrading gracefully on old files. Rejected: Strict KnownFields decode — turns legacy hand-drift into a hard loop failure. Introduced by: 260819-m7kq-operator-state-binary-subcommands

Window Markers Stay Separate Primitives

Decision: fab operator enroll/remove touch only the state file; the »/ window-name renames remain the operator's separate fab pane window-name ensure-prefix/replace-prefix calls. Why: Composable primitives match the existing pattern; rename-failure handling (log-and-continue) is operator policy layered on top, and enrollment durability must not depend on tmux reachability. Rejected: Folding the rename into the state verbs — couples a state write to a tmux side-effect with different failure semantics. Introduced by: 260819-m7kq-operator-state-binary-subcommands

Operator-Side PR Retarget Over a /git-pr Base Parameter

Decision: In stacked-prs, the operator retargets each dependent PR's base after creation (gh pr edit <pr> --base <dep-branch>); /git-pr stays unchanged and mode-unaware. Why: Mode is operator state — the worker agent running /git-pr inside the spawned pane has no mode awareness, and plumbing it through the spawn command adds a parameter to a shared skill for one consumer. Retarget-after-create is one gh call at a point where the operator already collects the PR URL. Rejected: A --base parameter on /git-pr — couples a pipeline-generic skill to operator-only state and still needs the operator to compute the base. Introduced by: 260820-t6rq-autopilot-merge-modes-stacked-prs

Mode Fixed at start for the Queue's Lifetime

Decision: --mode is set once at fab operator autopilot start and persisted; there is no mode-mutation verb. Changing mode means stop + a new start. Why: A queue's dependency topology is built per-mode at spawn time (branch-off-dep vs cherry-pick) — switching mid-queue would leave half the queue in the wrong topology. The persisted-at-start design is also the minimal fix for the /clear durability gap. Rejected: A mode mutation verb — could be added later without breaking this contract, but today it would imply a mid-queue topology switch the choreography cannot honor. Introduced by: 260820-t6rq-autopilot-merge-modes-stacked-prs

Auto-Merge Choreography: Sequential Arming with Verified-Merge Gating

Decision: Merge-all for the modes whose PRs target main (cherry-pick-ladder, and merge-auto's per-PR merges) rides GitHub auto-merge under a five-MUST-rule choreography: at most one armed PR per repo-sequence, armed only after the predecessor's merge is verified via a timeline event (never an assumption, never a PR whose base is another PR's branch); the three arming-failure shapes are handled (draft → gh pr ready, already-clean → merge directly, repo-disabled → foreground CI-wait fallback); the per-tick stall check treats a failed required check as the CI failure and, after 3 quiet ticks, polls gh pr view --json mergeable with CONFLICTING → disarm and escalate; the sequence persists as a kind: coordination note carrying per-repo-sequence positions (armed PRs outlive the operator, and the note is a loop run-condition); and a halt disarms the halted sequences' armed PRs (dependent cone — independent sub-sequences continue; a user stop disarms all). stacked-prs keeps the manual foreground merge-all. Why: Foreground CI-watching between ordered merges was the longest operator-busy stretch in the coordination lifecycle, at exactly the moment the fleet is otherwise idle. Arming converts it into a passive tick check — but a bare "just use auto-merge" is dangerous: an armed PR outlives the operator and fires without an event, so sequencing must be operator-verified, the sequence must survive /clear/crash, conflicts must be polled for (auto-merge fails silently), and halting must disarm what GitHub would otherwise still merge. Rejected: Arm-all-upfront (unordered merges; wrong-base hazard). Including stacked-prs (the win was always marginal there — its inter-merge choreography is operator-sequenced anyway — and its failure modes are the worst: an armed stacked PR can merge into its dependency's branch, destroying the stack silently, or fire on stale-green checks after GitHub's no-re-CI base retarget). A new state-file field for the sequence (the Phase A coordination note already carries exactly this shape). Introduced by: 260823-2c92-operator-auto-merge-choreography

Level-Triggered vs Consumed-on-Read Delta Classes

Decision: fab operator tick-start --diff emits deltas in two delivery classes. Level-triggered, re-emitted every tick until fab operator remove ackscompletion, pane_death, pane_mismatch, agent_exited: stateless predicates over the current snapshot that need no baseline read. completion is a display-state predicate at a stage (review-pr done/skipped, or at/past stop_stage); pane_death is the pane's absence from the snapshot; pane_mismatch is the entry's change ID disagreeing with the snapshot row's resolved change; agent_exited is the entry's pane present and change-matched, with both a shell foreground and no live agent process at the pane-tree root or among its descendants. The liveness walk runs only in the shell branch, positive live-agent evidence produces a clean join, and PID/tree errors fail toward emitting. agent_exited is evaluated after pane_death and pane_mismatch, so a mismatched shell-fronted pane emits only pane_mismatch. Removal is the natural ack: the entry disappears and the event stops. Consumed-on-read, baseline-diffedstage_advance and review_fail (review→apply): computed against the entry's stored stage and consumed by the same-write baseline update; a lost one costs a missed report only. pane_mismatch excludes a recycled pane from diffing, baseline writes, candidates:, and the fleet: join. agent_exited mirrors those exclusions because a wrapped interactive spawn leaves the pane alive as a shell and its stale @rk_pane_agent_state can still read as a nudge candidate. (260902-ssyf-operator-liveness-process-tree) Why: Action-demanding deltas re-emit until the operator reports and removes them, so a crash between diff and action loses nothing; report-only stage deltas need no durable per-event acknowledgement. Positive process-tree confirmation is necessary because on Linux the wrapped interactive spawn's shell remains the foreground process-group leader for the live agent's entire lifetime, while failing toward emitting prevents an unconfirmed bare shell from entering the nudge population. Rejected: One uniform consumed-on-read class for every delta (a crash between diff and action loses action-demanding events permanently). One uniform level-triggered class (requires durable per-event ack state for events whose loss is harmless). Stage-diffing completion (the stage string does not change at the final stage). Agent state as exit evidence (fab ignores its PID segment and the pane option can retain the agent's last value). pane_pid childlessness or #{pane_current_command} as a primary liveness signal: an unwrapped single-command spawn can make pane_pid the agent with zero children, while a wrapped Linux spawn keeps the shell as process-group leader for the live agent's entire lifetime. The selected secondary confirmation is different: it runs only after the shell trigger and searches the root plus descendants for a recognized live agent process, not merely for children. Introduced by: 260823-dbwg-operator-tick-diff-offload

Completion Is the Pipeline Terminus, Dependency Satisfaction Is Completion Plus PR

Decision: With stop_stage: null, tick-start --diff's completion predicate is stage == review-pr && display_state ∈ {done, skipped} — a display-state check at the terminus, never bare stage membership. Early parking is expressed through stop_stage (--stop-stage hydrate for /fab-ff spawns). Separately, the skill defines dependency satisfied once (pipeline completed and, for same-repo null-stop_stage deps, PR exists) and every consumer — both resolution tiers, the autopilot advance/spawn step, watches — points at it. Why: A hard-coded {hydrate, ship, review-pr} completion set fired on stage entry under /fab-fff — every hydrate/ship transition emitted completion each tick and drove premature fab operator remove (100% reproducible, 2026-08-28). Same-repo dependencies were gated only on the branch resolving in branch_map, which enroll writes at spawn, so a running dep looked satisfied; only the (buggy) completion signal protected autopilot. PR-exists is the cheapest observable proof a dep branch is pushed and stable for cherry-pick or a stacked retarget. Rejected: Keeping {hydrate, ship} in the set with an added display_state check — hydrate/done is a transient window in /fab-fff (finish auto-activates ship), so it still races. Branch-resolvable ⇒ satisfied (the implicit status quo). A parked-/fab-ff heuristic without stop_stage. Introduced by: 260828-nr3a-tick-completion-review-pr-only

Quiet Tick Replaces fleet: With Counts; Compact One-Line Frame on No-Change Ticks

Decision: fab operator tick-start --diff --quiet replaces the fleet: block with a five-count fleet_summary: on a no-delta tick that is not a multiple of the built-in constant 10; the skill renders a one-line compact frame from it and the full frame only when fleet: is present. Flagless --diff stays byte-identical and the skill always passes --quiet. Why: Every tick appended a full fleet document plus a full multi-table frame even when nothing changed, so context grew linearly with wall-clock time on a quiet fleet and real events were buried in identical frames. The binary already owns "did anything change" (deltas), so it is the right place to decide the payload; the every-10th-tick full document keeps a periodic complete view without a config surface. Rejected: A flag or config knob for the interval (hardcoded 10 mirrors the §5 30m precedent); emitting fleet_summary: alongside fleet: (no byte saving); a small table for the compact frame (one line is the point); making --quiet the default of --diff (breaks byte-identity and the version-skew fallback); a Go daemon heartbeat ([2ne8], separate). Introduced by: 260829-kbf2-operator-quiet-tick-compact-frame

Loop Prompt Is a Bare Token, Never a Slash Command

Decision: The operator's /loop prompt (and the dynamic-mode wakeup prompt) is the literal bare text operator tick; /fab-operator or any slash command in that position is prohibited, and the literals are printed at Init and at the loop-start point so the agent copies rather than composes. Why: Slash commands macro-expand their full source into the turn on every firing; fab-operator.md is ~21k tokens, so a /fab-operator loop prompt re-pays the whole skill each tick (~400k tokens/hour at 3m) and exhausts the context window in ~10 ticks — observed live 2026-08-29. The tick procedure is already in context; the prompt only needs to name it. Rejected: Leaving the rule as a single prose mention (it was there and still drifted); a per-tick --quiet frame (orthogonal — shipped separately as 260829-kbf2-operator-quiet-tick-compact-frame); a Go daemon heartbeat (removes the LLM tick entirely — much larger, backlog [2ne8]). Introduced by: 260829-4q3l-operator-loop-prompt-hygiene

Compaction Recovery Is a One-Shot Reload, Not an Agent /clear

Decision: The agent never /clears (it cannot). When a tick arrives and the Tick Behavior procedure is not in context, it runs /fab-operator exactly once (reload + §2 Init), treats that tick as consumed, and resumes bare operator tick firings. Autopilot has no separate cadence — it rides the single 3m/90s loop. Why: The old "Self-manage context" principle named a user-only mechanism and omitted the one that actually happens (harness auto-compaction), leaving the agent with no procedure for "told to tick, but no tick procedure in context". Level-triggered deltas re-emit on the next tick-start --diff, so the consumed tick loses nothing durable. A second autopilot cadence contradicted the one-loop invariant and had no real owner (circular pointer between fab-operator.md and _cli-external.md). Rejected: Instructing a periodic /clear (impossible for the agent); keeping /fab-operator in the loop prompt to "stay loaded" (the failure mode itself); relocating a 2m autopilot cadence into §4 (a second cadence for one loop). Introduced by: 260829-4q3l-operator-loop-prompt-hygiene