Pipeline Mechanics
Per-stage detail for the fixed pipeline SELECT → CHECK YOURSELF → PILOT → FAN-OUT → REPORT → REMEDIATE. The reality-check stages (CHECK YOURSELF, the pilot gate, the report reconciliation) are covered in safety-and-self-checks.md; this file covers the mechanical stages.
Select
Enumerate, then filter, using finding-targets.md. The output is an explicit, finite list of resolved targets — names for multi-repo, paths for monorepo — each annotated with the matched signal. Show the list to the user. A campaign that cannot name its targets is not ready to run.
Record the count. It anchors the reconciliation in REPORT: selected = applied + already-compliant + skipped-not-applicable + held-back + failed.
Pilot
Pick one representative target — not the easiest one; one whose shape is typical of the fleet. Run the recipe on it, surface the full diff, validate it. The pilot is the contract: "this exact change, ×N." Its mechanics are identical to one FAN-OUT iteration, except it stops for explicit confirmation and discards (or keeps, if the user approves) its branch. Lock the pr_spec here — title format, body template, labels — because FAN-OUT replicates it without re-prompting.
Fan-out
Process confirmed targets in chunks of at most max_targets_per_run — a per-chunk concurrency cap, not a campaign ceiling. The total fan-out (count + scope) must be confirmed with the user before the first chunk (see safety-and-self-checks.md); the cap then bounds each chunk. For agentic recipes, send one chunk's Agent calls in a single message so they run concurrently. Each target is handled in isolation — its failure is recorded and the rest continue.
Per target, in order:
Clone / locate. Multi-repo: shallow-clone into a scratch dir. Monorepo: operate on the project path within the single clone.
Re-verify applicability. Confirm the signal is actually present (code search can be stale). If absent →
skipped-not-applicable, stop here.Check idempotency. If the recipe's idempotency condition is already satisfied →
already-compliant, stop here (no branch, no commit, no PR).Branch. Create the deterministic branch from the
pr_spectemplate, cut from the target's default branch — never work on the default branch itself. Same input → same name, so a re-run reuses it rather than creating a duplicate.Apply the recipe. Deterministic: run the edit/script. Agentic: spawn the scoped sub-agent with only this target and its minimal toolset. If the result is an empty diff, treat it as
already-compliantand discard the branch.Second pass. Run the per-target skeptical review (see
safety-and-self-checks.md) — did the recipe do only what the intent describes?Diff-shape check. Compare this diff against the pilot's. A materially different shape (far more files, unexpected paths) is a red flag — the recipe hit something unanticipated. Flag it; do not silently ship it.
Validate. Run the target's gate (
validationfield +Skill(perform-preflight)). Fail →failed, no commit, no PR.Secrets-scan the staged diff. Any hit →
failed, no commit.Commit per
Skill(committing-changes), using the locked title/type.Push and open a draft PR per the locked
pr_spec— never to a default branch, never force-pushed. Capture the PR URL.Pass the body with
--body-file, never--body. The body came from the target repo's PR template and model-generated text, so it is untrusted content; interpolating it into a double-quoted shell argument would let backticks or$(…)execute, once per target. Write it to a file and handghthe path. The title needs the same handling by a different route, sinceghhas no--title-file: write it with theWritetool and use--title "$(cat <title-file>)", whose output is not re-parsed. Never assign the title to a shell variable and never build it withechoor a heredoc — a bash assignment expands$(…)while parsing, which runs the payload beforeghis reached.
If dry_run is set, perform steps 1–9 and stop before commit, push, and open-PR (steps 10–11); record what would have shipped. It mutates no git state, local or remote.
Report
Aggregate one row per target:
| Target | Status | PR | Notes |
|---|---|---|---|
<name> |
applied / already-compliant / skipped-not-applicable / held-back / failed | <url or —> |
<divergence, failure reason, or —> |
Then state the reconciliation explicitly: selected = applied + already-compliant + skipped-not-applicable + held-back + failed. If the arithmetic does not close, a target was dropped silently — find it before declaring done. Surface divergence flags and failure reasons in full; do not bury them under a success headline. See the prove-don't-declare discipline in safety-and-self-checks.md.
Remediate
Re-run the campaign against only the failed and skipped-not-applicable subset. Because every recipe declares an idempotency condition, a target that already succeeded is a clean no-op if it sneaks back into the set. Fix the root cause first — a recipe that failed validation on five targets the same way needs a recipe fix, not five retries.
Idempotency rules
- Before applying, the recipe's idempotency condition is checked. If already satisfied →
already-compliant: no branch, no commit, no PR. - Branch names are deterministic functions of the campaign + target, so re-runs reuse branches and PRs rather than multiplying them.
- A re-run of a fully-successful campaign produces zero new changes and zero new PRs.
Rate limits
Bulk enumeration plus per-target API calls can exhaust the GitHub rate limit. On a 403 or 429, back off exponentially and retry; insert a small delay between per-target API calls in large runs. Prefer one enumeration pass reused across the campaign over re-querying per target. A rate-limit pause is not a campaign failure — wait and resume.