Stage signals
See how well each workflow stage's own gate is calling it — which attempts get accepted, reworked, or never resolved, and how the gate's own accuracy holds up.
First-pass rate and review debt tell you how often work gets sent back and how long review takes. Stage signals answers a narrower, sharper question: for a given stage, was the call the gate made actually the right one? It looks at every attempt a stage made, works out whether that attempt held up, and then checks the gate’s own track record — did it approve work that later failed, or reject work that turned out fine?
Where to find it
There’s no web-app page for this report yet — it’s CLI and API only.
ch quality stage-signals
ch quality arms
Pass a workspace name, slug, slug path, or full ID as the first argument to scope it elsewhere; without one, the command uses the workspace the CLI is pointed at. See Using the ch CLI for how ch resolves a workspace reference. Add --json to get the raw data instead of the table.
Any member of the workspace can run this report — there’s no extra role or plan requirement.
Choosing a window
The CLI takes --window today, --window yesterday, --window week, or --window month. Without a --window or --since, the report covers the last 90 days.
For a custom range, use --since instead, with a relative duration (3d, 12h, 45m) or a full RFC 3339 timestamp — the same grammar as ch costs --since. --since and --window are mutually exclusive. Unlike composition outcomes, --since here has no 90-day limit — you can reach back as far as you need.
This report always covers exactly the workspace you asked for, with no cascade into child workspaces.
What each number means
The report prints two tables, one row per stage.
Attempt outcomes
Every time a stage’s work gets attempted, that attempt eventually lands in one of four buckets:
| Outcome | What it means |
|---|---|
| ACCEPTED | The attempt moved past its stage with no sign anything needed fixing. |
| REWORKED | Something pointed back at this attempt — a failed check, work sent back into the same stage, a rejected advance, or an escalation. |
| PENDING | The attempt hasn’t moved past its stage yet, and nothing has flagged it either. |
| CENSORED | The task was cancelled, archived, or otherwise abandoned before the attempt moved past its stage, with no sign anything needed fixing. |
Censored is never counted as accepted. An attempt that never got a clean pass shouldn’t read as one just because the task itself ended before anyone found out — CENSORED exists so an inconclusive attempt stays visibly inconclusive instead of padding your acceptance numbers.
The table’s columns:
| Column | What it tells you |
|---|---|
| STAGE | The workflow stage this row covers. |
| ACCEPTED | Attempts that came out clean. |
| REWORKED | Attempts that needed a second look. |
| PENDING | Attempts still in flight. |
| CENSORED | Attempts cut short by the task ending. |
| DECIDED | ACCEPTED plus REWORKED — the total attempts with a real verdict, and the denominator for an acceptance rate. |
| MEDIAN-REVEAL | The median time between an attempt leaving its stage and CodeHerder confirming whether it held up. |
| CONFOUNDED | Of the attempts already counted in ACCEPTED or REWORKED, how many had more than one gate sit between them and the point where they were confirmed — so you can’t tell which gate actually caught the problem. Not a separate bucket: every confounded attempt is also counted in ACCEPTED or REWORKED. |
| MIXED-ARM-EXCL | Attempts held out of every other column in this row, because more than one model or agent configuration touched the work, so no single configuration can take credit or blame. |
Review accuracy
For a stage that acts as a gate — one that can approve or reject work — the second table checks whether its calls held up:
| Verdict | What it means |
|---|---|
| AGREED | The gate approved and nothing later found a problem, or it rejected and the next attempt changed something. |
| FALSE-NEG | The gate approved, but a later stage caught a problem, or work got sent back to at or before the stage it approved out of. |
| FALSE-POS | The gate rejected, but the next attempt changed nothing meaningful — the rejection didn’t lead anywhere. |
| UNKNOWN | There isn’t enough evidence yet to say either way. |
UNKNOWN isn’t part of this table’s DECIDED count. DECIDED is AGREED plus FALSE-NEG plus FALSE-POS — the calls stage signals could actually judge. UNKNOWN sits outside it on purpose, so a stack of open questions never gets averaged away into an accuracy rate.
The table’s columns:
| Column | What it tells you |
|---|---|
| STAGE | The gating stage this row covers. |
| AGREED | Calls that held up. |
| FALSE-NEG | Approvals that turned out wrong. |
| FALSE-POS | Rejections that turned out wrong. |
| UNKNOWN | Calls with no evidence yet either way. |
| DECIDED | AGREED plus FALSE-NEG plus FALSE-POS. |
Reading the table
MEDIAN-REVEAL prints an em dash (—), never 0, for a stage with no confirmed attempts yet — so “nothing to measure” never gets mistaken for “instant.” If the window has no data at all, the report prints (no data).
Sample output, with made-up numbers:
ch quality stage-signals — window Jul 24 00:00 → Aug 23 00:00
STAGE ACCEPTED REWORKED PENDING CENSORED DECIDED MEDIAN-REVEAL CONFOUNDED MIXED-ARM-EXCL
code 58 14 3 1 72 2h10m0s 2 1
review 71 9 0 0 80 18m0s 0 0
STAGE AGREED FALSE-NEG FALSE-POS UNKNOWN DECIDED
review 62 4 5 9 71
Read this before you act on it
A stage with a low ACCEPTED count isn’t necessarily a stage doing bad work — check PENDING alongside it first. A stage where a lot of attempts are still in flight will always look thin on ACCEPTED and REWORKED until those attempts get confirmed one way or the other; that’s slow reveal, not a quality problem.
CONFOUNDED and MIXED-ARM-EXCL both flag attempts you can’t cleanly attribute, but they work differently, and mixing them up will throw off your arithmetic. MIXED-ARM-EXCL is held out of every other column — those attempts aren’t counted in ACCEPTED, REWORKED, PENDING, or CENSORED at all, because more than one model or configuration touched the work and none of them can be credited or blamed alone. CONFOUNDED is not an exclusion: it’s a flag on attempts already sitting inside ACCEPTED or REWORKED, marking that several gates sat between the attempt and its confirmation, so you can’t tell which one actually caught anything. A stage with high CONFOUNDED still has a real ACCEPTED and REWORKED count — you just can’t credit this stage alone for it.
Which setup did the work
ch quality stage-signals groups attempts by stage only. ch quality arms breaks each stage down further, by which setup ran it — the harness, the model, and the exact stage prompt used.
ch quality arms
ch quality arms — window Jul 24 00:00 → Aug 23 00:00
STAGE CLASS HARNESS MODEL STAGE-HASH RATE ACCEPTED REWORKED DECIDED PENDING CENSORED CONFOUNDED FULL-ARMS
code bug claude sonnet a1b2c3d4 92% 46 4 50 6 1 1 3
code story codex gpt e5f6a7b8 78% 14 4 18 2 0 0 1
excluded: 5 unsettled, 2 mixed-arm, 0 arm-unknown
Each row is one cell: a stage, a task class (the kind of task — bug, story, and so on), and a routing setup — the harness, the model, and a short hash standing in for the exact stage prompt used. RATE is ACCEPTED divided by DECIDED, the same acceptance rate stage-signals reports, just narrowed to this one setup. RATE prints an em dash (—), never 0%, for a cell with no decided attempts yet.
A cell is one stage attempt’s own verdict — never the whole task’s later reject history. If a task gets rejected three stages after this one, that rejection does not move this cell’s rate. Each row only counts what happened at its own stage.
FULL-ARMS is how many exact setups this row actually covers. The STAGE-HASH groups by stage prompt, harness, and model, but two runs with the same three can still differ underneath — a different reasoning-effort setting, a different launch configuration, or a different set of skills loaded. FULL-ARMS counts how many of those exact combinations landed in this row, so a 1 means the row is one setup through and through, and anything higher tells you the row is a blend. To see the exact setup behind any one attempt, run ch quality attempts and read that attempt’s own ARM value.
The excluded line below the table counts attempts this report could not place in any row: unsettled attempts still waiting on a verdict, mixed-arm attempts where more than one setup touched the work, and attempts whose setup could not be identified at all. If the workspace has more distinct cells than the report can show, it also prints a line naming how many got dropped.
Related guides
- Stage-attempt detail — the individual attempts behind every number on this page
- Monitoring your agents — first-pass rate and the rest of fleet health
- Review debt — how the review queue itself is doing: latency, rework, and cost
- Composition outcomes — whether letting an agent choose a task’s workflow actually pays off
- Understanding costs — the
--sincegrammar this report shares with the cost reports
