diff --git a/.tasks/2026-08-24-01063-mappa-closing-ritual.md b/.tasks/2026-08-24-01063-mappa-closing-ritual.md index c3e4e09..6d1d785 100644 --- a/.tasks/2026-08-24-01063-mappa-closing-ritual.md +++ b/.tasks/2026-08-24-01063-mappa-closing-ritual.md @@ -17,4 +17,14 @@ Handoff: sliding, per-project, versioned-история; read на старте ## 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 + +- 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. diff --git a/dist/mappa-closing-ritual.skill b/dist/mappa-closing-ritual.skill new file mode 100644 index 0000000..345a517 Binary files /dev/null and b/dist/mappa-closing-ritual.skill differ diff --git a/dist/session-handoff.skill b/dist/session-handoff.skill deleted file mode 100644 index f1e773b..0000000 Binary files a/dist/session-handoff.skill and /dev/null differ diff --git a/skills/mappa-closing-ritual/SKILL.md b/skills/mappa-closing-ritual/SKILL.md new file mode 100644 index 0000000..b466677 --- /dev/null +++ b/skills/mappa-closing-ritual/SKILL.md @@ -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: +where_stopped: <одна строка> +``` +Без борда — писать handoff без mid-task секции, не блокировать. + +### 3. Compose content (поля handoff) + +- `session_id` — `` или идентификатор сессии; +- `status` — `active` (работа продолжается) / `paused` (заморожено) / `done` (завершено); +- `summary` — связка: где остановились, mid-task, ключевые решения; +- `open_treks` — массив открытых треков (готовность + entry-point); +- `ask_user` — pending решения / ожидаемые разрешения; +- `guards` — «не делать» (preemptive guards); +- `recent_commits` — 3–5 последних коммитов (`: `). + +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). diff --git a/skills/session-handoff/SKILL.md b/skills/session-handoff/SKILL.md deleted file mode 100644 index 62fcf49..0000000 --- a/skills/session-handoff/SKILL.md +++ /dev/null @@ -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 от (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: - where_stopped: <одна строка> - ``` -3. **Compose content.** Собрать поля handoff: - - `session_id` — `` или идентификатор сессии; - - `status` — `active` (работа продолжается) / `paused` (заморожено) / `done` (завершено); - - `summary` — связка: где остановились, mid-task, ключевые решения; - - `open_treks` — массив открытых треков (готовность + entry-point); - - `ask_user` — pending решения / ожидаемые разрешения; - - `guards` — «не делать» (preemptive guards); - - `recent_commits` — 3–5 последних коммитов (`: `). -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 любую её часть. diff --git a/skills/session-handoff/hooks/README.md b/skills/session-handoff/hooks/README.md deleted file mode 100644 index 83d3681..0000000 --- a/skills/session-handoff/hooks/README.md +++ /dev/null @@ -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\\` 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\\\\.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//...`, which PowerShell's `-File` can't resolve to a real Windows path. - -Literal absolute path with `C:\\Users\\\\...` (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 ``: `` (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`. diff --git a/skills/session-handoff/hooks/commit-detector.ps1 b/skills/session-handoff/hooks/commit-detector.ps1 deleted file mode 100644 index 8e8e9dc..0000000 --- a/skills/session-handoff/hooks/commit-detector.ps1 +++ /dev/null @@ -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 '(?$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 diff --git a/skills/session-handoff/hooks/commit-detector.sh b/skills/session-handoff/hooks/commit-detector.sh deleted file mode 100644 index 22ac2fc..0000000 --- a/skills/session-handoff/hooks/commit-detector.sh +++ /dev/null @@ -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, -})) -"