# 0.9.0 — Foundation/capability axis, per-artifact milestone attribution, iteration efficiency filters


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

### `prime_directive` auto-syncs from Project registry to per-project `.wonderland/project.yaml`

Closes the follow-up from the original `prime_directive` field commit (`4be781d`): the Project registry stored `prime_directive` globally, but the per-project ProjectContext YAML (the thing actually seeded into MEETINGS via the `project_context` kind) didn't automatically pick it up. Operators had to manually edit `project.yaml`. Surfaced on obol-demo2 where the 6-milestone plan landed in the registry without the prime_directive carryover, so M4 / M5 / M8 design would have gone back to seeing only `stack + entry_point` — losing the "htop for money" framing after discovery.

Two sync points landed:

1. **`new_run.py:_launch_run`** — after writing the Project's last_run_id / last_workflow / prime_directive on run launch, mirror the prime_directive into the per-project ProjectContext. Best-effort, no-op when project.yaml doesn't exist (legacy projects without context memory), prime_directive is empty, or it's already in sync. Fires BEFORE `launch_background_run` so the subprocess reads the freshly-synced YAML.

2. **`skeleton.py:apply_skeleton` + `write_project_context_from_skeleton`** — both now accept an optional `prime_directive` parameter that gets written into the ProjectContext at project-create time. `new_project.py` passes `prime_norm` through both paths (bare-root apply + non-bare-root retrofit). Net-new projects get `prime_directive` in `project.yaml` from day one rather than waiting for first launch.

4 new tests in `test_skeleton.py` covering: directive-persists path, none-omitted back-compat, whitespace-treated-as-none, retrofit path via `write_project_context_from_skeleton`. 47/47 tests pass (test_skeleton + test_project_context).

For projects already on disk that were created before this commit: the next launch will sync automatically. Or use the one-line `load_project_context` / `save_project_context` round-trip if the operator wants to populate immediately without launching.

### Episodic memory branching — ContextVar → workflow-scoped global

Branching memory was nominally working: schema v2 has a `branch_id` column with index, `inheritance_chain()` correctly returns `[PROJECT_BRANCH, "design:m3-..."]` for active design scopes, and `run_workflow` calls `set_active_branch_id("design:<slug>")` at entry. But every single utterance from every run was tagged `branch_id='project'` regardless of the active milestone — diagnosed on obol M3 design where 1,179/1,179 of Caterpillar's utterances landed on `'project'`, letting M2 deliberation bleed into M3 recall and prompting the design caucus to loop on M2's Feature 002 instead of designing M3 budget features.

Root cause: `_active_branch` was implemented as a `ContextVar`, which captures its value at `asyncio.create_task` time. The Runner spawns each agent's `run()` as a background task during `Runner.start()` — BEFORE `run_workflow` later calls `set_active_branch_id`. Each agent task carries a snapshot of the default `PROJECT_BRANCH` for its entire lifetime; later `set_active_branch_id` calls modify only the workflow task's contextvar; agents' `memory.record(utterance)` reads its own task's stale snapshot and writes `branch_id='project'` every time.

Fix: replace the `ContextVar` with a module-level mutable `str`. The asyncio event loop is single-threaded, so process-wide visibility is structurally consistent with how workflow scope actually works. Pipeline parallelism within one workflow correctly shares the branch (all parallel ticket-impl threads belong to the same milestone-impl scope). Concurrent runners in the same process aren't a current use case; if they become one, the right fix is per-Runner state, not a return to ContextVar's brokenness.

The token returned by `set_active_branch_id` is now the prior branch_id string (was previously a ContextVar.Token); `reset_active_branch_id` tolerates legacy Token-shaped callers during migration by resetting to PROJECT_BRANCH on type mismatch — defensive, not load-bearing.

What this changes:
- Future workflow runs correctly tag utterances with their milestone-scoped branch id.
- Future agent recall (via `inheritance_chain`) correctly filters to project + active-branch — historical PROJECT_BRANCH content stops being the catch-all.

What this **doesn't** fix:
- Existing on-disk episodic data (e.g. obol's pilot history) is tagged `'project'` for every utterance and there's no way to retroactively re-attribute it to a milestone — the substrate didn't track that metadata at write time. Operators iterating on an existing project should wipe per-agent `episodic.sqlite` files (via `scripts/wipe-design.sh` or manual) before the first run on the new branching semantics; otherwise the legacy data continues to leak into recall through the PROJECT_BRANCH path.

Test: the prior `test_contextvar_isolation_between_tasks` was encoding the buggy semantic as the spec — it asserted that three concurrent tasks each see their own branch. Replaced with `test_active_branch_is_process_wide_across_spawned_tasks` which encodes the new contract: an observer spawned BEFORE the parent task's `set_active_branch_id` call still sees the new branch, because the global propagated. This is exactly the Runner-spawns-agents-first / run-workflow-sets-branch-later case that was broken in production.

284/284 tests pass across `test_episodic` + `test_workflow`.

Surfaced by obol M3 design pass (paper-grade): operator noticed M2 features leaking into M3 design caucus, traced to per-agent episodic memory. Branching memory was the architectural fix per `project_substrate_fixes_dont_propagate_through_memory.md`; this is the substrate work catching up to the design intent.

### Read-time phantom-citation filter in `seeds_fallback` — closes the obol M3 dangling-reference loop

Surfaced by the obol M3 design caucus looping for several turns on Feature 002's citations to non-existent stories. Root cause: phantom citations can become phantom *after* the on-emission strip catches them at composition time. A story gets retracted in a later run, or its .md file goes missing (substrate bug `d9c120d4`), and any features citing that story now carry dangling references through every downstream meeting that pulls them as seeds.

`seeds_fallback._load_features` and `_load_tickets` now run a citation-resolution check on each candidate artifact BEFORE the milestone-scope filter. Features whose `Sources:` line cites a story slug or GUID that doesn't resolve to a real story on disk get dropped from the seed pool with a `WARNING`-level log line naming the phantom citations. Same shape for tickets, which can cite features or stories.

The check reuses existing T-g5 machinery — `_source_resolves` and `_collect_disk_slugs_and_guids` from the on-emission strip path. New `collect_phantom_citations()` in `workflow` exports the per-sources phantom-detection logic so the on-load filter and the on-emission strip share validation logic, plus testing wedges directly on it without spinning up a full workflow.

Operator impact:
- Pilots no longer loop on coherence-fix proposals for artifacts whose cited stories were retracted in a prior run.
- The filter is read-only: it doesn't modify disk artifacts, only filters them out of seed pools. Operators clean up phantom citations manually (or via retract) when they want to fix the disk state.
- WARNING-level log emission on every drop, so the substrate is honest about what it found rather than the prior defensive-default that quietly kept corrupted records in scope.

What this **doesn't** fix:
- The root cause of phantom citations getting written in the first place (substrate bug `9231bcd5`) — that needs separate work on the agent-side citation-write path.
- The lost-feature-.md bug class (substrate bug `d9c120d4`) — needs investigation in the M2 substrate retract / wipe paths.

But it closes the loop on bug `0c98c694` (defensive-default leak): the prior `if primary is None: include` branch in `_load_features` was masking exactly this class of drift. With the new filter, unattributable features whose unattributability stems from phantom citations get explicitly dropped + warned rather than silently included.

6 new tests covering: clean-feature happy path, phantom-detection across slug/GUID/`<guid>:<slug>` citation forms, ticket-cites-feature-or-story polymorphism, the read-time `_load_features` drop, the read-time `_load_tickets` drop, and no-active-milestone-scope still applies the citation filter. Plus repairs to 6 existing fixtures that cited placeholder slugs (`"s"`, `"x"`, `"see-my-money-at-a-glance"`) without registering matching stories — latent corruption the new filter correctly surfaced; fixtures now register a placeholder story via a shared helper before citing.

384/384 tests pass across the affected suites (`test_seeds_fallback`, `test_workflow`, `test_coverage`, `test_feature`, `test_ticket`).

### `exec_smoke_probe` tool — runtime probe for Caterpillar's M8 review

Surfaced by the obol M2 post-mortem: M2 shipped a SQLite CHECK constraint (`CHECK (transaction_date <= DATE('now'))`) which SQLite refuses to enforce because `DATE('now')` is non-deterministic. The constraint is syntactically valid and passes static review, `verify_imports`, and the unit test suite — but every INSERT raises `DATABASE_ERROR: non-deterministic use of date() in a CHECK constraint`. The entire transaction-creation surface (manual entry + CSV import) was non-functional. Cat's three M8 reviews on that feature caught real findings (duplicate Account dataclass, LedgerScreen index-vs-id bug, etc.) but couldn't see the CHECK rejection — static review reading source code has no way to know which constraints SQLite will refuse at INSERT.

Fix: add a third runtime tool to Cat's review kit alongside `git_diff` and `verify_imports`. `exec_smoke_probe(snippet, timeout_seconds=30.0)` runs a Python snippet in the project root via `python -c`, captures stdout + stderr + exit code, truncates to ~4 KiB. Snippet length capped at 16 KiB. The tool's primary signal is the exit code + stderr content; ToolError only surfaces infrastructure problems (no Python, timeout, oversized snippet).

Cat's `_TOOLS_SECTION` directive prose gains a new bullet teaching when to reach for the probe: any diff that touches side-effect-producing code (DB writes, SQL execution, file I/O, subprocess invocation, network calls). The class of bug articulated: SQL CHECK rejected at INSERT, schema drift surfacing as FK violations, framework wiring that 404s or no-ops silently, async coroutines that deadlock.

Validated against the actual M2 bug: a 6-line probe (`init_test_accounts(); add_transaction(...)` → `print(result)`) surfaces the `non-deterministic use of date()` error in seconds. Cat reading that output would file a `block`-severity bug finding with the traceback message quoted.

11 new tests covering happy path, traceback capture, in-project-root execution, empty/oversized snippet rejection, timeout enforcement, output truncation, and dispatch wiring. 84/84 `test_tools.py` pass.

Caveats / follow-ups:
- The schema-drift bug class (M1 TEXT PK → M2 INTEGER PK without migration) is partially probe-catchable but harder — needs the probe to set up M1-era DB state, which Cat would have to know to do. A narrower `verify_schema_stable` tool (compare current CREATE TABLE statements against existing DB at standard paths) would catch it directly; filed as future work.
- Probes that write to the filesystem leave artifacts (test DBs, temp files). For obol-class projects this is benign; for projects with shared global state, Cat's directive guidance ("flag probe side-effects that the production code shouldn't have") is the discipline mechanism, not a substrate sandbox.
- Cost: each probe is a single tool-use round-trip; the snippet + output sit in Cat's context for the rest of the review. Tightening over time if probe-output-bloat becomes a cost driver.

Origin: operator post-mortem on the obol M2 review pass where multiple Cat verdicts shipped despite the runtime-blocking CHECK bug. The pattern echoes the mvp-demo2 stale-schema and partial-node_modules findings that surfaced during live verification — runtime-only bugs cluster as a class that static review structurally cannot reach.

### `demo/` reorganized into `demo/<pilot>/` for multi-pilot artifacts

Mvp-demo2's shipped artifact moved from `demo/` to `demo/mvp/`. The `demo/` parent is now reserved for additional pilot reference applications (e.g. `demo/crm/` if/when the CRM pilot ships, future obol artifact at `demo/obol/`, etc.). Pyproject's sdist exclusion still prefix-matches everything under `demo/`, so the PyPi build remains unaffected.

Path references updated across:
- `paper/README.md` — repo layout section reflects the new convention
- `paper/artifacts/code-quality-mvp-demo2.md` — all 8 file:line citations
- `paper/artifacts/comparison-baselines/README.md` — wonderland-trail links + cold-reviewer references; also fixed one stale relative-path link that had been one `../` short
- `paper/artifacts/limitations-chapter-source.md`, `mvp-demo2-pilot-narrative.md`, `future-work-chapter-source.md`
- `src/wonderland/closet/analyses/034-mvp-demo2-autonomous-pilot.md`
- `release-notes/0.8.1.md` — historical doc, but dead links updated so readers don't 404
- `demo/mvp/README.md` — self-references (header, run instructions, all 7 relative links retargeted from `../<path>` to `../../<path>`)

Git tracked all 700+ moves as renames (R) cleanly, so blame/log on the artifact files still threads through to the pre-rename commits.

### Foundation/capability axis lands across milestone, feature, story, requirement schemas

The big structural addition. The substrate had a flat kind taxonomy (`persona / situation / constraint / integration / scope / success_criterion / behavior / out_of_scope / deal_breaker`) that conflated WHAT SHAPE an artifact is with WHICH DIRECTION it points. The same kind can be foundation-oriented (a success_criterion about "persistence contract verified") or capability-oriented (a success_criterion about "user finds notes by phrase"). Pilots kept hitting the same failure mode shapes: foundation milestones consuming capability requirements, capability milestones tagged for foundation work, agents drifting cross-scope because the kind taxonomy couldn't disambiguate.

New `RequirementAxis` enum (`foundation | capability | both`) added to `RequirementPayload`. Default `both` for permissive back-compat — legacy requirements without explicit axis don't trigger validator rejections. Renderer adds `**Axis:**` line; parser extracts it; record carries it through.

Paired with `MilestoneKind` enum (`foundation | capability`) and `feature.milestone` / `story.milestone` explicit attribution fields. The substrate now classifies on TWO orthogonal axes: kind (what shape) and orientation (foundation vs capability), with every artifact type carrying both signals.

Substrate consumers of the new axis:
- `MilestoneRegistry.write` enforces kind-consistency: foundation milestones can't consume capability-axis requirements; capability milestones with all-foundation-axis consumes get rejected with a "should be foundation" suggestion
- `tdd-design` scoping phase's roster filter swaps Caterpillar ↔ Alice based on milestone kind (foundation → Cat solo, capability → Alice solo)
- `milestone-plan` convenor directive teaches the foundation/capability discipline explicitly with the requirement-shape allowed-list

### Explicit per-artifact milestone attribution

`feature.milestone` and `story.milestone` are now first-class fields on the respective payloads, parser-extractable from the rendered markdown. The substrate's attribution layer (TUI per-milestone grouping, seed-loader scope filter) reads these directly when present; the legacy Jaccard heuristic stays as fallback only for pre-T-ab5 artifacts.

Substrate enforcement:
- `FeatureRegistry.write` rejects features whose `milestone` field doesn't match the active scope at write time — Rabbit can't silently cross-emit features for other milestones during a scoped design run, gets the error back and either re-emits correctly or surfaces as a `concern`
- Iteration filters (M3 decomposition, M3.5 consolidation, M5 contract-negotiation) implicitly skip features whose milestone doesn't match the active scope — features tagged for other milestones get filtered at iteration entry, not burned through downstream phases
- Story/feature seed-loaders fall through the explicit field first, intersection-based attribution second

Eliminates a class of cost balloons observed across mvp-demo2 and earlier obol pilots: cross-emitted features generating wasted decomposition + consolidation + contract iterations.

### Iteration efficiency primitives

Three filters that prevent the substrate from burning budget on features/tickets that don't deserve iteration:

- `iterate_only_with_tickets` — per-item meeting filter, drops features with zero constituent tickets before iteration. Wired on `tdd-design.yaml` M5 contract-negotiation: a feature whose decomposition + consolidation produced no tickets has nothing for the Tweedles to negotiate a contract about; iterating it burns ~$0.30-0.60 for no output.
- `requires_active_scope_tickets` — meeting-level skip guard. When set + active milestone scope is active + zero in-scope features have tickets, skip the meeting entirely. Wired on M4 architecture (Mock Turtle's Lament): ADRs grounded in zero concrete deliverables end up generic/abstract; cheap to skip rather than burn budget on a no-op meeting.
- Implicit active-milestone iteration filter — when active scope is set and per_item is feature, only iterate over features whose explicit `milestone` field matches the active scope. No YAML opt-in needed; active-scope semantic IS the filter.

Observed savings on the failure-case (M2 over-produces 7 features, 6 collapse to 0 tickets after consolidation): would-have-been ~$2-3 of pure waste across the M5 + M4 cycles now eliminated.

### Milestone seed scope filter

`_load_milestones` in `seeds_fallback` now filters to the active milestone artifact when a scope is set. Sibling milestones drop from the seed pool entirely — agents see only the active milestone's goal + done-when + consumes_requirements as structured context. Without this filter, M2's design composition was seeing M1's full milestone artifact (slug "m1-foundation-data-layer-api-contract", body describing SQLite schema + FastAPI work), and Rabbit composed a foundation feature in M2's composition because the milestone-plan's summary "M1 already shipped" framing got drowned out by M1's full artifact in seeds.

### Milestone artifact seeded into every tdd-design meeting

Canonical tdd-design didn't seed the milestone artifact into any meeting — scope only conveyed via prepended directive text (system-prompt prose) on the entry meeting. Downstream meetings (composition / decomposition / architecture / contract-negotiation) had no structured milestone context, just inherited from earlier meetings via memory + seeded artifacts. Memory fades; the inheritance signal weakens through the workflow.

Added `kinds: [milestone]` to every tdd-design meeting's seed bindings. With the scope filter (above), only the active milestone surfaces — no sibling pollution. Now every meeting in every milestone-scoped run has the active milestone's goal + done-when + consumes_requirements in current context as structured artifact, not just as fading framing prose.

The paper-grade framing here: **agent attention decays through a multi-meeting workflow; load-bearing constraints must be re-surfaced via per-meeting seeds, not memory inheritance.** Generalizes beyond Wonderland to any multi-turn agent system.

### Review scope discipline by feature stack_span

`tdd-implement` M8 review directive now teaches Caterpillar to scope findings by the feature's `stack_span`. Backend-only features: don't read frontend files, don't flag missing frontend components — those belong to capability milestones with their own review passes. Same for frontend-only. Full-stack keeps current cross-ticket coherence check. Explicit "whole-project coherence is a separate workflow" framing to prevent scope drift.

Observed: M1 backend-feature review was surfacing findings about missing M2/M3 components (NoteEditor, TagFilter, SearchBar) because the directive told Cat to look at frontend files even for backend-only features. Each review spawned 5+ follow-up tickets attributed wrongly to M1's feature scope. The scope discipline edit redirects: M2/M3 component gaps are M2/M3's job, not M1's.

### Review routing gate: meeting ID instead of feature state

`_complete_tickets_on_accept_review` and `_route_blocking_review` previously gated on whether the feature was in implementation-state (queued / in_progress / ready_for_review). Proxy was fragile — features stuck in `designed` state (because some upstream step didn't transition them to `queued`) had their M8 accept verdicts silently swallowed, leaving tickets stuck in `queued` forever.

Replaced with explicit meeting-ID match (`meeting.id in {"review", "m8"}`). Discriminates M8 review from tdd-design's M3.5 consolidation directly without depending on feature-state bookkeeping. Tickets that were implemented now get their `queued → in_progress → done` fast-forward on accept verdicts regardless of upstream feature-state hiccups.

### Tools delete-guard on substrate-of-record paths

`Tools.delete_file` refuses to delete files under `.wonderland/milestones/`, `.wonderland/requirements/`, `.wonderland/memory/`, plus single files like `.wonderland/project.yaml` and the lifecycle `.jsonl` audit logs. Mutation (write_file / str_replace / insert) on these paths is still allowed — agents can amend a milestone's content to fix a typo or update a done-when bullet. Deletion is the load-bearing distinction.

Observed: agents using `write_file` / `delete_file` directly on `.wonderland/milestones/` to "fix" requirements they thought needed fixing, accidentally removing milestone files entirely. Subsequent design runs had empty milestone seed pools, fell back to project-level prime_directive, and designed for the full app surface. The error message points at the substrate-correct alternative: *"Use a `retract` speech_act instead of raw delete_file — retract is workflow-aware and reversible; file deletion is neither."*

### Prime_directive: capture only at project creation

Removed run-time `prime_directive` capture from `new_run.py`'s `_launch_run`. The previous heuristic (first-run captures the directive as the project's canonical prime_directive) contaminated projects where the first run was milestone-scoped: the run directive ("Design milestone m1-foundation...") became the project's permanent prime_directive, which then synced into project.yaml on every subsequent run, agents read it as "operator's original ask," and litigated the wrong milestone during M2/M3 design.

`prime_directive` is now project-level intent set ONLY at project creation (via `new_project.py` / skeleton apply). Run-time directives are ephemeral, used for this run only. Future operator edits to `prime_directive` require an explicit edit (or the roadmap'd project-context-form UI when that ships).

### Hatter reading discipline — ported from Tweedle directives

Mad Hatter's `_TOOLS_SECTION` now includes the same reading-discipline rules added to the Tweedles earlier (grep-first, don't re-read after write, remember what you've read). Hatter was running on the old guidance — 100 tool calls per implementation pass with `src/app.py` read 10× across 7 tea-parties, $0.20+ per meeting. Pilot-grade savings: M1 implementation Hatter tea-parties came in <$0.20/meeting (down from $0.22+ on obol baseline).

Also surfaced `run_tests` in Hatter's available-tools list with a "red test for the right reason > prose scenario" framing — he already had access via shared_tools but wasn't told.

### `tdd-design-no-stories.yaml` — A/B variant elides M1 story generation

New workflow file for the A/B experiment testing whether the story layer adds value beyond what milestone done-when criteria already provide. Drops the scoping phase entirely; M2 composition seeds from milestone done-when directly instead of from a story pool.

Same shape as canonical tdd-design otherwise: composition / decomposition / consolidation / architecture / contract-negotiation. Coverage check swapped from `milestone_realization` (walks through stories) to `done_when_coverage` (count-floor: features per milestone ≥ done-when item count).

Substrate-side change: `collect_phantom_citations(citing_kind="feature")` now also accepts milestone slugs/guids — features in B variant cite milestones directly without being treated as phantoms.

### Substrate observations + cost trajectory

M1 implementation (mvp-demo-rerun-A): $15.46 across 4 features, 27 tickets, 56 meetings. Per-ticket impl cost **$0.57** (vs $0.86 on mvp-demo2 project-wide — 34% reduction). Per-meeting impl cost **$0.28** (vs $0.60 on mvp-demo2 — 53% reduction). Same Haiku 4.5 model; cost delta is structural (fewer wasted iterations, tighter scope filters, better read discipline).

M1 total cost (design + implementation): **$18.91** vs mvp-demo2's per-milestone average of ~$28 (33% reduction). Includes operator-driven substrate iteration overhead from M3 troubleshooting; projected steady-state per-milestone cost with all current substrate pre-landed is ~$15-16.

The cost story isn't "we made Haiku cheaper" — it's "substrate prevents wasted work, per-call efficiency improved separately via read-discipline directives." Two compounding effects, both with receipts.
