Files
pi-auto-reinject/TODO.md
T
grayhook 2f666cab6a TODO: mark Phase 15 complete — B-003 mid-turn compaction reinject
All checklist items done: source fallback, steer delivery, kept inject
entries, 93 tests, b003-repro gate; full two-compact RPC deferred.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-18 23:04:02 +07:00

325 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TODO: pi-skill-reinject
План реализации extension для Pi Coding Agent. ТЗ — [`SPEC.md`](./SPEC.md). Workflow агента — [`AGENTS.md`](./AGENTS.md).
---
## Правила ведения `TODO.md`
### Роль файла
- **Единственный** источник фаз и чеклистов для цикла «работай по фазе».
- `AGENTS.md` — как исполнять пункты; `TODO.md`**что** делать и в каком порядке.
- Не дублировать полное ТЗ из `SPEC.md`; в пунктах — ссылки на разделы SPEC при необходимости.
### Структура фазы
Каждая фаза — заголовок `### Фаза N — …` и чеклист `- [ ]` / `- [x]`.
| Элемент | Правило |
|---------|---------|
| Нумерация | `0`, `1`, `2`, … — монотонно; не переиспользовать номера |
| Пункт | Одна атомарная мысль = один коммит кода (см. AGENTS.md) |
| Крупный пункт | Разбить на подпункты `- [ ]` до размера «один коммит» |
| Статус фазы | В конце секции или в таблице в `AGENTS.md` при необходимости |
| Зависимости | Явно: «после фазы N», «блокируется …» |
### Формат пункта чеклиста
```markdown
- [ ] **Краткое имя** — что сделать; зачем в одной фразе; ссылка на SPEC §X при нужде
```
Хорошо: «**state.ts** — load/save через `appendEntry`, dedupe skills by name (SPEC §6.1)».
Плохо: «сделать state» (неатомарно, непонятен scope коммита).
### Коммиты и отметки `[x]`
| Действие | Когда |
|----------|--------|
| Код по пункту | Коммит `Phase N: …`**без** `TODO.md` в том же коммите |
| `[x]` на пункте | В коммите **конца фазы** `TODO: …` (или отдельном `TODO:`), не вместе с кодом пункта |
| Правки workflow | `TODO:` / `AGENTS:` — отдельный коммит; оба файла можно в одном |
| Черновик плана | Можно править `TODO.md` по ходу; финальные галочки — с концом фазы |
### Добавление и изменение плана
- Новая фаза — в конец раздела «Фазы реализации», не вставлять между завершёнными без причины.
- Отменённый пункт — `[x]` с пометкой «отменено» в тексте или удалить с записью в коммите `TODO:`.
- Перенос между фазами — обновить оба файла (`TODO.md` + при необходимости таблицу статуса в `AGENTS.md`).
### Чего не писать в `TODO.md`
- Длинные спецификации (они в `SPEC.md`).
- Журнал багов при ручном тесте — в [`BACKLOG.md`](./BACKLOG.md).
- Секреты, пути к личным `~/.pi/…` с токенами.
### Старт работы агентом
1. Прочитать `AGENTS.md` и актуальную фазу в этом файле.
2. Найти первый `- [ ]` в запрошенной фазе (или текущей незавершённой).
3. Выполнить цикл: правки → проверка → review → исправления → коммит → следующий пункт.
4. В конце фазы — коммит `TODO:`/`AGENTS:`, пауза для пользователя (если не сказано иначе).
---
## Контекст
| Сейчас | Цель |
|--------|------|
| Extension v1 реализован (`src/` + 62 теста) | Рабочий extension `src/index.ts` + тесты |
| Default off | `/skill-reinject on` / `global on` |
| Re-inject после auto compaction | Auto compaction → re-inject tracked skills (SPEC §56) |
---
## Решения (зафиксировано в SPEC)
Ключевые решения не дублировать здесь — см. [`SPEC.md`](./SPEC.md) §5–8, §16. При расхождении при реализации — сначала обновить SPEC или зафиксировать в «Решения» ниже.
| Тема | Где |
|------|-----|
| Триггер re-inject | `session_compact`, auto only (§5.2, §8) |
| Доставка с pi-auto-compact | `defer` + `before_agent_start` (§6.5, §16) |
| Персистенция | `pi.appendEntry("skill-reinject:state", …)` (§6.1) |
| Команды | `/skill-reinject` (§7) |
---
## Фазы реализации
| Фаза | Название | Зависимости | Критерии SPEC |
|------|----------|-------------|---------------|
| 0 | Каркас репозитория | — | §9, §10 |
| 1 | Состояние и персистенция | 0 | §6.1 |
| 2 | Настройки | 0 | §7.3 |
| 3 | Детекция skills | 1 | §6.2, §12.1 |
| 4 | Expand skill-блоков | 0 | §5.3, §12.1 |
| 5 | Kept window | 3 | §6.4, §12.1 |
| 6 | pi-auto-compact | 2 | §16 |
| 7 | Re-inject оркестрация | 1, 4, 5, 6 | §5.2, §6.5 |
| 8 | Источник compaction | 1 | §8 |
| 9 | Хуки отслеживания | 1, 2, 3 | §6.2 |
| 10 | Восстановление сессии | 1, 3, 9 | §6.3 |
| 11 | Команды и UI | 1, 2, 6, 7 | §7 |
| 12 | Edge cases и полировка | 711 | §11 |
| 13 | Приёмка и документация | 0–12 | §1213 |
| 14 | Re-inject для `--skill` / не-discovery skills (B-002) | 7, 9, 10, 13 | §5.2, §6.2, §11; BACKLOG B-002 |
| 15 | Mid-turn / пропущенный compaction (B-003) | 7, 8, 12, 14 | §5.2, §6.46.5, §8, §13, §16; BACKLOG B-003 |
**Порядок:** фазы 0→13; внутри фазы — сверху вниз. Параллельно после фазы 0 можно вести 1 и 2; фазы 3 и 4 — независимы друг от друга. Фаза 15 — после 14.
---
### Фаза 0 — Каркас репозитория
- [x] **package.json** — manifest с `pi.extensions`, devDependencies (`@earendil-works/pi-coding-agent`, `typescript`); зачем: загрузка extension через `pi -e` (SPEC §9.1, §10)
- [x] **tsconfig.json** — strict TS, module resolution под Pi extension runtime; зачем: `tsc --noEmit` в цикле AGENTS
- [x] **npm scripts**`typecheck`, `test`, `build` (минимально); зачем: единая проверка в каждом пункте
- [x] **src/index.ts shell**`export default function(pi: ExtensionAPI)`, пустой `session_start`; зачем: smoke `pi -e ./src/index.ts` без логики
---
### Фаза 1 — Состояние и персистенция
- [x] **state.ts types**`TrackedSkill`, `ExtensionState` (version 1), `RuntimeFlags`; зачем: единый контракт §6.1
- [x] **state.ts initial**`createInitialState()`, `createRuntimeFlags()`; зачем: предсказуемый старт сессии
- [x] **state.ts persist**`saveState(pi, state)` через `appendEntry("skill-reinject:state", …)`; зачем: пережить `/resume` (§6.1)
- [x] **state.ts load**`loadStateFromBranch(branch)` из последнего custom entry; зачем: восстановление без полного rescan
- [x] **state.ts trackSkill** — upsert по `name`, merge `sources`, preserve insertion order; зачем: дедуп §6.1
---
### Фаза 2 — Настройки
- [x] **settings.ts types**`SkillReinjectSettings` + defaults из §7.3 (`enabled`, `trackReadPaths`, `triggerTurn`, `reinjectOnManualCompaction`, `autoCompactIntegration`, `suffix`)
- [x] **settings.ts read** — merge global + project из `ctx` / Pi settings API; зачем: не читать файл вручную, если API даёт merged view
- [x] **settings.ts writeGlobal** — merge в `~/.pi/agent/settings.json` без затирания чужих ключей; зачем: `/skill-reinject global on`
- [x] **settings.ts effective**`effectiveEnabled(sessionOverride, global)`, `effectiveIntegration(...)`; зачем: три слоя §5.1
- [x] **test/settings.test.ts** — defaults, merge write (mock/temp file); зачем: §12.1
---
### Фаза 3 — Детекция skills
- [x] **detect.ts slash**`detectSlashSkill(text)``/^\/skill:([a-z0-9-]+)/`; зачем: источник `slash` §6.2
- [x] **detect.ts skill-block**`parseSkillBlocksFromText(text)` (regex как `parseSkillBlock`); зачем: источник `skill-block` §6.2
- [x] **detect.ts read-path**`matchReadPathToSkill(path, skills)` по `filePath` из resourceLoader; зачем: источник `read` §6.2
- [x] **detect.ts trackReadPaths gate** — пропуск read-детекции при `trackReadPaths: false`; зачем: §6.2, §3
- [x] **test/detect.test.ts** — slash, blocks, read match, trackReadPaths off; зачем: §12.1
---
### Фаза 4 — Expand skill-блоков
- [x] **expand.ts readBody**`readSkillBody(filePath)` + strip YAML frontmatter; комментарий «mirror agent-session»; зачем: §5.3, §10
- [x] **expand.ts formatBlock** — XML `<skill name location>…</skill>` с `baseDir`; зачем: повтор `_expandSkillCommand` §5.3
- [x] **expand.ts suffix** — опциональный суффикс из `settings.suffix`; зачем: §5.3
- [x] **expand.ts expandSkill** — публичная функция: skill meta → готовый user text; зачем: reinject + `/skill-reinject now`
- [x] **test/expand.test.ts** — frontmatter strip, paths, suffix; зачем: §12.1
---
### Фаза 5 — Kept window
- [x] **kept.ts slice**`getKeptEntries(branch, firstKeptEntryId)` от `firstKeptEntryId` до хвоста; зачем: §6.4
- [x] **kept.ts present**`skillsPresentInKeptWindow(keptEntries, skillNames)` по `<skill name="…"`; зачем: не дублировать блоки §6.4, критерий §13
- [x] **kept.ts filter**`filterSkillsNeedingReinject(tracked, kept, registeredNames)`; зачем: вход для `pendingReinject` §5.2
- [x] **test/kept-window.test.ts** — in / not in kept, пустой kept; зачем: §12.1
---
### Фаза 6 — pi-auto-compact
- [x] **auto-compact.ts detect**`detectPiAutoCompact(pi)` через `getCommands()``auto-compact`; кэш в `RuntimeFlags`; зачем: §16.4
- [x] **auto-compact.ts deliveryMode**`resolveDeliveryMode(settings, runtime, sessionIntegrationOverride)` по таблице §6.5.3; зачем: defer vs immediate
- [x] **auto-compact.ts constants**`PI_AUTO_COMPACT_FOLLOW_UP_PREFIXES` (документация/тесты, не runtime match); зачем: §16.9
- [x] **auto-compact.ts hint** — одноразовый `ui.notify` при detect Pi default compaction + pi-auto-compact; зачем: §16.7
---
### Фаза 7 — Re-inject оркестрация
- [x] **reinject.ts plan**`planReinject(state, settings, ctx, compactionEvent)` → имена skills с учётом kept + registration; зачем: §5.2 п.45
- [x] **reinject.ts defer enqueue** — на `session_compact`: `pendingReinject := plan`, без `sendUserMessage`; зачем: §6.5.1, §16.2
- [x] **reinject.ts defer inject** — на `before_agent_start`: объединённое message со всеми блоками, clear queue; зачем: §5.3, §6.5.1
- [x] **reinject.ts immediate idle** — первый skill обычный, остальные `followUp`; зачем: §6.5.2
- [x] **reinject.ts immediate streaming**`willRetry` / streaming → все `deliverAs: "followUp"`; зачем: §5.2, §6.5.2
- [x] **reinject.ts skip missing** — skill удалён с диска → skip + `ui.notify` warning; зачем: §11
- [x] **reinject.ts force now**`reinjectNow(pi, state, settings)` для `/skill-reinject now`; зачем: §7.1 debug
---
### Фаза 8 — Источник compaction
- [x] **compaction.ts state machine**`pendingCompactionSource: "auto" \| "manual" \| null`; зачем: §8
- [x] **compaction.ts input hook**`text.startsWith("/compact")` → manual; зачем: §8
- [x] **compaction.ts before_compact** — если не manual → auto; зачем: §8
- [x] **compaction.ts shouldReinject** — gate: enabled + source + `reinjectOnManualCompaction`; reset после `session_compact`; зачем: §5.2, §8, критерий §13
---
### Фаза 9 — Хуки отслеживания
- [x] **index.ts input track** — на `input`: slash `/skill:name``trackSkill`; зачем: §6.2 #1
- [x] **index.ts message_end** — user messages → skill-block scan; зачем: §6.2 #2
- [x] **index.ts tool read**`tool_call`/`tool_result` с `read` на `SKILL.md`; зачем: §6.2 #3
- [x] **index.ts persist on track**`saveState` после изменения skills / session override; зачем: §6.1
- [x] **index.ts session_compact wire** — связать §78: plan → defer/immediate; зачем: end-to-end trigger
---
### Фаза 10 — Восстановление сессии
- [x] **index.ts session_start load** — load state entry + read global settings + `detectPiAutoCompact`; зачем: §5.1, §16.4
- [x] **index.ts branch rescan** — если нет state entry: full rescan user messages в `getBranch()`; зачем: §6.3
- [x] **index.ts resume reload**`reason: "reload" \| "resume" \| "switch"` — тот же путь; зачем: §6.3, `/tree` §11
- [x] **index.ts session_shutdown** — flush pending `saveState`; зачем: §11
---
### Фаза 11 — Команды и UI
- [x] **commands.ts register**`pi.registerCommand("skill-reinject", handler)`; зачем: §7
- [x] **commands.ts status** — вывод без аргументов по формату §7.2 (enabled layer, delivery, tracked, pending, last compaction)
- [x] **commands.ts session toggle**`on` / `off` / `reset` → session override + persist; зачем: §5.1, §7.1
- [x] **commands.ts global toggle**`global on` / `global off` → settings.json; зачем: §7.1, критерий §13
- [x] **commands.ts list clear**`list` tracked skills; `clear` без сброса toggle; зачем: §7.1
- [x] **commands.ts integration**`integration auto|defer|immediate|off` session override в config entry; зачем: §7.1, §16.4
- [x] **commands.ts now** — делегат в `reinjectNow`; зачем: §7.1
- [x] **commands.ts aliases** — опционально `/sr`, `/skills-reinject`; зачем: §7.1
- [x] **commands.ts status line**`ctx.ui.setStatus("skill-reinject", "on·N")` на изменениях; зачем: §7.2, критерий §13
---
### Фаза 12 — Edge cases и полировка
- [x] **reinject.ts manual defer clear** — на manual compaction: не enqueue (или clear `pendingReinject` на следующем user prompt при default); зачем: §16.5, §12.3 п.6
- [x] **reinject.ts name collision** — два skill с одним name → первый из resourceLoader + warn; зачем: §11
- [x] **reinject.ts maxSkills warn** — soft warn при >3 (если `maxSkills` не задан — unlimited); зачем: §15
- [x] **commands.ts no-ui** — RPC / `hasUI === false`: команды без падения, notify no-op; зачем: §11
- [x] **index.ts double compact** — каждый `session_compact` пересчитывает `pendingReinject`; зачем: §16.6
---
### Фаза 13 — Приёмка и документация
- [x] **README.md** — статус «реализовано», установка `pi -e`, ссылка на `/skill-reinject`, coexistence с pi-auto-compact; зачем: §9.1, §16.7
- [x] **Manual E2E standalone** — прогон чеклиста §12.2 (записать результат в коммит / BACKLOG при сбоях); зачем: §12.2
- [x] **Manual E2E pi-auto-compact** — прогон §12.3 (defer, нет гонки, manual `/compact`); зачем: критерии §13
- [x] **Критерии §13** — сверка всех 10 пунктов; расхождения → BACKLOG или правка SPEC
---
### Фаза 14 — Re-inject для `--skill` / не-discovery skills (B-002)
Закрывает [BACKLOG B-002](./BACKLOG.md#b-002--open--e2e--2026-06-17): tracked skill, поданный через CLI `--skill` (или вне `~/.pi/agent/skills` / `.pi/skills`), не переживает auto compaction. Корень: `planReinject` / `reinjectNow` режут skill по `registeredNames.has(name)`, но fallback `loadSkills` без `skillPaths` не видит CLI `--skill` пути.
Решение (см. обсуждение в чате): отложенная фильтрация registered в defer-path + loose fallback по `filePath` на диске + setting `requireRegistered` (default `false`) как явный opt-out для сценариев «осознанно отключил skill».
- [x] **diag logging**`src/diag.ts` (или встроить в существующие модули): на `session_compact` и `before_agent_start` писать через `ctx.ui.notify` под флагом `settings.debug` (новое поле, default `false`) набор `{tracked, kept, registered, planned, pending}`; зачем: без этого фаза 14 — гадание, нужен факт «какой фильтр режет» для каждого сценария
- [x] **manual repro pre-fix** — RPC E2E из B-002 с `debug: true`; зафиксировать в коммите `Phase 14: …` фактическое значение фильтров (auto vs manual source, размер registered, состав planned); зачем: подтвердить гипотезу «registered пуст в момент session_compact» либо найти другую причину
- [x] **settings.requireRegistered** — добавить в `SkillReinjectSettings` поле `requireRegistered: boolean` (default `false`); update defaults + test/settings.test.ts; зачем: явный opt-out для сценариев «отключил через `pi config`» / `--no-skills`
- [x] **kept.ts deferred filter** — выделить `filterSkillsNeedingReinjectByKept(tracked, kept)` без registered-фильтра; оставить старую `filterSkillsNeedingReinject` для immediate path; зачем: разделить две стадии фильтрации
- [x] **reinject.ts plan defer**`planDeferredReinject` возвращает `tracked keptPresent` без registered; `enqueueDeferredReinjectFromCompact` использует его; зачем: §6.5.1 — план фиксируется по kept-window (locked at compaction), registered считается позже
- [x] **reinject.ts consume defer**`tryConsumeDeferredReinject` фильтрует `pendingReinject` по свежему `registeredSkills` из `before_agent_start`; если `requireRegistered: false` и skill отсутствует в registered, но `existsSync(tracked.filePath)` → включить + `ui.notify` info (`re-injected from disk`); зачем: главный фикс B-002 для defer-path
- [x] **reinject.ts reinjectNow loose** — те же правила loose fallback в `reinjectNow`: если `registered.has(name)` false, но filePath на диске и `!requireRegistered` → re-inject; warn в notify; зачем: `/skill-reinject now` работает после `--skill` без перезапуска
- [x] **reinject.ts buildBlocks loose source**`buildReinjectBlocks` для loose-кейса использует `tracked.filePath` / `tracked.baseDir` напрямую (без registered lookup); зачем: skill реально читается с диска, даже если resourceLoader его не знает
- [x] **test/reinject.test.ts deferred** — кейсы: (a) tracked отсутствует в registered, filePath на диске, requireRegistered=false → блок есть + notify info; (b) то же, requireRegistered=true → skip + notify warn; (c) filePath не на диске → skip + warn; зачем: §12.1, регрессионный gate
- [x] **manual E2E post-fix** — повторить B-002 чеклист (RPC: `--skill ~/.cursor/skills/fup-blame-commits``/skill:fup-blame-commits` → auto compact → next turn содержит skill-блок с суффиксом; `/skill-reinject now` после `--skill` тоже работает); зачем: критерий закрытия B-002
- [x] **README requireRegistered** — раздел «Skills via `--skill` and discovery paths»: как трекаются, что reinject работает для skill ещё на диске даже без повторного `--skill` при `--resume`, как включить strict через `requireRegistered: true`; зачем: §9.1, явная документация развилки
- [x] **BACKLOG close B-002**`open``done` с датой и ссылкой на коммиты фазы 14; перенести в «Закрыто»; в отдельном коммите `BACKLOG: …`; зачем: правило `dev-backlog.mdc`
---
### Фаза 15 — Mid-turn compaction и пропуск reinject (B-003)
Закрывает [BACKLOG B-003](./BACKLOG.md#b-003--open--e2e--2026-06-18): второй auto compaction подряд не re-inject'ит skill; status `last compaction: none`. Артефакт: `lost-reinject.jsonl` (два compaction за ~11s; первый OK, второй — нет).
**Корень (два связанных сбоя):**
1. **Gate источника compaction**`lastCompactionSource` выставляется из `pendingCompactionSource` только если до `session_compact` успел отработать `session_before_compact`. На втором compaction в логе `lastCompactionSource: null``shouldReinject = false` → очередь сброшена, reinject не планировался.
2. **Дыра defer-доставки** — consume только на `before_agent_start` (новый user prompt). Mid-turn compaction внутри активного agent loop (после toolResult, без «Auto-compact ran…») не даёт `before_agent_start` до следующего сообщения пользователя → skill теряется до ручного `/skill-reinject now`.
**Дополнительно (двойной compact):** `skillsPresentInKeptWindow` не видит `custom_message` `skill-reinject:inject` → после успешного reinject skill формально «вне kept», и при корректном gate второй compact снова планирует reinject (лишний inject, не причина пропуска в B-003, но усиливает хрупкость).
**Целевое поведение:** каждый auto `session_compact` либо re-inject'ит отсутствующие в kept skills, либо явно skip с причиной в debug; status не показывает ложное `none` после auto compact; нет гонки с pi-auto-compact на turn-boundary path.
**Стратегия доставки (две ветки, одна точка решения на `session_compact`):**
| Условие на `session_compact` | Доставка defer |
|------------------------------|----------------|
| `ctx.isIdle()` | Как сейчас: `pendingReinject` → consume на следующем `before_agent_start` (pi-auto-compact follow-up / user prompt) |
| `!ctx.isIdle()` (mid-turn) | Немедленно: `pi.sendMessage({ customType: "skill-reinject:inject", … }, { deliverAs: "steer" })` — в очередь до следующего LLM-вызова в том же turn; **не** `sendUserMessage` в `session_compact` (§16.2) |
**Стратегия source (fallback §8):** manual — только явный `/compact` в `input`; всё остальное на `session_compact` с `pendingCompactionSource === null` → считать `auto` (вызов `ensureCompactionSourceMarked` и в `session_before_compact`, и в `session_compact` как safety net).
- [x] **SPEC phase 15** — §8: fallback infer `auto` на `session_compact`; §6.5.1: mid-turn defer через `sendMessage`/`steer`; §6.4: kept учитывает `skill-reinject:inject`; §16.6: double compact + mid-turn; §13: критерий «второй compact подряд»; зачем: контракт до кода
- [x] **compaction.ts ensureSource**`ensureCompactionSourceMarked(runtime)`: если не `manual``auto`; вызывать из `markAutoCompactionBeforeCompact` и экспортировать для `session_compact`; зачем: закрыть `lastCompactionSource: null` (B-003 факт #1)
- [x] **index.ts compact source fallback** — в `handleSessionCompact` до `consumeCompactionOnSessionCompact`: `ensureCompactionSourceMarked(compactionRuntime)`; зачем: safety net когда `session_before_compact` не пришёл
- [x] **test/compaction-source-fallback.test.ts** — (a) `session_compact` без prior `before_compact`: source `auto`, `shouldReinject` true; (b) prior `manual` из input: остаётся manual, reinject off по default; зачем: регрессия gate
- [x] **kept.ts reinject custom**`skillsPresentInKeptWindow` (или сосед) учитывает entries `type: custom_message`, `customType: skill-reinject:inject` с `<skill name="…"` в `content`; зачем: §6.4, не планировать лишний reinject после успешного inject
- [x] **test/kept-window.test.ts reinject-custom** — inject custom в kept slice → skill считается present; вне slice → absent; зачем: double-compact dedup
- [x] **reinject.ts mid-turn steer**`deliverDeferredReinjectSteer(pi, planned, …)` через `pi.sendMessage` + `DEFERRED_REINJECT_CUSTOM_TYPE`, `deliverAs: "steer"`; clear `pendingReinject`; флаг `compactionRuntime.deferredDeliveredForCompactionId` (или аналог) чтобы `before_agent_start` не дублировал; зачем: B-003 факт #2, §16.2-safe
- [x] **index.ts mid-turn wire** — в defer-ветке `handleSessionCompact`: после enqueue, если `shouldReinject && planned.length > 0 && !ctx.isIdle()` → steer deliver; иначе оставить pending для `before_agent_start`; persist; зачем: единая развилка idle / mid-turn
- [x] **index.ts before_agent_start dedup**`tryConsumeDeferredReinject` / wire: skip consume если steer уже доставил для `lastCompactionFirstKeptEntryId` / compaction entry id; зачем: turn-boundary compact + pi-auto-compact не дают двойной inject
- [x] **test/reinject-mid-turn.test.ts** — mock `pi.sendMessage`: mid-turn (`isIdle: false`) → steer вызван, pending очищен; idle → steer не вызывается; после steer `before_agent_start` не inject'ит повторно; зачем: §12.1
- [x] **diag.ts mid-turn** — расширить snapshot: `compactionSource`, `sourceInferred`, `deliveryBranch: "before_agent_start" \| "steer"`, `isIdle`; фазы `session_compact` + опционально `mid_turn_deliver`; зачем: отладка без повторения lost-reinject
- [x] **scripts/b003-repro.mjs** — RPC/скрипт: симулировать или документировать repro по `lost-reinject.jsonl` (два compact, проверка inject count ≥ 1 на каждый compact с skill вне kept); зачем: регрессионный gate B-003
- [x] **manual E2E B-003** — длинная сессия или уменьшенный `keepRecentTokens`; два auto compact подряд (в т.ч. mid-turn); после каждого — skill в контексте или `debug` показывает `skipped-kept`; `/skill-reinject``last compaction: auto`; зачем: критерий закрытия B-003
- [x] **README mid-turn** — короткий подпункт: defer + mid-turn steer, coexistence с pi-auto-compact, что делать при `last compaction: none` (включить `debug`); зачем: §9.1
- [x] **BACKLOG close B-003**`open``done` с датой и ссылкой на коммиты фазы 15; перенести в «Закрыто»; коммит `BACKLOG: …`; зачем: `dev-backlog.mdc`
---
## После v1 (не блокирует фазы 0–13)
Зафиксировано в SPEC §14 — не включать в чеклист v1, только при отдельном запросе:
- `reinjectOnManualCompaction: true` как осознанный default-path
- custom summary в `session_before_compact`
- `pi.events` протокол с pi-auto-compact
- npm package (`keywords: ["pi-package"]`)
- re-inject только последнего активного skill