---
phase: 25.3-project-context-envelope-persistence-encoding-defects
plan: 02
subsystem: psyche-prompt
tags: [defect-fix, llm-prompt, two-slice-envelope, option-a-locked, spt-routes, encoding-contract-consumer, consolidated-deploy]
requires:
  - 25.3-01 (Defect B2 — info.json-cwd resolver + (name=<X>) header token)
  - 25.3-03 (Defect C — encoding contract + LLM-stdin boundary helper)
provides:
  - psyche.md <absorption> section (rules 1-6)
  - Option A no-name <project-context> shape locked across psyche.md + commune.md + signoff/SKILL.md
  - 'SPT routes; you emit' routing-clarity phrasing
  - psyche_md_contains_25_3_absorption_section (integration test)
  - skill_files_have_no_project_context_name_attr (integration test, cycle-3 MEDIUM)
  - Consolidated v1.11.5 deploy of Plans 25.3-01 + 25.3-03 + 25.3-02
affects:
  - psyche.md (include_str! embedded into owl.exe)
  - plugin/spt/skills/commune/commune.md
  - plugin/spt/skills/signoff/SKILL.md
  - tests/native_owl.rs
  - CHANGELOG.md (v1.11.5 curated entry)
  - plugin/spt/.claude-plugin/plugin.json (bumped 1.11.4 -> 1.11.5)
  - Cargo.lock (bumped 1.11.4 -> 1.11.5)
tech_stack:
  added: []
  patterns:
    - "Option A no-name `<project-context>` envelope shape (locked across all three prompt-surface files)"
    - "'SPT routes; you emit' separation of concerns (LLM emits envelope shape; Rust runtime resolves project name via info.json cwd)"
    - "Absorption rules as inbound-merge contract — DELTAs over CURRENT_*_CONTEXT BASELINE blocks"
    - "Static prompt-contract tests guarding phrase drift via include_str! + std::fs::read_to_string (no pub(crate) calls — integration-crate placement correct per cycle-4 HIGH 4)"
key_files:
  created: []
  modified:
    - psyche.md
    - plugin/spt/skills/commune/commune.md
    - plugin/spt/skills/signoff/SKILL.md
    - tests/native_owl.rs
    - CHANGELOG.md
decisions:
  - "psyche.md <absorption> section inserted between <output_envelope> and <context_save> as a sibling top-level section (consistent with surrounding structure)"
  - "Stale <output_envelope> trailing phrasing rewritten: receivers split by tag name; SPT resolves destination project via Self's perch info.json (25.3-01 fix). cwd_project phrasing fully removed (zero remaining matches)"
  - "Absorption rule 5 (Encoding assumption) references Plan 03 LOCKED Option A literally — DECODED at LLM-stdin — so future editors cannot accidentally re-encode"
  - "Absorption rule 6 (Defensive parse) follows T-25.3.02-D1 threat-model mitigation — malformed envelopes never abort the session"
  - "Tests use only include_str! + std::fs::read_to_string — no pub(crate) helper calls — so tests/native_owl.rs placement is correct per cycle-4 HIGH 4"
  - "Consolidated deploy (Plans 01+03+02) at v1.11.5 — single DEPLOY.ps1 -Bump patch invocation to minimize binary-handoff churn during in-flight psyche-wrapper migrations"
metrics:
  duration: ~25min
  completed: 2026-05-23
---

# Phase 25.3 Plan 02: Defect D — psyche.md absorption rules + skill-file alignment Summary

Defect D from `.planning/debug/todlando-project-context-not-persisted.md` closed by adding an
explicit `<absorption>` section to `psyche.md` (the include_str!-embedded LLM prompt) that
teaches the Psyche LLM how to MERGE inbound `<live-context>` and `<project-context>` envelope
bodies, plus aligning `commune.md` and `signoff/SKILL.md` so the producer-side teaching exactly
matches the absorption-side teaching. Cross-references the Plan 01 resolver fix and Plan 03
encoding contract by name so future editors cannot accidentally drift apart.

## Plan 01 + Plan 03 Handoff Facts (Task 1 extraction)

### From 25.3-01 SUMMARY (Defect B2 — resolver refactor)

The `build_current_context_blocks` haiku prompt-builder now emits:

```
CURRENT_PROJECT_CONTEXT (name=<X>):
[body, or `(none — first commune in project)` literal on first-time-in-project]
```

— where `<X>` is the project name resolved from Self's perch `info.json` `cwd` field via the
shared helper `resolve_self_project_name_via_info_cwd(self_id)` in `src/common/owlery.rs`.

The `(name=<X>)` parenthetical is **debug-visible only**. psyche.md absorption rule 4 instructs
the LLM: "treat it as informational only — DO NOT echo it back as a `name=` attr on the
`<project-context>` tag." Option A no-name shape is structurally enforced producer-side and
consumer-side.

### From 25.3-03 SUMMARY (Defect C — encoding contract + LLM-stdin boundary)

The wrapper LLM-stdin boundary helper `compose_llm_prompt_from_envelope` (in
`src/live/wrapper/mod.rs`) is applied INSIDE `resume_session_with_exit` and `final_session`.
The LLM no longer sees an `<EVENT>` envelope at the prompt layer — it sees a natural-language
header followed by the DECODED body with literal `<live-context>` / `<project-context>` tags
(NOT entity-encoded `&lt;live-context&gt;`).

**psyche.md absorption rule 5 states this literally:** "At the LLM-stdin boundary, the EVENT
envelope body is in DECODED form — `<live-context>` and `<project-context>` are LITERAL tags
(you see `<`, not `&lt;`). Do NOT re-encode. Do NOT decode."

The cycle-3 reframed entity invariant is included verbatim: user-prose entities (`&amp;`,
`&lt; 5`, etc.) inside body content PASS THROUGH untouched — they are character content, not
transport encoding.

## psyche.md Diff Summary

Before edit:
- Line count: 355
- `<output_envelope>` ends at line 323
- `<context_save>` starts at line 325
- Trailing `<output_envelope>` rule referenced `<cwd_project>` (stale phrasing flagged by codex HIGH)
- NO `<absorption>` section

After edit:
- Line count: 375 (+20 lines, well under +70 cap)
- `<absorption>` section inserted as sibling between `<output_envelope>` and `<context_save>`
- Trailing `<output_envelope>` rule rewritten: receivers split by tag name; SPT resolves destination project via Self's perch info.json (25.3-01)
- ZERO `cwd_project` / `cwd-project` substrings remain (verified via Select-String)
- ZERO `<project-context name=` substrings remain (verified via test)

### New `<absorption>` section contents (rules 1-6)

1. **Live-context absorption** — merge inbound `<live-context>` body into in-memory live context; re-emit on next context-save fire.
2. **Project-context absorption** — merge inbound `<project-context>` body (no-name shape) into in-memory project context; re-emit on next context-save fire. Ignore any future name attribute on the opening tag (Option A locked).
3. **Inbound merge always active** — CURRENT_LIVE_CONTEXT / CURRENT_PROJECT_CONTEXT prompt blocks are BASELINE; inbound envelopes are DELTAS. Absence of a baseline block does NOT suppress absorption.
4. **SPT routes; you emit** — SPT writes the body to `projects/<resolved_project>/{{self_id}}.md` via Self's perch info.json `cwd` field (25.3-01 Defect B2 fix). The LLM does NOT name the project, does NOT know the filesystem path. If the prompt header carries an `(name=<X>)` parenthetical, treat it as informational only.
5. **Encoding assumption (25.3-03 Plan 03 LOCKED Option A)** — body at LLM-stdin is DECODED; literal `<` tags; do NOT re-encode; user-prose entities pass through (cycle-3 reframed entity invariant).
6. **Defensive parse** — malformed envelopes continue without merge + note inside next `<live-context>` body; never abort the session. Implements threat-model mitigation T-25.3.02-D1.

## commune.md Diff Summary

Added one block after the existing **Nested envelopes** paragraph:

- **Routing: emit this envelope; SPT routes it to disk (Phase 25.3-02).** Teaches `projects/<resolved_project>/{your_id}.md` schema (correcting the user's mistaken flat-layout expectation per the debug doc). Explicit "NOT 'Psyche writes the file.' NOT 'the agent persists to the project file.' You emit; SPT routes."
- Cross-reference to `psyche.md` §`<absorption>` rules added to closing parenthetical.

No-name `<project-context>` shape preserved (no pre-existing `name=` examples to remove). No new commune subcommands, flags, or file-drop conventions introduced.

## signoff/SKILL.md Diff Summary

Extended the closing paragraph of the existing **Two-slice body shape (Phase 25 D-10/D-11)** section to:
- Cross-reference `psyche.md` §`<absorption>` rules for the FINAL_COMMUNE round-trip.
- Add explicit "Routing: emit this envelope; SPT routes it to disk (Phase 25.3-02)" block with the same `projects/<resolved_project>/{your-id}.md` schema and "you emit; SPT routes" phrasing.

No-name `<project-context>` shape preserved.

## Static Prompt-Contract Test Pass Evidence

Both tests added to `tests/native_owl.rs` (placement: integration-crate per cycle-4 HIGH 4 N/A reasoning — only `include_str!` + `std::fs::read_to_string`, no `pub(crate)` calls).

```
$ cargo test --release --test native_owl psyche_md_contains_25_3_absorption_section
running 1 test
test psyche_md_contains_25_3_absorption_section ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 27 filtered out

$ cargo test --release --test native_owl skill_files_have_no_project_context_name_attr
running 1 test
test skill_files_have_no_project_context_name_attr ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 27 filtered out
```

Both tests guard:
- `psyche.md` contains `<absorption>` section.
- `psyche.md` contains "Absorbing inbound two-slice envelopes" section title.
- `psyche.md` contains "SPT routes" phrasing (codex HIGH).
- `psyche.md` does NOT contain `<project-context name=` substring (Option A locked).
- `psyche.md` references Plan 03 encoding contract ("25.3-03" / "DECODED" / "encoding contract").
- `commune.md` does NOT contain `<project-context name=` substring (cycle-3 MEDIUM extension).
- `signoff/SKILL.md` does NOT contain `<project-context name=` substring (cycle-3 MEDIUM extension).

## DEPLOY.ps1 Output Excerpt (Consolidated v1.11.5 Deploy)

```
==> Step 1: Resolve version from plugin.json
    Current version: 1.11.4
    Bumping (patch): 1.11.4 -> 1.11.5
==> Step 3: cargo build --release
    Compiling owl v1.11.5
    Finished `release` profile [optimized] target(s) in 7.50s
==> Step 4: Sync files to marketplace
    Copy owl.exe -> ...\plugins\spt
    Copy skills/* -> ...\plugins\spt\skills
    Copy hooks/* -> ...\plugins\spt\hooks
    Copy plugin.json -> ...\plugins\spt\.claude-plugin
==> Step 5: Commit + push marketplace
[main 43d14c7] spt: deploy v1.11.5
    Pushing marketplace
    Marketplace HEAD: 43d14c7fc048e91c6a23aced2fc2b4650080c6f8
==> Step 6-7: Sync new version + prune old cache
    Keep-set: 1.11.5, 1.11.4
==> Step 9: Refresh plugin state via Claude Code CLI
    Plugin "spt@cplugs" is already installed (scope: user)
==> Step 10: Verify installed_plugins.json reflects v1.11.5
    Pointer verified: spt@cplugs version=1.11.5
Deploy complete for v1.11.5.
```

Deploy succeeded end-to-end. CHANGELOG curation gate (Phase 34 D-08) closed via v1.11.5 entry committed as `8b011dd`. Marketplace HEAD `43d14c7` pushed to remote. installed_plugins.json patched atomically.

## Smoke Trace + Artifact Verification

**STATUS: deferred to user-side checkpoint verification (Task 5).** The plan's smoke trace
requires:

1. User runs `/reload-plugins` inside Claude Code (per CLAUDE.md DEPLOY contract).
2. Existing todlando Psyche wrapper self-migrates to the new v1.11.5 binary via the
   Phase 18.4/18.5 wrapper-state.json rehydration flow.
3. Next echo_commune fire (cadence-driven, or user-triggered via `/spt:commune`) produces
   BOTH `<live-context>` AND `<project-context>` envelopes in the haiku output.
4. `route_two_slice` (Plan 25 D-12) reads the haiku output, parses both envelopes, and writes
   `%LOCALAPPDATA%\spt\psyches\tracked\projects\claude_skill_owl\todlando.md`.

PowerShell verification commands (for the operator to run after `/reload-plugins`):

```powershell
# Smoke 1: Tail todlando.log and look for next [PSYCHE] resume (exit=0) line.
Get-Content "$env:LOCALAPPDATA\spt\logs_latest\todlando.log" -Tail 200 -Wait

# Smoke 2: Confirm projects/ directory now exists for claude_skill_owl.
Get-ChildItem "$env:LOCALAPPDATA\spt\psyches\tracked\projects\"

# Smoke 3 (HARD GATE): Confirm artifact materialized.
Test-Path "$env:LOCALAPPDATA\spt\psyches\tracked\projects\claude_skill_owl\todlando.md"
Get-Content "$env:LOCALAPPDATA\spt\psyches\tracked\projects\claude_skill_owl\todlando.md"

# Smoke 4: Encoding-contract smoke (Plan 03 verification).
# In the same log tail, confirm body content delivered to Psyche stdin shows DECODED state:
# NO `&lt;EVENT`, `&lt;/EVENT`, `&lt;project-context`, `&lt;/project-context`,
# `&lt;live-context`, `&lt;/live-context`, `&lt;br&gt;` substrings in body.
# (User-prose entities, if any, pass through.)
```

## Operator-Verify Response

(Pending — Task 5 checkpoint:human-verify gate=blocking.)

## Codex Review Resolution Table

| Cycle | Review Note | Closed By | Verified By |
|-------|-------------|-----------|-------------|
| Cycle-2 HIGH | Option A locked — no `name=` attr on `<project-context>` shape anywhere | psyche.md `<absorption>` rule 2 (no-name); rule 4 (no-name throughout); commune.md + signoff/SKILL.md preserve no-name | `psyche_md_contains_25_3_absorption_section` (no `<project-context name=` in psyche.md) + `skill_files_have_no_project_context_name_attr` (no `<project-context name=` in commune.md + signoff/SKILL.md) |
| Cycle-2 HIGH | "SPT routes; you emit" phrasing (vs ambiguous "Psyche writes the file") | psyche.md `<absorption>` rule 4 + commune.md + signoff/SKILL.md routing blocks | `psyche_md_contains_25_3_absorption_section` asserts "SPT routes" substring; manual review of skill-file routing block wording |
| Cycle-2 HIGH | depends_on: [25.3-01, 25.3-03] — plan reorder | Plan frontmatter `depends_on: [25.3-01, 25.3-03]`; execution wave 3 follows waves 1+2; Task 1 reads both SUMMARYs | This plan's execution order (after 25.3-01 + 25.3-03 SUMMARYs landed) |
| Cycle-2 MEDIUM | Static prompt-contract test scope — extend to commune.md + signoff/SKILL.md | `skill_files_have_no_project_context_name_attr` added in Task 2 step 4 | Test passes against all three files |
| Cycle-2 MEDIUM | Smoke verification rigor — `Test-Path` false MUST be reported as failure | Task 5 marked `checkpoint:human-verify gate="blocking"` (cycle 2 already correct); `<done>` block reinforced as HARD GATE | Task 5 checkpoint structure |
| Cycle-2 LOW | Task numbering gap (Task 0 → Task 2) | Tasks renumbered 1-5 sequentially | Plan file line numbering |
| Cycle-3 — | NO HIGHs or MEDIUMs targeted Plan 02 directly (Plan 03 + others took the fix burden) | (preservation-only pass) | n/a |
| Cycle-3 MEDIUM (preserved) | No-name guard extended to commune.md + signoff/SKILL.md | `skill_files_have_no_project_context_name_attr` test reads BOTH files and asserts on each | Test execution |
| Cycle-3 — | Plan 03 LOCKED Option A — body state at LLM-stdin = DECODED | psyche.md `<absorption>` rule 5 states this literally | `psyche_md_contains_25_3_absorption_section` asserts "DECODED" / "encoding contract" substring |
| Cycle-4 HIGH 4 | Test placement compliance check | Plan 02 tests use ONLY include_str! + std::fs::read_to_string — NO pub(crate) calls — so integration-crate placement is CORRECT | Manual code review of test file contents (no `crate::*` paths, no `pub(crate)` helper imports) |
| Cycle-4 — | All cycle-2/cycle-3 strengths preserved verbatim | Plan 02 was a preservation-only pass | (no deltas vs cycle-3 plan) |

## Task Commits

| Task | Description | Commit |
|------|-------------|--------|
| 1 | Read Plan 01 + Plan 03 SUMMARYs and extract handoff contracts (read-only) | (extraction recorded above) |
| 2 | psyche.md edits + 2 static prompt-contract tests | `6cd6b6e` |
| 3 | commune.md + signoff/SKILL.md alignment (no-name shape + 'SPT routes' phrasing + cross-ref) | `3e2ac81` |
| 4a | CHANGELOG v1.11.5 curation (Phase 34 D-08 gate closure) | `8b011dd` |
| 4b | Consolidated DEPLOY.ps1 -Bump patch (1.11.4 → 1.11.5) | `a24d271` (auto-commit) |
| 4c | Cargo.lock version sync | `48fc9f1` |

## Plan 04 Handoff Note

Inbound envelope absorption rules are in place; round-trip-via-LLM persistence is now
functional (Plan 02 closes the prompt-side gap; Plan 01 closes the resolver gap; Plan 03 closes
the encoding gap). The remaining defect — **Defect A: inbound commune file-drops bypass the
round-trip path entirely** — is Plan 04's scope.

Plan 04 adds:
- Wrapper-direct routing via `route_inbound_commune_body` consuming
  `crate::common::owlery::resolve_self_project_name_via_info_cwd(self_id)` (same shared helper
  as Plans 01+02+03 — no duplicated resolver logic).
- Explicit `RouteOutcome` enum (no false `project_routed` signaling).
- Precedence-metadata stale-overwrite mitigation via central marker-stripping read helper
  (cycle-4 HIGH 1: `read_context_body_stripped` strips `<!-- spt:source=... -->` before
  ANY prompt builder reads the file — psyche.md text is marker-agnostic by design).
- Signoff-path refactor (cycle-4 HIGH 3: `route_two_slice_signoff` consumes the shared helper
  + `RouteOutcome` + precedence guard, eliminating the divergent signoff resolver path).

Plan 02 does NOT need changes for Plan 04's cycle-4 HIGH 1 marker mitigation — the marker is
out-of-band metadata invisible to the LLM by Plan 04's design. Plan 02's absorption rules
continue to teach in terms of LITERAL `<live-context>` / `<project-context>` envelope tags.

## Self-Check: PASSED

Files verified to exist:
- `psyche.md` — line count 375, `<absorption>` section present at lines 326-345 (rules 1-6).
- `plugin/spt/skills/commune/commune.md` — routing block added + cross-reference to `<absorption>` rules.
- `plugin/spt/skills/signoff/SKILL.md` — routing block added + cross-reference to `<absorption>` rules.
- `tests/native_owl.rs` — 2 new tests at file-end (lines ~1014-1083).
- `CHANGELOG.md` — v1.11.5 curated entry present.
- `plugin/spt/.claude-plugin/plugin.json` — version 1.11.5.
- `Cargo.lock` — owl version 1.11.5.

Commits verified (via `git log --oneline -8`):
- `6cd6b6e` — feat(25.3-02): add psyche.md absorption rules + static prompt-contract tests
- `3e2ac81` — docs(25.3-02): align commune + signoff skill files with psyche.md absorption rules
- `8b011dd` — docs(25.3-02): fill v1.11.5 CHANGELOG entry
- `a24d271` — chore: bump spt plugin to v1.11.5 (deploy auto-commit)
- `48fc9f1` — chore: sync Cargo.lock owl version bump 1.11.4 -> 1.11.5

Static prompt-contract tests verified:
- `psyche_md_contains_25_3_absorption_section`: PASS
- `skill_files_have_no_project_context_name_attr`: PASS

Build witness: `cargo build --release` PASS during deploy (3 pre-existing dead-code warnings, none new).
Marketplace HEAD pushed: `43d14c7`.

**Awaiting Task 5 checkpoint:human-verify gate (blocking) — operator must `/reload-plugins`, observe next echo_commune fire on todlando, and confirm `projects/claude_skill_owl/todlando.md` materializes with non-empty body.**
