# lastTurnEntries — skip skill-body injections (sibling session find, 2026-05-30)

> **For agentic workers:** REQUIRED SUB-SKILL: `superpowers:test-driven-development`. RED test first, then fix, then GREEN, then full regression.

**Goal:** Fix `tools/enforce-hook-helpers.mjs::lastTurnEntries` so that harness-injected skill-body messages no longer become spurious turn boundaries — restoring correct behaviour of `enforce-memory-coverage` and `enforce-normative-content-rules::detectLegitSkillActive`.

**Discovery context:**

- Sibling Claude session inspected its own transcript JSONL and found: skill bodies are injected as `role: 'user'` messages with `isMeta: true`. They proposed: skip `isMeta: true` in the `lastTurnEntries` walk-back.
- This session verified the hypothesis on transcript `8f4ba767-f2fd-4b21-a0c0-fc049a552d25.jsonl` (29 `isMeta: true` entries) via `.scratch/debug-ismeta.mjs`. Result: `isMeta: true` appears on **multiple kinds** of harness injection, not just skill bodies:
  1. **Skill bodies** — HAS top-level `sourceToolUseID` (links back to Skill tool_use).
  2. **"Continue from where you left off."** auto-resume — NO `sourceToolUseID`.
  3. **Stop hook feedback** strings — NO `sourceToolUseID`.
  4. **`<local-command-caveat>`** wrappers — NO `sourceToolUseID`.

**Risk:** sibling's blanket `skip isMeta` would break turn boundaries for auto-resume and Stop hook feedback. Those are legitimately user-equivalent boundaries that should NOT be skipped.

**Refined fix:** skip only when BOTH `isMeta === true` AND `typeof sourceToolUseID === 'string'`. This precisely targets tool-spawned content (skill bodies, and potentially subagent return blocks if they share the same shape) while preserving all other `isMeta: true` paths.

**Why this fixes both guards:**

- **`enforce-memory-coverage`** finds the user's actual prompt (with its `coverage:` line) as the turn boundary instead of stopping at the injected skill body.
- **`enforce-normative-content-rules::detectLegitSkillActive`** sees the assistant message containing the Skill `tool_use` as part of the current turn (it sits between user prompt and skill body — currently outside the artificial boundary the skill body creates).

**Files:**

- Modify: `tools/enforce-hook-helpers.mjs` — `lastTurnEntries` body (1 added condition in the back-walk loop).
- Modify: `tools/enforce-hook-helpers.test.mjs` — add 3 new tests under the existing `lastTurnEntries / ...` describe block.

**Out of scope (NOT fixed by this commit):**

- `enforce-read-path-deny.mjs` LEGIT_SKILLS exemption gap (separate hook, no `lastTurnEntries` dependency).
- TDD-gate cross-actor blindness (different mechanism — actor session boundaries, not transcript turn detection).
- `detectFullTestRun` regex narrowness (command-pattern matching, unrelated).

---

## Tasks

### Task 1: RED tests for skill-body skip + negative tests for non-skill `isMeta`

**Files:**
- Modify: `tools/enforce-hook-helpers.test.mjs` — add 3 cases at end of `describe('lastTurnEntries / ...')` block.

- [ ] **Step 1:** Add a new `it()` block "lastTurnEntries skips skill body injections (isMeta + sourceToolUseID)" that constructs an entries array `[user-prompt, assistant+SkillToolUse, skillBody(isMeta=true, sourceToolUseID), assistant+follow-up]` and asserts `lastTurnEntries(entries)` returns starting from `user-prompt` (NOT from skill body).

- [ ] **Step 2:** Add `it()` block "lastTurnEntries does NOT skip Continue-from-where-you-left-off (isMeta but no sourceToolUseID)" that constructs `[old-user, old-assistant, continueMsg(isMeta=true, no sourceToolUseID), assistant-action]` and asserts the turn boundary is at `continueMsg` (preserves auto-resume as real boundary).

- [ ] **Step 3:** Add `it()` block "turnToolUses includes Skill tool_use spawned in same turn as injected skill body" — uses the Task 1 entries and asserts `turnToolUses` includes the Skill tool_use.

- [ ] **Step 4:** Run `node app/node_modules/vitest/vitest.mjs run --root ./app --config vitest.config.tools.mjs tools/enforce-hook-helpers.test.mjs 2>&1 | tail -10` and confirm Test 1 + Test 3 RED (Test 2 may already pass on current code since `Continue` has string content with .trim().length > 0).

### Task 2: Implement skill-body skip in lastTurnEntries

**Files:**
- Modify: `tools/enforce-hook-helpers.mjs` lines 100-115 (`lastTurnEntries` body).

- [ ] **Step 1:** In the back-walk loop, before checking `e.message.role === 'user'`, add: `if (e && e.isMeta === true && typeof e.sourceToolUseID === 'string') continue;` — this skips skill-body injections (isMeta + tool-spawned) while keeping all other `isMeta:true` cases as valid turn boundaries.

- [ ] **Step 2:** Run vitest again, confirm all 3 new tests GREEN and prior 4 tests in same describe block still GREEN.

- [ ] **Step 3:** Run `npm run test:tools` for full regression. Expected GREEN count baseline 1785 + 3 new tests = 1788. Any unrelated test breakage → STOP and investigate.

### Task 3: Commit

**Files:**
- Commit message in `.scratch/sibling-lastturn-fix-msg.txt`.

- [ ] **Step 1:** Pre-write approval records for:
  - `git add tools/enforce-hook-helpers.mjs tools/enforce-hook-helpers.test.mjs docs/superpowers/plans/2026-05-30-lastturnentries-skill-body-skip.md`
  - `git commit -F .scratch/sibling-lastturn-fix-msg.txt -- tools/enforce-hook-helpers.mjs tools/enforce-hook-helpers.test.mjs docs/superpowers/plans/2026-05-30-lastturnentries-skill-body-skip.md`

- [ ] **Step 2:** Commit, push.

- [ ] **Step 3:** Verify in live session — try a memory write with `coverage: direct:memory-sync` after a Skill invocation; expect normative-content-rules to pass.

---

## Self-review

**Spec coverage:** sibling proposal acknowledged + refined; risk analysis explicit; out-of-scope explicit.

**No placeholders:** every step is concrete with file paths + assertion shapes.

**Safety:** refined `isMeta + sourceToolUseID` discriminator preserves turn boundary for auto-resume / Stop hook feedback / local-command-caveat. The discriminator field is harness-controlled (not controller-writable from inside a tool call), so it cannot be spoofed by the controller as a fake "this is a skill body, please skip me" signal. Path-deny on `~/.claude/projects/` blocks any controller attempt to mutate the live transcript.

**Plan satisfies §17 bugfix classifier requirement** (plan file referenced before first prod-code edit).
