# 0.7.0 — GUID identity + substrate hardening: every artifact gets a stable home

The release that closes the slug-as-identity gap.

Across the validation pilots — eight pilots, $20-ish total — the
same shape of bug kept showing up: phantom slug citations,
near-duplicate features under slightly different slugs, the
infamous "9 features for 3 concepts" from validation4. Slugs are
human-readable but they're not identity; agents paraphrase them,
drift them, recoin them.

**P18 (`p18-guid-identity`) replaces slug-with-identity with
GUID-with-identity across all 12 artifact registries.** Every
payload gets a ULID at creation, preserved across re-emissions.
Filenames carry an 8-char ULID prefix. Source citations resolve
guid-first; phantom guids become substrate-detectable rather than
silent drift. The slug stays load-bearing for human browsing —
filenames are still scannable, markdown still has readable
titles — but the substrate routes on guid.

Plus two follow-on substrate fixes the validation5 pilot surfaced:

- **Snapshot semantics for milestone_plan**: agents reason about
  the plan as a single list they ship each rotation. The substrate
  now treats each speaker's most-recent milestone_plan as their
  current claim; the union across speakers is the active plan;
  files outside the union get deleted. Eliminates the
  "consolidated to 3 but disk still has 4" mismatch.
- **Scoped retract guard**: ticket retracts in per_item meetings
  now require the target ticket's sources to include the current
  iteration's slug. Caterpillar can't accidentally delete other
  features' tickets while reviewing his own iteration — the
  failure shape that wiped 9 tickets in validation5.

And the substrate hardening queued up while P18 was being built:
foundation-milestone protocol, lifecycle gate, ticket source
attribution, M9 verify (the build-check meeting), frontend build
check, verify_imports tool for Caterpillar, slug-stability
tightening for Alice. Every one closes a class of bug the pilots
made visible.

---

### P18 — GUID identity across the registry layer

Every artifact payload (Story, Feature, Ticket, ADR, Ruling,
Review, ContractNote, Milestone, Requirement, TestScenario,
Implementation, Observation) carries a `guid: str` field
defaulting to a fresh ULID at creation. The full GUID lands in
the markdown body as `**GUID:** <ulid>`; round-trip parse/render
preserves it across re-emissions.

Registry `write()` now looks up existing records by guid first,
slug as back-compat fallback. Re-emitting with the same guid
updates in place regardless of slug drift; coining a new guid
creates a new artifact. The 9-features-for-3-concepts pattern
becomes substrate-impossible — agents converging on the same
concept via the same guid end up with one file, not three.

Filename format moves to `<kind>-<short_guid>-<slug>.md` where
`short_guid` is the first 8 chars of the ULID. The display
number stays in the markdown H2 header. Legacy
`<kind>-NNN-<slug>.md` files continue to read correctly — the
regex accepts both shapes — so mixed-vintage directories don't
need a migration.

Source resolution accepts three citation forms in `sources`
lists:

- `"<slug>"` — legacy slug-only (pre-P18)
- `"<guid>"` — 26-char ULID, matches by GUID directly
- `"<guid>:<slug>"` — guid-primary, slug-hint pair (P18 canonical)

Resolver tries guid match first, falls back to slug if the guid
doesn't resolve. Phantom citations (an unresolvable guid) become
substrate-detectable rather than silent drift; the
phantom-sources stripper catches them at utterance time. Agent
protocols in `tdd-design.yaml` and `milestone-plan.yaml` teach
the new citation forms; constitutions untouched.

### Snapshot semantics for milestone_plan

Validation5 surfaced: two agents converged from
`m3-routine-generation-from-equipment` to
`m3-equipment-and-routine-generation` across rotations, but the
substrate left both M3s on disk. Agent prose said "three
consolidated milestones" while disk had four.

Root cause: `milestone_plan` was additive. Agents reason about
the plan as a single list ("here's my view of the milestones")
but the substrate wrote each emission's milestones to disk
without ever deleting ones that vanished from later emissions.

Fix: at meeting end, `_apply_milestone_plan_snapshot` walks the
meeting's milestone_plan utterances, groups by speaker, takes
each speaker's MOST RECENT emission, and computes the union of
slugs. Any milestone file on disk whose slug isn't in the active
union got abandoned by its author(s); the substrate deletes it.
Multi-agent collaboration preserves each agent's last-stated
claims (alice plans [m1, m2], rabbit plans [m3] — all three
survive). 5 unit tests including the exact validation5 repro
pattern.

### Scoped retract guard for per_item meetings

Validation5's other failure: Caterpillar in the consolidation
iteration for feature-A saw tickets on the bus that belonged to
feature-B, feature-C, feature-D — and emitted retractions for
each with "Belongs to <other-feature>, not multi-user-auth." His
intent: "out of scope for THIS iteration." His effect: nine
tickets that legitimately belonged to other features got deleted
from disk.

Fix: `_apply_retraction_for_utterance` now takes
`current_item_slug` from the meeting loop. For ticket retracts
in per_item meetings, the ticket's `**Sources:**` line must
include the current iteration's slug (or a `<guid>:<slug>`
citation matching it). Out-of-scope retracts get rejected — the
utterance stays in the transcript as an audit record of the
attempt. Non-iterated meetings (milestone-plan, etc.) pass
`current_item_slug=None` and the guard is a no-op.

### Foundation milestones get their own protocol — author lane, framing, roster filter

Three pilots in a row (validation, validation2, validation3)
deadlocked at M1 when the milestone scope was pure infrastructure
(auth substrate, schema seams, sync layer contracts). Alice would
try Marcus-anchored stories that didn't fit, Caterpillar would
concern them, neither authored, M1 exited empty. The
constitutional handoff between user-facing-author (Alice) and
plumbing-author (Caterpillar) was implicit; the substrate had no
signal for "this milestone is foundation-shaped, hand it to
Caterpillar."

Five fixes stack to close the gap end-to-end:

- **Milestone shape classification** — `_classify_milestone_shape`
  reads each consumed requirement's Kind and returns `foundation`
  (all infrastructure-kind), `capability` (any persona/situation),
  or `mixed`. The framing prepend uses this.
- **M1 lead-assignment block** — when shape is `foundation`, the
  prepend gets a top-of-directive block naming "M1 LEAD:
  Caterpillar" + telling him his first move must be `decision:
  story` with payload, not `question`/`silence`/`deference`.
  Names the deadlock pattern so the agent recognizes the failure
  mode.
- **Caterpillar constitution** — foundation-only milestones
  invert the Alice/Caterpillar ratio (3-6 foundation stories
  from Caterpillar, Alice supports with concern). Validator
  enforces `decision='story'` requires non-empty `stories[]`
  payload to close the "declared the decision but shipped
  nothing" deadlock shape from validation2.
- **Alice constitution** — foundation work is not her lane;
  default `silence` or `concern`, never a forced user-persona
  story.
- **M3 roster filter** — per_item_roster_filter on `feature.kind`
  drops Alice from M3 iterations when the feature is `kind:
  foundation`. Same primitive tdd-implement's M7 uses for
  stack-span. Required Rabbit's `_record_features` to include
  `kind` in the bus payload (was missing).

End result: foundation milestones now ship 3-6 plumbing stories
cleanly in M1, decompose into tickets in M3 without Alice's
user-persona-grounding instinct hanging the iteration.

### Lifecycle gate prevents tdd-design from triggering implementation routing

projects/validation pilot revealed Caterpillar's M3.5
consolidation review (verdict=request-changes naming duplicate
tickets) silently triggered the `_route_blocking_review` path —
which back-filled freshly-emitted M3 tickets to `in_progress`,
immediately marked them `done`, and synthesized follow-up
tickets. Implementation-stage routing in a design-stage meeting.

Fix: `_feature_in_implementation_state` helper gates routing on
feature lifecycle state. Routing only fires when feature is in
`queued` / `in_progress` / `ready_for_review`. Design-stage
features (proposed / in_design / designed) skip routing;
Caterpillar's design-time review artifacts persist on disk as
diagnostic value without firing ticket-state churn.

### Substrate auto-attributes ticket sources to iteration feature

Rabbit's M3 ticket emissions drift across iterations — earlier
ones cite the feature slug correctly, later ones invent `story-*`
slugs that don't resolve. Dashboard "tickets for feature X" query
matches by feature slug; phantom citations break attribution
silently (3 of 4 features render as 0 tickets).

Substrate now auto-attributes at end of per_item=feature
meetings: for each ticket whose sources don't include the
iteration's feature slug, prepend it as the first source — both
in the bus artifact payload (downstream sees correct linkage)
and the on-disk `**Sources:**` line (dashboard query matches).
Whatever Rabbit cited alongside survives as later entries;
substrate adds the anchor, doesn't replace.

**Thread-scope fix (validation4):** parallel M3 iterations share
the global capture-slice; without scoping, each iteration's
slice grew to include sibling iterations' tickets, and every
iteration attributed its feature slug to every ticket.
Diagnostic stderr logging caught it: `fired ... utterances=4
scoped=1` after thread-filtering. The function now filters
`new_utterances` to the iteration's own `thread_id` before
walking artifacts.

### M9 verify auto-installs deps before checking

validation3 M9 verify failed with `ModuleNotFoundError: No
module named 'fastapi'` and `Cannot find module 'react'` — real
issues, but not the issues the checks were supposed to surface.
Missing-deps noise drowned actionable signal.

- `pytest_collects` triggers on `pyproject.toml` existence (not
  `uv.lock`); `uv run --with pytest pytest --collect-only`
  auto-syncs deps + guarantees pytest is available even when
  project's pyproject doesn't declare it.
- `npm_build` detects missing `node_modules` and runs `npm ci`
  (with lockfile) or `npm install` (without) before invoking the
  build script. Install failures surface as their own finding
  ("dependency resolution failed") distinct from build failures.

Next verify pass exercises real signal — structural bugs
Caterpillar's static review missed — instead of deps noise.

### Consolidation: prefer `retract` over `delete_file`

validation4 first design pass deleted every ticket via aggressive
`delete_file` calls — the "ACTION FIRST, AUDIT SECOND" framing
was too binary; the model interpreted "choose deletion every
time" as license to over-prune.

Reframed M3.5 directive: prefer the substrate's first-class
`retract` primitive (slug-scoped, auditable, transcript-logged)
over raw `delete_file` (raw escape hatch, reserved for orphaned
files). Hard rule: never delete more than you can name in
writing as a confirmed duplicate. Self-check at meeting end —
zero tickets on a feature = over-pruned, back off.

### Frontend build check + verify_imports tool + slug-stability tightening

Three bundled improvements ahead of the next pilot:

**1. Frontend build check** as a second M9 check.
`Meeting.build_check` now accepts either a single name or a list;
tdd-implement's M9 declares `[pytest_collects, npm_build]`. The
new `npm_build` check detects frontend infrastructure
(package.json with a `typecheck` or `build` script at project
root or `frontend/`), runs the chosen script via `npm run
<script>`, and synthesizes review findings on failure. Skipped
cleanly on backend-only skeletons (python-cli, python-tui, bare
python-fastapi). Prefers `typecheck` over `build` when both
exist — cheaper, catches the same class of bug (TypeScript
errors, missing imports, the two-frontend-trees orphan-component
shape).

**2. `verify_imports` tool for Caterpillar.** New tool on the
`Tools` class that runs `ruff check --select F,E9` against a
single Python file. Catches the bug class that lives BETWEEN
code review and pytest collection: Pydantic shadow fields,
misnamed decorators (`@app.get` vs `@router.get`), missing
imports yielding `NameError` at runtime, unresolved forward
references. Caterpillar's M8 review constitution now teaches the
tool — call it proactively for files with non-trivial
decorator/dependency wiring (FastAPI routes, Pydantic models,
SQLAlchemy declarations). Frontend files get a pointer back to
the M9 `npm_build` check rather than a broken single-file tsc
invocation.

**3. Slug-stability tightening for Alice.** Discovery5 shipped 8
stories for 4 concepts because near-duplicate slugs
(`user-signup` / `user-sign-up` / `signup-flow`) all hashed
differently — the registry's dedup only catches identical slugs.
Two layers of fix:

- New `_format_existing_stories_block` injects existing stories
  on disk (slug + title + persona) into the milestone-framing
  prepend, so Alice sees what already exists at M1 start.
- Alice's constitution adds an explicit "near-duplicate slug
  check" procedure: same persona + same need + overlapping
  acceptance → near-duplicate → leave alone or retract+replace,
  never coin a fresh slug.

### M9 — Verify: the build-check meeting that catches structural bugs Caterpillar's review can't

Caterpillar reviews code quality (does the contract match? do
tests cover the acceptance criteria?). Pytest reviews code
function (does it actually run?). These are different questions,
and during the discovery5 pilot we shipped a feature where M8
ACCEPTED the code while `get_current_user` was missing
`Depends(get_db)` — the import-time FastAPI error never
surfaced because the review was reading code, not running it.

New: a verify-kind meeting type that bypasses agent convening
entirely. The substrate runs a registered `build_check` against
the project, emits a structured `BuildCheckEvent` wrapped in
standard `MeetingStart/End` events, and on failure synthesizes a
system review on disk + routes through the existing
`_route_blocking_review` path. Follow-up tickets land on
whichever feature was most recently reviewed (preferring
`ready_for_review` → `in_progress` → `queued` → any); the
feature stays out of ready state until they're worked, so the
next implementation run picks them up.

Two checks ship in v1, both pure Python with subprocess-bound
`uv run pytest`:

- `pytest_collects` — runs `pytest --collect-only`. Catches the
  import-time class of bug (`Depends` misuse, syntax errors,
  missing dependencies) for ~30s of cost.
- `pytest_passes` — runs `pytest -q`. Catches assertion failures
  once collection succeeds.

Skipped semantics are first-class: no `pyproject.toml`, no
pytest binary, or pytest exit code 5 (no tests collected) all
degrade to `skipped=True` informational events. Projects that
haven't set up tests yet don't get punished with auto-tickets
they can't act on.

Wired into tdd-implement as M9, the meeting fires once per
feature lane in the pipelined runner — natural per-feature
attribution when a failure traces back to a specific feature's
tickets. Configurable via the workflow YAML (just add `kind:
verify` + `build_check: <name>`); other workflows can opt in by
declaring their own M-numbered verify meeting.

### Auto-synthesize follow-up tickets from blocking review findings

The discovery5 pilot showed Caterpillar shipping high-quality
`request-changes` reviews with 5+ blocking findings, but no
follow-up tickets ever landed on disk. Traced to a substrate
gap: when a ReviewRegistry is wired (the normal case), the bus
artifact's payload only carried path-pointer metadata (`number /
slug / title / verdict / target_files / path`) — no findings.
`_route_blocking_review` correctly fired on
verdict=request-changes + marked the iteration's in-flight
tickets DONE, but the synthesis loop iterated over an empty
findings list because the bus payload didn't carry them.

The fix: include `findings` inline on the bus artifact even when
a registry is wired (a few KB per review — well within bus
budget). The full structured findings list now travels with
every review utterance, so `_synthesize_followup_ticket_from_finding`
can iterate over them and produce tickets. Combined with the
existing forcing function — synthesized tickets land on the
parent feature, which therefore stays out of `ready_review`
until they're worked — this means a blocking review now
produces a real piece of work the team has to address, not just
a markdown file on disk that may or may not get read.

Regression test pins the contract:
`test_persisted_review_artifact_carries_findings` asserts findings
are present on the bus artifact when a registry is wired.
