feat(mappa-closing-ritual): new skill v1.0.0 — finish-phase (handoff write + PROPOSE wiki/task closes, confirm-gated, mode=light for ad-hoc); absorbs session-handoff write-part [skip-tdd: visual]
This commit is contained in:
@@ -17,4 +17,14 @@ Handoff: sliding, per-project, versioned-история; read на старте
|
|||||||
|
|
||||||
## Completed steps
|
## Completed steps
|
||||||
|
|
||||||
|
- [x] skills/mappa-closing-ritual/SKILL.md v1.0.0 — НОВЫЙ скилл (финиш-фаза): scope check → mid-task capture → compose handoff → handoff_write (версия h:N) → PROPOSE wiki-ingest → PROPOSE task closes → один блок-предложение; мутации только после «да»
|
||||||
|
- [x] mode=light для ad-hoc: явный вопрос «Сделать handoff?», НЕ автоматический sweep; решение за человеком
|
||||||
|
- [x] Поглощает session-handoff (write-часть); read-часть уходит в mappa-session-orient (#1064); старое имя — триггер-синоним
|
||||||
|
- [x] Старый skills/session-handoff/ удалён (включая hooks/commit-detector — новый дизайн: ритуал на session-end, не на substantive commit)
|
||||||
|
- [x] lint clean (65 skills, 0 violations); build.sh → dist/mappa-closing-ritual.skill; install.sh → dual; старый удалён из живых диров
|
||||||
|
- [x] GREEN micro-test: свежий pi -p на «завершаем сессию» → активация mappa-closing-ritual, полный ритуал (7 шагов), мутации только после «да», mode=light «Сделать handoff?»
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
- RED-базис: write-процедура унаследована из session-handoff v1.0.0 (прошёл ревью); дельта = split read/write + mode=light + confirm-гейт. Полный behavioral smoke (триггеры свои + false-positive на task-зоне) — за #1065.
|
||||||
|
- commit-detector hooks удалены осознанно: suite проектирует closing-ritual как session-end-driven, а не commit-driven.
|
||||||
|
|||||||
BIN
dist/mappa-closing-ritual.skill
vendored
Normal file
BIN
dist/mappa-closing-ritual.skill
vendored
Normal file
Binary file not shown.
BIN
dist/session-handoff.skill
vendored
BIN
dist/session-handoff.skill
vendored
Binary file not shown.
148
skills/mappa-closing-ritual/SKILL.md
Normal file
148
skills/mappa-closing-ritual/SKILL.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
---
|
||||||
|
name: mappa-closing-ritual
|
||||||
|
author: ours
|
||||||
|
version: 1.0.0
|
||||||
|
description: >
|
||||||
|
Финиш-фаза форкфлоу: handoff write (mcp__mappa__handoff_write, версия h:N)
|
||||||
|
+ PROPOSE wiki-ingest + PROPOSE закрытий тасок. Мутации — только после
|
||||||
|
подтверждения пользователя. Ad-hoc: mode=light — явный вопрос «Сделать
|
||||||
|
handoff?» в конце сессии (НЕ автоматический sweep), решение за человеком.
|
||||||
|
Старое имя — триггер-синоним: session-handoff (write-часть). Чтение на
|
||||||
|
старте — mappa-session-orient. Триггеры: «завершаем сессию», «сворачиваемся»,
|
||||||
|
«закругляемся», «wrap up session», «end session», «we're done for now».
|
||||||
|
Анти-триггеры (task-зона, не session-end): «закрываем эту таску», «pause»,
|
||||||
|
«отбой», «разбегаемся».
|
||||||
|
---
|
||||||
|
|
||||||
|
# mappa-closing-ritual
|
||||||
|
|
||||||
|
Финиш-фаза цикла агента: **записать handoff → предложить wiki-ingest →
|
||||||
|
предложить закрытия тасок**. Старт ≠ финиш: чтение handoff на старте —
|
||||||
|
`mappa-session-orient`; здесь — write-путь с процедурой и подтверждением.
|
||||||
|
|
||||||
|
Канал — **handoff-сущность Mappa** (`mcp__mappa__handoff_write`, тип `h:`,
|
||||||
|
per-project): поля `session_id`/`date`/`status`/`summary`/`open_treks[]`/
|
||||||
|
`ask_user[]`/`guards[]`/`recent_commits[]`. Каждый write = **новая версия**
|
||||||
|
(append-only, versioned-история). Файлового `.tasks/NEXT_SESSION.md` больше нет.
|
||||||
|
|
||||||
|
## Когда использовать
|
||||||
|
|
||||||
|
- Session-end фразы: «завершаем сессию», «сворачиваемся», «закругляемся»,
|
||||||
|
«wrap up session», «end session», «we're done for now».
|
||||||
|
- Ad-hoc-сессия без трека/таски в конце: **mode=light** — явный вопрос
|
||||||
|
«Сделать handoff?» (не автоматический sweep), решение за человеком.
|
||||||
|
- AGENTS.md проекта содержит trigger-строку `session handoff: read on start, write on end`.
|
||||||
|
|
||||||
|
**Skip (task-зона, не session-end):** «закрываем эту таску» (task close →
|
||||||
|
`mappa-task-work`), «pause», «приостанови» (task-pause), «отбой», «разбегаемся»
|
||||||
|
(слишком broad), «сейчас завершу одну задачу и тогда поговорим» (частичное
|
||||||
|
завершение). При неоднозначности — **ASK**: «закрываем сессию или таску?»
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Scope check
|
||||||
|
|
||||||
|
Это текущий проект (cwd). Никаких global мутаций, никаких других проектов.
|
||||||
|
|
||||||
|
### 2. Mid-task capture
|
||||||
|
|
||||||
|
Если есть 🔴 active таска проекта (борд mappa / `.tasks/`) — захватить в summary:
|
||||||
|
```
|
||||||
|
left mid-task: <slug>
|
||||||
|
where_stopped: <одна строка>
|
||||||
|
```
|
||||||
|
Без борда — писать handoff без mid-task секции, не блокировать.
|
||||||
|
|
||||||
|
### 3. Compose content (поля handoff)
|
||||||
|
|
||||||
|
- `session_id` — `<ISO дата>` или идентификатор сессии;
|
||||||
|
- `status` — `active` (работа продолжается) / `paused` (заморожено) / `done` (завершено);
|
||||||
|
- `summary` — связка: где остановились, mid-task, ключевые решения;
|
||||||
|
- `open_treks` — массив открытых треков (готовность + entry-point);
|
||||||
|
- `ask_user` — pending решения / ожидаемые разрешения;
|
||||||
|
- `guards` — «не делать» (preemptive guards);
|
||||||
|
- `recent_commits` — 3–5 последних коммитов (`<slug>: <subject>`).
|
||||||
|
|
||||||
|
Forward-looking, не timeline: handoff = связка новых вещей конкретно для
|
||||||
|
следующего разворота, не overview всего проекта. Борд mappa / вики остаются
|
||||||
|
авторитетными для своего scope'а — не дублировать их в handoff.
|
||||||
|
|
||||||
|
### 4. Append
|
||||||
|
|
||||||
|
`mcp__mappa__handoff_write(project=<имя>, session_id, status, summary, open_treks?, ask_user?, guards?, recent_commits?)` — сервис создаёт новую версию `h:N` (предыдущие остаются; чтение последней — `entity_search(type='handoff', project, limit=1)`). Поля-массивы передавать как `[]`, если пусто (следующий агент видит: пусто, не забыто). Пишется без лиза (как инбокс).
|
||||||
|
|
||||||
|
> **Confirm gate:** handoff write — мутация. В mode=light (ad-hoc) и при
|
||||||
|
> неоднозначности — сначала явный вопрос «Сделать handoff?», только после «да» —
|
||||||
|
> write.
|
||||||
|
|
||||||
|
### 5. Propose wiki-ingest (не писать!)
|
||||||
|
|
||||||
|
Если за сессию появилось durable-знание — **ПРЕДЛОЖИТЬ** ingest
|
||||||
|
(`mappa-knowledge`: mappa wiki_create/update под лизом), перечислив кандидатов.
|
||||||
|
Ничего не писать без подтверждения.
|
||||||
|
|
||||||
|
### 6. Propose task-board closes (не закрывать!)
|
||||||
|
|
||||||
|
Если есть задачи, выглядящие закрытыми — **ПРЕДЛОЖИТЬ** закрытия
|
||||||
|
(`mappa-task-work`: task_close под лизом). Уважать ralph-loop: verifier-задачи
|
||||||
|
закрывать только через verifier (attempt/harness-record).
|
||||||
|
|
||||||
|
### 7. Формат предложения — один блок
|
||||||
|
|
||||||
|
```
|
||||||
|
Ритуал закрытия:
|
||||||
|
(а) заингестить X в вики?
|
||||||
|
(б) закрыть Y?
|
||||||
|
(в) ничего.
|
||||||
|
```
|
||||||
|
Ждать ответа. Отказ = пропуск (не настаивать, не повторять в этой сессии).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## mode=light (ad-hoc-сессии)
|
||||||
|
|
||||||
|
Ad-hoc-сессия (без таски/трека, но могут рождаться артефакты): след в mappa
|
||||||
|
пишется всегда (контракт сервиса — session live-ingest), но **структурирование —
|
||||||
|
по явному вопросу**: в конце сессии спроси «Сделать handoff?» (решение за
|
||||||
|
человеком). НЕ автоматический sweep: без «да» — ничего не писать, след
|
||||||
|
остаётся в mappa как есть.
|
||||||
|
|
||||||
|
## Failure modes
|
||||||
|
|
||||||
|
- **Secret detected.** Контент матчит паттерны секретов (`AKIA...`, `sk-...`,
|
||||||
|
`ghp_...`, `ssh-rsa`, `BEGIN PRIVATE KEY`, `password=`/`token=`) → **abort
|
||||||
|
write**. Сообщить user'у с указанием подозрительной строки.
|
||||||
|
- **Неоднозначная фраза** → ASK «закрываем сессию или таску?», не угадывать.
|
||||||
|
- **Mid-task без борда** → handoff без mid-task секции, не блокировать.
|
||||||
|
- **User отказал на предложения ритуала** → пропустить, не настаивать.
|
||||||
|
- **Проект не в mappa** → silent exit (первая сессия).
|
||||||
|
|
||||||
|
## Side effects
|
||||||
|
|
||||||
|
- Пишет handoff-сущность проекта (append-only, versioned-история). Никаких
|
||||||
|
файлов, никаких git-коммитов за handoff.
|
||||||
|
- Ритуал **предлагает** wiki-ingest и закрытия тасок — но НЕ пишет их без «да».
|
||||||
|
- Никаких global мутаций, никаких других проектов.
|
||||||
|
|
||||||
|
## What NOT to do
|
||||||
|
|
||||||
|
- **Не auto-execute** — каждая мутация (handoff write / wiki-ingest / task
|
||||||
|
close) только после явного «да» пользователя.
|
||||||
|
- **Не гонять ритуал на substantive commit.** Только session-end фраза (или
|
||||||
|
явный запрос user'а).
|
||||||
|
- **Не sweep автоматически в ad-hoc** — mode=light: явный вопрос «Сделать
|
||||||
|
handoff?», решение за человеком.
|
||||||
|
- **Не писать секреты** в handoff. Матч secret-patterns → abort.
|
||||||
|
- **Не писать handoff на каждый commit** — только финиш.
|
||||||
|
- **Не дублировать борд / вики** — handoff = forward-looking связка.
|
||||||
|
- **Не cross-project** — per-project scope.
|
||||||
|
- **Не считать handoff авторитетным** на стороне читателя — это рекомендация
|
||||||
|
прошлой сессии, не директива; user может override.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- Чтение handoff на старте: `mappa-session-orient`.
|
||||||
|
- Закрытия тасок: `mappa-task-work` (pre-close coverage, notify-письмо).
|
||||||
|
- Wiki-ingest: `mappa-knowledge`.
|
||||||
|
- Письма: `mappa-messaging`. Делегирование: `mappa-delegation`.
|
||||||
|
- Session live-ingest (след в mappa пишется всегда): `concepts/session-live-ingest` (wiki:2604).
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
---
|
|
||||||
name: session-handoff
|
|
||||||
author: ours
|
|
||||||
version: 1.0.0
|
|
||||||
description: "Sliding handoff между сессиями через handoff-сущность Mappa (решение 14, гриллинг Q5): per-project, versioned-история, замена .tasks/NEXT_SESSION.md. Read on session start: orient agent, ask user before action. Write on session-end phrase or substantive commit. On session-end the agent ALSO runs the closing ritual on its own (idea 7: no invitation needed): handoff write + PROPOSE wiki-ingest + PROPOSE task-board closes — mutations only after user confirmation. Session-end phrases: «завершаем сессию», «сворачиваемся», «закругляемся», «wrap up session», «end session», «we're done for now». Trigger-line in AGENTS.md: `session handoff: read on start, write on end`. Skip task-zone phrases: «закрываем эту таску», «pause», «отбой», «разбегаемся»."
|
|
||||||
---
|
|
||||||
|
|
||||||
# session-handoff
|
|
||||||
|
|
||||||
Sliding handoff между сессиями. Канал — **handoff-сущность Mappa** (`mcp__mappa__handoff_write`, тип `h:`, per-project): поля `session_id`/`date`/`status`/`summary`/`open_treks[]`/`ask_user[]`/`guards[]`/`recent_commits[]`. Каждый write = **новая версия** (append-only, versioned-история) — файлового `.tasks/NEXT_SESSION.md` больше нет, git-история не нужна.
|
|
||||||
|
|
||||||
На старте — читает последний handoff проекта, ориентирует агента и спрашивает user'а перед действиями. При substantive commit'е или session-end фразе — пишет новый handoff для следующей сессии.
|
|
||||||
|
|
||||||
Forward-looking, не timeline: handoff = связка новых вещей конкретно для следующего разворота, не overview всего проекта. STATUS.md-эквиваленты (борд mappa) / вики остаются авторитетными для своего scope'а.
|
|
||||||
|
|
||||||
## MCP-поверхность
|
|
||||||
|
|
||||||
| Операция | Тул | Примечание |
|
|
||||||
|---|---|---|
|
|
||||||
| Запись handoff | `mcp__mappa__handoff_write(project, session_id, status, summary, open_treks?, ask_user?, guards?, recent_commits?)` | append-only; без лиза (как инбокс) |
|
|
||||||
| Чтение последнего | `mcp__mappa__entity_search(q, type='handoff', project=<имя>, limit=1)` | ORDER BY created_at DESC → первый = последний |
|
|
||||||
| Полное чтение версии | `mcp__mappa__entity_get(id)` | id internal из search |
|
|
||||||
|
|
||||||
`status` ∈ `active | paused | done`. Поля-массивы (open_treks/ask_user/guards/recent_commits) — строковые массивы; пустые секции передавать как `[]` (аналог пометки «(нет на этом раунде)» — next агент видит: пусто, не забыто).
|
|
||||||
|
|
||||||
## When to use
|
|
||||||
|
|
||||||
**Read mode (session start):**
|
|
||||||
- AGENTS.md проекта содержит trigger-строку `session handoff: read on start, write on end`.
|
|
||||||
- В mappa есть handoff-сущности проекта (search не пуст).
|
|
||||||
|
|
||||||
**Write mode (session end / substantive commit):**
|
|
||||||
- User'ская фраза из whitelist:
|
|
||||||
- русский: «завершаем сессию», «сворачиваемся», «закругляемся»
|
|
||||||
- английский: «wrap up session», «end session», «we're done for now»
|
|
||||||
- ИЛИ — agent только что сделал substantive commit. Эвристика:
|
|
||||||
- prefix НЕ в (`meta:`|`docs:`|`style:`|`chore:`|`fix typo`)
|
|
||||||
- AND (body length > 200 символов OR files changed > 3)
|
|
||||||
- Плюс: **первый** non-trivial commit сессии — всегда триггерит, даже если ниже порога (старт работы = context shift).
|
|
||||||
|
|
||||||
**Skip (false-positive guards):**
|
|
||||||
- «закрываем эту таску» — task close, не session. Это зона using-tasks.
|
|
||||||
- «pause», «приостанови» — task-pause, не session-end.
|
|
||||||
- «отбой», «разбегаемся» — слишком broad.
|
|
||||||
- «сейчас завершу одну задачу и тогда поговорим» — частичное завершение.
|
|
||||||
- Проект не в mappa / нет handoff-сущностей — silent exit.
|
|
||||||
- AGENTS.md проекта НЕ содержит trigger-строку — silent exit.
|
|
||||||
|
|
||||||
При неоднозначности — **ASK**, не угадывать: «закрываем сессию или таску?»
|
|
||||||
|
|
||||||
## Steps
|
|
||||||
|
|
||||||
### Read mode
|
|
||||||
|
|
||||||
1. **Detect.** `mcp__mappa__entity_search(q='', type='handoff', project=<имя проекта>, limit=1)` — если пусто, silent exit (первая сессия проекта).
|
|
||||||
2. **Staleness check.** `meta.date` последнего handoff'а. Возраст > 7 дней → отметить user'у:
|
|
||||||
```
|
|
||||||
handoff от <date> (N дней назад) — возможно устарел.
|
|
||||||
Оверрайдить или продолжить?
|
|
||||||
```
|
|
||||||
Дождаться ответа перед продолжением.
|
|
||||||
3. **Summarize.** Прочитать мета последнего: summary / open_treks / ask_user / guards / recent_commits.
|
|
||||||
4. **Orient.** Пересказать user'у одним блоком: «прошлая сессия предложила X (open треки + ask-items + guards). Делаем?»
|
|
||||||
5. **Wait.** Не делать никаких действий до подтверждения user'ом. Default = orient + ask, **никакого auto-execute**.
|
|
||||||
|
|
||||||
### Write mode
|
|
||||||
|
|
||||||
1. **Scope check.** Это текущий проект (cwd). Никаких global мутаций, никаких других проектов.
|
|
||||||
2. **Mid-task capture.** Если есть 🔴 active таска проекта (борд mappa / `.tasks/`) — захватить в summary:
|
|
||||||
```
|
|
||||||
left mid-task: <slug>
|
|
||||||
where_stopped: <одна строка>
|
|
||||||
```
|
|
||||||
3. **Compose content.** Собрать поля handoff:
|
|
||||||
- `session_id` — `<ISO дата>` или идентификатор сессии;
|
|
||||||
- `status` — `active` (работа продолжается) / `paused` (заморожено) / `done` (завершено);
|
|
||||||
- `summary` — связка: где остановились, mid-task, ключевые решения;
|
|
||||||
- `open_treks` — массив открытых треков (готовность + entry-point);
|
|
||||||
- `ask_user` — pending решения / ожидаемые разрешения;
|
|
||||||
- `guards` — «не делать» (preemptive guards);
|
|
||||||
- `recent_commits` — 3–5 последних коммитов (`<slug>: <subject>`).
|
|
||||||
4. **Append.** `mcp__mappa__handoff_write(project=<имя>, ...)` — сервис создаёт новую версию `h:N` (versioned-история; предыдущие версии остаются, `entity_search` вернёт свежую).
|
|
||||||
5. **Closing ritual (idea 7).** На session-end фразе (НЕ на substantive commit) после handoff-write агент сам, без приглашения, предлагает закрытие:
|
|
||||||
- **(2) Propose wiki-ingest.** Если за сессию появилось durable-знание — ПРЕДЛОЖИТЬ ingest (using-wiki v2: mappa wiki_create/update под лизом), перечислив кандидатов. Ничего не писать без подтверждения.
|
|
||||||
- **(3) Propose task-board closes.** Если есть задачи, выглядящие закрытыми — ПРЕДЛОЖИТЬ закрытия (using-tasks / mappa task_close под лизом). Уважать ralph-loop: verifier-задачи закрывать только через verifier.
|
|
||||||
- Формат — один блок: «Ритуал закрытия: (а) заингестить X в вики? (б) закрыть Y? (в) ничего.» Ждать ответа. Отказ = пропуск.
|
|
||||||
|
|
||||||
## Failure modes
|
|
||||||
|
|
||||||
- **AGENTS.md без trigger-строки** → silent exit. Скил project-opt-in.
|
|
||||||
- **Проект не в mappa / нет handoff-сущностей** → silent exit.
|
|
||||||
- **Stale handoff (> 7 дней)** в read mode → не silent, **спросить** user'а оверрайдить или продолжить.
|
|
||||||
- **Неоднозначная фраза** → ASK «закрываем сессию или таску?», не угадывать.
|
|
||||||
- **Secret detected.** Контент матчит паттерны секретов (`AKIA...`, `sk-...`, `ghp_...`, `ssh-rsa`, `BEGIN PRIVATE KEY`, `password=`/`token=`) → **abort write**. Сообщить user'у с указанием подозрительной строки.
|
|
||||||
- **Mid-task без борда** → писать handoff без mid-task секции, не блокировать.
|
|
||||||
- **Ритуал: user отказал** → пропустить, не настаивать, не повторять в этой сессии.
|
|
||||||
|
|
||||||
## Side effects
|
|
||||||
|
|
||||||
- Пишет handoff-сущность проекта (append-only, versioned-история). Никаких файлов, никаких git-коммитов за handoff.
|
|
||||||
- Ритуал закрытия предлагает wiki-ingest и закрытия тасок — но НЕ пишет их.
|
|
||||||
- Никаких global мутаций, никаких других проектов, никаких user-level config writes.
|
|
||||||
|
|
||||||
## What NOT to do
|
|
||||||
|
|
||||||
- **Не auto-execute** действия из read handoff'а. Default = orient + ask.
|
|
||||||
- **Не писать в вики / не закрывать таски по ритуалу молча.** Ритуал = предложения. Каждая мутация — после явного «да».
|
|
||||||
- **Не гонять ритуал на substantive commit.** Только session-end фраза.
|
|
||||||
- **Не писать секреты** в handoff. Если контент матчит secret-patterns — abort.
|
|
||||||
- **Не на каждом commit'е.** Только substantive (см. эвристику).
|
|
||||||
- **Не дублировать борд / вики.** Handoff = forward-looking связка, не overview.
|
|
||||||
- **Не cross-project.** Per-project scope.
|
|
||||||
- **Не считать handoff авторитетным** на стороне читателя. Это рекомендация прошлой сессии, не директива. User может override любую её часть.
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
# session-handoff hooks
|
|
||||||
|
|
||||||
Opt-in PostToolUse hook that detects substantive `git commit` invocations and signals the agent ("consider running session-handoff write-mode") via a system reminder. Replaces the agent-side commit-detection heuristic in the SKILL.md `When to use` section with a deterministic harness-side trigger.
|
|
||||||
|
|
||||||
## Why opt-in (not auto-installed)
|
|
||||||
|
|
||||||
`install.sh` deliberately does **not** mutate `~/.claude/settings.json`. Auto-rewriting the user's hook config on every skill install is the wrong shape — user expects `install.sh` to copy files, nothing more. The hook is shipped as scripts; user enables it once per machine.
|
|
||||||
|
|
||||||
## Enable on Windows (PowerShell)
|
|
||||||
|
|
||||||
Add to `~/.claude/settings.json`. Use `powershell` for stock Windows (PS 5.1, always present); use `pwsh` if you have PowerShell 7+ installed. The hook script runs cleanly under both.
|
|
||||||
|
|
||||||
**Substitute `C:\\Users\\<you>` with your actual home path before pasting** — see "Why literal path" below.
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{
|
|
||||||
"hooks": {
|
|
||||||
"PostToolUse": [
|
|
||||||
{
|
|
||||||
"matcher": "Bash",
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "powershell -NoProfile -ExecutionPolicy Bypass -File \"C:\\Users\\<you>\\.claude\\skills\\session-handoff\\hooks\\commit-detector.ps1\""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Swap `powershell` for `pwsh` if you prefer PS 7+. To check which you have: `Get-Command pwsh -ErrorAction SilentlyContinue` (empty → PS 7 not installed → use `powershell`).
|
|
||||||
|
|
||||||
If `hooks.PostToolUse` already exists — append the matcher block to the array. Don't overwrite existing entries.
|
|
||||||
|
|
||||||
**Restart Claude Code after editing `settings.json`** — hooks are loaded at session start. A running session won't pick up the new hook until it's restarted (close + reopen the CC instance). Verify the hook is active by making a substantive commit and checking for a `Substantive commit detected on ...` system reminder in the next turn.
|
|
||||||
|
|
||||||
### Why literal path (no `$env:USERPROFILE` / `%USERPROFILE%` / `~`)
|
|
||||||
|
|
||||||
Claude Code on Windows invokes hook commands through **git-bash**, not PowerShell or cmd.exe directly. The outer-shell layer mangles shell-specific variable references before PowerShell ever sees the args:
|
|
||||||
|
|
||||||
- `$env:USERPROFILE` (PowerShell syntax) → bash treats `$env` as an empty variable and the rest `:USERPROFILE\...` becomes a literal, so PowerShell receives `-File ":USERPROFILE\..."` and fails with `неверный формат имени` / "invalid filename format".
|
|
||||||
- `%USERPROFILE%` (cmd syntax) → bash passes through literally, PowerShell doesn't expand it, same failure.
|
|
||||||
- `~/.claude/...` → bash expands `~` to git-bash-style `/c/Users/<you>/...`, which PowerShell's `-File` can't resolve to a real Windows path.
|
|
||||||
|
|
||||||
Literal absolute path with `C:\\Users\\<you>\\...` (escaped backslashes for JSON) survives every outer shell unchanged. `settings.json` is per-user anyway — portability across machines isn't a concern at this layer.
|
|
||||||
|
|
||||||
## Enable on Linux / macOS (bash)
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{
|
|
||||||
"hooks": {
|
|
||||||
"PostToolUse": [
|
|
||||||
{
|
|
||||||
"matcher": "Bash",
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "bash ~/.claude/skills/session-handoff/hooks/commit-detector.sh"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The bash variant needs `python3` on PATH (used to parse the PostToolUse JSON stdin).
|
|
||||||
|
|
||||||
## What gets signalled
|
|
||||||
|
|
||||||
On a successful `git commit` whose subject prefix is not in {`meta`, `docs`, `style`, `chore`} or `fix typo`, **and** whose body exceeds 200 characters or which touches more than 3 files — the hook emits a `hookSpecificOutput.additionalContext` system reminder of shape:
|
|
||||||
|
|
||||||
> Substantive commit detected on `<cwd>`: `<subject>` (N files changed, body M chars). Consider invoking session-handoff write-mode to update `.tasks/NEXT_SESSION.md`.
|
|
||||||
|
|
||||||
On any of these → silent skip (exit 0, no JSON):
|
|
||||||
|
|
||||||
- malformed PostToolUse stdin
|
|
||||||
- Bash command isn't `git commit`
|
|
||||||
- command is `git commit --amend`
|
|
||||||
- commit returned non-zero exit
|
|
||||||
- cwd isn't a git work-tree
|
|
||||||
- subject prefix is in trivial set
|
|
||||||
- body ≤ 200 chars AND files ≤ 3
|
|
||||||
|
|
||||||
## Smoke test (without enabling the hook)
|
|
||||||
|
|
||||||
Pipe a synthetic PostToolUse JSON to the script. On Windows:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$payload = @{
|
|
||||||
tool_input = @{ command = 'git commit -m "subject"' }
|
|
||||||
tool_response = @{ exit_code = 0 }
|
|
||||||
cwd = (Get-Location).Path
|
|
||||||
} | ConvertTo-Json -Compress
|
|
||||||
|
|
||||||
$payload | pwsh -NoProfile -File .\skills\session-handoff\hooks\commit-detector.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
If your `HEAD` is a non-trivial commit (e.g. recent `feat:` with > 200-char body or > 3 files), output is JSON containing `additionalContext`. Otherwise — empty stdout (silent skip).
|
|
||||||
|
|
||||||
## Caveats
|
|
||||||
|
|
||||||
- **Rebase / cherry-pick noise.** Every commit in a rebase or cherry-pick batch will re-fire the hook. Deferred to a follow-up if it actually annoys in practice; the hook is opt-in so the cost is bounded.
|
|
||||||
- **First-non-trivial commit of session.** The agent-side heuristic in SKILL.md treats the *first* non-trivial commit of a session as "always substantive" regardless of thresholds. The hook can't see session boundaries — uses only body/file thresholds. Slight under-detection on small first commits; acceptable trade-off for harness-side determinism.
|
|
||||||
- **No automatic write-mode invocation.** Hook only signals. The agent still decides whether to run session-handoff write-mode in response — keeps the user-agency invariant from SKILL.md `What NOT to do`.
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
#!/usr/bin/env pwsh
|
|
||||||
# session-handoff PostToolUse hook (PowerShell).
|
|
||||||
#
|
|
||||||
# Reads PostToolUse JSON from stdin, detects whether the just-completed
|
|
||||||
# Bash tool call was a substantive `git commit`, and on hit emits JSON to
|
|
||||||
# stdout with `additionalContext` so Claude Code surfaces a system reminder
|
|
||||||
# in the next agent iteration ("substantive commit — consider session-handoff
|
|
||||||
# write-mode").
|
|
||||||
#
|
|
||||||
# Substantive heuristic (mirrors session-handoff SKILL.md):
|
|
||||||
# prefix NOT in (meta:|docs:|style:|chore:|fix typo) AND
|
|
||||||
# (body > 200 chars OR files > 3)
|
|
||||||
#
|
|
||||||
# Silent skip on: malformed JSON, no command, --amend, failed commit,
|
|
||||||
# non-git cwd, trivial prefix, below thresholds. Never blocks the tool call
|
|
||||||
# (PostToolUse cannot, by design).
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
# Read stdin
|
|
||||||
try {
|
|
||||||
$raw = [Console]::In.ReadToEnd()
|
|
||||||
if ([string]::IsNullOrWhiteSpace($raw)) { exit 0 }
|
|
||||||
$hook = $raw | ConvertFrom-Json -ErrorAction Stop
|
|
||||||
} catch {
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Only Bash tool, only git commit (not --amend)
|
|
||||||
$cmd = $hook.tool_input.command
|
|
||||||
if (-not $cmd) { exit 0 }
|
|
||||||
if ($cmd -notmatch '(?<![\w-])git\s+commit(?![\w-])') { exit 0 }
|
|
||||||
if ($cmd -match '(?<![\w-])git\s+commit\b.*--amend') { exit 0 }
|
|
||||||
|
|
||||||
# Only on successful commit
|
|
||||||
if ($null -ne $hook.tool_response.exit_code -and $hook.tool_response.exit_code -ne 0) { exit 0 }
|
|
||||||
|
|
||||||
# Resolve cwd; require a git work-tree
|
|
||||||
$cwd = $hook.cwd
|
|
||||||
if (-not $cwd) { $cwd = (Get-Location).Path }
|
|
||||||
$inside = & git -C $cwd rev-parse --is-inside-work-tree 2>$null
|
|
||||||
if ($inside -ne 'true') { exit 0 }
|
|
||||||
|
|
||||||
# Parse last commit
|
|
||||||
$subject = (& git -C $cwd log -1 --format='%s').Trim()
|
|
||||||
# PowerShell collapses multi-line subprocess output into string[] — join back so
|
|
||||||
# .Length below is char count, not line count.
|
|
||||||
$body = ((& git -C $cwd log -1 --format='%b') -join "`n")
|
|
||||||
$files = ((& git -C $cwd diff-tree --no-commit-id --name-only -r HEAD) | Measure-Object).Count
|
|
||||||
|
|
||||||
# Trivial-prefix check (Conventional Commits prefix before optional scope + colon)
|
|
||||||
$prefix = $subject -replace '^([a-z]+)(\([^)]+\))?:.*$','$1'
|
|
||||||
$trivial = @('meta','docs','style','chore')
|
|
||||||
if ($trivial -contains $prefix) { exit 0 }
|
|
||||||
if ($subject -match 'fix\s+typo') { exit 0 }
|
|
||||||
|
|
||||||
# Threshold check (chars for body, count for files)
|
|
||||||
$bodyLen = if ($body) { $body.Length } else { 0 }
|
|
||||||
if ($bodyLen -le 200 -and $files -le 3) { exit 0 }
|
|
||||||
|
|
||||||
|
|
||||||
# Substantive — emit JSON
|
|
||||||
$msg = "Substantive commit detected on " + $cwd + ": ``" + $subject + "`` (" + $files + " files changed, body " + $bodyLen + " chars). Consider invoking session-handoff write-mode to update .tasks/NEXT_SESSION.md."
|
|
||||||
|
|
||||||
@{
|
|
||||||
hookSpecificOutput = @{
|
|
||||||
hookEventName = 'PostToolUse'
|
|
||||||
additionalContext = $msg
|
|
||||||
}
|
|
||||||
systemMessage = 'session-handoff: substantive commit detected'
|
|
||||||
suppressOutput = $false
|
|
||||||
} | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# session-handoff PostToolUse hook (POSIX). See commit-detector.ps1 for prose.
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
raw=$(cat)
|
|
||||||
[[ -z "$raw" ]] && exit 0
|
|
||||||
|
|
||||||
# Helper: extract a JSON path via python3
|
|
||||||
jget() {
|
|
||||||
python3 -c "
|
|
||||||
import sys, json
|
|
||||||
try:
|
|
||||||
d = json.loads(sys.argv[1])
|
|
||||||
out = d
|
|
||||||
for k in sys.argv[2].split('.'):
|
|
||||||
if isinstance(out, dict):
|
|
||||||
out = out.get(k)
|
|
||||||
else:
|
|
||||||
out = None
|
|
||||||
break
|
|
||||||
print('' if out is None else out)
|
|
||||||
" "$raw" "$1" 2>/dev/null || echo ''
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd=$(jget tool_input.command)
|
|
||||||
[[ -z "$cmd" ]] && exit 0
|
|
||||||
|
|
||||||
# Only `git commit`, not `--amend`
|
|
||||||
if ! echo "$cmd" | grep -qE '(^|[^[:alnum:]_-])git[[:space:]]+commit($|[^[:alnum:]_-])'; then exit 0; fi
|
|
||||||
if echo "$cmd" | grep -qE '(^|[^[:alnum:]_-])git[[:space:]]+commit\b.*--amend'; then exit 0; fi
|
|
||||||
|
|
||||||
# Only on successful commit (if exit_code present and non-zero, skip)
|
|
||||||
exit_code=$(jget tool_response.exit_code)
|
|
||||||
if [[ -n "$exit_code" && "$exit_code" != "0" ]]; then exit 0; fi
|
|
||||||
|
|
||||||
cwd=$(jget cwd)
|
|
||||||
[[ -z "$cwd" ]] && cwd=$(pwd)
|
|
||||||
|
|
||||||
# Require git work-tree
|
|
||||||
git -C "$cwd" rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
|
|
||||||
|
|
||||||
subject=$(git -C "$cwd" log -1 --format='%s')
|
|
||||||
body=$(git -C "$cwd" log -1 --format='%b')
|
|
||||||
files=$(git -C "$cwd" diff-tree --no-commit-id --name-only -r HEAD | wc -l | tr -d ' ')
|
|
||||||
|
|
||||||
# Trivial-prefix check
|
|
||||||
prefix=$(echo "$subject" | sed -E 's/^([a-z]+)(\([^)]+\))?:.*$/\1/')
|
|
||||||
case "$prefix" in
|
|
||||||
meta|docs|style|chore) exit 0 ;;
|
|
||||||
esac
|
|
||||||
echo "$subject" | grep -qE 'fix[[:space:]]+typo' && exit 0
|
|
||||||
|
|
||||||
# Threshold
|
|
||||||
body_len=${#body}
|
|
||||||
if [[ "$body_len" -le 200 && "$files" -le 3 ]]; then exit 0; fi
|
|
||||||
|
|
||||||
# Substantive — emit JSON via python3 to handle quoting safely
|
|
||||||
python3 -c "
|
|
||||||
import json, sys
|
|
||||||
msg = (
|
|
||||||
'Substantive commit detected on $cwd: \`' + '''$subject''' + '\` '
|
|
||||||
+ '($files files changed, body $body_len chars). '
|
|
||||||
+ 'Consider invoking session-handoff write-mode to update .tasks/NEXT_SESSION.md.'
|
|
||||||
)
|
|
||||||
print(json.dumps({
|
|
||||||
'hookSpecificOutput': {
|
|
||||||
'hookEventName': 'PostToolUse',
|
|
||||||
'additionalContext': msg,
|
|
||||||
},
|
|
||||||
'systemMessage': 'session-handoff: substantive commit detected',
|
|
||||||
'suppressOutput': False,
|
|
||||||
}))
|
|
||||||
"
|
|
||||||
Reference in New Issue
Block a user