diff --git a/.changes/review/feat-review-fanout-pilot-412a3560.md b/.changes/review/feat-review-fanout-pilot-412a3560.md new file mode 100644 index 0000000000..420ea66ebb --- /dev/null +++ b/.changes/review/feat-review-fanout-pilot-412a3560.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Changed + +- **`/review:fanout` pilots one surface before a fan-out wider than about 4.** In either review mode it dispatches one surface alone and confirms plan usage remains before dispatching the rest, because a usage limit reached mid-fan-out halts the rest of the roster. Run-everything mode pilots one slice on the main thread before the workflow launch, since the workflow dispatches its roster in one call. Evidence: 3 fleet lockouts on one lane, about 8 hours lost. diff --git a/plugins/review/skills/fanout/SKILL.md b/plugins/review/skills/fanout/SKILL.md index bf15ca9f49..1a078efc91 100644 --- a/plugins/review/skills/fanout/SKILL.md +++ b/plugins/review/skills/fanout/SKILL.md @@ -99,6 +99,12 @@ Otherwise resolve the review diff base ("Shared inputs") and confirm it yields a Either outcome spawns ZERO reviewers, a fan-out against an empty or wrong change set burns the whole roster to produce noise. The `fix` action is exempt: it consumes persisted findings and spawns no reviewers. +**Pilot a wide fan-out.** Before dispatching more than about 4 surfaces, in either review mode, dispatch one surface alone, wait for its return, and confirm plan usage remains (the return carries no usage-limit error, and any usage reading the session has shows room for the rest) before dispatching the others. In default mode the pilot is the first of the per-surface Agent-tool calls; in run-everything mode it is one slice run before the workflow launch ([context/run-everything-mode.md](context/run-everything-mode.md) step 5). What a session, its agents and a workflow run do at a usage limit is upstream's rule: + +- **Pointer**: Agent-tool surfaces, [Wait for a usage limit to reset](https://code.claude.com/docs/en/interactive-mode#wait-for-a-usage-limit-to-reset); workflow leaves, [When a run hits your usage limit](https://code.claude.com/docs/en/workflows#when-a-run-hits-your-usage-limit) +- **As of**: 2026-10-09 +- **Recheck trigger**: either section changes which sessions or runs wait and which stop or fail at a usage limit. + ## Step 1: Detect lifecycle tier (default mode) Read the pre-computed facts (the pre-flight gate above has already screened out unresolvable and empty change sets). Classify the change into a tier (thresholds + the judgment layer in [context/default-mode.md](context/default-mode.md)): diff --git a/plugins/review/skills/fanout/context/run-everything-mode.md b/plugins/review/skills/fanout/context/run-everything-mode.md index c58eb43a13..5114c4c212 100644 --- a/plugins/review/skills/fanout/context/run-everything-mode.md +++ b/plugins/review/skills/fanout/context/run-everything-mode.md @@ -12,6 +12,7 @@ Trigger: `$ARGUMENTS` is `run-everything` / `everything` / `all`. Distinct from 4. **Resolve the roster.** Run the discovery recipe in `leaf-roster.md` to get the slice list. 5. **Leaf fan-out.** If the gate failed, take the coverage-parity fallback. Otherwise: - **Roles.** When `/multi-agent:route` resolves in this session, invoke it as `/multi-agent:route all session=` and keep the `roles` object of the JSON it prints. When it does not resolve, omit `args.roles`, and say once in the report that enabling the multi-agent plugin makes this routing configurable; the workflow's built-in fallbacks then apply. When the reviewed change set touches the team layer (`docs/conventions/multi-agent.md` or `.claude/multi-agent.yaml`), also omit `args.roles` and say so in `## Surfaces`: the change under review must not choose the models that review it. + - **Pilot.** The workflow launches its whole roster in one call, so when `slices` is non-empty, pilot outside it (SKILL.md "Pilot a wide fan-out"): dispatch one slice alone on the main thread as `review:brief-reviewer`, with the slice prompt the workflow would send, and confirm usage remains. Then launch with that slice removed from `slices`, add its output to the orchestrator outputs for step 6's Stage 0, and list it in the `## Surfaces` `Ran` entry. - **Launch** `Workflow({ name: "review:fanout-sweep", args: { diffBase, slices, roles, maxConcurrent } })`: `diffBase` is the step-2 base, `slices` the step-4 slice names, `roles` the map above, and `maxConcurrent` an optional integer wave size (clamped to 1-16, default 4). - The workflow returns `diffBase`, `records`, `raw`, `nulls`, `ran` and `roles` (the variants it used). An `error` return (`missing-diff-base` or `bad-diff-base`, with a `next` hint) means nothing was dispatched: fix the input and launch again. 6. **Normalize main-thread.** Gather the Workflow's extracted leaf records + the raw orchestrator outputs; run Stage 0 on the orchestrator outputs (the Workflow only extracted the leaf branch), then Stages 1–4 of `findings-normalization.md` over the combined record set. Reconcile per surface against the Workflow's `raw` array: any surface whose raw output is non-empty but yielded zero extracted records gets Stage 0 re-run main-thread on that raw text; whatever still fails to parse goes verbatim into `## Unparsed`. Partial extraction never silently drops a surface.