# 0.10.2 — The mvp-demo-redux receipt: same notebook spec for 36% the cost

The release that lands with the strongest cost-trajectory receipt we've ever shipped. mvp-demo-redux ran the same FastAPI + React notebook spec that mvp-demo2 shipped for $83.78 on 0.8.0 — and produced a working app for **$30.58** on the 0.10.2 substrate. 63% reduction. App boots, persists, all CRUD + search + tag filtering functional. The per-milestone trajectory shows the substrate's "foundation-once, capability-cheap" claim in numbers for the first time: M1 foundation $15.59, M2 capability $10.91, **M3 capability $3.72**.

Full pilot writeup at [`analyses/046-mvp-redux-cost-receipt.md`](../analyses/046-mvp-redux-cost-receipt.md). Pilot artifact preserved at [`demo/mvp-redux/`](../demo/mvp-redux/) alongside the original [`demo/mvp/`](../demo/mvp/) for direct comparison.

This release also bundles three substrate fixes shipped pre-pilot to address gaps observed mid-run (T-ab58/T-ab59/T-ab60), plus the analysis + showcase update that surfaces the cost-savings story prominently in the README.

---

## milestone_plan directive engages foundation/capability decision per milestone (T-ab58)

obol-260522-1 planner shipped 9 milestones, ALL kind=capability — even ones the operator manually had to flip to foundation (Plaid integration M7, plus M9 infrastructure). The existing directive had detailed foundation/capability guidance but three weak spots that caused agents to default to capability:

1. **The "(default)" framing on capability** read as "pick this when in doubt." Removed.
2. **The "MUST produce kind=foundation M1" rule (T-ab32) only covered M1.** No equivalent guidance for non-M1 foundation milestones (mid-project infrastructure like external API integrations, auth/session, build pipeline maturity).
3. **No quick decision rubric.** Agents read the long kind-discrimination paragraphs but had no compact "is this foundation?" test to engage per-milestone.

Three changes to the directive:

- **Removed "(default)" from the capability description.** Reframed as "deliberate per-milestone choice, not a default-to-capability fallback. Both kinds are equally valid; the question is what the milestone primarily DELIVERS."
- **Added a quick decision rubric**: read done_when aloud → "project/developer gains the ability to X" → foundation; "end user can do X" → capability; mixed → split or pick by what downstream depends on (foundation deps cascade).
- **Added explicit guidance on non-M1 foundation milestones**: external API integrations (Plaid, OAuth, payment), auth/session/multi-user infrastructure introduced mid-project, persistence layer changes, build/deployment maturity. Specifically called out the "API can fetch X from Plaid" done_when shape as foundation, separate from the capability that consumes it ("Kohl sees transactions").
- **Named the obol failure mode** as a "don't be that pilot" caution at the end of the kind-discrimination block — concrete observed precedent makes the rule load-bearing.

Pure directive change in `milestone-plan.yaml`. No substrate code touched. 30/30 milestone+load tests pass.

Receipt: next pilot's `milestone_plan` output should have a non-trivial number of kind=foundation milestones for projects with external integrations, multi-user features, or build pipeline work beyond M1. If everything still ships as capability, the directive needs another iteration (maybe enum the foundation-shape signals in the requirement filter directly).

## M8 review always produces a verdict artifact (T-ab59)

mvp-demo-redux pilot surfaced a hang: post-T-ab54 (caterpillar-only M8 roster) + T-ab49-revert (single-agent succession on 1 PASSED window) means a caterpillar who picks `decision: silence` on his only window ends the meeting cleanly (PhaseEnded reason=succession, MeetingEnded outcome=COMPLETE) but with ZERO review artifact on bus or disk. Feature's tickets stay IN_PROGRESS forever because the lifecycle transitions depend on a review existing.

Two-prong fix:

**Directive prong** — Caterpillar's M8 convenor_directive now explicitly states "You MUST emit a review artifact" and walks through the three valid verdicts (`accept` / `request-changes` / `block`). For the "nothing concerning" case, the directive tells him to ship `verdict: accept` with a brief approvals note rather than silence — the audit trail is cleaner when caterpillar himself ships the verdict.

**Substrate prong** — `_apply_review_verdict_routing` now calls `_synthesize_default_accept_review` when both bus and disk reconciliation find nothing. The synthesized review is tagged with "caterpillar silenced" in the title + approvals so the audit trail makes clear this was substrate-generated. Lifecycle unblocks; M9 verify still runs as a separate substrate-side check so structural issues aren't masked.

`_reconcile_accept_reviews_from_disk` now returns bool (routed-anything) so the caller can decide whether synthesis is needed. The change is backward-compatible — existing callers ignoring the return value continue to work.

3 regression tests added in test_workflow.py:
- `test_t_ab59_synthesizes_accept_review_when_caterpillar_silences` — happy path: no bus utterance, no disk review, synthesis fires, ticket→DONE.
- `test_t_ab59_skips_synthesis_when_disk_already_has_review` — guard against duplicate synthesis when caterpillar wrote a review via write_file (T-ab43 path).
- Updated `test_t_ab43_skips_pre_meeting_disk_reviews` — pre-meeting reviews still don't drive routing, but T-ab59 synthesis now unblocks the lifecycle regardless. New assertion: ticket DONE via synthesis, two reviews on disk (stale + synthesized).

284/288 tests pass. Combined with T-ab58 (milestone-plan kind discrimination), this branch is ready for the in-flight mvp-demo-redux pilot to pick up without restart.

## Source-line context in npm build failure findings (T-ab60)

mvp-demo-redux M2 surfaced a 5-cycle convergence loop on a TypeScript type-mismatch error:

```
src/NoteEditor.tsx(169,13): error TS2322: Type '{...; resize: string; }' is
not assignable to type 'Properties<...>'.
  Types of property 'resize' are incompatible.
    Type 'string' is not assignable to type 'Resize | undefined'.
```

The error message is dense but the fix requires *seeing the actual `resize: ...` value* at line 169 — which the agent couldn't see in the verify-spawned ticket. Each Tweedle cycle had to re-read the file before they could even diagnose. 5 cycles × ~$2/cycle = ~$10 of avoidable iteration cost on a single ticket. Operator manually intervened to get past it; substrate would have looped further.

Two enhancements to `verification.check_npm_build` on failure:

1. **All error locations extracted, not just first.** New `_extract_all_npm_error_locations` deduplicates and preserves order across the build output. Cascading errors (one bad import → 3 type errors downstream) get all surfaced rather than just the topmost.

2. **Source-line context per location.** New `_format_source_context` reads the file at each error location and renders the failing line ± 3 surrounding lines as a code block with the failing line marked (`→`). Caps at 5 distinct locations to keep finding size bounded; beyond that, the remaining error count is noted and the raw output covers the tail.

Path resolution handles npm's convention of emitting paths relative to `package.json` dir (e.g. `src/foo.tsx` when running from `frontend/` but project_root is one level up): walks `rglob` by basename as fallback when the direct relative path doesn't resolve.

Output shape for the synthesized ticket's request field:
```
Run `npm run build` locally and fix the errors below...

**Source context at error locations** (failing line marked with `→`):

```src/NoteEditor.tsx:169 (col 13)
  166 |     border: '1px solid #ddd',
  167 |     borderRadius: '4px',
  168 |     fontFamily: 'inherit',
→ 169 |     resize: 'vertical',
  170 |     flex: 1,
  171 |     lineHeight: 1.5,
  172 |   },
```

**Raw build output:**
[full npm error]
```

Tweedles now see the actual offending line and can diagnose without re-reading. Same conversion cost (1 cycle vs 5) ≈ **$8/ticket saved on type-error failures.**

4 regression tests cover: multi-error extraction + dedup, source context formatting with line markers, missing-file defensive return, package.json-relative path resolution fallback.

pytest equivalent: `_extract_test_traceback` (T-ab30) already includes the failing source line via pytest's verbose output. T-ab60 closes the same gap for npm errors specifically.
