Files
pi-auto-reinject/TODO.md
T
grayhook 6c82594392 TODO: mark Phase 14 diag + pre-fix repro complete
Reflect completed checklist items for Phase 14 so the phase state matches
already-committed implementation and repro evidence.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 17:04:54 +07:00

284 lines
22 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 |
**Порядок:** фазы 0→13; внутри фазы — сверху вниз. Параллельно после фазы 0 можно вести 1 и 2; фазы 3 и 4 — независимы друг от друга.
---
### Фаза 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» либо найти другую причину
- [ ] **settings.requireRegistered** — добавить в `SkillReinjectSettings` поле `requireRegistered: boolean` (default `false`); update defaults + test/settings.test.ts; зачем: явный opt-out для сценариев «отключил через `pi config`» / `--no-skills`
- [ ] **kept.ts deferred filter** — выделить `filterSkillsNeedingReinjectByKept(tracked, kept)` без registered-фильтра; оставить старую `filterSkillsNeedingReinject` для immediate path; зачем: разделить две стадии фильтрации
- [ ] **reinject.ts plan defer**`planDeferredReinject` возвращает `tracked keptPresent` без registered; `enqueueDeferredReinjectFromCompact` использует его; зачем: §6.5.1 — план фиксируется по kept-window (locked at compaction), registered считается позже
- [ ] **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
- [ ] **reinject.ts reinjectNow loose** — те же правила loose fallback в `reinjectNow`: если `registered.has(name)` false, но filePath на диске и `!requireRegistered` → re-inject; warn в notify; зачем: `/skill-reinject now` работает после `--skill` без перезапуска
- [ ] **reinject.ts buildBlocks loose source**`buildReinjectBlocks` для loose-кейса использует `tracked.filePath` / `tracked.baseDir` напрямую (без registered lookup); зачем: skill реально читается с диска, даже если resourceLoader его не знает
- [ ] **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
- [ ] **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
- [ ] **README requireRegistered** — раздел «Skills via `--skill` and discovery paths»: как трекаются, что reinject работает для skill ещё на диске даже без повторного `--skill` при `--resume`, как включить strict через `requireRegistered: true`; зачем: §9.1, явная документация развилки
- [ ] **BACKLOG close B-002**`open``done` с датой и ссылкой на коммиты фазы 14; перенести в «Закрыто»; в отдельном коммите `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