Version. 0.2 · Date. 2026-08-07 · Author. Claude.ai (v0.1 drafting) / Claude Code (v0.2 amendment) / Marvin Percival (approving).
Supersedes. v0.1 at record 43841fe, which stands as a sibling.
Target. /Users/dunin7/loomworks-engine and /Users/dunin7/loomworks.
Baseline. Engine 699f9ea (post CR-2026-183); surface 852124b.
What changed from v0.1. Step 1's read disproved the premise §2 was written against. The defect is real and worse than described; the remedy moves; two build steps are withdrawn on finding; and the engine half turns out to be already complete.
v0.1 said: "When a Shape production job fails, the surface shows nothing that distinguishes it from a job still running. The person waits for something that is never coming."
It does not look like one still running. It drops out of the active list entirely.
dashboard.py:93 filters sj.status IN ('queued', 'dispatched'); render jobs the same at :111; the counts query repeats both at :145-150. A job that reaches failed or declined no longer matches, so it disappears.
> So the person is not left waiting on an item that never resolves. They watch the item vanish and reasonably conclude it finished. That is a positive false impression, not an absence of information — and it is the worse defect. A person who is still waiting will eventually ask; a person who believes the work completed will not.
Operator, 2026-08-07: "That's a positive false impression, not missing information, and it's the worse defect."
A failed production produces no Shape at all. In agents/shaping.py, the ShapeEvent is constructed and appended inside the try block (:688, :719); mark_completed(shape_event_object_id=shape_event.id) runs only after. Any exception before that reaches mark_failed (:399) — and no ShapeEvent was ever created.
shape_event_object_id on shaping_jobs is therefore a result pointer, not a link: populated at completion (shaping_jobs.py:111), and necessarily null on failure because there is nothing to point at.
Two build steps are withdrawn on finding, not skipped:
Both were written against a premise Step 1 disproved. Recording them as withdrawn-on-finding rather than as shortfall, per the Operator's direction.
v0.1 settled: "a new field on the existing response, not a new route."
Operator, 2026-08-07: "The decision transfers in spirit, not in object. What I was settling was 'no new route, no new auth and caching story for status.' That holds. But a failed production produces no Shape, so the Shape response can't carry it. Put the status on whichever existing response already carries the JOB."
Confirmed at Step 1 rather than assumed: the dashboard's active list is built from shaping_jobs and render_jobs directly (dashboard.py:90, :110). It already carries the job. It is also the only place a person watches for this work, and the surface's DashboardView is what renders it.
Remedy: widen the active list to include terminal-failed, and carry the status and reason through. Existing response, existing auth, existing caching. No new route.
Rejected, in the Operator's words:
GET /engagements/{eid}/shaping-jobs/{shaping_job_id} (shape_events.py:352) already returns status — including failed — and error_message, which mark_failed populates with the exception text and mark_declined with the reason.
The surface has never called it. Zero references to shaping-jobs anywhere in the surface.
So no new failure-capture was needed, exactly as v0.1 required — but for a stronger reason than it anticipated: the exposure already existed and was simply unreachable in practice, keyed by an id the surface holds only at trigger time and never again.
> This is the fifth item this session opened and found already built or already satisfied — after B-12 (satisfied), B-13 (built), B-17 (built, then unbuilt by a redesign), and B-43 (data layer built, unconsumed). B-19's standing check is extended accordingly: the list's statuses are not the only thing drifting from reality — its READY items include work already done.
mark_failed is terminal. Nothing re-dispatches a failed shaping job; there is no retry path in the engine. Therefore no retry control appears on the surface, per v0.1 §2.5's only-show-what-is-available instruction. Reported, not built.
| Step | What | Mode | State |
|---|---|---|---|
| 0 | Pre-flight, both repos | Auto | done |
| 1 | Read the job record; confirm which response carries the job | Auto | done — §1–§5 |
| — | ~~Populate the link column at creation~~ | — | withdrawn on finding (§2) |
| — | ~~Status field on the Shape response~~ | — | withdrawn on finding (§2) |
| 2 | Active list includes terminal-failed; carries status + reason | Auto | pending |
| 3 | Surface renders the failed state distinctly; no retry control | Auto | pending |
| 4 | Engine suite + surface sweep; ruff 0; mypy at/below baseline | Auto | pending |
| A | Checkpoint | Checkpoint | pending |
| 5 | Tag cr-2026-184-b37-failed-production-visible in both repos | Auto | pending |
Dropped from v0.1's gate: "link column populated at creation; existing rows with a null link reported and backfilled." There is nothing to backfill — a null link on a failed job is correct and permanent.
DUNIN7 — Done In Seven LLC — Miami, Florida CR-2026-184 — B-37 failed production visible — v0.2 — 2026-08-07