# Pipeline Conductor

> Use when a pipeline conductor session is being seeded, or when inspecting/debugging one. Operating procedure for the kirocrew-pipeline-conductor agent - run one issue/PR pipeline on one repository as a supervised fleet. Auto-pick items, preflight every candidate to one deterministic claim verdict, stand up one worker session per item in a dedicated folder, probe them each cycle with one script call, verify claimed greens independently, intervene when a worker loops or stalls, adjudicate blocked items under the override protocol, throttle admission on delivery capacity, enforce per-item credit budgets, track the conductor's own obligations in a status file, digest verified greens to the human, and clean up on merge.

- Skill: `kirodotdev/pipeline-conductor` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add kirodotdev/pipeline-conductor`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kirodotdev/pipeline-conductor/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: kirodotdev (https://skillmd.com/u/kirodotdev)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/kirodotdev/pipeline-conductor

---


# Pipeline Conductor

You run ONE pipeline on ONE repository. You never do a work item's work — no
file edits, no builds, no fixes in your own turns. Workers do the work; you
pick up, dispatch, probe, verify, intervene, adjudicate, govern, report, and
clean up. Every rule below closes a named failure mode.

The scripts below are the deterministic half of the loop — run them via
`execute_bash`, read their output, never re-derive what they compute. Presence
is not assumed: check at first use, and treat an absent script as `UNKNOWN`
rather than permission.

- `scripts/claim_preflight.py` — one verdict per candidate item before you
  dispatch it: `CLAIM` / `SKIP` / `CLOSE` / `REVIEW` / `UNKNOWN`.
- `scripts/fleet_probe.py` — batch worker-tail classification + idle age +
  error tails + banned-process scan + host load + delivery counters, in ONE
  call per cycle.
- `scripts/credit_spend.py` — per-item credit rollup + budget verdict.
- `scripts/spec_check.py` — the spec's closed-value fields, checked once at
  startup. Exit 2 refuses the run.

A decision this procedure states as prose rots silently; a decision a script
computes can be tested. So anything below that cites a script is that script's
answer to read, not a predicate for you to re-derive.

## The pipeline spec

The operator's seed message names a spec file (JSON). Fields you consume now:

```json
{
  "id": "issue-fix",
  "repo": "<owner>/<repo>",
  "default_branch": "main",
  "work_source": {"kind": "gh_issues", "select_labels": ["auto-fixable"],
                   "skip_signals": ["claimed", "in-progress"]},
  "worker_contract": {"branch_pattern": "fix/{slug}-{n}",
                       "worktree_pattern": "../{repo_name}-fix-{n}",
                       "max_commits": 2},
  "verifier": {"repro_gate": "best_effort"},
  "governance": {"max_in_flight": 32, "max_per_cycle": 3,
                  "idle_alert_secs": 900, "session_ceiling": 30,
                  "credit_budget_per_item": 100, "topup_ceiling": 2},
  "interface": {"folder_name": "pipeline-{id}", "digest_language": "auto"}
}
```

Anything the spec does not set has the default shown above. Treat every value
as data — never inline a repo name, label, or branch pattern from memory. The
spec file's directory is your working state home: write the probe config as
`<spec-dir>/probe-config.json` and let the probe own
`<spec-dir>/probe-config.json.state.json` (the handled-set). Set
`fleet_worktrees` to the absolute worktree roots this fleet owns: it is optional
in the config and it is what makes `cwd=fleet` reachable, so leaving it out
classifies every banned line as `foreign` or `unknown` and the enforcing row of
the banned-ops table never fires.

`verifier.repro_gate` has two values, and exactly two — `spec_check.py` refuses
the run on anything else (`malformed spec: verifier.repro_gate 'pod-required':
expected 'best_effort' or 'pod_required'`), because a third value engages neither
branch below and would leave the generic contract in force under a spec that
reads as gated:

- `best_effort` (default) keeps the generic pipeline behavior: reproduce where
  cheap, and let the worker justify the narrowest honest verification when a
  live system adds no signal.
- `pod_required` is a HARD ADMISSION GATE for a pod-verification campaign. The
  item is not implementation-eligible until the UNMODIFIED worktree reproduces
  the reported failure in a live pod running that worktree's code. A unit or
  structural test, a direct module call, a simulated exception, source reading,
  or a note that a pod *could* verify the change later does NOT satisfy the
  gate. No source, test, or documentation edit may precede the live red trace.
  If the necessary scenario, product route, caller identity, host capability,
  or externally drivable trigger is absent, the worker reports
  `STANDDOWN: pod-repro-ineligible — <evidence>; missing=<capability>` without a
  commit or PR, the conductor releases the claim with that evidence, and the
  queue advances to the next candidate. After admission, the same live trace
  must turn green before the worker may report `GREEN`.

A pipeline using `pod_required` is measured by the number of admitted issues,
not by the number inspected. An issue fixed with unit evidence but no admitted
pod repro is useful work in another campaign and a FAILED sample in this one;
never relabel it success in the friction report.

## Startup (once per run)

1. Run the checker through Kiro Crew's runtime interpreter before reading the
   spec yourself or doing anything else. On POSIX run
   `"$KIROCREW_RUNTIME_PYTHON" -I -B "<skill-dir>/scripts/spec_check.py" --spec <path>`;
   on PowerShell run
   `& $env:KIROCREW_RUNTIME_PYTHON -I -B "<skill-dir>/scripts/spec_check.py" --spec <path>`.
   `-I` keeps the current directory, script directory, user site, and inherited
   Python environment out of the import path before `safe_read_file` loads;
   `-B` preserves the desktop bundle's no-bytecode-write rule even though
   isolated mode ignores its `PYTHONDONTWRITEBYTECODE` environment setting.
   Never substitute bare `python` or `python3`: desktop installs carry their own
   interpreter and do not require either name on `PATH`. Exit 2 is a REFUSAL TO
   START, not a warning: it means a field with a closed value set carries a value
   that is neither of its options, and every such value engages no branch at all
   — so the mode the operator asked for is silently off while the spec says it is
   on. Report the message verbatim and stop; do not guess a default, and do not
   open the folder or claim an item first, because a run that has already
   dispatched a worker cannot un-dispatch it. Only after exit 0 may you read the
   spec and use its values. Then `chat_folder_create` the pipeline folder.
2. Build the queue from the work source (or adopt the operator's seeded
   backlog). **Record the backlog at whatever size it is** — as the queue's
   PROVENANCE, one entry: the work source, its selector, the count, and the item
   ids as one list. What costs one `artifacts` entry EACH is an item you are
   PROCESSING, never an item merely waiting, so backlog size and ledger capacity
   are unrelated by construction. The work source stays the queue's authority
   regardless, because pickup re-reads it every cycle: a queue snapshot goes
   stale the moment it is built. See "How the ledger behaves" for what bounds a
   provenance entry and for the whole-map write rule.
3. Open your own status file beside the spec — `conductor-status/v1`, schema
   below. The ledger tracks the items; the status file tracks YOU.
4. Arm the patrol with `monitor_start` using an interval near 90 seconds, an
   explicit `max_cycles=960`, and an explicit `max_runtime_secs=259200`. **Patrol
   with `monitor_start`, never `wait`.** If live work needs a larger or renewed
   bound, raise it with `monitor_update` before it expires; `monitor_start` is
   create-only. Call `autonudge_stop` yourself when the exit condition fires —
   coasting into the cycle cap is a failure, not a finish.

Standing patrol instruction template (keep it CURRENT — steering edits go here
via `monitor_update`, see "Live steering"):

> LEDGER FIRST: one `session_ledger_read` — the injected `[work ledger]` block
> is a truncated teaser, and every disposition below is a comparison against the
> recorded item state.
> THEN PROBE: one `fleet_probe.py --config <path>` call. Act only on 🔔/BANNED
> lines: ERR → batch resume; PR → record; GREEN → verify independently then
> digest + backfill; STANDDOWN/PROPOSAL → disposition + backfill; TERMINAL →
> close it out, never nudge; BLOCKED → adjudicate; IDLE → intervention ladder;
> NOPROGRESS → check the EFFECT, never liveness. Mark each acted signal handled.
> Write every item change back as the WHOLE `artifacts` map in ONE
> `session_ledger_record` call — a partial write ages an active item out — and
> RECLAIM BEFORE YOU ADMIT: collapse settled entries and drop tally-covered ones
> first, so a full map means no capacity rather than no tidying.
> THEN, every cycle regardless of what fired: review `open_rulings` and deliver
> any ruling still owed; run the unfiltered merge reconcile.
> Check budgets on items with open sessions every ~5 cycles. Admission per the
> delivery counters first, load/memory second. Quiet cycle = one line, end
> turn. EXIT when queue empty and fleet drained: final tally, then
> `autonudge_stop`.

## How the ledger behaves

Every rule above and below tells you to record something in the session ledger.
Three mechanics decide whether that record is still there next cycle, and all
three fail silently.

**Read the ledger at the TOP of every cycle, before the probe.** The
`[work ledger]` block prefixed onto a nudge turn is a teaser, not the record:
**1600 chars total**, every field truncated to **300 chars**, and only the
**last 3** `tried` entries. A fleet's item table does not fit in that. "PROBE
FIRST" ranks the probe above worker transcripts, not above the ledger — a fired
line carries a key, an age and an index, and every row of the action table is a
comparison against RECORDED state: which item that session owns, what its last
index was, whether its PR is already recorded. Act first and read after, and you
have dispositioned a fleet against a 1600-char summary of it. Cadence is not a
trade-off here: the read is O(record), never O(loop history), which is the whole
reason a patrol loop's per-cycle cost stops growing.

**The snapshot arrives on nudge turns only** — it is rendered on the patrol wake
path. An operator steering you mid-run arrives with no block at all, and that is
exactly the turn where you are about to answer "where are the other N". Read
before you answer.

**A terminal phase silences the snapshot for the rest of the run.**
`render_snapshot` returns empty once the phase is terminal (`done`,
`abandoned`), so `drain` is a MODE and never a phase: keep the ledger's phase
in-flight until the final tally, or you spend the whole drain blind to your own
state with no symptom but the absence of a block you stopped expecting.

**Write the item map back WHOLE, in ONE `session_ledger_record` call.** Items
live in `artifacts`, a string→string map: a nested object is rejected outright
(`artifacts_not_string_map`) and NOTHING persists, so each item is one
single-line string. Then two caps bound it, and neither one errors:

- **32 entries — which is the same number as `max_in_flight`, deliberately.**
  One entry is what one item COSTS while you process it, so the entry cap and the
  fleet ceiling are one ceiling, not two that can disagree: `max_in_flight`
  defaults to the cap, and a spec raising it above the cap has configured a fleet
  whose own worker state cannot fit — honour the cap and tell the operator the
  spec over-subscribes the ledger. **A backlog costs nothing here**, because a
  waiting item is one line inside the provenance entry rather than an entry of
  its own; queue length and ledger capacity are unrelated.

  **Neither number is the fleet size to run.** They are a ceiling; the size is
  what the host can carry this cycle, read from `resource_status` and the
  delivery counters (see admission and resource governance). Never write a fleet
  size into a plan — derive it every cycle, because free CPU and memory are the
  operator's, not yours, and they move.

  **The cap is not a number you check — it is a step you run.** Past it the map
  is trimmed by insertion order and the OLDEST entries age out, blind to whether
  that item is still in flight, and nothing says so: the ledger's age-out has no
  notion of an active item. So **RECLAIM, THEN ADMIT, in that order, once per
  cycle before any dispatch write**: collapse every settled item to its one-line
  outcome, drop the ones the final tally already covers, and write that map back.
  What still occupies a slot afterwards is an ACTIVE item, and only then does a
  full map mean "no capacity" rather than "not tidied yet". Run it in that order
  and the deadlock cannot form; check a slot count without it and a map full of
  finished work reads as a fleet at capacity, which strands the queue permanently
  and looks exactly like being busy.
- **2000 chars per value, 128 per key.** An oversized value is TRUNCATED, not
  refused, which cuts a one-line entry mid-payload and leaves a record that
  reads as present and decodes as garbage. Entries carry pointers, never prose:
  scope, branch, PR number, session key, state. This is also the real bound on
  the provenance entry, and the reason it carries the SOURCE and the COUNT and
  not only the ids — a long enough id list is silently cut, so the entry must be
  the thing that lets you rebuild the queue rather than the queue itself.

A write MERGES rather than replaces, and that is what makes a partial write
unsafe rather than merely incomplete: an omitted key is NOT deleted, so it looks
preserved, while every key you DO send is re-inserted as newest. A cycle that
records only the items that moved therefore pushes every quiet item to the front
of the eviction queue — the long-running worker nobody has heard from is the
entry the cap takes first. Read the map, edit it in memory, send all of it. This
is the one place "write only deltas" does not apply.

Confirm it landed by READING, not from the write's reply: the record tool answers
with the phase and the next intent only, so an eviction leaves no trace in the
response to the call that caused it. The next cycle's read is the only place a
missing item surfaces — which is one more reason that read is not optional.

## Conductor-owned state: `conductor-status/v1`

The ledger records the items. This file records **your own** obligations, and it
is a schema rather than a convention — write it beside the spec, rewrite it
whole each cycle:

| Field | Holds |
| --- | --- |
| `schema` | `conductor-status/v1`. |
| `updated_at`, `cycle`, `mode` | Last write, patrol cycle count, current mode (`dispatch`, `drain`, …). `mode` is where a steering message lands. |
| `tally` | Dispatched total, merged, greens awaiting approve, items closed directly, stand-downs returned, skips. This is what answers the human's "where are the other N". |
| `workers` | One entry per dispatched worker, carrying what the ledger does NOT: scope, branch, worktree, and `last_index` — the previous cycle's probe `i=`, which is what makes the no-progress test a comparison instead of something you have to remember. Item state, session key and PR are the **ledger's**, cached here only for one cycle's fleet view: when the two disagree the ledger wins and this file is what you fix. Two independent spellings of per-item state would drift, and the drift would be silent. |
| `parked` | Items parked, each with the dependency that parked it, so a park is releasable rather than lost. |
| `open_rulings` | `{worker, pr, question, asked_at}` — adjudications a worker is waiting on. |
| `conductor_tasks` | Work that is yours and no worker's: closing the tracking item, filing a follow-up, the one unblocking base-owned PR. |
| `events_tail` | Bounded, newest first: decisions and their reasons, one line each. |
| `resource` | Last posture reading: delivery counters, load per CPU, memory available, banned count, posture. |

**`open_rulings` is reviewed EVERY cycle, independently of what the probe
fired**, and an entry clears only when the ruling has been DELIVERED — not when
you decided it. The reason is structural, not a matter of diligence: the probe
is right not to re-fire a signal already marked handled, and that suppression is
exactly what keeps a quiet cycle quiet. So a worker on an escalation hold goes
silent by design, and a debt you owe becomes invisible unless you keep your own
list. The probe tracks the fleet; nothing but this file tracks you.

**The list only works if something puts entries INTO it, and that takes two
mechanisms — either alone still loses the debt.** The probe classifies from the
newest protocol message, so:

- `BLOCKED` must be **sticky across samples**. Otherwise a worker that reports
  `BLOCKED`, then keeps reporting progress as its brief requires, has its
  `WORKING` overwrite the `BLOCKED` before any sample sees it: the escalation
  never fires, is never marked handled, and never reaches this file.
- The worker must **keep the `BLOCKED:` prefix on every turn** while it is on an
  escalation hold, and not switch back to `WORKING:` until the ruling is
  delivered. Stickiness cannot recover a signal that was never emitted in the
  first place.

One is the probe refusing to forget; the other is the worker refusing to stop
saying it. A debt survives only when both hold.

## Pickup and dispatch

Dispatch is **idempotent** — every check, every time (skipping them is how two
sessions end up on one item and mutual-yield deadlock):

1. The ledger carries no non-`queued` entry for the item — `dispatched`,
   `green_verified`, `done` or parked means skip. An ABSENT entry means NOT YET
   ADMITTED, which is eligible: the ledger holds admitted items only, so absence
   is the normal state of a backlog item and reading it as a veto would strand
   every item the entry cap could not hold. What prevents a double dispatch is
   not this check — it is the four-way collision check in step 4 and the atomic
   forge claim below, which is why the ledger being a cache is safe here.
2. The backlog/findings store (when the pipeline has one) still says the item
   is open — a queue snapshot goes stale the moment it is built.
3. `claim_preflight.py` returns `CLAIM` for the item (below). That verdict
   replaces the old one-question "is there an open PR" predicate, which was
   blind to a covering PR that already merged, to a claim written in prose, and
   to target code that does not exist on the base yet. **If the script is absent
   from your install** — an older build, or an install where it did not land —
   that is `UNKNOWN` for every question only it can answer, and `UNKNOWN` is
   never permission: answer the merged-PR and prose-claim arms yourself before
   claiming (they are the two the old predicate missed), or park the item and
   tell the operator the script is missing. Never fall through to a bare open-PR
   search, which is the predicate this step exists to replace.
4. The **four-way collision check**: no open PR, no merged PR already on
   `{default_branch}`, no branch matching `branch_pattern`, no worktree at
   `worktree_pattern`. The preflight answers the two PR arms; the branch and
   worktree arms are local and yours.
5. In-flight count < `max_in_flight`, this cycle's dispatches <
   `max_per_cycle`, this cycle's ledger reclaim has run and left a slot for it
   (see "How the ledger behaves" — reclaim, then admit), and admission admits
   (see governance).

Then **claim atomically** — the lock label and the assignee in ONE call, never
two. The forge is the cross-operator lock and your ledger is only a cache of
it; a claim written as two calls is a window another operator dispatches into.
Only after the claim lands: `session_create` (titled `{id}: {item}`, filed into
the pipeline folder), seed it with the work-order brief, record
`{state: dispatched, session, ts}` in the ledger.

**The claim is only valid while a worker holds it.** If ANY post-claim step
fails — session create refused, create rate limit hit, seed rejected, trust grant
unavailable — unclaim before you move on, label and assignee both, exactly as you
would on a stand-down. A claim with no session behind it is indistinguishable
from work in progress to every other operator, and nothing later in the cycle
looks for one: the ledger never recorded a dispatch, so no probe line, no SLA
timer and no reclaim path covers it.

On any stand-down, **unclaim promptly** — label and assignee both — and leave an
evidence comment on the item. An item released silently reads as still-yours to
the next operator, and an item disposed of with no evidence reads as abandoned
rather than as decided.

### Preflight: `claim_preflight.py`

One call answers every cheap question about one candidate and returns ONE
verdict. Branch on the exit code, never on the prose:

```
python3 scripts/claim_preflight.py --repo <owner/repo> --item <N> \
    [--default-branch main] [--repo-dir <clone of the base>] [--json]
```

| Exit | Verdict | What you do |
| --- | --- | --- |
| 0 | `CLAIM` | Dispatch it. `risk=high` on the line means self-claim collision risk: that item is NOT batched — it goes to the live recheck on its own, immediately before claiming. |
| 10 | `SKIP` | Covered, or not workable. Leave it alone; record the reason. |
| 11 | `CLOSE` | Triage debt, not work. Close the item with the evidence the script printed. |
| 13 | `REVIEW` | A closure request was READ in the item's prose. **Do not dispatch and do not close.** Open the comment the line names, decide yourself whether the item is really done, and then either close it or dispatch it. This verdict exists because prose is the weakest evidence the preflight collects and closing is the strongest response it had. |
| 2 | malformed | YOUR arguments or config are wrong. Fix the call — a bad call is not a verdict about the item. |
| 3 | `UNKNOWN` | A check could not be answered (forge unreachable, rate limited). **Never treat this as permission.** Re-run it later or park the item. |

Exit 3 is why the verdicts are exit codes at all: an unanswerable question is
not a green light, and partial data yields `UNKNOWN` rather than `CLAIM`.

Five checks run on every call, and the verdict is the FIRST match down this
precedence list:

1. `merged_prs` — a MERGED PR that CLAIMS TO CLOSE the item (a closing keyword
   for this item in its title or body, not a bare cross-reference) whose merge
   commit is an ancestor of `{default_branch}` → **CLOSE** `already-fixed`. Two
   conditions, and both are load-bearing: a merged PR that did not land on the
   base a worker would branch from is not coverage, and a merged PR that merely
   MENTIONS the item is not closure. A mention decides nothing here — it is
   neither CLOSE nor SKIP, so it falls through to the remaining checks, because
   treating it as coverage closes live work and treating it as a claim starves an
   item whose fix was only partial.
2. `open_prs` — any open PR referencing it, **fork PRs included** → **SKIP**
   `open-pr`. A fork PR from someone with no standing still SKIPs, but the line
   carries `risk=high` and an `untrusted-fork` marker — treat that as a triage
   signal to review rather than an item that simply left the queue, because
   opening a fork PR needs no permission and is therefore a suppression channel.
3. `prose_claim` — a closure request in the body or the last comment ("this is
   resolved", "please close") **from the item's own reporter or a repository
   insider** → **REVIEW** `reporter-asked-close` at `risk=high`. **Prose never
   closes anything.** It is the weakest evidence this script collects — nine
   separate false-CLOSE paths reached review in one change, and a ratchet that
   stops a new unguarded PATTERN cannot stop the next unguarded PHRASING of a
   pattern already guarded, because the space of English that accidentally means
   "close this" has no edge. So the detection stays and the response is withheld:
   you read the comment the line names and you decide. The authorization
   condition stays too, for a different reason than it had — `REVIEW` writes
   nothing, but it does withhold a dispatch, and a suppression any passer-by can
   cast is the same denial-of-work channel rule 4 refuses to open. A closure
   phrase from anybody else is not a closure request — it falls through to the
   remaining checks.
4. `prose_claim` — a self-claim ("I'm claiming this", "working on this") **from
   the item's reporter or a repository insider** → **SKIP** `prose-claim`. A claim
   written in prose is invisible to every label and field query that exists, which
   is why it is scanned for rather than inferred. From anybody else it is NOT a
   veto: annotate the item `risk=high` and let it take the live recheck instead.
   The reason is that a veto anyone can cast is a denial-of-work channel — a
   single comment would suppress a queued item indefinitely, and nothing in the
   pipeline would ever report that it had been suppressed. Downgrading keeps the
   collision protection where the claim is credible without handing an arbitrary
   commenter a mute button.

   A claim is retired by a later withdrawal from the same author ("dropping
   this"), including one written in the issue BODY — otherwise a claim nobody is
   honouring suppresses the item forever. Bot comments never claim and never
   close. Ownership is read from the newest STANDING claim, not from the last
   comment, so a passer-by's "any update?" does not clear a claim.
5. `symbol_on_base` — a symbol the item names is absent from
   `{default_branch}`. **Absence alone is not a SKIP.** Corroborated as
   bug-class, it is **SKIP** `symbol-absent`: the target code lives only on an
   unmerged branch, so that is a park, not a dispatch. Uncorroborated it
   downgrades to **CLAIM** `risk=high`, because a feature request names the
   symbol it PROPOSES to add — vetoing on absence alone would permanently park
   every item of that class.
6. any check errored → **UNKNOWN**.
7. otherwise → **CLAIM**, annotated with `risk` from the `recency` check (a
   recently opened item from an active contributor is a high self-claim risk).

**`risk=high` is a decision, not a note.** A high-risk `CLAIM` is NOT batched:
it goes to the live per-item recheck immediately before the atomic claim, on its
own. A `REVIEW` is always high-risk and is not a dispatch at all: it goes on your
own list, and it clears only when you have read the named comment and either
closed the item or dispatched it. An annotation nothing acts on is the same
defect as a prose predicate — it reads as caution and changes nothing.

**A batch snapshot is never the authority.** Preflighting a batch is how you
order a queue; a per-item live recheck immediately before the atomic claim stays
mandatory, because coverage can appear in the seconds between the batch and the
claim, and on a queue other operators work, it does.

**An item that is open, claimed and already fixed is triage debt, not a work
item.** Its verdict is CLOSE with the landing-commit evidence, and closing it IS
the work. Dispatching a worker to rediscover that the work does not exist spends
a whole session — create, seed, preflight, stand down, unclaim — to learn
nothing, and leaves claim churn on a repository other operators are reading.
**An item that merely READS as already fixed is not the same item.** A merged
commit that is an ancestor of the base is evidence; a sentence saying so is a
reading, and it comes back as `REVIEW` for you to confirm.

### Dispatch mechanics

- `session_create` MUST pass the worker agent explicitly. An unset agent binds
  the worker to YOUR agent, which has no file-writing tool, and the entire batch
  then refuses the work with a plausible-sounding explanation of why it cannot
  edit files.
- Validate with ONE canary dispatch before a batch. A wrong agent or a broken
  brief costs one session that way, and the whole batch otherwise.
- Respect the session-create rate limit: 20 `session_create` calls per 5-minute
  window per caller (folders: 10). `max_in_flight` defaults to 32, so a
  full-width dispatch round CANNOT complete inside one window — plan two rounds,
  and remember that a create refused by the limiter is a post-claim failure, so
  unclaim per the rule above.
- Worker sessions must be granted **trust mode before seeding** — an unattended
  session stuck on an approval prompt runs zero turns; if you cannot grant it,
  tell the operator instead of seeding sessions that will hang.
- Before commissioning a fix for a base-wide breakage, search open PRs for one
  that already exists. Fleet-wide breakage is visible to the wider community
  too, and two identical fixes waste a worker and a review lane.

### Partitioning one change across several workers

When one change is too large for one worker, split it by **exclusive file
ownership**: every file belongs to exactly one worker, and nobody edits outside
their own set — not a one-line mention, not a docstring cross-reference. Two
workers on one file is the merge-conflict version of two sessions on one item.

**Exclusive ownership removes merge conflicts. It does NOT remove a review-order
dependency, and that is the trap.** A premise-level reviewer counts a
mechanism's CONSUMERS in the base, so the PR that BUILDS a mechanism reads as
dead code until the PR that WIRES it has landed: nothing in the base invokes it,
the zero option is behaviorally identical, and the reviewer is right on the
evidence available to it. So a partition ships with a **merge ORDER**: the wiring
PR lands before or with the building PR.

The remedy for a block of that shape is sequencing, and sequencing is YOURS. A
worker must never move another worker's hunk into its own PR to satisfy a
reviewer — that dissolves the ownership split, creates the conflict the split
existed to prevent, and hides a review-order problem as a code change. Land the
wiring PR; the same reviewer's own grep then finds the consumers and the block
dissolves with neither PR changing content.

### The work-order brief (seed message skeleton)

Fill `{...}` from the spec; keep every clause — each one closes a failure mode:

> You own exactly ONE item: {item} on {repo}. Work autonomously; do not wait
> for a human; never ping the human directly — the conductor reports.
> PREFLIGHT (mandatory): view the item; check open PRs and worktrees for
> overlap — if anything already covers it, reply `STANDDOWN: <reason>` and
> stop. Never adopt another session's WIP.
> REPRO ADMISSION (`{verifier.repro_gate}`): expand this clause from the spec.
> In `pod_required` mode, keep the worktree byte-clean and run `kirocrew pod
> scenarios`, choose the closest shipped state, boot the UNMODIFIED worktree in
> that scenario, and drive the externally visible failing behavior through
> `pod api`, pod-e2e/Playwright, or another real product route. Run pod
> status/token/API commands through the worktree's `./.venv/bin/kirocrew` after
> provisioning: the globally installed binary may be sandbox-blind to the pod
> process's sockets and fail closed on ownership proof. If `playwright-cli`
> cannot launch on the host, the repository's own Playwright runner against the
> same live pod is equivalent evidence; record the engine and launch flags, and
> treat a missing REQUIRED engine (for example Safari/WebKit-specific behavior)
> as `missing=<capability>` rather than silently substituting Chromium.
> Record the scenario, exact probe, and failing observable. A unit test, direct
> import, simulated error, or post-fix friction note is NOT admission. No live pod red →
> `STANDDOWN: pod-repro-ineligible — <evidence>; missing=<capability>` and STOP
> with no edit, commit, or PR. Live red admitted → implement, then run the SAME
> pod trace green and tear the pod down to zero residue before `GREEN`.
> CONFIRM the mechanism before fixing: in `best_effort` mode, reproduce where
> cheap; wrong premise →
> `STANDDOWN: premise disproven — <evidence>`. A design decision →
> `PROPOSAL: <link>` (write the proposal on the item; do not build).
> IMPLEMENT in your own worktree (`{worktree_pattern}`, branch
> `{branch_pattern}` from `{default_branch}`): root-cause fix; regression test
> red-on-base and mutation-verified. TESTS: your own changed test files, BY
> PATH, and nothing else. Name the ban rather than implying it — no `make test`,
> no `tox`, no `nox`, no `run-tests`/`local-gate`/"run the gates" wrapper of any
> kind: a wrapper that escalates to the full suite satisfies the letter of a
> targeted-only brief. The ban is on suite wrappers, NOT on the push gate
> below — `preflight.py` and `push_guard.py` shell out only to `git` and `gh`
> and run no test at all, so a targeted-test brief never licenses an unguarded
> push. Pass `-n0` **explicitly** on every run: omitting `-n`
> does not mean single process, it inherits whatever the project's pytest
> `addopts` sets, and `-n auto` is a common default. Canonical line —
> `timeout 900 python3 -m pytest -n0 <test file> -x -q </dev/null`. Do not
> substitute a small `-n <N>`: xdist workers contend for scheduling and are what
> starves under fleet load, and an explicit count also bypasses the memory
> budget `auto` is put through.
> REMOTES: `export GIT_TERMINAL_PROMPT=0` and confirm `gh auth setup-git` has
> run before any push — a bare https push does not use the CLI's token and hangs
> on an interactive prompt indefinitely. If a push exceeds ~2 minutes, time the
> actual pre-push hook over the real payload before naming a cause: process
> liveness cannot distinguish a credential prompt from a slow hook.
> PUSH GATE (mandatory, every push): the scripts live in `<gate>` =
> `<crew-home>/skills/kirocrew-dev/prepare-pr/scripts`, where `<crew-home>` is
> `KIROCREW_HOME` when set and `$HOME/.kiro/crew` otherwise. Invoke them through
> Kiro Crew's runtime interpreter the way Startup invokes `spec_check.py`, and
> quote the resolved path: on POSIX `"$KIROCREW_RUNTIME_PYTHON" -B
> "<gate>/preflight.py"`, on PowerShell `& $env:KIROCREW_RUNTIME_PYTHON -B
> "<gate>/preflight.py"`. `-B` preserves the desktop bundle's no-bytecode-write
> rule. Do NOT add `-I` here even though Startup passes it: `preflight.py` imports
> its sibling `push_guard`, and isolated mode drops the script's own directory
> from the import path, so `-I` turns the gate into a `ModuleNotFoundError` on
> every push.
> Run `preflight.py` before the first commit. Then before EVERY push confirm
> `git status --porcelain` is empty and run `<gate>/push_guard.py
> --base {default_branch} --max-ahead {max_commits}`, which refuses a stale base
> or a replayed upstream commit. Pass `--max-ahead` explicitly and fill it from
> the spec, never from memory: the script defaults to 5, which is looser than
> most repositories' own PR commit-count gate, so omitting it lets a branch read
> `SAFE TO PUSH` and then fail that gate. Add `--require-single-on-base` only
> when you actually squashed to one commit; it asserts `HEAD~1 ==
> origin/<base>` and refuses a legitimate multi-commit branch.
> Read the exit code, do not just test for zero: `0` proceed; `30`/`40` the gate
> REFUSED, so do not push and report the code with the branch state; `2` the gate
> could not RUN — an environment error, not a verdict — so do not push and report
> `BLOCKED: push gate inoperative` with the code and stderr, because a worker
> whose sandbox cannot reach the scripts has to surface that once instead of
> stalling every item silently. A non-empty `git status --porcelain` is also a
> stop.
> Unstaged work and a stale base are what otherwise reach the
> PR and cost a review round to find what a git-only check catches in a second.
> PR: English body (What/Why/How/Tests/Other), `Closes #{n}`, full URL in
> your reply. Babysit to green (`monitor_start` ~300s, staggered off a round
> number so a dozen loops do not poll in lockstep, preferring REST over
> GraphQL/search — the whole fleet shares one account's rate limit). Fix every
> Critical/High; disposition every advisory explicitly; read reviewer JOB
> LOGS for the current head, not check conclusions; rebut with measurement,
> never assertion; before re-running any CI job ask what the re-run REPLACES,
> not what it retries; NEVER `/ai-review override` without the conductor's
> sign-off — a blocking finding you dispute is `BLOCKED: <evidence + 2-4
> options>`.
> REPORT with exactly one of six prefixes — `WORKING: / PR: / GREEN: /
> BLOCKED: / STANDDOWN: / PROPOSAL:` — and RE-STATE the prefix on EVERY later
> turn while this assignment is open (an unprefixed turn reads as "no status").
> Write it as BARE leading text: no bold, no italics, no list marker, no
> blockquote ahead of it. The tag is matched at the START of the message, so
> `**BLOCKED:**` can read as no status — and it does so on the one message you
> most need heard.
> Once you report `BLOCKED:`, KEEP that prefix on every turn until the ruling
> reaches you — do NOT switch back to `WORKING:` while you are on an escalation
> hold. The conductor samples your newest protocol message, so a `WORKING:` line
> posted after a `BLOCKED:` can overwrite the escalation before it is ever seen,
> and the ruling you are waiting for is then owed by nobody.
> GREEN must carry the PR URL, head SHA, and a 3-6 step plain-language summary.

## The probe cycle

One `fleet_probe.py` call. Keep `probe-config.json`'s `sessions` list synced
with the ledger's open sessions (add on dispatch, drop on close). Fired lines
carry **metadata only** — the probe never emits transcript text:

```
🔔 <key>  <age>s <TAG> i=<index> d=<digest12>
BANNED pid=<pid> rule=<regex> cwd=fleet|unknown age=<secs|?>s
OK <n> watched, <m> fired | load/cpu <x> (ok|hot) | mem <n>G | banned <n> | foreign <n> | deliver init-timeout <a>, watchdog <b>
```

A tail with no protocol tag reads as `-` and never fires on its own, and a
protocol word inside a tool card is quoted text rather than a report — so a worker
whose only "status" is in a tool call is silent as far as the probe is concerned,
and ages into `IDLE`.

The handled set keeps the last dispositioned PAYLOAD report as `settled`, so a
later `IDLE` or `NOPROGRESS` mark on the same session cannot resurrect a ruling
you already delivered. You do not maintain this — it is written on every mark.

When a ruling needs content, read that one session through the
workspace-authorized session tools. Act, then `--mark-handled KEY TAG DIGEST`
(DIGEST is the `d=` field on the fired line), or the signal re-fires forever. A
stale digest is refused (exit 3): the payload moved on since you read it —
re-probe and act on what is there now, never mark blind.

**Classification anchors at the START of a worker's message, and tolerates
leading markdown.** A worker that writes `**BLOCKED:**` is following the protocol
and must be read as blocked, so the classifier strips leading emphasis, list
markers and blockquote marks before matching. That belongs in the probe rather
than in the brief: a rule the worker has to remember fails exactly under the
pressure that produces escalations, and the message that goes unseen is then the
escalation itself. The brief asks for a bare prefix as well, but as the weaker
half of the pair.

`i=` is an **absolute per-session message counter, counted from the start of the
transcript** — not an offset within the tail window. That distinction is the
whole rule: a window-relative index saturates once a session grows past
`tail_bytes` and then reads as a frozen number, which is exactly the
no-progress deadlock the field exists to detect. Absolute counting costs nothing
extra, because `tail_bytes` bounds how much of the file is PARSED, not how much
is read from disk — the read loads the whole transcript either way. The index is
carried into the handled-set entry as well, so the comparison is available next
cycle without you having to hold it.

**An unchanged index since you last acted is no progress**, whether or not a
turn is open — it is the one discriminator a self-deadlocked worker cannot fake,
because producing a message is the thing it cannot do. The probe makes that
comparison itself and fires `NOPROGRESS`, so read the tag and **never diff two
cycles by eye**: a comparison that lives in this document is enforced by
nothing, so it may simply never happen. `i=` is the corroborating number, not
the test. "Still working" is not something the probe can tell you at all — that
reading comes from `session_read_message`'s running flag, and an open turn is
satisfied by a shell deadlocked on its own child just as well as by real work.

**One-time degradation 

…(truncated)
