2f666cab6a
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>
325 lines
29 KiB
Markdown
325 lines
29 KiB
Markdown
# 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 §5–6) |
|
||
|
||
---
|
||
|
||
## Решения (зафиксировано в 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 и полировка | 7–11 | §11 |
|
||
| 13 | Приёмка и документация | 0–12 | §12–13 |
|
||
| 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.4–6.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 п.4–5
|
||
- [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** — связать §7–8: 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
|