# Dev — unreleased

Active changes accumulating toward the next cut. On release, copy this file to `release-notes/<version>.md` and wipe back to header-only.

### Dashboard feature attribution strips guid:slug prefix from sources

T-g5 introduced guid:slug-prefixed source forms (e.g., ``01KRWJ4M:install-react-markdown-...``). The dashboard's primary-milestone attribution logic was still comparing raw source strings against plain story slugs from the milestone story-scope, producing zero overlap → orphan features. Mvp-demo M3 surfaced this: the markdown-preview feature shipped cleanly with 3 tickets but didn't appear under any milestone in the dashboard because its sources used the guid:slug form.

Fix: ``_compute_primary_milestone_per_feature`` now strips any ``guid:`` prefix from each source before computing overlap. Plain slugs still match; guid:slug forms now match via the slug suffix.

### T-a1 instrumentation: milestone-unlink audit log

Mvp-demo lost the M1 milestone file twice during this pilot, and the deletion path remained untraced — the snapshot empty-emission guard is in place, the retract scope-guard handles tickets, the dashboard's only delete_by_slug caller is the ticket prune action. Some path is missing from the audit. Added a `_log_milestone_unlink` instrumentation wrapper at every known unlink call site (MilestoneRegistry.write rename path, MilestoneRegistry.delete_by_slug, the workflow snapshot deletion loop) that writes attributed log lines + stack traces to `<project_root>/.wonderland/milestone-unlink.log` plus a stderr line for live visibility. The next M1-deletion repro will be debuggable from the log.

### Auto-synthesize default directive from milestone scope

When `run_workflow` fires with empty/whitespace directive AND a milestone scope is active, the substrate now synthesizes one from milestone fields — name + slug + goal + done_when bullets + sibling milestone disambiguation block ("M1 already shipped, M3 future — leave their territory"). Surfaced mid-mvp-demo2 M2 design: Alice was drifting into M1-flavored capture stories during M2 design because no per-run signal said "you are designing M2 specifically." The framing block helps at the entry meeting but the directive is operator-facing intent; missing one was a missing signal. Substrate-side derivation, zero operator burden. Operator can still pass explicit directive to override. Filed/shipped as `124b5858`.

### T-a5: cross-feature ticket consolidation (M3.75)

Validation5 + mvp-demo both surfaced near-duplicate tickets across features after parallel M3 decomposition: "sqlite schema for notes" ended up under both the persistence feature AND the test-infra feature. M3.5 catches duplicates WITHIN a feature; nothing catches them across features.

New module `wonderland.cross_feature` does substrate-side detection + auto-retract. No agent calls. Algorithm: cluster tickets by their upstream source frozenset (excluding the parent feature slug); flag clusters with ≥2 tickets across ≥2 distinct parent features as cross-feature duplicates. Best-home selection scores each candidate parent by Jaccard similarity between the cluster's upstream sources and the feature's source set; winner keeps its ticket, losers get walked through the lifecycle to ABORTED (PENDING → QUEUED → IN_PROGRESS → ABORTED as needed). Files left on disk for forensics; lifecycle is the canonical state signal.

Handles the T-g5 `guid:slug` source form (strips the prefix before clustering). Conservative — only consolidates on EXACT match of non-parent sources, no fuzzy matching. False negatives (missing some dupes) are preferable to false positives (collapsing legitimately-distinct work).

Wired into `run_workflow` — fires automatically at end of `tdd-design` and `tdd-decompose` runs via `_maybe_fire_cross_feature_consolidation`. No YAML changes needed. Skips silently for other workflow names. Stderr emits `[cross-feature-consolidation]` with cluster count + per-decision summary for live visibility.

9 new tests. Full sweep: 398 passing.

### T-a4: env-class verify-failure routing

Mvp-demo F2 hit a wedge: pytest collection failed because `fastapi` wasn't in pyproject deps, the substrate routed the fix as an implementation ticket, Tweedles correctly refused (`"this is environment setup, not implementation"`), 2 consecutive runs hit MEETING_BUDGET. The team can't honestly install a dep — that's operator/devops work.

New module `wonderland.verify_routing` classifies each verify-failure finding as code-class or env-class via regex patterns: `ModuleNotFoundError: No module named`, `npm ERR! Cannot find module`, `pytest cannot collect tests`, etc. Conservative default — unmatched findings stay code-class to avoid false-positive operator noise.

In `_run_verify_meeting`, after the review's findings are assembled but before `_route_blocking_review` fires, the findings get partitioned. Env-class findings are written to `.wonderland/operator-attention/operator-attention-<short_guid>-<feature_slug>.md` with a suggested command (e.g., `uv add fastapi`, `cd frontend && npm install react-markdown`). Only code-class findings go through ticket synthesis. If ALL findings are env-class, `_route_blocking_review` doesn't get called at all — no ghost tickets, no MEETING_BUDGET cycle.

Stderr emits `[operator-attention]` for live visibility. The build_check meeting's `artifact_kinds` count includes `operator_attention` so the run summary surfaces it. Dashboard UI surface is a future chunk; the on-disk artifact + stderr signal are the durable channels.

12 new tests (classifier coverage, partition correctness, suggested-command generation, end-to-end routing). Full sweep: 389 passing.

### T-a3: convergence-failure detection

Mvp-demo F1 burned 5+ review passes oscillating on DELETE shape + tag-validation error shape (~$11) because Contract Note 001 + ADR-001 were under-specified — implementers swung across valid interpretations, Caterpillar rejected whichever was current. No substrate signal said "this is spec ambiguity, not a code bug."

New module `wonderland.convergence` detects the pattern. Computes a fingerprint per finding `(file_location_no_line_numbers, normalized_concern_first_60_chars)`; if the same fingerprint appears in EVERY review across a 3-pass window (current + 2 prior) on the same feature, emits a `ConvergenceFailure` flagging spec ambiguity. Best-effort extraction of cited Contract Note / ADR references from the recurring concerns helps the operator find which artifact to disambiguate.

Persists to `.wonderland/spec-ambiguity/spec-ambiguity-<short_guid>-<feature_slug>.md` so the dashboard can surface it (TUI work is a future chunk; the on-disk artifact is the durable signal). Stderr emits a `[convergence-failure]` line for live visibility during runs.

Wired into `_route_blocking_review` after the existing ticket-synthesis path; gated on `auto_complete_in_flight_tickets=True` so the build_check path opts out (build_check failures are env/test-class, not spec ambiguity). Tickets still get synthesized as usual; the spec_ambiguity artifact is additional substrate signal.

Distinguishes oscillation (same fingerprint across passes — recurring) from progressive deepening (different finding each pass — converging on deeper layers). Mvp-demo F1 was oscillation; F2 was progressive deepening.

10 new tests. Full sweep: 377 passing.

### T-a2: branching episodic memory — chunks A+B+C shipped

Schema v2 of EpisodicStore adds `branch_id` per utterance. A task-local ContextVar drives active branch tagging at write time; mvp-demo's wedge pattern (M2 stale-req discussion bleeding 291 utterances into M4 design) becomes structurally impossible because each milestone's design pass tags its memory under `design:<slug>` and queries scope reads via inheritance_chain (project + own-branch only — design churn does NOT cross-bleed to sibling milestones).

Operator decisions baked in: impl does NOT inherit design memory (Q1, the design-to-impl bridge is contract artifacts on disk, not memory); consolidation fires when the last unverified feature in a milestone transitions to VERIFIED (Q2).

**Chunk A (schema + read/write path)**: `branch_id` column on utterances + index, idempotent v1→v2 migration, `get_active_branch_id() / set_active_branch_id() / reset_active_branch_id()` helpers using `contextvars.ContextVar`, `inheritance_chain(branch_id) -> list[str]` returning `[project, branch_id]`. `record()` reads active branch automatically. Query methods accept `branches=[...]` filter; default `None` matches old behavior (operator/dashboard view).

**Chunk B (runner integration)**: workflow.py sets the branch at run start (via `_derive_branch_id(workflow_name, scope)`) and resets in `finally`. Derivation: tdd-design/tdd-decompose + scope → `design:<slug>`; tdd-implement + scope → `impl:<slug>`; everything else → `project` or `run:<workflow>:<slug>` fallback. Concurrent pipeline-parallel runs each get their own branch via ContextVar task-locality.

**Chunk C (consolidation primitive + dashboard hook)**: new `wonderland.memory.consolidation.consolidate_milestone()` orchestrator iterates every per-agent EpisodicStore, archives `design:<slug>` and `impl:<slug>` branches (rewrites to `archived:design:<slug>` and `archived:impl:<slug>` — still on disk for forensics, excluded from default reads), records a project-level summary utterance attributed to Mock Turtle (substrate-generated, no LLM call). Dashboard verify action fires `_maybe_consolidate_milestone_for_feature` — checks if every primary-attributed feature in the milestone is now VERIFIED, fires consolidation if so. Operator gets a notify with per-agent archive counts.

Tests: 11 new (6 in test_episodic.py for branching semantics, 5 in test_memory_consolidation.py for orchestrator). Full sweep: 411 passing, 4 skipped, no regressions.

### T-a1 instrumentation: milestone-unlink audit log

### `success_criterion` requirements also exempt from realization (round 2)

Following the round-1 fix exempting `scope` and `constraint` kinds, mvp-demo M4 surfaced the same wedge shape on `success_criterion` requirements (e.g., "v1 ships when developer can clone-run in 5 minutes"). These are measurable outcome statements — operator-validated, not feature-realized — same conceptual category as scope/constraint. Added to `_NON_REALIZABLE_REQUIREMENT_KINDS`. M4 design ships stories but composition was budget-exhausted on phantom coverage gaps; the fix unblocks it.

### `scope` and `constraint` requirements exempt from milestone-realization check

The `milestone_realization` coverage check previously demanded that EVERY requirement in a milestone's `consumes_requirements` list be realized by a feature — but `scope` (boundary statements like "v1 has no auth") and `constraint` (system properties like "backend availability is hard") are meta-statements, not buildable features. When milestones consumed them, Dodo's check kept flagging phantom gaps that the agents couldn't resolve, wedging 22-rotation design loops in mvp-demo M2/M3 design passes.

Split the filter into two sets: `_NON_DECOMPOSABLE_REQUIREMENT_KINDS` (persona/situation/out_of_scope/deal_breaker — orphan check exempts these) and the new `_NON_REALIZABLE_REQUIREMENT_KINDS` (adds scope/constraint — realization check exempts these too). The orphan check still flags scope/constraint as needing milestone acknowledgment; the realization check no longer demands feature decomposition for them. Surgical fix to the right layer.

### Existing-code awareness in design-phase framing

Non-first milestones now see what's already shipped as **source code on disk** before their design pass starts. M1 design pass on a project always reads "what code exists?" as "none." M2+ design previously did too — leading the design agents to re-emit stories for foundation work that already shipped in prior milestones. Mvp-demo surfaced the failure: M2's design Caterpillar emitted backend-CRUD + SQLite-schema stories during a milestone whose actual scope was localStorage drafts + capture UX. The foundation was done; the agents didn't know.

``_format_existing_code_block`` probes a small whitelist of source paths (``src/``, ``frontend/src/``, ``tests/``, ``app/``, ``lib/``), lists ``.py / .ts / .tsx / .js / .jsx / .sql / .rs / .go`` files with line counts, skips trivial files + build artifacts + virtualenvs, and renders the list as a "DON'T RE-DESIGN" block at the head of the milestone framing. Capped at 40 rows — larger projects belong on the existing-projects onramp (P19) which builds a structured map rather than a flat file list.

Forward implication: P19's existing-state primitives are load-bearing not just for "drop into a third-party codebase" but for **every milestone past the first** within a single project. Each new milestone is "an existing codebase from the agents' perspective."

### Dashboard: features attribute to a single primary milestone

When over-broad acceptance requirements (e.g., ``v1-ships-when-all-core-capabilities-work-end-to-end``) get consumed by multiple milestones, any feature realizing them appeared under every consuming milestone's pane — visual duplication, attribution noise, and the operator couldn't tell which milestone "owned" a feature.

``_compute_primary_milestone_per_feature`` now does **strongest-overlap-wins** attribution: count the number of feature.sources that fall into each milestone's story scope, attribute to the milestone with the highest count, break ties on order (foundation wins), then on slug (deterministic). Each feature appears under exactly one milestone in the dashboard. Mvp-demo: M1's CRUD-endpoints feature now correctly sits only under M1 instead of also appearing under M2 (which incidentally shared one over-broad acceptance requirement).

### Active-milestone block prepended to non-entry meetings

Only the entry meeting (M1) got the full milestone framing via ``_prepend_milestone_framing``; M2–M5 saw the requirement set and the inherited memory, but not the milestone's own ``goal`` + ``done_when``. Mvp-demo surfaced the symptom: subsequent design phases were quietly absorbing concerns that belonged to a future milestone, because the agents had no visible criterion for "is this in scope for *this* milestone."

Fix: ``_convene_one`` now prepends a compact ``**Active milestone:** … **Done when:** …`` block to every non-entry meeting's convener directive when ``get_active_milestone_scope()`` returns non-None. Each meeting's roster sees the active milestone's name + slug + goal + done-when bullets right before its own directive, with an instruction to flag out-of-scope work as a concern instead of absorbing it. Cheap (a few hundred tokens per meeting), and the substrate already tracks the scope via the contextvar so no plumbing change.

### TUI: Queue action available on in_progress tickets

Validation5 surfaced a stuck-ticket pattern — synthesized follow-up tickets that didn't close cleanly on their implementation pass got marooned in `in_progress`. The dashboard's only action was "Mark done," which would lie about their state. The state machine already permitted `in_progress → queued` (the un-abort path in `ticket_lifecycle.LEGAL_TRANSITIONS`); the UI just wasn't exposing it. Operator can now re-queue a stuck ticket for the next implementation pass without having to fake its completion first.

### TUI: Live Call feed actually displays calls for subprocess runs

The Live Call feed in `LiveRunScreen` was reading `runner.telemetry.entries` directly via `getattr(self.handle, "_runner", None)`. That only works for in-process runs — the default `wonderland run-bg` path uses `SubprocessRunHandle` which has no `_runner` attribute (the runner lives in a separate process), so the feed stayed blank for every real pilot.

Replaced with an event-driven implementation: the dispatcher's `AgentActed` events now feed the table directly. Works for both in-process and subprocess runs since event streams are the common interface. Per-call rows show `time · agent · phase` (cost-per-call isn't on `AgentActed`; the per-agent rollup still lands in the status bar via `AgentTelemetryDelta`). Past events get buffered (capped at 200) so meeting-selection changes can replay historical activity for the newly-focused thread instead of leaving the operator staring at residue from the prior filter.

### Meeting.primary_speaker — single-author lead for snapshot decisions

Substrate primitive for designating a single roster member as the
primary author of a meeting's snapshot-shaped decisions. Currently
load-bearing only for ``milestone_plan``: when a meeting sets
``primary_speaker: white_rabbit``, the snapshot semantic uses ONLY
Rabbit's most-recent milestone_plan claims. Other speakers' (Alice,
Cat) milestone_plan emissions get snapshot-cleaned at meeting end.
Their concerns / observations / questions still flow; only the
primary decision is single-authored.

Mvp-demo surfaced the need: Alice emitted 5 persona-anchored
milestones (kohl-captures, kohl-discovers, ...); Rabbit emitted 4
technical-layer milestones (persistence, api-surface, ...) at the
same orders 2-5. Both survived the union-of-authors snapshot
because the slugs differed. Disk ended up with 9 milestones for
5 conceptual positions.

Wired ``primary_speaker: white_rabbit`` into milestone-plan.yaml;
convener directive teaches Alice + Cat to support via concern
/ observation instead of emitting parallel milestone_plans. The
substrate enforces; the directive prevents the waste.

Same pattern available for any future workflow that wants
snapshot-shaped single-author semantics. Probably not the right
primitive for multi-author negotiation meetings (M3 / M5
contract negotiation) where parallel emissions ARE the point.

### New directive: ``notebook`` — paper-MVP-ready fullstack demo

Reference directive for the Wonderland paper's reproducible MVP. Single-user markdown notebook with SQLite persistence, FastAPI backend, React + Vite frontend, client-side markdown rendering, tags, search. Designed so a paper reader can clone the repo, run the demo, and verify a working app in <5 minutes of post-install time — no signup, no user data input required.

Differentiated from the existing ``markdown-notes`` directive (which is the pure-frontend, no-backend stress-test variant) — ``notebook`` exercises the full discovery → milestone → tdd-design → tdd-implement pipeline and produces a reviewable full-stack artifact.

Target economics: 5 features × ~$5-15 = $30-60 total MVP spend. The reference cost point we'll cite in the paper.

### Fix: review-synthesized tickets weren't getting marked done after their iteration

Validation5 surfaced a ticket-state drift: 4 review-synthesized follow-up tickets shipped through implementation cleanly (Tweedles worked the threads, Caterpillar reviewed the result) but their lifecycle stayed stuck at ``queued`` instead of progressing to ``done``. Inflated the operator-visible queue count and broke cost-per-feature attribution math.

Root cause: ``_route_blocking_review`` and ``auto_complete_iteration_tickets_on_accept`` both have an auto-complete loop that marks worked tickets done — but the loop's guard required ``state == TicketState.IN_PROGRESS``. Tickets that were queued for the iteration but never had their ``queue → in_progress`` transition fire hit the guard and got skipped.

Fix: both auto-complete paths now fast-forward ``queued → in_progress`` before the done mark when they encounter a queued ticket. Same back-fill pattern that already handled ``None → in_progress``; extended to cover the queued case.

### tdd-decompose workflow + dashboard "Decompose tickets" button

New workflow for the partial-design-rerun use case: features that
landed in ``designed`` state but with insufficient or wrong ticket
sets. Validation5 surfaced the immediate need — features 4 + 5 had
zero tickets attributed (M3 slug drift left the parent feature
slugs out of the synthesized ticket sources), making them
undeployable from the implementation queue.

Three pieces ship together:

- **New lifecycle transition**: ``FeatureState.DESIGNED → IN_DESIGN``
  is now legal. The operator's "I want to redo this feature's
  decomposition" move; was previously a dead-end (designed could
  only go to queued or rejected).
- **New workflow** ``tdd-decompose.yaml``: M3 (decomposition) +
  M3.5 (consolidation) only, filtered on ``in_design``. Features
  picked up from disk via seed-fallback; M3.5 transitions back to
  ``designed`` so the operator can queue for implementation. Budget
  defaults to $1.00 (typical decompose pass is $0.10-$0.30 per
  feature).
- **Dashboard button** "Decompose tickets" on ``designed`` features.
  Transitions the feature back to ``in_design``; the operator then
  runs ``tdd-decompose`` from the run-launcher. Mirrors the
  Queue → tdd-implement UX pattern.

Known limitation: re-decomposing a feature that already has tickets
on disk will accumulate duplicates unless M3.5 actively consolidates
them (which it can, but discipline-dependent). Long-term substrate
fix is snapshot semantics for tickets, parallel to milestone_plan
snapshot semantics in 0.7.0; tracked as roadmap follow-up.

### M9 verify adds pytest_passes alongside pytest_collects

Three-tier verification now runs at end of each feature lane:
pytest_collects (do the tests import?) → pytest_passes (do they pass?) → npm_build (frontend TypeScript + Vite build).

Validation5 feature 2 surfaced the empirical motivation: Caterpillar's M8 review verdict was clean accept on code that didn't actually run. Three runtime bugs the static review couldn't see:
1. Schema drift — `Session` model declared `synced_at`; live SQLite DB had stale schema; every endpoint returned 500 with `OperationalError: no such column`.
2. Contract drift — test expects `synced_at` set on online create, impl returns None.
3. Datetime tz mismatch — `server_updated_at >= client_updated_at` raised `TypeError: can't compare offset-naive and offset-aware datetimes` at runtime.

All three would have synthesized follow-up tickets if `pytest_passes` had been in the M9 chain. Now it is. Next implementation pass picks up the bugs as queued tickets via the existing `_route_blocking_review` path.

### Tea-party skips review-synthesized tickets by default

Cost optimization for the review-loop iterations. Review-synthesized tickets (the ones the substrate creates from Caterpillar's M8 findings) come with a complete spec built in — `location` + `quote` + `read` + `concern` + `request` — so the adversarial test-scenario design pass (tea-party / M6) was adding ~$0.50/ticket of overhead for what's structurally a code-correctness restoration on already-tested paths.

Two new fields land:

- `TicketPayload.source` (TicketSource enum: `m3_decomposition` | `review_synthesis` | `operator`). Default `m3_decomposition`. Auto-set to `review_synthesis` when the substrate synthesizes a follow-up ticket from a review finding.
- `TicketPayload.test_coverage_required: bool | None`. Operator/agent override on the tea-party iteration filter. `None` (default) = use source-based default (`m3_decomposition` and `operator` pass through; `review_synthesis` skips). `True` forces tea-party inclusion; `False` forces skip.

Plus `ReviewFinding.test_coverage_required: bool` (default false) so Caterpillar can mark a finding at review time as needing fresh test design — the synthesized ticket inherits the flag. Caterpillar's directive in `tdd-implement.yaml` M8 teaches when to set it (genuinely new behavior the existing tests don't cover, e.g. "add JWT validation," "implement retry with backoff").

Tea-party gets a new `requires_test_design: true` field in `tdd-implement.yaml`; substrate filters per-ticket in `_run_inner_block` via the `read_ticket_needs_test_design` helper. Completion events still fire for skipped tickets so dependency-gated downstream lanes don't hang.

Expected savings: ~$0.50/ticket per review pass × 2-5 follow-up tickets per pass × 2-3 review passes per feature × 5 features = ~$20-35 saved across a 5-feature MVP. 10-15% cost reduction.
