# Feature: Plan critic, one round

<!-- toc -->
- [When it runs](#when-it-runs)
- [Why one round, fixed roles](#why-one-round-fixed-roles)
- [The critic](#the-critic)
- [The reply](#the-reply)
- [The judge](#the-judge)
- [Advisory objections](#advisory-objections)
- [The ledger, and why it is not mandatory at commit](#the-ledger-and-why-it-is-not-mandatory-at-commit)
- [Limits](#limits)
- [Reference](#reference)
<!-- /toc -->

**Pattern**: before a plan is built on, one critic with one task asks why it
will fail. The planner answers each objection once, and a script decides, by
constitution rule id, whether any answer leaves a binding rule broken. There is
no second round and no model scoring the exchange.

## When it runs

At the end of Phase 1, after Step 12 (the plan validated, spec consistency
checked) and before Phase 2, only while the quality gates are active
(`lib/unattended.mjs`, `gatesActive`: `MULTI_AGENT_UNATTENDED=1` or
`state.autopilot === true`). An attended run is unchanged: no critic is
dispatched and plan approval stays with the person, who is the critic there. If
`plan-critique-gate.mjs` is invoked attended anyway, it prints its report marked
advisory, exits 0 (3 on a usage error) and writes nothing.

The pipeline has no separate product mode. The nearest thing, a repo-less
analysis (`No platform yet`, Locked 34) followed by `/multi-agent:analysis-jira`,
has no planner to answer an objection: its story tree is derived from the
document's own ids by `analysis-story-tree.mjs`, deterministically, and a flaw in
it is fixed in the analysis. The critic runs only on a Phase 1 plan.

## Why one round, fixed roles

Multi-round debate between agents tends to move them toward each other rather
than toward the right answer: agreement grows round by round whether or not
accuracy does ("Debate or Vote", NeurIPS 2025; "Talk Isn't Always Cheap"). So
the exchange is bounded the way `ai-common-toolkit:council` bounds a decision,
three voices, one round, one page, and each side keeps one role: the critic
objects, the planner answers, the gate judges. Nobody replies to a reply.

## The critic

Persona `agents/plan-critic.md`, dispatched through the model router so it
follows the fable switch the way review does: fable while the rung is on, opus
while it is off.

```bash
CRITIC_RUNG=$(bash "$HOME/.claude/lib/model-dispatch.sh" subagent --persona plan-critic --phase 1 --default fable)
```

It reads the plan, the analysis document(s), the constitution and the worktree,
and writes objections under four lenses: `scope`, `feasibility`, `security`,
`alternative` (a cheaper plan with the same result). Each objection has an id
(`OBJ-NN`), a lens, a claim, at least one evidence anchor and, when the plan
breaks one, a constitution rule id. The orchestrator writes the output to
`$WORKTREE/.pipeline/plan-critique.json` (`schemas/plan-critique.schema.json`)
with `replies` empty.

**Why a persona and not council.** Council fits a decision with several
credible options: three voices argue, and the caller decides. The critic has no
options to weigh and must not decide. It is one voice attacking one plan, and
its output is anchored JSON a script can check, not a tradeoff table. Council's
own table sends "checking whether output is correct" to a verification pass.
What the critic takes from council is the bound: one round.

| Anchor | `ref` | Checked by the gate |
|---|---|---|
| `file` | `path:line`, optional `quote` | resolved in the working tree the way `verify-citations.mjs` resolves a citation: the file exists, the line is in range, the quote is on the line |
| `plan` | a task id | the plan has that task |
| `analysis` | a requirement id, or the whole text of a heading | an analysis document defines the id, or has a heading with exactly that text (its section number optional); a fragment of a heading does not resolve |
| `inference` | the one-line reasoning | never |

An objection none of whose anchors resolves is labelled `inference`. An anchor
that does not resolve is listed in the report.

## The reply

The planner answers every objection exactly once, in `replies[]` of the same
file:

- `accept`: revise the plan (overwrite `plan.json` and re-run
  `validate-planning.mjs`), say how in `revision`, and anchor the reply to the
  task that now carries the fix (`plan`).
- `rebut`: show that the objection does not hold, with at least one anchor.

## The judge

```bash
node "$HOME/.claude/scripts/plan-critique-gate.mjs" --state "$STATE_FILE" \
  --critique "$WORKTREE/.pipeline/plan-critique.json" --plan "$WORKTREE/.pipeline/plan.json"
```

`--analysis` defaults to `state.analysis.docPath`, `--constitution` to the
knowledge store (`constitution.mjs`). No model call, and the mapping is by id:

| Objection | Reply | Result |
|---|---|---|
| cites a `binding` rule | accept with a revision and a `plan` anchor that resolves, or rebut with an anchor that resolves; a `file` anchor counts only as `path:line` with a `quote` of that line | advisory |
| cites a `binding` rule | anything else, including a rebuttal that is inference alone | **blocks** |
| cites a `proposed` rule, a rule the constitution does not define, or no rule | any | advisory |
| any, with no constitution file | any | advisory |

The protocol is checked in code before any objection is judged: `round` other
than 1, an objection with no reply, one with two replies, a reply to an
objection nobody raised, or a second critique of a run already judged (a
different critique, or different replies, from the one in
`state.planCritique`) is a protocol failure. Re-running the gate on the same
file is the same verdict.

| Exit | Verdict in the ledger | The run |
|---|---|---|
| 0 | `pass` | continues to Phase 2 |
| 1 | `fail`: a blocking objection, or the protocol broken | parked as `verification-failed` by the gate itself (`verificationFailed.gate: plan-critique`); Phase 2 does not start |
| 2 | `not-applicable`: no plan | continues |
| 3 | `fail`: the critique, plan, analysis or constitution unreadable, or the critique not in schema shape | parked the same way |

A failure is not retried: a second attempt would be the second round this
design refuses.

## Advisory objections

Everything that does not block is returned in `advisory[]` and kept in
`state.planCritique.advisory`, each with its lens, claim, label, rule status and
the reply's action. Phase 1 carries them into the plan render (the Step 5b
plan, logged in autopilot); Phase 4 lists them in the PR summary under `## Related`,
marked advisory, so a reviewer sees what the critic raised and how it was
answered.

## The ledger, and why it is not mandatory at commit

With the gates active every verdict is appended to `state.gates[]` as
`plan-critique`. It is not in `MANDATORY_AT_COMMIT`. A gate joins that list only
when every gated run has something for it to check, and not every gated run has
a plan: `/multi-agent:resume autopilot` over work with no run behind it runs the
pipeline tail, review then commit, with no Plan phase. A mandatory entry would
block those commits for a phase they never entered. The decision point is the
start of Phase 2, and a failing verdict parks the run there.

## Limits

- The gate checks that an anchor resolves, not that it proves the claim. A
  rebuttal citing a real but irrelevant line resolves a binding objection;
  review is where that is caught.
- The critic is invoked by the agent at the end of Phase 1. An agent that skips
  it leaves no ledger entry, and nothing blocks the commit on that account.
- While the fable rung is off the critic runs on opus, the planner's model.

## Reference

Script: `plan-critique-gate.mjs`. Persona: `agents/plan-critic.md`. Schema:
`plan-critique.schema.json`; state key `planCritique` in
`agent-state.schema.json`. Tests: `test/plan-critique-gate.test.mjs`, fixtures in
`test/fixtures/plan-critique/`; smokes `smoke-gates-interactive-noop.sh`,
`smoke-model-dispatch.sh`. Related: `features/constitution.md`,
`features/unattended-gates.md`.
