# 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 `` с `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)` по `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). - [ ] **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 подряд»; зачем: контракт до кода - [ ] **compaction.ts ensureSource** — `ensureCompactionSourceMarked(runtime)`: если не `manual` → `auto`; вызывать из `markAutoCompactionBeforeCompact` и экспортировать для `session_compact`; зачем: закрыть `lastCompactionSource: null` (B-003 факт #1) - [ ] **index.ts compact source fallback** — в `handleSessionCompact` до `consumeCompactionOnSessionCompact`: `ensureCompactionSourceMarked(compactionRuntime)`; зачем: safety net когда `session_before_compact` не пришёл - [ ] **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 - [ ] **kept.ts reinject custom** — `skillsPresentInKeptWindow` (или сосед) учитывает entries `type: custom_message`, `customType: skill-reinject:inject` с ` 0 && !ctx.isIdle()` → steer deliver; иначе оставить pending для `before_agent_start`; persist; зачем: единая развилка idle / mid-turn - [ ] **index.ts before_agent_start dedup** — `tryConsumeDeferredReinject` / wire: skip consume если steer уже доставил для `lastCompactionFirstKeptEntryId` / compaction entry id; зачем: turn-boundary compact + pi-auto-compact не дают двойной inject - [ ] **test/reinject-mid-turn.test.ts** — mock `pi.sendMessage`: mid-turn (`isIdle: false`) → steer вызван, pending очищен; idle → steer не вызывается; после steer `before_agent_start` не inject'ит повторно; зачем: §12.1 - [ ] **diag.ts mid-turn** — расширить snapshot: `compactionSource`, `sourceInferred`, `deliveryBranch: "before_agent_start" \| "steer"`, `isIdle`; фазы `session_compact` + опционально `mid_turn_deliver`; зачем: отладка без повторения lost-reinject - [ ] **scripts/b003-repro.mjs** — RPC/скрипт: симулировать или документировать repro по `lost-reinject.jsonl` (два compact, проверка inject count ≥ 1 на каждый compact с skill вне kept); зачем: регрессионный gate B-003 - [ ] **manual E2E B-003** — длинная сессия или уменьшенный `keepRecentTokens`; два auto compact подряд (в т.ч. mid-turn); после каждого — skill в контексте или `debug` показывает `skipped-kept`; `/skill-reinject` → `last compaction: auto`; зачем: критерий закрытия B-003 - [ ] **README mid-turn** — короткий подпункт: defer + mid-turn steer, coexistence с pi-auto-compact, что делать при `last compaction: none` (включить `debug`); зачем: §9.1 - [ ] **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