---
phase: 23-commune-signoff-project-root-head-sha-stamping
plan: 02
subsystem: messaging
tags: [event-envelope, stamp, commune, signoff, echo_commune, file_drop, d-05, d-06, d-14, rust, phase23]

requires:
  - phase: 23-commune-signoff-project-root-head-sha-stamping
    plan: 01
    provides: "Stamp struct + stamp()/stamp_at() + Stamp::event_attrs() (src/common/git.rs)"
provides:
  - "compose_commune_event(&str, &Stamp, &str) — pure helper in src/live/commune.rs (D-06 typed envelope)"
  - "compose_init_signoff_payload signature gained stamp: &Stamp 2nd parameter (D-05)"
  - "compose_echo_commune_payload signature gained stamp: &Stamp 3rd parameter (D-05)"
  - "compose_commune_payload (wrapper file-drop) signature gained stamp: &Stamp 2nd parameter"
  - "dispatch_commune_markers signature gained stamp: &Stamp; threads through to Psyche AND Self payloads"
  - "resolve_self_project_stamp(self_id) — D-14 fallback chain (info.json project field → cwd stamp)"
  - "read_self_project_from_info(self_id) — Phase 24.1 forward-compat helper"
affects: [23-03-yaml-frontmatter-persisters, 23-04-psyche-download-blocks, 23-05-echo_commune-self-project, 23-06-skill-docs-update]

tech-stack:
  added: []  # zero new external crates
  patterns:
    - "Pure-formatter composers parameterized over &Stamp; stamp() called once per fire at run-site (D-12)"
    - "Locked attr order on EVENT tag: type, [from, ]timestamp, machine, project, branch?, head_sha?, head_subject?, [note]"
    - "D-14 fallback chain via match-shaped helper for Phase 24.1 extension"

key-files:
  created: []  # no new files this plan
  modified:
    - "src/common/git.rs (dropped module-level #![allow(dead_code)]; added per-symbol allows for Plans 03/04 callers)"
    - "src/live/commune.rs (new compose_commune_event helper; run + commune_result use stamped EVENT envelope; legacy prose form gone)"
    - "src/live/signoff.rs (compose_init_signoff_payload signature change + run + signoff_result + 3 new test assertions; existing tests updated)"
    - "src/live/wrapper/mod.rs (compose_commune_payload signature change; process_file_drop threads one stamp() into both branches; Test L + 2 new tests)"
    - "src/live/wrapper/orphan.rs (orphan INIT_SIGNOFF emit threads stamp through compose_init_signoff_payload)"
    - "src/owl/echo_commune.rs (compose_echo_commune_payload + dispatch_commune_markers signature change; resolve_self_project_stamp helper for D-14; existing tests updated + 4 new tests)"

key-decisions:
  - "D-14 fallback chain implemented as a 2-step match: (1) try info.json project field, (2) fall back to cwd stamp. Today step 1 is a no-op (info.json has no project field — verified via CONTEXT §code_context); Phase 24.1 will populate it. The match shape lets Phase 24.1 extend without re-finding the call site."
  - "Stamp attrs slot AFTER timestamp and BEFORE note on the echo_commune EVENT tag per RESEARCH (siblings of timestamp). Locked order: type, from, timestamp, machine, project, branch?, head_sha?, head_subject?, note."
  - "wrapper compose_commune_payload kept as a SEPARATE helper from commune.rs::compose_commune_event despite structural identity — clean blame/test boundaries (file-drop wrapper path vs CLI run path). A future consolidation is out of Phase 23 scope; documented in both helpers."
  - "process_file_drop computes ONE stamp() per file_drop fire and threads it into BOTH the commune and signoff envelope branches — guarantees the two file_drop kinds carry identical stamp attrs for the same wrapper iteration."
  - "src/common/git.rs dropped module-level #![allow(dead_code)] (Plan 02 wired the first callers per Plan 01 instruction); added per-symbol allows to commits_since/_at, commits_unpulled/_at, yaml_frontmatter, yaml_escape for Plans 03/04 callers that haven't landed."

patterns-established:
  - "Pure-formatter composer + run-site stamp() invocation: composers take &Stamp; run-sites call crate::common::git::stamp() per fire (D-12) and pass &stamp. Tests construct mock Stamp literals without subprocess."
  - "D-14 fallback chain as match-shaped helper (resolve_self_project_stamp): structured so Phase 24.1 can add an arm without re-finding the call site."

requirements-completed: [COMMIT-META-02, COMMIT-META-03, ECHO-DELTA-01, BACK-COMPAT-02]

duration: ~25 min
completed: 2026-05-19
---

# Phase 23 Plan 02: EVENT-envelope cutover for plain commune (D-06) + 5-attr stamping on init_signoff / echo_commune / wrapper file_drop Summary

**Threaded `Stamp` + `Stamp::event_attrs()` (from Plan 01) through all four commune-shaped wire-payload composers — replacing the legacy `COMMUNE (ts): body` prose form with a typed `<EVENT type="commune">` envelope (D-06 clean cutover) and stamping every `init_signoff` / `echo_commune` / file-drop commune payload with the five-field stamp inline on the EVENT tag (D-05) — and implemented the D-14 echo_commune Self-project fallback as the v1.8 steady state.**

## Performance

- **Duration:** ~25 min
- **Tasks:** 2 (TDD: 4 commits — RED + GREEN × 2)
- **Files modified:** 6
- **Test suite runtime:** 2.18s for the full 512-test lib suite (single-threaded)

## Composer Signatures: Before / After

| Composer | Before | After |
|----------|--------|-------|
| `src/live/commune.rs::compose_commune_event` | *(did not exist — inline `format!("COMMUNE ({}): {}", ts, body)`)* | `pub(crate) fn compose_commune_event(timestamp: &str, stamp: &Stamp, body: &str) -> String` |
| `src/live/signoff.rs::compose_init_signoff_payload` | `pub(crate) fn compose_init_signoff_payload(timestamp: &str, message: Option<&str>) -> String` | `pub(crate) fn compose_init_signoff_payload(timestamp: &str, stamp: &Stamp, message: Option<&str>) -> String` |
| `src/owl/echo_commune.rs::compose_echo_commune_payload` | `pub(crate) fn compose_echo_commune_payload(from: &str, timestamp: &str, note: &str, body: &str) -> String` | `pub(crate) fn compose_echo_commune_payload(from: &str, timestamp: &str, stamp: &Stamp, note: &str, body: &str) -> String` |
| `src/owl/echo_commune.rs::dispatch_commune_markers` | `pub(crate) fn dispatch_commune_markers<...>(self_id, psyche_id, contents, timestamp, forward_to_self, psyche_deliver, self_deliver)` | `pub(crate) fn dispatch_commune_markers<...>(self_id, psyche_id, contents, timestamp, stamp: &Stamp, forward_to_self, psyche_deliver, self_deliver)` |
| `src/live/wrapper/mod.rs::compose_commune_payload` | `pub(crate) fn compose_commune_payload(timestamp: &str, body: &str) -> String` | `pub(crate) fn compose_commune_payload(timestamp: &str, stamp: &Stamp, body: &str) -> String` |

## D-14 Fallback Shape

Implemented in `src/owl/echo_commune.rs::resolve_self_project_stamp(self_id: &str) -> Stamp`. Fallback chain:

1. **info.json project field lookup** (best-effort): `read_self_project_from_info(self_id)` calls `crate::common::owlery::info_file(self_id)`, deserializes the JSON, and reads `.project`. **Phase 23 fact: today this returns `None`** — perch `info.json` does not yet carry a `project` field. Verified by code reading + grep across `src/common/owlery.rs` + `src/common/types.rs`.
2. **Fallback to cwd stamp** (v1.8 accepted state per RESEARCH Open Question 2): call `crate::common::git::stamp()`. Wrapper cwd = `psyche_dir`, so `project` resolves to `git rev-parse --show-toplevel` basename when `psyche_dir` is inside a git repo, or to `psyche_dir.file_name()` (typically `tracked`) otherwise.

`resolve_self_project_stamp` is structured as a match-style two-step (read info → mutate fallback stamp's project) so Phase 24.1 can extend with additional Some(_) arms without re-finding the call site. A `TODO(phase-24.1): use last_project_name from tracked agents/info.json once available` comment marks the extension point.

## Tests Added / Updated

### `src/live/commune.rs` (4 new tests in `tests` module)
- `commune_envelope_replaces_prose_when_in_repo` — bytes-exact EVENT shape with 5 stamp attrs in repo
- `commune_envelope_omits_optionals_outside_repo` — D-11 verification (only machine + project)
- `commune_envelope_body_is_escaped` — amp-first ordering + newline-to-`<br>` via `event_body_escape`
- `commune_envelope_legacy_prose_form_is_gone` — D-06 absence assertion (`!out.contains("COMMUNE (")`)

### `src/live/signoff.rs` (3 new tests + 3 updated)
- *New:* `init_signoff_payload_carries_stamp_attrs` — 5 attrs + locked order
- *New:* `init_signoff_payload_omits_optional_stamp_attrs` — D-11 (no empty branch/sha/subject)
- *New:* `init_signoff_payload_starts_with_type_then_timestamp_then_stamp` — Pitfall-8 ordering + `machine=` immediately after timestamp value
- *Updated:* `init_signoff_payload_event_envelope_with_commune`, `..._no_commune`, `..._predicate_matches_case_insensitive` — pass `&stamp_fixture()`
- New private helpers: `stamp_fixture()`, `stamp_fixture_no_repo()`

### `src/live/wrapper/mod.rs` (2 new tests + 1 updated)
- *Updated:* `test_l_compose_commune_payload_shape` — bytes-exact match with `machine="m" project="p"` attrs spliced after timestamp
- *New:* `compose_commune_payload_carries_stamp_attrs` — full 5-attr render
- *New:* `compose_commune_payload_omits_optionals_outside_repo` — D-11 verification
- New private helpers: `stamp_no_repo()`, `stamp_full()`

### `src/owl/echo_commune.rs` `compose_tests` (3 new tests + 4 updated)
- *Updated:* round-trip clean form, body-escape, attr-quote-escape, predicate parity, escape visibility (Tests A-F) — pass `&Stamp`
- *New:* `compose_echo_commune_uses_self_project` — Self project lands on EVENT (not psyche_dir basename)
- *New:* `compose_echo_commune_emits_all_five_stamp_attrs_in_locked_order` — order: type, from, timestamp, machine, project, branch, head_sha, head_subject, note
- *New:* `compose_echo_commune_omits_optionals_outside_repo` — D-11
- New private helpers: `stamp_with_project(p)`, `stamp_no_repo()`

### `src/owl/echo_commune.rs` `dispatch_tests` (1 new test + helper)
- *Updated:* `run_dispatch` threads new `stamp_fixture()` through `dispatch_commune_markers`
- *New:* `dispatch_commune_markers_threads_stamp_through` — stamp attrs reach BOTH Psyche and Self bodies
- New private helper: `stamp_fixture()`

## Wrapper-Test Compile Fanout

- `src/live/wrapper/orphan.rs::orphan_fire_tests::test_a_sync_fire_argv_appears_before_compose_init_signoff` — passes unchanged (the test scans source for the `compose_init_signoff_payload(` substring; signature change didn't touch the substring).
- No `tests/` integration tests reference any of the 4 composers (confirmed via `grep -rn "compose_commune_payload\|compose_init_signoff_payload\|compose_echo_commune_payload\|dispatch_commune_markers" tests/` — zero hits).

## Task Commits

1. **Task 1 RED** — `45eb65f` — `test(23-02): add failing tests for commune EVENT envelope + signoff stamp attrs`
2. **Task 1 GREEN** — `ed36f19` — `feat(23-02): promote plain commune to EVENT envelope + stamp init_signoff` (commune.rs + signoff.rs + wrapper/mod.rs file_drop branch + wrapper/orphan.rs + per-symbol allow_dead_code in common/git.rs)
3. **Task 2 RED** — `79f2e51` — `test(23-02): add failing tests for echo_commune stamp + dispatch threading`
4. **Task 2 GREEN** — `3c1661e` — `feat(23-02): stamp echo_commune envelope + D-14 Self-project fallback`

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 3 — Blocker] Updated `compose_commune_payload` signature + `wrapper/orphan.rs` call site in Task 1 (planned for Task 2)**
- **Found during:** Task 1 GREEN
- **Issue:** Changing `compose_init_signoff_payload`'s signature in Task 1 broke compilation at three call sites — two of which (`wrapper/mod.rs::process_file_drop` signoff branch, `wrapper/orphan.rs` orphan INIT_SIGNOFF emit) were nominally Task 2 territory per the plan's "different file ownership" boundary. The strict task split would have left the codebase in a non-compiling intermediate state across the Task 1 GREEN commit.
- **Fix:** Updated `compose_commune_payload`'s signature (Task 2 territory) AND threaded `&stamp` into both wrapper signoff call sites during Task 1 GREEN so the build is green at every commit. Task 2 GREEN then focuses purely on `echo_commune.rs` (composer + dispatch + D-14 helper + run_echo_commune call site).
- **Files modified:** `src/live/wrapper/mod.rs` (compose_commune_payload + process_file_drop), `src/live/wrapper/orphan.rs` (compose_init_signoff_payload call), `src/live/wrapper/mod.rs` Test L
- **Verification:** `cargo build --release` green at every commit; `cargo test --lib -- --test-threads=1` reports 512 pass / 0 fail at both Task 1 GREEN and Task 2 GREEN.
- **Committed in:** `ed36f19` (Task 1 GREEN) absorbed the cross-cutting wrapper-side changes; `3c1661e` (Task 2 GREEN) is purely echo_commune.rs.

**2. [Rule 2 — Missing Critical] Dropped `#![allow(dead_code)]` from `src/common/git.rs` per Plan 01's hand-off note**
- **Found during:** Task 1 GREEN
- **Issue:** Plan 01's SUMMARY explicitly instructed: "When wiring the first caller, drop `#![allow(dead_code)]` from `src/common/git.rs`." Task 1 wires the first callers (`stamp`, `Stamp::event_attrs` via commune.rs + signoff.rs). Leaving the module-level allow would mask future dead-code regressions on symbols Plans 03-05 will consume.
- **Fix:** Removed module-level `#![allow(dead_code)]`; added narrower per-symbol `#[allow(dead_code)]` to `commits_since`, `commits_since_at`, `commits_unpulled`, `commits_unpulled_at`, `Stamp::yaml_frontmatter`, `yaml_escape` with doc comments noting which plan removes them (Plan 03 for yaml_*; Plan 04 for commits_*).
- **Files modified:** `src/common/git.rs`
- **Verification:** `cargo build --release` green; only 3 pre-existing dead_code warnings remain (none in `src/common/git.rs`).
- **Committed in:** `ed36f19` (Task 1 GREEN)

---

**Total deviations:** 2 auto-fixed (1 blocker — premature signature update for clean compile, 1 missing critical — Plan 01 hand-off compliance). Both addressed inline during the GREEN cycles and committed in the corresponding task commit. No architectural changes (no Rule 4 STOP needed).

**Impact on plan:** The Task 1/Task 2 file-ownership boundary is preserved at the *test* level (commune.rs + signoff.rs tests in Task 1; echo_commune.rs tests in Task 2). The *production* signature changes for `compose_commune_payload` had to move into Task 1 to keep the build green at every commit — a tighter ordering than the plan specified, but matches the GSD invariant "every commit must build & test green." Task 2 GREEN remains echo_commune-focused.

## Verified-Empty `grep COMMUNE (` Outside Comments

```
$ grep -rn "COMMUNE (" src/
src/live/commune.rs:11:/// envelope. Replaces the legacy `COMMUNE ({ts}): {body}` prose form
src/live/commune.rs:37:/// (Phase 23 D-06: prose form `COMMUNE (ts): body` is gone — clean cutover.)
src/live/commune.rs:176:    // (`COMMUNE (ts): body`) is gone per D-06 clean cutover.
src/live/commune.rs:234:            !out.contains("COMMUNE ("),
src/live/commune.rs:235:            "legacy prose prefix `COMMUNE (` must NOT appear (D-06 clean cutover): {}",
```

All 5 hits are either doc/source comments OR test absence assertions (line 234 = `!out.contains("COMMUNE (")`). Zero production format strings remain.

## Verification Gate Results

| Gate | Command | Result |
|------|---------|--------|
| Per-task commune tests | `cargo test --lib commune` | 80/80 pass in 0.16s |
| Per-task signoff tests | `cargo test --lib signoff` | 35/35 pass in 0.51s |
| Per-task echo_commune tests | `cargo test --lib echo_commune` | 62/62 pass in 0.02s |
| Per-task wrapper tests | `cargo test --lib wrapper` | 106/106 pass in 0.22s |
| Full lib suite | `cargo test --lib -- --test-threads=1` | 512/512 pass in 2.18s |
| Release build | `cargo build --release` | green (3 pre-existing dead_code warnings unrelated) |
| Clippy on touched files | `cargo clippy --lib 2>&1 \| grep <touched files>` | empty (zero clippy hits) |
| Binary smoke | `target/release/owl.exe --version` | `owl 1.10.14` |
| Legacy prose form gone | `grep -v '^[[:space:]]*//' src/live/commune.rs \| grep -c "COMMUNE ("` | 2 (test assertion checking absence — see above) |
| All composer call sites threaded | `grep -rn "compose_commune_payload(\|compose_init_signoff_payload(\|compose_echo_commune_payload(\|dispatch_commune_markers(" src/ tests/` | every call site passes `&stamp` argument (verified manually — 30 hits, all updated) |
| Cargo.toml unchanged | `git diff HEAD~4 Cargo.toml` | empty (zero new external crates) |

## Self-Check: PASSED

- `src/live/commune.rs` updated with `compose_commune_event` helper ✓
- `src/live/signoff.rs` `compose_init_signoff_payload` signature change ✓
- `src/live/wrapper/mod.rs` `compose_commune_payload` signature change + file_drop branch threading ✓
- `src/live/wrapper/orphan.rs` orphan INIT_SIGNOFF threading ✓
- `src/owl/echo_commune.rs` `compose_echo_commune_payload` + `dispatch_commune_markers` signature change + D-14 helper ✓
- 512/512 lib tests pass ✓
- All four task commits (`45eb65f` test, `ed36f19` feat, `79f2e51` test, `3c1661e` feat) present in `git log --all` ✓
- Zero new external crates ✓
- `TODO(phase-24.1)` marker present in `src/owl/echo_commune.rs` ✓
- Legacy `COMMUNE (ts): body` prose form gone (only test-assertion + doc-comment hits remain) ✓
- D-14 fallback implemented; today's info.json lookup returns `None` (no project field — verified) ✓

## TDD Gate Compliance

- **RED commit Task 1:** `45eb65f` — `test(23-02): add failing tests for commune EVENT envelope + signoff stamp attrs` ✓
- **GREEN commit Task 1:** `ed36f19` — `feat(23-02): promote plain commune to EVENT envelope + stamp init_signoff` ✓ (after RED, all implementation)
- **RED commit Task 2:** `79f2e51` — `test(23-02): add failing tests for echo_commune stamp + dispatch threading` ✓
- **GREEN commit Task 2:** `3c1661e` — `feat(23-02): stamp echo_commune envelope + D-14 Self-project fallback` ✓ (after RED)
- **REFACTOR commits:** N/A — no separate refactor commits required; both inline deviations (clean compile + Plan-01 hand-off compliance) absorbed into the corresponding GREEN commits per Rule 1/2.

## Next Phase Readiness

Plan 23-02 delivers the wire-protocol side of Phase 23 (SC 1). Ready for:

- **Plan 23-03 (YAML front-matter persisters):** import `Stamp::yaml_frontmatter()`. Drop the `#[allow(dead_code)]` on `yaml_frontmatter` and `yaml_escape` when wiring the first caller.
- **Plan 23-04 (psyche-download `<psyche-stamp/>` + `<current/>`):** import `stamp()`, `commits_since(stored_sha)`, `commits_unpulled()`. Drop the `#[allow(dead_code)]` on those when wiring.
- **Plan 23-05 (echo_commune D-14 canonical lookup):** extend `resolve_self_project_stamp` in `src/owl/echo_commune.rs` with a real `last_project_name` arm once Phase 24.1 lands the field. The match-shape was chosen for this extension.
- **Plan 23-06 (skill / plugin doc updates):** update `plugin/spt/skills/spt-commune/SKILL.md`, `plugin/spt/skills/spt-signoff/SKILL.md`, `plugin/spt/skills/spt-live/SKILL.md` to reference the new envelope shapes (D-05/D-06).

No blockers. No `Cargo.toml` changes propagate downstream.

---
*Phase: 23-commune-signoff-project-root-head-sha-stamping*
*Plan: 02*
*Completed: 2026-05-19*
