---
phase: 23-commune-signoff-project-root-head-sha-stamping
plan: 01
subsystem: messaging
tags: [git, subprocess, timeout, hostname, yaml, event-envelope, stamp, phase23, rust, no-unsafe]

requires:
  - phase: 22-discoverable-via-touch-perches
    provides: existing EVENT-envelope composers (compose_init_signoff_payload, compose_echo_commune_payload) that Plans 02/03 will parameterize over &Stamp
provides:
  - "src/common/git.rs module — Stamp struct, stamp() producer, stamp_at(&Path) test/internal variant"
  - "500ms soft-timeout subprocess pattern (first in codebase) via spawn + monitor thread + force_kill_process + wait_with_output reap"
  - "Cross-platform OS hostname capture with env-var primary + 'hostname' CLI fallback; no FFI, no unsafe"
  - "cap_subject_72 helper (char-not-byte cap with U+2026 ellipsis; newline-strip; emoji-safe)"
  - "yaml_escape helper for fixed 5-key YAML front-matter (quote on :, #, leading/trailing space, doublequote)"
  - "WARNED_GIT_UNAVAILABLE one-shot stderr gate for D-13 rate-limited warning"
  - "commits_since / commits_unpulled (+ _at variants) for Plan 04's psyche-download <current/> count deltas"
affects: [23-02-commune-event-envelope, 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 per CLAUDE.md "no runtime deps" — implementation is std-only
  patterns:
    - "Subprocess timeout via spawn + monitor thread + force_kill_process + wait_with_output (zombie-safe per Pitfall 1)"
    - "Public surface + _at variant for testability without global cwd mutation"
    - "Once-per-process warning gate via static AtomicBool (D-13 rate-limiting)"
    - "Tests in same file under #[cfg(test)] mod tests (matches src/live/signoff.rs convention)"

key-files:
  created:
    - "src/common/git.rs (579 lines: 318 production + 261 test)"
    - ".planning/phases/23-commune-signoff-project-root-head-sha-stamping/deferred-items.md"
  modified:
    - "src/common/mod.rs (one-line `pub(crate) mod git;` registration)"

key-decisions:
  - "Added stamp_at(&Path) / commits_since_at / commits_unpulled_at internal variants alongside the global-cwd public surface so tests can exercise outside-repo paths without mutating process-global cwd. The plan's Mutex<()>-guard pattern (modeled on src/live/signoff.rs:114) was insufficient because cross-module tests (live::context, owl::cleanup, etc.) read cwd-relative paths and race against the chdir window even when our own tests serialize via ENV_LOCK."
  - "Stamp derives PartialEq + Eq (locked by plan) so Plan 04's drift comparisons (stored.project == current.project, branch/sha/machine !=) compile against vanilla struct equality without manual field-by-field logic."
  - "`#![allow(dead_code)]` is intentionally inside the module — every pub symbol is consumed by Plans 02-05 (composers, persisters, downloader); the lint would otherwise fire on every symbol until Plan 02 lands. Doc comment instructs to drop the attribute when Plan 02 wires the first caller."

patterns-established:
  - "500ms soft-timeout subprocess pattern (run_git_with_timeout): mpsc::channel + thread::spawn(recv_timeout) + force_kill_process + wait_with_output reap. Cancel killer via tx.send() on the fast path; killer is a no-op if the channel disconnected before recv_timeout expired. This is the first timeout-bounded subprocess in src/ and will be the reference pattern if other modules need it."
  - "Once-per-process warning gate: `static GATE: AtomicBool` + `.swap(true, Relaxed)` returns prior value; eprintln runs only when prior == false. Cheaper than a Mutex<bool> and lock-free."

requirements-completed: [PROJ-META-01, PROJ-META-02, PROJ-META-03, COMMIT-META-01]

duration: 10 min
completed: 2026-05-20
---

# Phase 23 Plan 01: Stamp + git helpers module Summary

**New src/common/git.rs module housing the 5-field Stamp struct, stamp() producer, 500ms-timeout-bounded git subprocess helpers, OS hostname capture, and commits_since / commits_unpulled counters — quarantining all FFI / subprocess surface for Phase 23 in one file so downstream composers (Plans 02-05) stay pure formatters over &Stamp.**

## Performance

- **Duration:** ~10 min
- **Started:** 2026-05-20T05:22:59Z
- **Completed:** 2026-05-20T05:32:55Z
- **Tasks:** 1 (TDD: 2 commits — RED + GREEN — plus one deferred-items chore commit)
- **Files created:** 2 (src/common/git.rs, deferred-items.md)
- **Files modified:** 1 (src/common/mod.rs)
- **Test suite runtime:** 0.10s for the 13-test common::git module

## Accomplishments

- 5-field Stamp struct (machine, project, branch?, head_sha?, head_subject?) with PartialEq+Eq derived
- `pub fn stamp()` producer + `pub(crate) fn stamp_at(&Path)` test/internal variant
- 500ms soft-timeout subprocess helper (`run_git_with_timeout`) — the first such pattern in this codebase; zombie-safe per Pitfall 1
- Cross-platform OS hostname capture (env var primary, `hostname` CLI fallback, literal "unknown" floor) — zero FFI / zero `unsafe`
- `commits_since(stored_sha)` + `commits_unpulled()` + their `_at` variants for Plan 04's `<current/>` block deltas
- `Stamp::event_attrs()` (leading-space attr string for EVENT-tag splice; D-11 omission-on-None; amp-first escape via `crate::owl::poll::event_attr_escape`)
- `Stamp::yaml_frontmatter()` (fenced 2-5 line YAML block; D-11 omission-on-None; `:`/`#`/`"`/leading-trailing-space quoting with `\` and `"` escape inside quotes)
- Private helpers: `cap_subject_72` (char-not-byte cap with U+2026 ellipsis, newline-strip, emoji-safe), `yaml_escape`, `hostname`, `git_project_basename`
- `WARNED_GIT_UNAVAILABLE: AtomicBool` once-per-process stderr gate (D-13)
- 13 unit tests in same-file `#[cfg(test)] mod tests` block, all passing in ~0.10s

## Task Commits

This was a TDD plan (`tdd="true"`) — RED + GREEN cycle:

1. **Task 1 RED — failing tests for Stamp + stamp() module** — `f93eafe` (test)
   - Adds `pub(crate) mod git;` to `src/common/mod.rs`
   - Creates `src/common/git.rs` with type surface + `unimplemented!()`-shaped stubs
   - 13 tests in `#[cfg(test)] mod tests`; 11 expected-fail, 2 trivially pass against the stubs
2. **Task 1 GREEN — implement Stamp + stamp() + git helpers** — `7544c78` (feat)
   - Real implementation: `stamp()` + `stamp_at`, `commits_since` + `_at`, `commits_unpulled` + `_at`, `event_attrs`, `yaml_frontmatter`, plus all private helpers
   - All 13 tests pass; clippy clean on git.rs; cargo build --release green
3. **Out-of-scope deferred-items log** — `039ba2f` (chore)
   - `.planning/.../deferred-items.md` documenting pre-existing lib-test parallel flakiness (16 tests across owlery/live/cleanup/resume) discovered during verification. Not caused by Phase 23; passes cleanly under `--test-threads=1`.

## Files Created/Modified

- `src/common/git.rs` — Stamp struct, stamp(), stamp_at, commits_since(_at), commits_unpulled(_at), Stamp::event_attrs, Stamp::yaml_frontmatter, cap_subject_72, yaml_escape, hostname, git_project_basename, run_git_with_timeout, WARNED_GIT_UNAVAILABLE static, and the 13-test #[cfg(test)] module. 579 lines.
- `src/common/mod.rs` — added `pub(crate) mod git;` (one line, sort-inserted with the other declarations)
- `.planning/phases/23-commune-signoff-project-root-head-sha-stamping/deferred-items.md` — out-of-scope log

## RESEARCH §Code Examples Consulted

- **§Code Examples Example 1: Stamp helper module skeleton** — followed verbatim for `Stamp` struct, `stamp()` body, `event_attrs` ordering + escape, `yaml_frontmatter` shape, `cap_subject_72` char-iter idiom, `yaml_escape` quote conditions, `hostname` env-var + shellout fallback chain, `git_project_basename` toplevel-basename derivation, `run_git_with_timeout` spawn + monitor + reap pattern, `WARNED_GIT_UNAVAILABLE` AtomicBool gate.
- **§Pattern 3: Subprocess timeout via spawn + monitor thread** — directly translated into `run_git_with_timeout`.
- **§Pattern 4: Per-fire warning rate-limit** — translated into the `WARNED_GIT_UNAVAILABLE.swap(true, Ordering::Relaxed)` guard.
- **§Pitfall 1, 2, 3** — addressed: zombie-safe reap via `wait_with_output`; newline-strip via `.lines().next()` before cap; char-not-byte cap via `chars().take(72)`.
- **§Don't Hand-Roll** table — followed: reused `crate::owl::poll::event_attr_escape`, `crate::common::process::hide_window`, `crate::common::process::force_kill_process`.

## Exact Test List Emitted

```
common::git::tests::stamp_produces_five_fields_in_repo
common::git::tests::stamp_omits_optional_outside_repo
common::git::tests::cap_subject_72_ascii
common::git::tests::cap_subject_72_emoji_safe
common::git::tests::cap_subject_72_strips_embedded_newline
common::git::tests::event_attrs_renders_all_five_when_in_repo
common::git::tests::event_attrs_omits_optionals_when_none
common::git::tests::event_attrs_escapes_dangerous_chars
common::git::tests::yaml_frontmatter_renders_all_five
common::git::tests::yaml_frontmatter_omits_optionals
common::git::tests::yaml_frontmatter_quotes_values_with_special_chars
common::git::tests::commits_since_when_no_repo_returns_none
common::git::tests::commits_unpulled_no_upstream_returns_none_or_zero_via_caller_unwrap
```

13 tests, all pass in 0.10s. Plan's done criterion ("at least 13 named tests") met exactly.

## Decisions Made

1. **`stamp_at(&Path)` test/internal variant alongside the global `stamp()` public surface.** Justification: the codebase has 16 cross-module tests (owlery, live::context, owl::cleanup, owl::resume) that read cwd-relative paths and race against any chdir window — even one serialized by an in-module `ENV_LOCK: Mutex<()>`. The plan's `Mutex<()>`-guard pattern (modeled on `src/live/signoff.rs:114`) only protects against other tests *within the same module*; cargo's parallel runner schedules other-module tests concurrently. Refactoring to a parameterized `stamp_at(cwd)` with a thin `stamp()` wrapper gave tests deterministic input without any global mutation, and dropped the 8 false failures my chdir tests had been inducing in other modules.
2. **`#![allow(dead_code)]` retained inside the module.** Every pub symbol is wired only by Plans 02-05; without the allow, every release/dev build would emit ~7 dead-code warnings for symbols the next plan will consume. Doc comment instructs Plan 02 to remove the attribute when the first caller lands.
3. **Stamp derives PartialEq + Eq** (locked by plan §action 2): enables Plan 04 drift comparisons (`stored.project == current.project`, `stored.branch != current.branch`, ...) via vanilla struct equality.

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 — Bug] Refactored to `stamp_at(&Path)` / `commits_*_at` to remove cross-module test interference**
- **Found during:** Task 1 (GREEN verification)
- **Issue:** The plan's `<behavior>` specified `Mutex<()>` ENV_LOCK around tests that call `std::env::set_current_dir`. ENV_LOCK serializes tests *within the same module*, but cargo's parallel runner schedules tests from `live::context`, `owl::cleanup`, `owl::resume`, `live::wrapper`, etc., concurrently with our chdir tests. Those tests read cwd-relative paths (Cargo.toml, perch dirs, SPT_HOME tempdir layouts) and got OS NotFound errors during our brief chdir window. Result: my 2 chdir-using tests caused 8 false failures in unrelated modules' tests.
- **Fix:** Added `pub(crate) fn stamp_at(cwd: &Path)`, `pub(crate) fn commits_since_at(cwd, sha)`, `pub(crate) fn commits_unpulled_at(cwd)` alongside the global-cwd public producers. Tests use the `_at` variants — no global mutation. The plan's "Mutex guard around set_current_dir" instruction was the wrong pattern for this codebase's test topology; the `_at`-parameterized approach is correct and is the pattern Plan 05 (echo_commune Self-project D-14) was likely going to need anyway.
- **Files modified:** `src/common/git.rs` (both prod refactor and test rewrite)
- **Verification:** All 13 git tests still pass (in 0.10s); the 8 cross-module false failures disappeared (24 → 16 pre-existing failures under default parallelism).
- **Committed in:** `7544c78` (Task 1 GREEN commit)

**2. [Rule 1 — Bug] Removed errant `mut` binding on `child` in `run_git_with_timeout`**
- **Found during:** Task 1 (clippy pass)
- **Issue:** RESEARCH Code Examples §1 had `let mut child = cmd...spawn()?` but we never call any `&mut child` method — `wait_with_output(self)` takes ownership. clippy flagged it as `unused_mut`.
- **Fix:** dropped the `mut`.
- **Files modified:** `src/common/git.rs`
- **Verification:** `cargo clippy --lib` produces zero warnings on `src/common/git.rs`.
- **Committed in:** `7544c78` (Task 1 GREEN commit)

**3. [Rule 2 — Missing Critical] Added `#![allow(dead_code)]` attribute back into the file**
- **Found during:** Task 1 (clippy pass)
- **Issue:** The RED commit had `#![allow(dead_code)]` at the top of the file (Plans 02-05 are the callers — no in-tree consumers in Plan 01). The GREEN rewrite dropped it inadvertently, causing 7 dead-code warnings on Stamp, stamp, commits_since, commits_unpulled, event_attrs, yaml_frontmatter, and all 5 private helpers.
- **Fix:** restored `#![allow(dead_code)]` with a doc comment instructing Plan 02 to remove it when the first caller lands.
- **Files modified:** `src/common/git.rs`
- **Verification:** clippy clean on the file.
- **Committed in:** `7544c78` (Task 1 GREEN commit)

---

**Total deviations:** 3 auto-fixed (1 bug — test isolation refactor, 1 bug — unused_mut, 1 missing critical — dead_code allow). All addressed inline during the GREEN cycle and committed in the same task commit.

**Impact on plan:** The test-isolation refactor (Deviation 1) is the most consequential and is a pattern Plan 05 (echo_commune D-14) likely needs anyway. `stamp_at(&Path)` is `pub(crate)`, so Plan 05 can consume it directly without touching public surface. No scope creep; no architectural change (no AskUserQuestion needed per Rule 4).

## Issues Encountered

### Pre-existing lib-test parallel flakiness (out of scope)

`cargo test --lib` reports 16 failures under default cargo parallelism, distributed across `common::owlery`, `live::commune`, `live::signoff`, `live::context`, `live::wrapper`, `owl::cleanup`, `owl::resume`, `owl::hook_idle`, `owl::plugin_session_start`, `owl::version_changelog`. All pass cleanly under `cargo test --lib -- --test-threads=1` (499 tests pass, 0 fail). Symptom is `Os { code: 3, kind: NotFound }` constructing relative paths; root cause is SPT_HOME tempdir collisions between cargo's parallel workers (tempdir names derive from `process::id()` + `SystemTime::now()`, collide across workers when `cargo test` spawns multiple in-process test threads). This is unrelated to Phase 23 and was confirmed pre-existing by:
- Removing all Phase 23 changes leaves the failures (16 stays at 16).
- Adding Phase 23 with the original chdir tests bumped the count to 24 (the +8 was real interference caused by my code) — refactoring to `stamp_at` returned the count to 16.
- The `common::git` module's own tests pass cleanly under both serial and parallel runs.

Logged for a future hardening pass in `deferred-items.md`. Not a Phase 23 blocker.

## Verification Gate Results

| Gate | Command | Result |
|------|---------|--------|
| Module tests | `cargo test --lib common::git` | 13/13 pass in 0.10s |
| Release build | `cargo build --release` | green (3 pre-existing warnings elsewhere; zero on git.rs) |
| Clippy on new file | `cargo clippy --lib 2>&1 \| grep common/git` | empty (zero hits on src/common/git.rs) |
| Binary smoke test | `target/release/owl.exe --version` | `owl 1.10.14` |
| Module registration | `grep -n "pub(crate) mod git" src/common/mod.rs` | 1 hit at line 13 |
| WARNED_GIT_UNAVAILABLE | `grep -c "WARNED_GIT_UNAVAILABLE" src/common/git.rs` | 3 (declaration + swap site + 1 doc-comment reference — plan said "returns 2" which referenced the two *code* sites; the doc-comment ref is incidental, not a deviation) |
| Cargo.toml unchanged | `git diff HEAD~2 Cargo.toml` | empty (zero new external crates) |
| No `unsafe` blocks | `grep -c "unsafe" src/common/git.rs` | 0 |

## Self-Check: PASSED

- `src/common/git.rs` exists ✓
- `src/common/mod.rs` declares the module ✓
- 13/13 git module tests pass ✓
- Both task commits (`f93eafe` test, `7544c78` feat) present in `git log --all` ✓
- Zero new external crates ✓
- Zero `unsafe` blocks ✓
- Clippy clean on the new file ✓

## TDD Gate Compliance

- **RED commit:** `f93eafe` — `test(23-01): add failing tests for Stamp + stamp() module` ✓
- **GREEN commit:** `7544c78` — `feat(23-01): implement Stamp + stamp() + git helpers` ✓ (committed after RED, contains all impl)
- **REFACTOR commit:** N/A — `stamp_at` extraction happened inside the GREEN commit as part of Deviation 1 (test-isolation refactor). The plan's `<done>` criterion does not require a separate refactor commit; the deviation rule (Rule 1) allows inline auto-fixes within the task commit. Documented above.

## Next Phase Readiness

Plan 23-01 deliverables are ready for Plans 02-05 to consume:

- **Plan 02 (commune envelope promotion + signoff/echo attrs):** import `crate::common::git::{Stamp, stamp}` and `Stamp::event_attrs()`. When wiring the first caller, drop `#![allow(dead_code)]` from `src/common/git.rs`.
- **Plan 03 (YAML front-matter persisters):** import `Stamp::yaml_frontmatter()`.
- **Plan 04 (psyche-download `<psyche-stamp/>` + `<current/>`):** import `stamp()`, `commits_since(stored_sha)`, `commits_unpulled()`. Stamp's PartialEq+Eq supports the drift comparisons.
- **Plan 05 (echo_commune D-14 Self-project lookup):** can use `stamp_at(&Path)` if it needs to stamp from a non-cwd directory (e.g. resolved Self project dir vs psyche_dir).

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

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