# M9 Wave 2 — `live-role.md` writer survey (the no-automated-writer proof)

> **G2 artifact (REQ-EP-7).** The `live-role.md` guarantee is **mechanical**, not
> conventional: *no automated writer exists*. This survey enumerates **every**
> code path that writes into an agent's `tracked/` mind, classifies each, and
> proves the durable role file has **exactly one** writer (the `spt endpoint
> role --overwrite` CLI) and **zero** automated ones. A missed writer here is a
> silent contract break — the same class of risk as a missed `adapter_name`
> consumer in the Wave-1 addressing survey, so this lists the writers themselves,
> not just the diff.

## What is being guaranteed

`tracked/agents/<id>/live-role.md` (CONTEXT §live role) is a durable statement of
an agent's broad purpose. It lives in the mind beside `live-context.md`, on the
same `a-<id>` branch, so it replicates across nodes. The guarantee: **only a
deliberate user action writes it.** Psyche reconcile, echo-communes, and signoff
must *structurally* never touch it — not "don't currently," but "cannot."

## The mind layout (where writes land)

```
tracked/
  .seed.git                         bare BranchStore repo
  agents/<id>/                      worktree of branch a-<id>
    live-context.md                 the live tier  — synced to ALL instances
    live-role.md       <-- NEW      the durable role — same branch, same sync
    .conflicts/<file>.<hash8>.md    surfaced concurrent-write artifacts
  projects/<project_id>/<id>/       worktree of branch p-<project_id>
    project-context.md              the project tier — same-project instances
```

`live-role.md` shares the `a-<id>` worktree + branch with `live-context.md`, so
it replicates through the *same* sync — but replication is **transport**, not
authoring (see §Sync transport below).

## The ONE legitimate writer

| Writer | Site | Target | Class |
|--------|------|--------|-------|
| `spt endpoint role --overwrite <file>` | `crates/spt/src/cli.rs` `cmd_role` | `tracked/agents/<id>/live-role.md` (atomic write + `commit_live`) | **USER-DIRECTED** |

This is the sole path that resolves `ContextStore::live_role_path` /
`contextstore::live_role_file` and writes the file. Bare `spt endpoint role`
reads it; nothing else writes it.

## Every `tracked/` writer, classified

### AUTOMATED writers — and the exact files each writes (none is `live-role.md`)

| # | Writer | Site | Writes ONLY | Reaches role? |
|---|--------|------|-------------|---------------|
| 1 | Echo-commune / drop **ingest** | `crates/spt-live/src/ingest.rs` `route_slices` → `cs.live_context_path` / `cs.project_context_path` | `live-context.md`, `project-context.md` | **No** |
| 2 | **Signoff** resume-commune | `crates/spt-live/src/signoff.rs` `write_resume_commune` → `route_slices` | `live-context.md`, `project-context.md` (provenance-stamped) | **No** |
| 3 | Psyche **reconcile** | driver `crates/spt-daemon/src/sync.rs` `reconcile_after_sync` → `reconcile_file(file_rel)` | only the **conflicted** `file_rel`, with `live-role.md` **structurally excluded in the driver loop** (`RoleExcluded`) + clears `.conflicts/*` | **No (structurally skipped)** |
| 4 | Sync **merge-driver** conflict record | `crates/spt-store/src/syncmerge.rs` → `contextstore::record_conflict` | `.conflicts/<file>.<hash8>.md` | **No** |
| 5 | Sync **merge-driver** accept write | `crates/spt-store/src/syncmerge.rs` | the incoming tier file (`live-context.md` / `project-context.md`) | **transport — see below** |
| 6 | Agent **rename** | `crates/spt-store/src/contextstore.rs` `rename_agent_in_all_projects` | renames `agents/<old>→<new>` + per-project files | **No (renames, never authors role content)** |

**Why none can reach `live-role.md`:**

- **Ingest & signoff (#1, #2)** route through `parse_two_slice`, whose grammar has
  **only** `<live-context>` and `<project-context>` slices. There is no
  `<live-role>` slice in the inbound grammar — a commune that *names* one is inert
  (pinned by `ingest::tests::ingest_never_writes_the_live_role`: a smuggled
  `<live-role>` slice leaves the role byte-unchanged).
- **Reconcile (#3) is structurally excluded — not merely rare.** The reconcile
  driver (`sync.rs` `reconcile_after_sync`) enumerates `report.conflicts()`
  *dynamically*, and the merge-driver (`syncmerge.rs`) mints a `.conflicts/`
  artifact for **any** conflicted file — including `live-role.md` if a role ever
  conflicts cross-node. So the guarantee is **not** "role conflicts never
  happen." It is enforced one level up: the driver loop **skips
  `file == LIVE_ROLE_FILE`** and returns `ReconcileOutcome::RoleExcluded`
  *before* `reconcile_file` is ever called. A role conflict's artifact persists
  for the user to resolve via `spt endpoint role --overwrite`; reconcile
  **structurally never authors role content** — pinned by
  `sync::tests::reconcile_driver_structurally_excludes_live_role` (a failing
  runtime cannot change the `RoleExcluded` outcome, proving `reconcile_file` is
  never invoked for the role).
- **None writes a directory glob.** Every automated writer targets a *named*
  filename constant (`LIVE_CONTEXT_FILE`, `PROJECT_CONTEXT_FILE`) or a
  deterministic `.conflicts/<file>.<hash8>.md` — never "every file under
  `agents/<id>/`". A new file beside them is therefore untouched by construction.

### Sync transport ≠ authoring (#5)

The merge-driver's accept write replicates whatever bytes a branch tip carries —
and because `live-role.md` lives on the `a-<id>` branch, a *role authored on
node A* does replicate to node B via this path. That is **transport, not
authoring**: the only thing that put role *content* onto the branch was the sole
writer (`spt endpoint role`). CONTEXT §live role's guarantee — "Psyche reconcile,
echo-communes, and signoff structurally never touch it" — is about *content
authorship*, which the survey confirms. Replicating an already-authored role is
the mind sync working as designed.

> **Conflict note:** if `live-role.md` ever conflicts cross-node, the
> merge-driver records the conflict as an ordinary precedence-marker artifact
> (so it is preserved, never lost) — but the reconcile driver **structurally
> skips it** (the `LIVE_ROLE_FILE` exclusion above), so no automated turn ever
> authors role content. The user resolves the conflict by setting the canonical
> role with `spt endpoint role --overwrite`. This is the right behavior for a
> single-author, user-directed file: the user owns their own role. The
> "structurally never touches it" guarantee is therefore **literal**, not a
> matter of role conflicts being rare.

## Test evidence

- `crates/spt-daemon/src/sync.rs` — `reconcile_driver_structurally_excludes_live_role`
  (the driver skips `live-role.md` → `RoleExcluded`; a failing runtime can't
  change it, proving `reconcile_file` is never invoked for the role; the
  conflict artifact persists, no role content authored).
- `crates/spt-live/src/ingest.rs` — `ingest_never_writes_the_live_role`
  (behavioral: a role-slice-smuggling commune leaves `live-role.md` unchanged).
- `crates/spt-live/src/resume.rs` — `download_renders_role_first_then_live_then_project`
  (injection order role → live-context → project-context).
- `crates/spt/src/cli.rs` — `endpoint_role_overwrite_is_the_sole_writer`
  (the one writer authors + replaces).

## Deferred tightening (recorded)

A **hard gate** restricting `spt endpoint role` writes to user-backed origins is
a recorded later tightening (CONTEXT §live role) — it rides the `user-msg`
identity plumbing delivered in M9-T4. M9-T6 ships the sole-writer guarantee
*structurally* (one code path, zero automated writers); the origin gate on that
one path is a separate, later beat.
