---
status: resolved
trigger: "todlando psyche-download in tracked claude_skill_owl repo emits no <project-context-resolved/> sentinel; bootstrap chicken-and-egg suspected"
created: 2026-05-22
updated: 2026-05-22
resolved_by:
  - aedfdea  # v1.11.2 Self-side detection rule (commune/signoff skills) keyed on <current/> project=
  - a4ad971  # haiku-prompt builder + tests (260522-9zk fix commit)
  - b2a579e  # psyche.md teaching (260522-9zk doc commit)
---

# Root Cause Report — Sentinel Bootstrap Gap

## Symptom

todlando, a live agent running in tracked project `claude_skill_owl` (cwd `C:\Users\decid\Documents\projects\claude_skill_owl`, branch `main`), ran `$LIVE psyche-download todlando` against owl v1.11.1. Output contained NO `<project-context-resolved name="claude_skill_owl"/>` sentinel. todlando (interpreting `plugin/spt/skills/signoff/SKILL.md` as the user reports) concluded "outside tracked project" and wrote a signoff with `<live-context>` only — no `<project-context>` slice. The user reports todlando IS in a tracked project and expected the two-slice form.

## Reproduction

```
cd C:\Users\decid\Documents\projects\claude_skill_owl
C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.1\owl.exe live psyche-download todlando 2>&1 | grep -E "<current |project-context-resolved"
```

Observed:
```
<current machine="HFENDULEAM" project="claude_skill_owl" branch="main" head_sha="ab8e8b2..." head_subject="docs(quick-260522-4sq): clarify commune/signoff in-project detection rul…" commits_since="0" commits_unpulled="0"/>
```

No `<project-context-resolved/>` anywhere in payload. Filesystem confirms the cause: `Test-Path C:/Users/decid/AppData/Local/spt/psyches/tracked/projects` returns `False`. The entire `projects/` subtree does not exist on this machine. Every agent (deployah, doyle, dunsen, executor, higsby, mica, todlando, webber, witty) has `agents/<id>/live_context.md` only.

## Investigation

### 1. Sentinel emission site (`src/live/context.rs:460-470`)

```rust
if let Some(project_name) = owlery::derive_current_repo_names().first() {
    let proj_path = owlery::project_worktree_path(project_name).join(format!("{}.md", self_id));
    if proj_path.exists() {                                  // GUARD (b)
        if let Ok(content) = fs::read_to_string(&proj_path) { // GUARD (c)
            out.push_str(&format!("<project-context-resolved name=\"{}\"/>\n", project_name));
            out.push_str(&content);
            has_any = true;
        }
    }
}
```

Sentinel sits inside the same three serial guards as the project body push. Per commit 5717e2b (Phase 25.1 D-25.1-04), this was deliberate: presence/absence is meant to be a "hard signal" of project-file-on-disk, not in-project-membership.

### 2. Project-file write sites (the absorber)

`project_worktree_path(...).join("<id>.md")` is referenced from:
- `src/owl/echo_commune.rs:324, 399, 1415, 1452, 1495, 1528, 1582` — all inside `route_two_slice` / `route_project_slot` (and test code).
- `src/live/signoff.rs:153, 797, 829, 869, 905, 971` — all inside `route_two_slice_signoff` (and test code).
- `src/live/context.rs:462, 2258, 2321, 2437` — line 462 is the sentinel READ; the others are test code.

The ONLY non-test production writer is `route_project_slot` (`src/owl/echo_commune.rs:305-353`). It writes when, and only when, the parsed payload from the haiku-child contains a `<project-context>` envelope:

```rust
let project_body = match &slices.project {
    Some(b) => b,
    None => return Ok(()),     // No project envelope → no write, ever
};
let names = owlery::derive_current_repo_names();
let project_name = match names.first() {
    Some(n) => n,
    None => return Ok(()),
};
let proj_dir = owlery::project_worktree_path(project_name);
if let Err(e) = std::fs::create_dir_all(&proj_dir) { ... }
let proj_path = proj_dir.join(format!("{}.md", self_id));
std::fs::write(&proj_path, project_body);
tracked::commit_project_payload(project_name, &[proj_file_name.as_str()], &subject);
```

No code creates `projects/<name>/<id>.md` on `$LIVE start`, on `$LIVE revive`, on `psyche-download`, or anywhere else outside the absorber.

### 3. Haiku-child's emit-`<project-context>` decision

The composer `build_current_context_blocks` (`src/owl/echo_commune.rs:388-406`) reads `projects/<cwd>/<self_id>.md` from disk and inlines it as a `CURRENT_PROJECT_CONTEXT:` block in the haiku prompt. When the file is missing OR `derive_current_repo_names()` returns empty, the entire `CURRENT_PROJECT_CONTEXT` block is omitted from the prompt (D-09 strict, no-fallback).

`psyche.md` `<output_envelope>` rule 2 (line 317) instructs the haiku:

> If the prompt does NOT contain a `CURRENT_PROJECT_CONTEXT` block (no cwd project), emit ONLY the `<live-context>` envelope; omit `<project-context>` entirely.

`signoff/SKILL.md` line 354 mirrors this rule.

### 4. The bootstrap loop

```
file missing (projects/<cwd>/<id>.md does not exist)
    └─> build_current_context_blocks omits CURRENT_PROJECT_CONTEXT
            └─> haiku prompt has no CURRENT_PROJECT_CONTEXT block
                    └─> haiku (per psyche.md rule 2) omits <project-context> envelope
                            └─> route_project_slot returns Ok(()) with no write
                                    └─> file remains missing  ←┐
                                                                │
                                            (loop forever)  ────┘
```

No actor breaks this loop. Every haiku-child output in a fresh-project agent will lack `<project-context>`. The sentinel guard-stack is a downstream amplifier of the same fault: missing file → sentinel suppressed → Self misreads (if Self uses the sentinel as the detection signal, per the user-reported reading of `signoff/SKILL.md`).

### 5. Phase 25 / 25.1 design intent vs shipped state

- **Phase 25 D-13:** "Project worktrees materialize lazily on first commune that resolves a cwd_project. Reuse Phase 24 D-16 lazy creation: `tracked::ensure_project_worktree(name)` on the **write path**; first hit for a given project name calls `git -C seed worktree add ../projects/<name> p-<name>`." — This is correctly implemented in `route_project_slot`. But "first hit" presupposes the haiku-child emits the envelope at least once. The plan does not specify a bootstrap creator.
- **Phase 25 D-09 strict:** "psyche-download project-section lookup uses strict existence check. No fallback. If `projects/<cwd_project>/<agent_id>.md` does not exist, the project section is omitted." — Hardens the missing-file branch, no fallback.
- **Phase 25.1 D-25.1-04:** Sentinel emitted iff body present. Confirmed hard signal of "body exists", not "in-project".

**No Phase 25 / 25.1 plan file specifies a bootstrap creator.** `ensure_project_worktree` exists in `src/common/tracked.rs:458` and would safely create the worktree on demand, but no production caller invokes it outside the write path. Phase 28 plans were not searched, but the symptom and the v1.10.x / v1.11.x changelogs indicate this gap was not previously identified.

### 6. Stale-skill misread on top of the bootstrap fault

The user reports todlando read `signoff/SKILL.md` as teaching "tracked project detected via `<project-context-resolved` marker". The current `signoff/SKILL.md` line 59 actually teaches detection via the `project="..."` attribute on `<current/>`, and `plugin/spt/skills/commune/commune.md:21` (D-25.1-04) is explicit: the sentinel is "NOT the routing rule". Commits `ab8e8b2` and `aedfdea` (both 2026-05-22, on this branch) revised the skill docs for exactly this reason. todlando may have read a cached / pre-revision form of the skill, or misattributed the routing rule to the sentinel by visual prominence.

The fact remains that **even with the corrected skill teaching**, the bootstrap loop in §4 makes the haiku omit `<project-context>` on first encounter — so the project file would still never be created. Fixing the skill teaching alone unblocks only the symptom (signoff body shape); the structural fault stays.

## Root Cause

Two interlocked faults:

1. **(Primary, structural) Haiku-prompt bootstrap loop.** The composer omits `CURRENT_PROJECT_CONTEXT` when the project file does not exist; `psyche.md` rule 2 + `signoff/SKILL.md` line 354 instruct the haiku to omit `<project-context>` when its prompt lacks `CURRENT_PROJECT_CONTEXT`; the absorber only writes the project file when `<project-context>` was parsed. A fresh agent in a new tracked project can never produce its first `projects/<cwd>/<self_id>.md`. The `projects/` subtree on this machine being entirely empty confirms this empirically across nine agents.

2. **(Secondary, downstream) Sentinel guard-stack amplifies the symptom.** `download_payload` gates `<project-context-resolved name="..."/>` on the same three guards as the project body push. As long as fault (1) holds, the sentinel is suppressed forever. Combined with the (now-revised) stale skill teaching that used the sentinel as the routing signal, the user-observed signoff-body collapse to `<live-context>`-only is a deterministic downstream consequence — not a one-off.

## Proposed Fix

### Recommendation: **Option A** (drop the file-existence guard on the sentinel) **PLUS a haiku-prompt revision** to unblock the primary loop.

Option A alone makes the sentinel a true "in tracked project" marker without altering its meaning for Self — fully consistent with D-25.1-04's "hard signal, no LLM judgment" intent. But Option A does NOT unblock the bootstrap loop; the haiku still skips `<project-context>` because its prompt has no `CURRENT_PROJECT_CONTEXT` block. To fix that, the psyche.md / SKILL.md rule that ties envelope emission to `CURRENT_PROJECT_CONTEXT` presence must be replaced with "tie to `<current/>` project= presence" (already the routing rule for Self in §commune.md D-25.1-04; just extend it to the haiku-prompt-side `<output_envelope>` rule too).

### Why not the alternatives

- **Option B** (synthesize empty stub on `$LIVE start`) breaks "absence = no prior context" as a diagnostic signal — once stubs exist, you cannot distinguish "never communed in this project" from "communed but emitted nothing". Hides bugs.
- **Option C** (two sentinel forms `<project-context-resolved/>` vs `<project-context-empty/>`) adds protocol surface for a signal that Self already gets from `<current ... project="..."/>` unconditionally. Redundant.
- **Option D** (relax docs only) is already done via commits ab8e8b2 / aedfdea; it does NOT solve the haiku-prompt loop. Without a code-side change, fresh-project agents stay broken even with perfect skill docs.

### Minimal diff sketch

Three changes, ordered by file:

**(1) `src/live/context.rs:460-470` — Option A: drop guard (b).**

```rust
// Phase 25.1 D-25.1-04 (revised): emit sentinel whenever cwd resolves to a
// tracked project, regardless of project-body presence. Sentinel now means
// "Self is in tracked project <name>" — a hard, deterministic signal that
// pairs with the new haiku-prompt rule (psyche.md §<output_envelope> rule 2)
// keyed on the same `<current ... project="..."/>` attribute.
if let Some(project_name) = owlery::derive_current_repo_names().first() {
    out.push_str(&format!("<project-context-resolved name=\"{}\"/>\n", project_name));
    let proj_path = owlery::project_worktree_path(project_name).join(format!("{}.md", self_id));
    if proj_path.exists() {
        if let Ok(content) = fs::read_to_string(&proj_path) {
            out.push_str(&content);
            has_any = true;
        }
    }
    // Sentinel alone does NOT set has_any — preserves NO-CONTEXT exit when
    // memformat + live_context + pending sections are all absent (e.g. truly
    // first-ever invocation for an unborn agent in a tracked repo).
}
```

**(2) `psyche.md` `<output_envelope>` rule 2 — re-key on `<current/>` project= attribute.**

Replace lines 317-318 with:

```
2. If the inlined `<current/>` tag carries a `project="..."` attribute (the deterministic in-project signal per D-25.1-04), emit `<project-context>...</project-context>`. If you have nothing project-specific to merge this cycle, emit `<project-context></project-context>` with an empty body — empty is the "in-project but quiet" signal (D-25.1-02). Only omit `<project-context>` entirely when the prompt's `<current/>` has NO `project=` attribute (cwd outside any tracked repo, D-25.1-03 missing-tag signal).
```

**(3) `signoff/SKILL.md` line 354 — mirror change.**

Replace "If the prompt has NO `CURRENT_PROJECT_CONTEXT` block (cwd outside any tracked project), omit the `<project-context>` envelope entirely." with the same rule-2 wording above. Already conformant for Step 3 → keep.

### What ships with this fix

- Sentinel becomes "in-project" hard signal (Option A intent).
- Haiku-child stops gating `<project-context>` on prior-content presence; instead gates on the same `<current/>` `project=` attribute that Self already uses for routing — single in-project signal across both directions of the round trip.
- First commune in a new tracked project writes the first `projects/<cwd>/<id>.md` (lazy-create via `ensure_project_worktree` already wired in `commit_project_payload`). Subsequent communes merge with prior content.
- todlando's reported signoff-body collapse is fixed because (a) sentinel now appears, and (b) `<current/>` project= attribute is the documented routing rule on Self side already (per ab8e8b2 / aedfdea).

## Risk

- **Schema test churn.** Tests that assert "no sentinel out-of-project" still pass. New test needed: "sentinel present in-project even when project file absent" (covers Option A). New test needed: "haiku-prompt-side emits `<project-context>` when `<current/>` has project= but no `CURRENT_PROJECT_CONTEXT` block" — likely a unit test on the haiku prompt builder or a manual fixture test.
- **First-commune empty-body race.** If the haiku emits a meaningful `<project-context>` body on first encounter (great — file populated), all is well. If it emits `<project-context></project-context>` (per the "nothing to say yet" rule), `route_project_slot` still writes an empty file — which is the deliberate "in-project but quiet" signal. The empty-file write triggers `commit_project_payload` and creates the project worktree branch. Subsequent communes have prior content to merge. Acceptable.
- **psyche.md is embedded via `include_str!`** — change requires a rebuild + redeploy. Existing v1.11.1 wrappers will see the old rule until they hot-handoff to the new binary. Standard Phase 18.4/18.5 handoff behavior covers this.
- **Skill docs update is non-binary.** `commune.md` is already correct (line 21). `psyche.md` and `signoff/SKILL.md` need the rule-2 rewrite. Both are doc-only edits on top of code change (1).

## Test Plan

- **Unit (Rust):**
  - `download_payload_emits_sentinel_in_project_even_when_file_absent` — set cwd inside a git worktree, ensure no `projects/<name>/<id>.md`, call `download_payload`, assert output contains `<project-context-resolved name="<name>"/>` and NO body following it.
  - `download_payload_emits_sentinel_and_body_when_file_present` — pre-write project file, assert sentinel + body both present.
  - `download_payload_omits_sentinel_outside_repo` — cwd outside any git repo / `derive_current_repo_names()` empty, assert no sentinel.
- **Integration (golden):**
  - Capture-fixture for `psyche-download` in a tempdir git repo with no project file — golden contains sentinel only, no body.
- **End-to-end (manual on todlando):**
  1. Apply patches, rebuild owl.exe, redeploy via `docs/DEPLOY.ps1`.
  2. `cd C:\Users\decid\Documents\projects\claude_skill_owl; $LIVE psyche-download todlando` → confirm `<project-context-resolved name="claude_skill_owl"/>` appears immediately after `<current/>`.
  3. Trigger todlando commune cycle (drop `.claude/todlando-commune.md` with a small body); wait for psyche absorption; confirm `C:\Users\decid\AppData\Local\spt\psyches\tracked\projects\claude_skill_owl\todlando.md` appears on disk with the haiku-emitted project slice.
  4. Re-run `$LIVE psyche-download todlando` and confirm both sentinel AND body now present.
  5. Trigger `/spt:signoff` and confirm the dropped signoff body uses the two-slice envelope.

## Resolution

root_cause: see "Root Cause" section above. Two interlocked faults; v1.11.2
fixed the doc/skill-side (Self detection rule); 260522-9zk fix commit a4ad971
fixed the haiku-prompt-builder bootstrap loop on the code side.

### Fix shipped (in order)

1. **v1.11.2, commit aedfdea (Self side).** Skill docs (`commune/SKILL.md`,
   `commune/commune.md`, `signoff/SKILL.md`) rewrote the in-project detection
   rule to key on the `<current/>` `project="..."` attribute emitted
   unconditionally by `psyche-download`. The `<project-context-resolved/>`
   sentinel was demoted to a secondary "prior content" indicator. This alone
   fixed Self's signoff-body shape going forward (Self now emits two-slice
   on first commune in a tracked project regardless of whether the project
   file exists yet).

2. **260522-9zk, commit a4ad971 (haiku side).**
   `build_current_context_blocks` in `src/owl/echo_commune.rs` was updated
   to key `CURRENT_PROJECT_CONTEXT` block emission on cwd_project
   RESOLUTION rather than project-file existence. When the file is missing
   it emits the block with the literal `(none — first commune in project)`
   body (mirrors the live-side first-commune fallback pattern). This breaks
   the bootstrap loop: the haiku now sees the block on first commune in a
   fresh tracked project, follows the rewritten `psyche.md` rule 2, emits
   `<project-context>`, and the absorber (`route_project_slot`) lazy-creates
   `projects/<name>/<self_id>.md` via `commit_project_payload`. Unit tests
   for both `echo_commune` and `signoff` builders updated to assert the new
   first-commune-in-project literal contract.

3. **260522-9zk, commit b2a579e (haiku teaching).** `psyche.md`
   `<output_envelope>` rule 2 + `<init_signoff>` + `<context_save>`
   rewritten to teach: a block whose body is the first-commune-in-project
   literal still counts as PRESENT — emit both envelopes. Only true block
   absence (cwd outside any tracked project) means emit live-only.

### Recommendation re-evaluation (post-v1.11.2)

- **Recommendation #1 — `src/live/context.rs:460-470` sentinel guard: SKIPPED.**
  Under v1.11.2 the sentinel was demoted from "in-project routing signal" to
  "secondary prior-content indicator". The existing file-exists guard
  correctly matches the new role (sentinel fires iff prior content on
  disk). The original "in-project" role moved to the unconditional
  `<current/>` `project="..."` attribute that `download_payload` already
  emits above (and the new haiku-side `CURRENT_PROJECT_CONTEXT` block
  pattern emits in parallel). Keeping the guard preserves the new
  semantic; removing it would conflate two distinct signals.
- **Recommendation #2 — `psyche.md` rule 2: IMPLEMENTED differently.** The
  original sketch proposed re-keying rule 2 on `<current/>` `project=`,
  but the haiku prompt does NOT contain `<current/>` (only Self's
  `psyche-download` output does). Cleaner equivalent: re-key on
  `CURRENT_PROJECT_CONTEXT` block presence and ALWAYS emit that block
  when cwd_project resolves. Same outcome (single in-project signal on
  the haiku side, 1:1 with cwd_project resolution), correct surface.
- **Recommendation #3 — `signoff/SKILL.md` mirror: ALREADY SHIPPED** by
  v1.11.2 commit aedfdea. No further skill changes needed.

### Residual / followups

None reachable in normal operation. Two notes for completeness:

- The just-finished todlando signoff that triggered this investigation
  was authored under the OLD v1.11.1 doc and binary, so
  `projects/claude_skill_owl/todlando.md` was NOT created. Next commune
  cycle on todlando under the new binary will create it naturally — no
  backfill action.
- The `<project-context-resolved/>` sentinel will remain absent on
  `psyche-download` output until each agent's first new-binary commune
  writes its project file. Expected and correct under the new
  "prior content" semantic.

verification:
- `cargo build --release` clean (only pre-existing dead-code warnings).
- `cargo test --lib --release prompt_` passes 10/10 incl. new
  `prompt_emits_current_project_context_with_first_commune_literal_when_file_absent`
  and the signoff-side mirror.
- `cargo test --lib --release echo_commune` passes 70/70.
- `cargo test --lib --release signoff` passes 52/52.
- End-to-end manual verification on todlando deferred to next commune
  cycle post-redeploy.

files_changed:
  - src/owl/echo_commune.rs        # builder + prompt-string + tests
  - src/live/signoff.rs            # mirror test
  - psyche.md                      # rule 2 + init_signoff + context_save
  - Cargo.lock                     # owl crate version 1.11.2 (was missed from cd2dba0)
  - .planning/quick/260522-9zk-haiku-bootstrap-fix-key-project-block-on-cwd/260522-9zk-PLAN.md
