Appearance
Convergence loop and review barrier
This page describes what happens between the agent saying code.done and the issue actually entering the done phase. The "harness" of Smithers in the user-facing sense is this loop — not the test/proof harness in tests/.
The phases
gathering → working → sign-off → reviewing → barrier → continuing → done | error | escalatedThese are concrete ConvergencePhase values from src/types/convergence.ts. Each phase is a real supervisor state, not a UI label.
| Phase | Active because |
|---|---|
gathering | ConstraintGraph.graph.constraints-for(N) is collecting the projection. |
working | The agent (Path A CodingAgent or Path B SDK worker) is running. |
sign-off | The agent emitted code.done; the supervisor is now forcing the agent to argue every constraint. |
reviewing | Sign-off passed; reviewer fan-out is in flight. |
barrier | Reviews are gathered; supervisor is waiting for the slowest reviewer or a quorum. |
continuing | Reviews fed rejection feedback back to the agent; a new round is starting. |
done | Approved; ratchet updated; session closed. |
error | Unrecoverable failure recorded in _history. |
escalated | Escalation policy fired; manual intervention queued. |
The round protocol
A "round" is one pass of working → sign-off → reviewing → barrier → (continuing | done). Rounds count up in ActiveIssueInfo.round (SmithersSupervisor.ts:178).
mermaid
stateDiagram-v2
[*] --> gathering
gathering --> working: prompt built
working --> sign-off: agent emits code.done
sign-off --> working: argument incomplete (forced restart)
sign-off --> reviewing: argument complete
reviewing --> barrier: all reviewers dispatched
barrier --> done: every constraint approved → ratchet
barrier --> continuing: any rejection
continuing --> working: rejection feedback injected
continuing --> escalated: too many rejections
working --> error: unrecoverablemaxRoundsPerProvider (default 3, SmithersConfig.limits.maxRoundsPerProvider) caps how many rounds happen on one provider before sequential fallback to the next.
Parallel review fan-out
When the supervisor enters reviewing, it iterates over the gathered constraints and dispatches each to a ReviewerAgent in parallel:
ts
// SmithersSupervisor.ts:1590-1593 (excerpt)
const result = await reviewer.onReviewerEvaluate({ ... });ReviewerAgent evaluates one constraint against the agent's evidence (file diff, tool-call transcript, mechanical-check output). It returns a ReviewResult:
ts
type ReviewResult = {
constraintId: string;
approved: boolean;
argument: string;
evidence: ReviewEvidence[];
rejectionReason?: string;
}Importantly, every reviewer sees the same evidence. There is no "first reviewer wins" or partial review — they all evaluate against the same agentOutput snapshot.
The barrier
barrier is the gather phase. The supervisor waits until every dispatched reviewer either returns or times out:
- All
approved: true— proceed to ratchet. - Any
approved: false— collect every rejection and build feedback for the agent. - Reviewer timeout — counted as
approved: falsewithrejectionReason: 'timeout'.
The barrier is what makes review not piecemeal (per DESIGN.md §1) — agents cannot ship after one reviewer approves; everyone votes against the same evidence.
Rejection feedback and restart
If the barrier rejects, _buildRejectionFeedback (SmithersSupervisor.ts:1886+) constructs a new prompt segment listing what failed and why. The supervisor then calls onSmithersStart again with round + 1, the same agentMount, the same systemPrompt, and the new feedback appended to userMessage.
This is restart-on-rejection — the agent gets a fresh session with the same identity and the rejection context. It does NOT have access to its previous transcript automatically; the rejection feedback is the only carryover (this prevents agents from rationalising past attempts).
Ratchet
When all reviews approve, the supervisor writes a RatchetEntry per constraint via RatchetStore:
ts
// projects/matrix-3/packages/smithers/src/actors/RatchetStore.ts
export interface RatchetEntry {
constraintId: string;
testConstraintId: string; // toRatchetTestConstraintId(constraintId, ...)
satisfiedBy: { issueNumber, branch, evidence };
ratchetedAt: string;
}The ratchet is append-only. smithers.ratchet returns the count and entries; the workbench renders this in the Governance route. A future issue cannot regress a ratcheted constraint — ConstraintGraph.graph.constraints-for will surface previously-ratcheted constraints as invariants the agent must continue to satisfy.
Escalation policy
EscalationPolicy (src/actors/EscalationPolicy.ts) decides when to give up vs continue. Inputs:
- Round count.
- Number of consecutive rejections.
- Provider fallback exhaustion.
- Time-since-start.
Decision values include continue, fallback-provider, escalate-human, mark-error. The supervisor consults the policy on each barrier rejection (SmithersSupervisor.ts:1445-1450).
When escalate-human fires, the supervisor:
- Sets phase to
escalated. - Emits
smithers.escalated(declared inmatrix.jsonemits). - Writes the convergence log entry
event: 'escalated'. - Stops the agent, leaves the session record in place for human review.
Escalated issues appear in the Topology Rail's escalated queue.
Workstream-request inbox
Smithers itself can be the recipient of workstream-requests from other actors:
bash
matrix invoke smithers smithers.request-service '{
"targetWorkstream": "smithers",
"summary": "Re-run convergence on issue 73 with relaxed constraint X",
"linkedIssueNumber": 73,
"priority": "high"
}'onSmithersRequestService (SmithersSupervisor.ts:2833+) appends to _workstreamRequests. The supervisor periodically services pending requests via _servicePendingWorkstreamRequests. Requests can be dispatched (smithers.request-dispatch), acknowledged, messaged, or resolved — full inbox semantics.
See also
Source:
projects/matrix-3/packages/smithers/src/actors/SmithersSupervisor.ts:1410-1510(barrier),:1886-1900(rejection feedback),:2294-2469(sign-off),:1445-1450(escalation policy invocation).