Practice review pipeline
A practice review passes through four stages, in this order: Occasion, Capture, Observation, Delivery. Each stage can stop independently and records a different reason.
Use the stage that stopped the review to decide what to fix. Do not translate one stage's reason into another stage's vocabulary.
Choose the page for your change
This page owns the runtime sequence and the seams between its stages. Start elsewhere when your change belongs to one contract:
| You are changing | Start with |
|---|---|
| A source, its capture states, retention, or permitted uses | Artifact-source contract |
| A practice's criteria, occasion, or evidence requirements | Practice catalogue |
| Observation or feedback persistence, identity, or API projections | Practice review data model |
| Product and model-facing terminology | Practice feedback language |
| Run provenance and evaluation joins | Evaluation provenance |
| An exact enum meaning after you know the stage | Practice review glossary |
Return here when a change crosses two stages. That boundary is usually where an apparently harmless shortcut can turn a capture failure into a claim about someone's work, or a delivery choice into a missing measurement.
Where a review can stop
Each stage owns exactly one refusal vocabulary, and they are not interchangeable. Answering a question from one stage in another stage's words is the failure this table exists to prevent.
| Stage | Refusal vocabulary | Lands in | Means |
|---|---|---|---|
| Occasion | SignalStateReason | artifact_signal.state_reason | We chose not to look, or not yet |
| Capture | SourceAbsenceReason per source, SourceReadinessReason per practice check | agent_job.evidence_snapshot (the manifest), agent_job.review_readiness | We could not look |
| Observation | Presence.INCONCLUSIVE | observation.presence | We looked and could not tell |
| Delivery | FeedbackSuppressionReason | feedback.suppression_reason | We know, and chose to stay quiet |
The four are not ranked and do not substitute for one another. "We could not look" is deliberately not
representable as a Presence value, because a Presence is a claim about the developer's work and a
missing source is a fact about the instrument — which is exactly what lets the whole observation table be
read as behaviour. In the other direction, "we chose to stay quiet" must never be recorded as an absent
observation: requiring human approval is a release decision, and the measurement still happened.
When you add a reason code, the first question is which of these four columns it belongs in. If the honest
answer is "two of them", the reason is really two reasons.
Occasion
An occasion is one named signal on one revision of one artifact. It is recorded in
artifact_signal before admission, so a review that does not start still has an auditable outcome.
SignalState has five values: RECORDED for an occurrence nobody has decided on yet, TRIGGERED for one
that started a review, terminal SUPPRESSED, retryable PENDING, and retired LAPSED. Terminal rows
never reopen; pending rows are re-offered until their reason clears or their deadline passes. Retryability
belongs to the reason, not to the caller: each SignalStateReason declares the state it resolves to. The canonical reason list, provenance mapping,
and UI labels are in How a review was occasioned.
Operator remediation is in When a workspace goes quiet.
The occurrence ledger has no pruning job; capacity planning must treat it as append-only. Retry and lapse thresholds are documented in the admin guide.
Capture
Capture stages every source the contract says applies to the artifact kind under review — all of them, every time. Practice bindings decide which practices may run, never which files reach the sandbox; there is no per-run source selection. What a capture then reports about each source (available, not collected, unavailable, redacted, errored — with a typed reason) and how a practice's stances turn those reports into a readiness decision is the whole subject of the artifact-source contract, which is versioned and validated at startup.
Do not restate that contract here. The one fact this page owns is where the readiness verdict lands:
agent_job.review_readiness, its own jsonb column, split out from evidence_snapshot because a snapshot
carries one entry per staged file and a TOASTed jsonb has no partial read.
Observation
An observation is one practice's answer about one piece of work, written once and immutable. It carries a
Presence — PRESENT, ABSENT, NOT_APPLICABLE or INCONCLUSIVE — and, only when the presence carries
valence, an assessment. Admission removes assessment from an INCONCLUSIVE observation, so a detector that
hedges cannot attach an unearned strength to the series.
The glossary defines what each presence claims and what NOT_APPLICABLE has to carry to be allowed to claim
it — see Outcome states. Two facts belong to this stage
rather than to that vocabulary. INCONCLUSIVE is not "we could not look": a missing, errored or
governance-blocked source produces a readiness decision in the Capture stage and no observation at all.
And the NOT_APPLICABLE grounding rule is enforced twice, in the in-sandbox normalizer and again at
server-side delivery, for the same reason the citation and recorded-search rules are — the sandbox guard runs
inside the thing it is checking.
Identity is assigned at persistence, not by the detector: an occurrence key dedupes within a run
(insertIfAbsent, ON CONFLICT DO NOTHING), and a recurrence key hashes the locus so the same observation
is recognisable across runs.
An observation carries evidenceRationale and no advice. evidenceRationale is the measurement's own
justification — "the change adds a tax-exempt branch, and no test file in this diff calls total" —
and it is the only prose the observation carries anywhere. The detector emits no next step: there is no
guidance field on the tool, on the parsed observation, or on the ledger. Advice is a property of a
delivery (ADR 0021, ADR 0022), and the stage that decides deliveries is the subject of the next
section.
Measurement and intervention
Everything above this line is the system taking a reading. Everything below it is the system deciding to say something to a person. Those are separate turns in one agent session. An authenticated server callback admits the observations between them, so composition retains context but can use only the persisted projection. Keeping that boundary is why one fact can read three different ways.
A measurement is falsifiable. "This change adds a tax-exempt branch, and nothing in the change tests it" is either true or false, and you check it by opening the file. It says nothing about what anybody should do, and it remains unedited for as long as it is retained, so later readings can be compared with it.
An intervention must stay grounded, but grounding alone does not make it useful. "Write the assertion that distinguishes the new branch before you write the branch" can be supported by the observation and still be badly timed, repetitive, or irrelevant to the recipient. Composition therefore considers the audience, surface, history, and current conversation as well as the evidence.
The line the code draws, stated once so it fits in a prompt: if you can check it by reading the artifact, it is measurement; if you can only check it by watching what the person does next, it is intervention.
They are separated because the good version of each ruins the other. A measurement authored as advice starts bending toward whatever makes good advice — the observation you can write a nice tip about gets reported, the one you cannot gets quietly dropped. It also bends toward fault, because a next step presupposes something to step away from: the three answers that assert nothing is wrong (a strength, a practice with no subject here, a question the evidence left open) are the ones that suffer for it. And advice written at the moment of measurement can only ever be about the one thing just measured: it cannot know that this is the third time, it cannot know the developer was already told, and it cannot decide to stay quiet.
Note where the arrows converge. The model proposes; the server admits. Origin, effective autonomy, channel-specific routing, placement checks, and per-lane caps still apply after composition. Giving the model more context is not a reason to relax these gates; it gives them more proposals to assess.
Composition inputs and outputs
After Java admits observations, the runner resumes the same model session with the composition prompt. The measurement tool is closed, and feedback may bind only to the durable observations returned by admission. The resumed session retains artifact context without weakening the server boundary.
The composer receives the current admitted observations, bounded recent observation and feedback history, unread prepared feedback, per-locus deltas, practice definitions, and the enabled lanes with their limits. The exact files belong to the agent workspace ABI, not this guide.
Three constraints shape how that context may be used:
- Only pull-request and issue handlers enable composition, and not for backfills. Document and conversation reviews record observations but compose no feedback.
- History is bounded and therefore supports recurrence claims, never claims that something has never happened before.
- Deltas describe a locus on one artifact as
NEW,RECURRING,UNCHANGED, orRESOLVED. Cross-artifact habits require distinct-artifact evidence from history; an unobserved prior strength is notRESOLVED.
Accepted tool calls update the composition output incrementally. The server parses proposed units and applies all delivery gates; the model never writes the feedback ledger directly.
Channels are distinct interventions
The channel contract is defined in the practice review glossary. The composition turn must not render one advice string three ways:
IN_CONTEXTis public task-level feedback about the current work. It binds to a current admitted observation and uses either server-resolvedDIFFplacement or artifact-level placement. History alone cannot support a public claim.IN_APPis private process-level feedback about a pattern across several pieces of the recipient's work.IN_CHATis private context for a later mentor turn. It preparessituation,capability,evidenceSummary, andinConversationSignalas notes, not dialogue. The mentor receives the original evidence and live conversation, then may adapt, defer, or discard the note.
An issue has no diff and therefore permits only artifact-level in-context placement. Pull requests permit both placements. Per-lane limits are supplied in composition configuration, not fixed in this guide.
What the composer is not allowed to do
The tool schema and server admission refuse the same four things. Validation exists on both sides of the sandbox boundary because a guard the constrained party can skip is advice, not a boundary. An invalid unit is dropped and logged without failing the completed measurement.
- No verdict fields.
report_feedbacktakes no presence, assessment, severity or confidence. An intervention that could carry a verdict would eventually be read back as one. - No model-authored path or line number.
placement.kind: "DIFF"names an admitted{ observationId, citationIndex }, and the server resolves the file, side, and line from that observation's citation.placement.kind: "ARTIFACT"carries no coordinates and stays in the summary. Both forms require a current admitted observation of the same practice. - No invented supersession target.
action: "SUPERSEDE"must name athreadKeythat was staged inprepared.json. - No second unit on the same lane about the same practice. One unit per
(channel, practiceSlug); the rest are dropped.
Staying quiet is the fourth action, not a gap: action: "WITHHOLD" with NO_MATERIAL_CHANGE,
ALREADY_SAID or BELOW_BAR. Those three are the composer's own vocabulary and are deliberately not
FeedbackSuppressionReason values — the glossary says
why the two must never be merged.
Supersession is likewise the glossary's, under Thread key and supersession. The only part this stage owns is that composition proposes the thread action; the server, not the model, decides whether the named thread may be superseded at all.
When composition does not run
A composition failure does not invalidate admitted observations. An empty result is distinct from a turn
that never ran. In-context feedback may fall back to the observation's evidenceRationale and the
practice's whyItMatters, but invents no next step. Longitudinal lanes have no content fallback; per-lane
preparation marks let a sweeper retry only turns that did not run.
Delivery and human approval
Composition creates immutable feedback units before release authority is applied. A composer WITHHOLD
means there was nothing useful to say and creates no feedback row. A policy refusal is instead persisted as
SUPPRESSED with a server-owned reason. These outcomes must not be conflated with a human rejection.
Autonomy is only the authority step, and the glossary owns it in full — the resolution chain and the three values in Practice autonomy, the approval lifecycle alongside them, and the ordered list of gates every delivery walks in the delivery policy check order. Read those before changing anything here, and do not restate them.
One fact belongs to this stage rather than to that vocabulary: PracticeAutonomyPolicy is a second,
narrower predicate, and each channel's router applies it itself. It admits a unit only when the
observation's immutable ObservationOrigin entitles it to that channel and the effective autonomy is
AUTOMATIC. That is why a backfilled observation can never reach the artifact or a mentor turn whatever
anybody configures: the entitlement is a property of how the measurement was obtained, not of what the
workspace currently permits. The two halves answer different questions — origin asks what a provenance may
ever be said on, and DeliveryPolicyResolver asks whether today is the day.
Runtime and extension points
The server records an occasion and enqueues an agent_job; a worker claims it, captures evidence, runs the
sandbox, admits observations, resumes composition, applies delivery gates, and calls the provider. Runtime
roles, transaction boundaries, extension ownership, and change guidance are documented in
Practice review runtime.
Reading a real review back
PracticeTraceEntryDTO is the derived answer to "what did every practice make of this piece of work", and
it is what the Review activity screen renders for every member. It is derived, never stored:
PracticeTraceDeriver walks an ordered precedence list to a single PracticeTraceOutcome.
The trace separates two axes on purpose, and reading it wrongly is the fastest way to misdiagnose a quiet workspace:
outcomeis about the measurement, and is one of these values:REVIEWED,RUNNING,PENDING,SKIPPED,NOT_ASSESSABLE,TURNED_OFF,NOT_OCCASIONED,DORMANT,LAPSED,FAILED.observationCount,deliveredCountandwithheldReasonsare about the intervention.
At HUMAN_APPROVAL, REVIEWED and a positive observation count describe the measurement; the proposal's
AWAITING_APPROVAL, PREPARED, DISCARDED, or eventual DELIVERED state describes the intervention. Do
not represent a pending human decision as suppression: it is an actionable workflow state.
The precedence rule is not "configuration beats mechanics" — it is what a run recorded beats what today's
configuration implies. A verdict a run wrote against this practice by name (REVIEWED, SKIPPED,
NOT_ASSESSABLE) is returned even after the practice is switched off, because it happened. TURNED_OFF
comes next, and therefore outranks everything merely re-derived from the occurrence ledger — FAILED,
LAPSED, PENDING — so a practice the workspace switched off reads as switched off rather than as broken.
A new outcome has to be placed on the right side of that line, not ranked against the others by severity.
Operator-facing guidance for reading the same screen is on Practice review; the vocabulary both pages use is defined in the practice review glossary. Persistence concepts and evaluation joins are in Practice review data model and Evaluation provenance.