---
name: code-autopilot-batch
description: >
  Batch orchestrator above /code-autopilot: pull agent::ready tickets from GitLab, score
  them deterministically plus a Haiku LLM signal, then run /code-autopilot ticket by ticket
  until the cap is hit, and post a summary to Slack. Scoring runs as parallel in-session
  subagents (no per-ticket claude process) and processing runs sequentially. Use when the
  user says /code-autopilot-batch, /batch, 'autopilot batch', 'lance le batch', 'vide la queue agent',
  'vide la file de tickets', 'traite tous les tickets prêts', 'process the agent queue',
  'drain the ready tickets', 'run the autopilot batch', 'work through the agent backlog',
  or wants to drain many ready tickets unattended. Distinct from /code-autopilot, which
  handles a single issue you specify — this one loops over the whole agent::ready queue.
argument-hint: "[--project SHIFT] [--max-tickets N] [--dry-run] [--no-slack]"
allowed-tools:
  - Bash
  - Agent
---

Batch orchestrator: drain `agent::ready` tickets across one or more Pysae repos, **orchestrated in-session**.

The deterministic steps (pull, score-metric, sensitive filter, ranking, label lifecycle, deploy-watch, report) live in `pysae-ai-tools agent …` sub-commands — this skill only sequences them and runs the LLM work as **in-session subagents**: parallel Haiku scoring + sequential `/code-autopilot` processing. No `claude -p` subprocess is spawned; the heavy per-candidate process fan-out that used to exhaust memory is gone.

## Required tools

- `glab`
- `jq`

Run `pysae-ai-tools tools require <tools>` before starting; if it exits non-zero, abort and report the install command from stderr to the user.

## Project configuration

Honour `.pysae-ai-tools.yaml` (this repo's `CLAUDE.md` is NOT in your context at runtime). Check flags with `pysae-ai-tools project flag <path>` (exit 0 = on, 1 = off — no `jq`):

- `autopilot.enabled` — **per candidate repo**: before processing a repo's tickets, run `pysae-ai-tools project flag autopilot.enabled --root <repo-path>`; if it exits non-zero, **skip that repo entirely** (it has not opted into the batch). Defaults to off.
- `autopilot.completeness` — before the step-3 Sonnet audit, run `pysae-ai-tools project flag autopilot.completeness`; when off (or with `--no-completeness`), skip the completeness audit for that repo. Defaults to on.

The other `autopilot.*` knobs are resolved **inside the deterministic `agent` commands** — you do not read them yourself, you just avoid overriding them:

- `autopilot.max_tickets` / `autopilot.min_success_probability` — `agent rank` resolves the cap and the per-candidate floor from each candidate's repo config. Pass `--max-tickets` / `--min-success-probability` to `rank` **only when the user gave them on the command line**; otherwise omit them so the repo config wins. Defaults: 5 / 50.
- `autopilot.deploy_watch_jobs` / `deploy_watch_timeout` / `deploy_watch_max_retries` — `agent watch-deploy` resolves the jobs to watch and the timeout/retries from the outcome's repo config. An **empty `deploy_watch_jobs`** means the command no-ops (no watch, no false escalation). Defaults: `[deploy_dev]` / 20m / 3.

## Arguments

Parse from `$ARGUMENTS`: `--project <slug>` (repeatable), `--max-tickets N`, `--min-success-probability F`, `--concurrency N`, `--no-completeness`, `--no-watch-deploy`, `--no-slack`, `--dry-run`, `--checkpoint <path>` (overrides `autopilot.checkpoint_path`). `--max-tickets` / `--min-success-probability` / `--concurrency` are **optional overrides**: when omitted, they resolve from each repo's `autopilot` config (defaults 5 / 50 / 4). Track whether the user passed each so you know whether to forward it to `agent rank` (see step 4).

Resolve concurrency once: `CONCURRENCY="${concurrency_cli:-$(pysae-ai-tools project show autopilot.concurrency)}"`. `1` → the sequential loop (§6); `> 1` → the parallel Workflow (§6bis).

## How it works (in-session orchestration)

**Never use `AskUserQuestion`** — this skill runs unattended (local loop or CI). Subagents work headless in CI too.

### 1. Candidate pool (deterministic, no LLM)

```bash
POOL=$(pysae-ai-tools agent candidates --reclaim-orphans --json $PROJECT_FLAGS)
```

This reclaims WIP orphans (>2h → `agent::blocked`), pulls `agent::ready`, and emits each candidate with `business_score`, `sensitive_path`, `structural_ready`, `structural_violations`, `author_username`. In `--dry-run`, drop `--reclaim-orphans`.

Two **non-blocking, advisory-only** passes here — the ticket still continues to scoring in both cases (parity with `agent run` steps 2b + 3). Skip both in `--dry-run`.

- **Structural** — for each candidate with `structural_ready == false`, post the ready-check advisory (body = `"Garde-fous structurels non respectés (avancement quand même) :\n- <violation>\n- …"`):
  ```bash
  pysae-ai-tools agent advise "$PROJECT" "$IID" --kind ready-check --body "$BODY" --author "$AUTHOR"
  ```
- **Sensitive paths (regex)** — for each candidate with `sensitive_path == true`, post the sensitive-paths advisory. **This does NOT block** — the regex is broad and matches incidental mentions; the authoritative sensitive gate is the LLM verdict in step 4. Body = `"Garde-fou regex paths sensibles (auth/secrets/infra/migration) déclenché. On avance quand même : l'audit Haiku domaine sensible décide en aval. Ajoute \`agent::override-sensitive\` pour court-circuiter les deux passes."`:
  ```bash
  pysae-ai-tools agent advise "$PROJECT" "$IID" --kind sensitive-paths --body "$BODY" --author "$AUTHOR"
  ```

### 2. Score in parallel (in-session Haiku subagents)

```bash
PROMPTS=$(echo "$POOL" | pysae-ai-tools agent score-prompt assessment)   # {iid: canonical prompt}
```

Spawn **one subagent per candidate, in parallel** (the harness caps concurrency automatically), each with **model haiku**, given that candidate's prompt from `$PROMPTS`. Each subagent returns ONLY the JSON `{success_probability, sensitive_domain_match, rationale}`. Merge **all three** (`success_probability`, `sensitive_domain_match`, `rationale`) back into each candidate — `agent rank` embeds `rationale` in the below-floor escalation reason, so dropping it diverges from the pipeline's comment. Scoring is independent and read-only → safe to parallelise.

### 3. Completeness audit (optional, in-session Sonnet subagents)

Unless `--no-completeness` **or the repo's `autopilot.completeness` flag is off**: for candidates that are **not sensitive** and have `success_probability >= min-success-probability`, get the completeness prompts (`agent score-prompt completeness`) and spawn **model sonnet** subagents. For any returning `verdict: "incomplete"`, post an advisory (non-blocking) with the pipeline's exact body — `"Audit Sonnet : spec incomplète, on avance quand même.\nL'implémentation devra trancher ces points (l'auteur peut commenter si une décision est nécessaire) :\n- <missing_spec>\n- …"`:

```bash
pysae-ai-tools agent advise "$PROJECT" "$IID" --kind completeness --body "$BODY" --author "$AUTHOR"
```

### 4. Rank (deterministic)

```bash
# Pass --max-tickets / --min-success-probability ONLY if the user gave them on the CLI;
# otherwise omit so rank resolves the cap and per-candidate floor from repo config.
RANKED=$(echo "$SCORED" | pysae-ai-tools agent rank --project "$PROJECT" $MAX_FLAG $FLOOR_FLAG)
```

Returns `{"process": [...capped, sorted...], "escalate": [...]}` applying the exact `score.py` weighting, the sensitive filter (regex + LLM, honouring `agent::override-sensitive`) and the success-probability floor. When `--max-tickets` / `--min-success-probability` are omitted, `rank` uses `--project`'s `autopilot.max_tickets` and each candidate's repo `autopilot.min_success_probability`.

In `--dry-run`: print `RANKED` and **stop here** (no labels, no processing).

### 5. Escalate the filtered-out tickets

For each entry in `.escalate` (LLM-sensitive or below the success-probability floor), block it **and record a `not_ready` outcome** so it shows in the final report — parity with `agent run`'s `_append_not_ready`:

```bash
pysae-ai-tools agent label block "$PROJECT" "$IID" --reason "$ESCALATION_REASON" --author "$AUTHOR"
```

Append to `OUTCOMES` an object `{ticket_iid, project_path, status: "not_ready", mr_url: null, mr_iid: null, escalation_reason: "$ESCALATION_REASON", not_ready_violations: ["$ESCALATION_REASON"], ticket_title, ticket_url, author_username}`.

### 6. Process sequentially (in-session subagents, one at a time)

**Used only when `CONCURRENCY == 1`.** For `CONCURRENCY > 1`, skip this section and use §6bis (parallel Workflow) instead.

Merges land on `main`, so process **strictly sequentially** — never in parallel. Keep the run legible: emit **one plain progress line per phase transition** (the user is otherwise blind until the final report), e.g. `▶ #35 Unify LLM calls — claiming`, `#35 — implementing`, `#35 — merged (MR !92), watching deploy`, `#35 — escalated: <reason>`. For each ticket in `.process`, in order:

1. `pysae-ai-tools agent label claim "$PROJECT" "$IID"` (`agent::ready` → `agent::wip`).
2. Spawn **one subagent** with the prompt: *"Run `/code-autopilot <web_url> --approve=auto --merge=auto`. Run it to completion synchronously — if a step waits on a pipeline, block until that pipeline is terminal; do not stop early. Return ONLY the `<<<AUTOPILOT_RESULT … AUTOPILOT_RESULT>>>` JSON footer."*
   - **Resume handshake (no footer yet).** If the subagent returns *without* a footer, it has yielded on an async CI/pipeline monitor (its message names the pipeline it is waiting on) — this is **not** a result. Wait for that pipeline to reach a terminal state (`pysae-ai-tools ci run wait -p <id>`, or poll `glab ci get -p <id>`), then **resume the same subagent** with a message telling it the pipeline finished and to continue and emit the footer. Repeat until a footer comes back. **Never fabricate the footer, never spawn a fresh subagent for the same ticket** (it would re-implement from scratch).
   - Parse the footer into `status`, `mr_url`, `mr_iid`, `escalation_reason`.
3. Build the `Outcome` object (`ticket_iid`, `project_path`, `status`, `mr_url`, `mr_iid`, `escalation_reason`, `ticket_title`, `ticket_url`, `author_username`). **Fill `tokens_used` / `duration_seconds` from the subagent's usage metadata, not the footer** — the in-session footer self-reports `0`, whereas the Agent result carries a `<usage>` block (`subagent_tokens` → `tokens_used`, `duration_ms / 1000` → `duration_seconds`). **Sum across every resume** of that subagent so the report's cost/time columns are real.
4. On `status == success`: **first** clear WIP — `pysae-ai-tools agent label done "$PROJECT" "$IID"` — **then**, unless `--no-watch-deploy` and if there is an `mr_iid`, `OUTCOME=$(echo "$OUTCOME" | pysae-ai-tools agent watch-deploy)`. `watch-deploy` watches the repo's post-merge CI jobs declared in `autopilot.deploy_watch_jobs` (default `deploy_dev`; an empty list watches nothing — the command then no-ops, no false escalation), with the repo's `deploy_watch_timeout` / `deploy_watch_max_retries`, and, on a real failure, demotes the outcome to `escalated` **and reopens the issue itself**. If it demotes, **do NOT add `agent::blocked`** — keep the demoted outcome only for the report/exit-code. (Parity: the pipeline clears WIP, then demotes the outcome + reopens, without re-labelling to blocked.)
5. On `status == escalated | failed`: `pysae-ai-tools agent label block "$PROJECT" "$IID" --reason "$ESCALATION_REASON" --author "$AUTHOR"`.
6. Append the outcome to `OUTCOMES`, then **rewrite the checkpoint** (see below).

### 6bis. Process in parallel (Workflow, `CONCURRENCY > 1`)

> **Experimental — validate live before trusting unattended.** The JS orchestration (dependency waves, parallel phase-1, strictly serial merge) is unit-tested (`parallel_batch.test.mjs`, `node --test` in CI), but the runtime behaviour — real parallel `/code-autopilot` agents in worktrees — has no automated coverage. On first use, prefer a small `--max-tickets` and watch `/workflows`.

Merges still land on `main` one at a time — parallelism is only in phase 1 (implement → review → green CI, stopping before merge). The serial re-validated merge-gate keeps `main` correct.

1. **Dependency plan.** Feed the `.process` set to `pysae-ai-tools agent dep-graph` → `{edges, deferred, cycles}`. Record the tickets that won't run (parity with the sequential `agent run`): each `deferred` → a `deferred` outcome (`not_ready_violations` = its `blocked_by`), each `cycles` member → an `escalated` outcome (`escalation_reason` = "dependency cycle: …"). `agent checkpoint append` each. `SCHEDULABLE` = `.process` refs minus deferred minus cycles.
2. **Claim** each schedulable ref: `pysae-ai-tools agent label claim "$PROJECT" "$IID"`.
3. **Invoke the Workflow** (the script ships next to this SKILL.md as `parallel_batch.workflow.js`) — call the Workflow tool with `scriptPath` = that file and `args`:
   ```json
   {
     "plan": { "ordered": ["<SCHEDULABLE refs>"], "edges": [{"blocker": "…", "blocked": "…"}] },
     "concurrency": <CONCURRENCY>,
     "mergeStrategy": "<pysae-ai-tools project show autopilot.merge_strategy>",
     "checkpointPath": "<CKPT>",
     "webUrlOf": { "<ref>": "<web_url>", … }
   }
   ```
   The Workflow runs dependency **waves**: each wave's phase-1 tickets (all blockers already merged) run in parallel in isolated worktrees (capped at `concurrency`), then are merged **serially** via `agent merge-gate` (which rebases + re-CIs on the current `main`, and on a rebase conflict runs `/glab-mr-rebase --rebase-only` then retries). **Each ticket is finalised in place, in real time** — the moment its merge lands (or its phase-1 fails), the same agent applies the label lifecycle (`label done` + `watch-deploy` on success, `label block` on escalation/failure) and appends the outcome to `checkpointPath`. It returns `{ outcomes: [...] }`.
4. **Report.** Labels + checkpoint are already done by the Workflow per ticket (step 3) — do **not** re-finalise. Just merge the returned `outcomes` into `OUTCOMES` (with the deferred/cycle outcomes from step 1) and go to §7.

Never use `AskUserQuestion` here either — the Workflow agents run headless.

### Checkpoint & resume (crash safety)

A batch can span many long subagent runs; a session crash or interruption mid-way must not lose the merges already done or re-process them. Persist progress to a checkpoint file that lives **outside any repo** — the default is under the user home (`~/.cache/pysae-ai-tools/autopilot-batch/checkpoint.json`), never in the repo tree, because the processing subagents run `/code-implement` which stages and commits (`git add`); a repo-internal checkpoint would be swept into a ticket's MR.

**Do not hand-write the file** — drive it through `pysae-ai-tools agent checkpoint` (append/load/clear). The command manages atomic writes and per-outcome timestamps in code, so durability does not depend on the orchestrator remembering to write correctly (parity with the "deterministic logic stays in `agent` sub-commands" rule). Resolve the path once (CLI > config > default) and pass it to every call:

```bash
CKPT="${CHECKPOINT_ARG:-$(pysae-ai-tools project show autopilot.checkpoint_path)}"   # absolute home path by default
```

- **Before step 1**, seed from any prior run: `OUTCOMES=$(pysae-ai-tools agent checkpoint load --path "$CKPT")` (prints `[]` when absent). **Skip** every ticket already terminal in it (`success` / `escalated` / `failed` / `not_ready`) — do not re-claim or re-spawn it.
- **In step 6.6** (and after each step-5 escalation), upsert the outcome: `echo "$OUTCOME" | pysae-ai-tools agent checkpoint append --path "$CKPT"`. Append is an **upsert by `(project_path, ticket_iid)`**: re-appending the same ticket (e.g. a success later demoted by `watch-deploy`) updates it in place — `created` preserved, `last_updated` bumped.
- **After step 7** completes (report posted), `pysae-ai-tools agent checkpoint clear --path "$CKPT"`.

If you override `--checkpoint` (or `autopilot.checkpoint_path`) to a path **inside** a repo, git-ignore it yourself — the default home path is already safe.

**File structure**: a JSON array of `Outcome` objects — the exact shape `agent report` consumes. Each entry is one terminal ticket (`status`: `success` \| `escalated` \| `failed` \| `not_ready`) carrying `ticket_iid`, `project_path`, `status`, `mr_url`/`mr_iid`, `escalation_reason`, the real `tokens_used`/`duration_seconds`, `ticket_title`/`ticket_url`/`author_username`, `deploy_status`/`deploy_job_url` (when watched), and its own **`created`** (first recorded) / **`last_updated`** (last mutation) — per-outcome, so you can see when each ticket was processed and spot a stale in-flight entry on resume.

### 7. Report

```bash
echo "$OUTCOMES" | pysae-ai-tools agent report --json --max-tickets "$MAX" $SLACK_FLAGS
```

Prints the ASCII summary table (emit as-is, do not reformat) and posts to Slack unless `--no-slack`. Per-project routing via `--slack-channel-map '<json>'`.

## Parity with `pysae-ai-tools agent run`

This skill reproduces the headless `agent run` pipeline step-for-step, delegating every gating decision to the same code via the `agent` sub-commands. The **blocking logic is identical**: only the LLM `sensitive_domain_match` and the success-probability floor block a ticket (same escalation reason strings); the structural ready-check and the **regex** sensitive-paths pass are **advisory only** (the ticket still ranks), exactly as in the pipeline. Escalated tickets are reported as `not_ready`, like `_append_not_ready`.

**Known, documented differences** (none affect which tickets block vs process):

- **Token budget** (`--max-tokens`): approximated by `--max-tickets` (identical cap on *how many* tickets run). Per-ticket token/duration usage **is** captured (from each subagent's `<usage>` metadata) so the report's cost/time columns are real, but it is not enforced as a mid-run token ceiling, and the wall-clock `--timeout`/per-ticket-timeout are not enforced.
- **Explicit `--tickets` mode**: not handled in-session — delegate to `pysae-ai-tools agent run --tickets <urls>` for a hand-picked list.
- **Scoring model tiers**: subagents run at the session's `haiku` / `sonnet` tiers; `agent run` pins `claude-haiku-4-5` (assessment) and `claude-sonnet-4-6` (completeness). Same prompts (byte-identical via `agent score-prompt`); a model-version drift on the **assessment** tier could in principle change a block/no-block decision — worth pinning if strict determinism is required.
- **Tie-break at the cap boundary**: for candidates with equal `final_score` straddling `--max-tickets`, the in-session path breaks ties in business-score order (the `agent candidates` output order) whereas the pipeline preserves pull order. Rare, and only affects *which* of two equal-scored tickets runs.

`agent run` remains available unchanged for pure-headless / CI contexts.

## Exit codes

- `0`: no ticket escalated **during processing**.
- `1`: ≥1 **processing-stage** escalation — a picked-up ticket that `/code-autopilot` escalated/failed, or was demoted by the deploy-watch. **Pre-pickup blocks** (LLM-sensitive / below-floor → `not_ready`) do **NOT** flip the exit code, matching `agent run` (`run.py` exits 1 only on `result.escalations > 0`, which counts ESCALATED outcomes, not NOT_READY).
- `2`: technical failure (GitLab unreachable, etc.).

## Critical rules

- **Deterministic logic stays in `agent` sub-commands** — never re-implement scoring, ranking, label transitions or reporting inline; call the CLI.
- **Scoring parallel, processing sequential** — parallelise only the independent, read-only scoring subagents; process (implement + merge) one ticket at a time.
- **No `AskUserQuestion`** — unattended by design.
- **Do not reformat the `agent report` table** — Slack and stdout consumers depend on it.

$ARGUMENTS
