Found during merged-main verification of #40 (c046ef8) by an adversarial sweep. Recording only — no scope expansion during the merge closeout.
The Build view renders what happened but not why, on every failure path. foldBuildEvents captures progress.report (which carries the emitter's verbatim refusal, per-attempt adapterError, and the S3 findings), and the view never reads them.
| Failure |
What the user sees |
What was available |
Emit refusal (failed-gate) — the dominant real-model failure |
three green gates + outcome: failed-gate |
the verbatim refusal in report.emitted |
| Adapter failure (model not pulled, Ollama down, non-JSON) |
outcome: failed-adapter |
the typed error in report.attempts[].adapterError |
| Governance (S3) failure on the deciding attempt |
S3 FAIL with an empty tooltip |
rule id, message and rationale in the streamed findings |
| Accept refused server-side |
Accept failed: agent replied 422 |
the full findings array in the 422 body |
Two mechanisms:
apps/composer/app/views/build-view.tsx:87-92 renders outcome: and only progress.error (set by RUN_ERROR/dspack.error, never by a pipeline failure outcome). packages/composer-core/src/build.ts:108-113 stores the report but the fold exposes no reason field.
apps/composer/app/agent-client.ts:47 — the shared post<T> helper maps any non-2xx to { ok: false, error: payload.error ?? "agent replied " + status }. saveExample deliberately answers 422 { ok: false, findings: [...] } with no error key, so state.tsx's findings-aware branch never runs and the gate reasons are discarded.
Related: lintSurface records S1/S2 problems as gate errors and S3 as findings, so the repair transcript rendered verbatim in Build reads "violates 0 governance rule finding(s)" whenever the failure was S1/S2 — technically accurate, actively confusing as the visible proof of the governed repair loop.
Server-side correctness is unaffected and tested (the 422 refusal writes nothing; e2e/composer-build.spec.ts:142). This is purely the diagnostic surface.
🤖 Generated with Claude Code
Found during merged-main verification of #40 (c046ef8) by an adversarial sweep. Recording only — no scope expansion during the merge closeout.
The Build view renders what happened but not why, on every failure path.
foldBuildEventscapturesprogress.report(which carries the emitter's verbatim refusal, per-attemptadapterError, and the S3 findings), and the view never reads them.failed-gate) — the dominant real-model failureoutcome: failed-gatereport.emittedoutcome: failed-adapterreport.attempts[].adapterErrorS3 FAILwith an empty tooltipAccept failed: agent replied 422Two mechanisms:
apps/composer/app/views/build-view.tsx:87-92rendersoutcome:and onlyprogress.error(set byRUN_ERROR/dspack.error, never by a pipeline failure outcome).packages/composer-core/src/build.ts:108-113stores the report but the fold exposes no reason field.apps/composer/app/agent-client.ts:47— the sharedpost<T>helper maps any non-2xx to{ ok: false, error: payload.error ?? "agent replied " + status }.saveExampledeliberately answers422 { ok: false, findings: [...] }with noerrorkey, sostate.tsx's findings-aware branch never runs and the gate reasons are discarded.Related:
lintSurfacerecords S1/S2 problems as gate errors and S3 as findings, so the repair transcript rendered verbatim in Build reads "violates 0 governance rule finding(s)" whenever the failure was S1/S2 — technically accurate, actively confusing as the visible proof of the governed repair loop.Server-side correctness is unaffected and tested (the 422 refusal writes nothing;
e2e/composer-build.spec.ts:142). This is purely the diagnostic surface.🤖 Generated with Claude Code