From c3e1ce7b408639fa2b5b7bd2e1ffbfc10f706e61 Mon Sep 17 00:00:00 2001 From: vitya Date: Wed, 17 Jun 2026 10:48:50 +0300 Subject: [PATCH] feat(session-inbox-monitor): SessionStart hook + fill SKILL body v0.2.0 Core content task of the session-inbox-monitor line. Two deliverables: 1. SessionStart hook `skills/session-inbox-monitor/hooks/inbox-monitor.ps1` (versioned for multi-machine rollout; deployed to ~/.claude/hooks/ and registered in ~/.claude/settings.json SessionStart): - sweep: Get-CimInstance | Stop-Process orphaned monitors of THIS inbox, matched by sentinel CLAUDE_INBOX_MONITOR + inbox path (a /clear leaves the poll process alive -> re-raise without sweep stacks duplicates); - inject: hookSpecificOutput.additionalContext with the exact persistent Monitor command (Monitor tool, not background Bash); - opt-in gate: fires only on .claude-inbox/ dir or CLAUDE.md trigger line. ASCII-only (em-dash -> mojibake under WinPS 5.1 without BOM, fixed). 2. SKILL.md body filled (When to use / Inputs / Steps / Deployment / Failure modes / Side effects / What NOT to do); bump 0.1.0 -> 0.2.0 MINOR. Headless: no hook-level signal exists (verified via claude-code-guide) -> agent-side best-effort skip, default errs toward raising (false-skip in interactive loses the feature; false-raise in headless is a harmless no-op). Live-verified: inject -> valid JSON; sweep -> killed a planted orphan (PASS); real Monitor tool spawns a bash process carrying the sentinel (sweep will find real orphans); settings.json stays valid. Sweep over-match edge and multi-session-per-project limit documented honestly in Failure modes. Closes [session-inbox-monitor-sessionstart-hook]. Co-Authored-By: Claude Opus 4.8 (1M context) --- .tasks/STATUS.md | 8 +- ...session-inbox-monitor-sessionstart-hook.md | 49 ++++++++++ skills/session-inbox-monitor/SKILL.md | 92 +++++++++++++++++-- .../hooks/inbox-monitor.ps1 | 86 +++++++++++++++++ 4 files changed, 224 insertions(+), 11 deletions(-) create mode 100644 .tasks/session-inbox-monitor-sessionstart-hook.md create mode 100644 skills/session-inbox-monitor/hooks/inbox-monitor.ps1 diff --git a/.tasks/STATUS.md b/.tasks/STATUS.md index bda3b05..8e0b902 100644 --- a/.tasks/STATUS.md +++ b/.tasks/STATUS.md @@ -1201,11 +1201,11 @@ Acceptance 1-6 all met by test evidence (not by inspection). Per-task detail in --- -## ⚪ [session-inbox-monitor-sessionstart-hook] — Content-таска (ядро скила). Написать SessionStart-хук-инжектор (machine-local в ~/.claude/hooks/ + регистрация в ~/.claude/settings.json). Две функции: (a) **уборка** — Get-CimInstance | Stop-Process прибить осиротевшие inbox-мониторы ЭТОГО проекта по сигнатуре командной строки (`/clear` оставляет их жить → re-raise без уборки плодит дубли); (b) **инжект** `hookSpecificOutput.additionalContext` с инструкцией «подними persistent Monitor (Monitor tool, не background Bash) на /.claude-inbox/». Headless-детект → skip (Monitor там не работает; полагаться на Stop-хук pickup + Notify). Дизайн: см. .workshop archive trace + concepts/session-inbox-monitor. Брат-близнец poller-interactive-lock-writer (хук+CLI паттерн). После написания — дописать тело каркаса SKILL.md (When to use / Steps / Failure modes / Side effects / What NOT to do). +## 🟢 [session-inbox-monitor-sessionstart-hook] — Content-таска (ядро скила). Написать SessionStart-хук-инжектор (machine-local в ~/.claude/hooks/ + регистрация в ~/.claude/settings.json). Две функции: (a) **уборка** — Get-CimInstance | Stop-Process прибить осиротевшие inbox-мониторы ЭТОГО проекта по сигнатуре командной строки (`/clear` оставляет их жить → re-raise без уборки плодит дубли); (b) **инжект** `hookSpecificOutput.additionalContext` с инструкцией «подними persistent Monitor (Monitor tool, не background Bash) на /.claude-inbox/». Headless-детект → skip (Monitor там не работает; полагаться на Stop-хук pickup + Notify). Дизайн: см. .workshop archive trace + concepts/session-inbox-monitor. Брат-близнец poller-interactive-lock-writer (хук+CLI паттерн). После написания — дописать тело каркаса SKILL.md (When to use / Steps / Failure modes / Side effects / What NOT to do). -**Status:** ready -**Where I stopped:** (not started) -**Next action:** Написать SessionStart PS-хук (уборка + инжект, headless-skip), зарегистрировать в settings.json (operator-флип по ОК user). Дописать тело SKILL.md глазами из archive. +**Status:** done +**Where I stopped:** Шипнуто 2026-06-17. (1) Хук `skills/session-inbox-monitor/hooks/inbox-monitor.ps1` — sweep (Get-CimInstance|Stop-Process по сентинелу `CLAUDE_INBOX_MONITOR`+inbox-путь) + inject (`hookSpecificOutput.additionalContext` с точной Monitor-командой) + opt-in гейт (`.claude-inbox/` или CLAUDE.md-строка). ASCII-only (пофикшен em-dash→mojibake баг под WinPS 5.1 без BOM). (2) Деплой в `~/.claude/hooks/` (hash-parity с репо) + регистрация в `~/.claude/settings.json` SessionStart (4-й элемент, без matcher; бэкап `.bak-inbox-monitor`). (3) Тело SKILL.md дописано, v0.1.0→0.2.0 MINOR. **Live-верификация:** inject→валидный JSON; sweep→убил подсадного сироту (pid 32920 PASS); реальный Monitor tool спавнит bash-процесс несущий сентинел (sweep его найдёт); settings.json валиден. **Headless-факт (claude-code-guide):** хук-уровня сигнала НЕТ — skip отдан агенту best-effort, дефолт «поднять» (ложный skip в интерактиве теряет фичу; ложный raise в headless безвреден). Острый угол sweep (любой live-процесс с сентинелом+путём гибнет; на реальном SessionStart активности агента нет → безопасно) + multi-session-лимит задокументированы в SKILL Failure modes. +**Next action:** (none — kept until merged). Follow-up'ы по ленте: `-install` (добавить opt-in строку в CLAUDE.md + проверка активации), `-hermes-mapping`, `-test-trigger` (чистая сессия), `-stophook-blockfix-proof`, `-review` (зонтик). Возможный follow-up: per-session изоляция монитора (сейчас инвариант «один на инбокс/машину»). **Branch:** n/a **Notify:** OpeItcLoc03/workshop diff --git a/.tasks/session-inbox-monitor-sessionstart-hook.md b/.tasks/session-inbox-monitor-sessionstart-hook.md new file mode 100644 index 0000000..0c645ed --- /dev/null +++ b/.tasks/session-inbox-monitor-sessionstart-hook.md @@ -0,0 +1,49 @@ +# session-inbox-monitor-sessionstart-hook — working context + +**Status:** 🟢 done (shipped 2026-06-17) — hook written+deployed+registered, SKILL body filled (v0.2.0), live-verified (sweep+inject+real-Monitor signature). See STATUS.md block for full evidence. +**Owner:** vitya (interactive session) +**Notify:** OpeItcLoc03/workshop + +Ядро ленты `session-inbox-monitor`. Написать SessionStart-хук (уборка + инжект, +headless-skip) и дописать тело SKILL.md. Дизайн согласован в воркшопе: +- archive: `~/projects/.workshop/.archive/2026-06-17-session-inbox-monitor.md` +- concept: `~/projects/.workshop/.wiki/concepts/session-inbox-monitor.md` + +## Verified facts (механика) + +- **Monitor tool** запускает shell-команду (через Bash env), `persistent:true` живёт + до session end / TaskStop. Хук сам tool поднять НЕ может → инжектит инструкцию, + агент поднимает первым ходом (прецедент — так инжектится `using-superpowers`). +- **`/clear` НЕ вызывает SessionEnd** → teardown на SessionEnd для `/clear` бесполезен. + Поэтому уборка идемпотентно в SessionStart: «прибей старые мониторы этого инбокса → + подними ровно один». +- **Сигнатура уборки** (решение этой сессии): зашить **сентинел** в poll-команду + Monitor'а. OS-процесс, спавненный Monitor'ом, несёт poll-команду в своей командной + строке → уборка матчит `Get-CimInstance Win32_Process` по сентинелу + inbox-пути. + Снимает риск over-match произвольных процессов. +- **Stop-хук block-фикс** уже в проде (`~/.claude/hooks/stop-dispatcher.ps1` стр. 116–125): + inbox-путь отдаёт `decision:block` с телом письма. Это смежная таска `-stophook-blockfix-proof`. +- **Близнец** `interactive-lock.ps1` — machine-local PS-хук, регистрируется в settings.json + SessionStart/SessionEnd. Тот же паттерн установки. + +## Решения по реализации + +1. Хук-файл версионируем в репо: `skills/session-inbox-monitor/hooks/inbox-monitor.ps1` + (deployment goal: multi-machine rollout). Install/дока деплоит его в `~/.claude/hooks/`. +2. Регистрация в `~/.claude/settings.json` SessionStart — мутация user-level конфига → + **гейт: пауза + ОК user** перед записью (как setup-скилы). +3. Committable: SKILL.md тело + hook-файл + per-task + STATUS.md. settings.json — вне репо. + +## Open question (surface to user / flag as failure mode) + +- **Мульти-сессия на одном проекте.** Уборка по inbox-пути прибьёт монитор ДРУГОЙ живой + интерактивной сессии того же проекта (сигнатура per-inbox, не per-session). Дизайн + воркшопа явно выбрал «прибей все → подними один». Задокументировать как known + limitation в SKILL.md; при необходимости — follow-up таска. Связь: [[inter-session-peer-discipline]]. + +## Acceptance (из -review зонтика) + +- Уборка реально прибивает осиротевшие мониторы этого инбокса. +- Инжект поднимает РОВНО один Monitor. +- Headless → skip. +- Тело SKILL.md (Steps/Failure modes/etc.) дописано и соответствует реальности. diff --git a/skills/session-inbox-monitor/SKILL.md b/skills/session-inbox-monitor/SKILL.md index a802428..122cd47 100644 --- a/skills/session-inbox-monitor/SKILL.md +++ b/skills/session-inbox-monitor/SKILL.md @@ -1,6 +1,6 @@ --- name: session-inbox-monitor -version: 0.1.0 +version: 0.2.0 description: > Raises a persistent Monitor (Monitor tool, NOT background Bash) on the project's `.claude-inbox/` at the start of an interactive session, so @@ -26,24 +26,102 @@ for free on session end. Headless sessions skip it and rely on the pull-model ## When to use -<пусто — дописывается во втором проходе> +- **Automatic (the common path).** The paired SessionStart hook injects an + instruction at the start of every interactive session of an opted-in project. + You act on that injection — raise the monitor as your first action — without a + user phrase. +- **On request.** CLAUDE.md line `inbox monitor: raise on start`, or «подними + монитор почты», «настрой авто-монитор инбокса», «raise inbox monitor», + «auto-arm inbox watcher». +- **NOT for** handling the content of a received message (→ + `inter-session-peer-discipline`), nor the multi-machine delivery backend (→ + `cross-machine-inbox`). This skill is only the monitor's *lifecycle* on one + machine. ## Inputs -<пусто> +- `/.claude-inbox/` — the watched directory. Direct-child `*.md` files + are inbox messages (the Stop-hook moves them to `.read/` once handled). +- The SessionStart hook supplies the **exact Monitor command** to run, with the + sweep sentinel (`CLAUDE_INBOX_MONITOR`) and the absolute inbox path baked in. + Use it verbatim — do not hand-author a different poll command, or the sweep + won't recognise the process it spawns. ## Steps -<пусто> +1. **Mode check.** If this is a headless / non-interactive run (`claude -p`), + **STOP — do not raise a monitor.** The Stop-hook inbox pickup plus `Notify:`/ + ntfy cover delivery there; a Monitor can't idle-watch in headless and is + killed ~5s after the run. There is no hook-level headless signal, so this is + your judgement call from the run context. +2. **Raise exactly one persistent Monitor** using the command the hook injected: + the **Monitor tool** with `persistent: true`, `description: "inbox watcher"`. + The hook already swept any orphan before injecting, so you start from a clean + slate — raise one, not more. +3. **Do not sweep yourself.** Killing orphans is the hook's job (it runs before + you, at SessionStart, when no other session activity is live). +4. **On an event** (`New inter-session message in inbox: `), read + `.claude-inbox/` and handle the message per `inter-session-peer-discipline`. + The Stop-hook also force-delivers any inbox messages at end of turn as a + backstop, so nothing is lost if the monitor missed a beat. +5. **Teardown is automatic.** The Monitor dies at session end. Do **not** add a + SessionEnd teardown — and note `/clear` does not fire SessionEnd anyway + (that's why the sweep lives in SessionStart, not SessionEnd). + +## Deployment (machine-local) + +- Hook script: `skills/session-inbox-monitor/hooks/inbox-monitor.ps1` (versioned + here) → deploy to `~/.claude/hooks/inbox-monitor.ps1`. +- Register in `~/.claude/settings.json` under `hooks.SessionStart` (no matcher → + fires on startup/resume/clear/compact), e.g.: + ```json + { "hooks": [ { "type": "command", + "command": "powershell -NoProfile -ExecutionPolicy Bypass -File \"C:\\Users\\\\.claude\\hooks\\inbox-monitor.ps1\"", + "timeout": 15, "statusMessage": "inbox-monitor" } ] } + ``` +- Twin pattern: `poller-interactive-lock-writer` (`interactive-lock.ps1`). +- Opt-in per project: the hook fires only when the project has a `.claude-inbox/` + directory **or** a CLAUDE.md line `inbox monitor: raise on start`. ## Failure modes -<пусто> +- **No inbox, no opt-in line** → the hook injects nothing; no monitor. Expected + for projects that don't use inter-session messaging. +- **Two live interactive sessions on the same project** → the second session's + SessionStart sweep kills the first session's monitor (the match is + per-inbox-path, not per-session). Known limitation; the deliberate invariant is + "exactly one monitor per inbox per machine." If the first session is still + active, its next Stop-hook turn still delivers inbox mail — only the real-time + paging is lost until it re-raises. See `inter-session-peer-discipline`. +- **Sweep over-match** → any *live* process whose command line contains both the + sentinel `CLAUDE_INBOX_MONITOR` and the inbox path is killed. At a real + SessionStart no agent/tool processes are running yet, so only the orphaned + monitor matches. Don't echo or run a command carrying that sentinel+path during + a session's startup. +- **Headless didn't skip** → a monitor raised in headless is a harmless no-op, + killed ~5s after the run ends. The default errs toward raising because a + false-skip in an interactive session would silently lose the feature. +- **Monitor auto-stopped** → the harness stops monitors that emit too many + events. The injected poll command de-dups by filename (pages once per message, + not every 15s) to stay under that bar. ## Side effects -<пусто> +- Spawns one Monitor (and its backing Git-Bash poll process) per interactive + session; both die at session end. +- Force-kills orphaned monitor processes of this inbox at every SessionStart. +- **No repo writes.** The hook and its `~/.claude/settings.json` registration are + machine-local; only this skill (docs) and `.claude-inbox/` activity are in play. ## What NOT to do -<пусто> +- **Don't watch the inbox with a background Bash** (`run_in_background`) — it + leaks across `/clear` and accumulates zombies. Use the Monitor tool. +- **Don't add a SessionEnd teardown hook** — the Monitor self-terminates, and + `/clear` never fires SessionEnd. +- **Don't raise more than one monitor.** The hook guarantees a clean slate before + you raise. +- **Don't handle message content here** — that's `inter-session-peer-discipline`. +- **Don't rely on this in headless** — use the pull model (Stop-hook + Notify). + Active headless polling, if ever needed, is a separate cron Routine, not this + skill. diff --git a/skills/session-inbox-monitor/hooks/inbox-monitor.ps1 b/skills/session-inbox-monitor/hooks/inbox-monitor.ps1 new file mode 100644 index 0000000..869137a --- /dev/null +++ b/skills/session-inbox-monitor/hooks/inbox-monitor.ps1 @@ -0,0 +1,86 @@ +# SessionStart inbox-monitor injector hook (session-inbox-monitor skill). +# +# Two jobs, run on every SessionStart (startup / resume / clear / compact): +# (a) SWEEP - kill orphaned inbox-monitor OS processes of THIS project. +# A `/clear` does NOT fire SessionEnd, so a Monitor's underlying +# poll process can outlive the session it belonged to. Without a +# sweep, re-raising would stack duplicates. Match is by a sentinel +# string (CLAUDE_INBOX_MONITOR) baked into the poll command PLUS +# this project's inbox path - so we never touch unrelated processes. +# (b) INJECT - additionalContext telling the agent to raise a persistent +# Monitor (Monitor TOOL, not background Bash) on /.claude-inbox. +# +# Opt-in per project: fires only when the project has a `.claude-inbox/` dir OR a +# CLAUDE.md line `inbox monitor: raise on start`. +# +# Headless (`claude -p`): there is NO reliable hook-level signal to detect it +# (verified 2026-06-17 - `source` and CLAUDE_* env vars don't distinguish it). +# So the hook injects unconditionally and the SKILL instructs the agent to skip +# when headless. A Monitor raised in headless is harmless (killed ~5s after the +# run ends); a false-skip in an interactive session would silently lose the +# feature - so the default errs toward raising. +# +# Twin pattern: poller-interactive-lock-writer (interactive-lock.ps1). +# Machine-local deploy target: ~/.claude/hooks/inbox-monitor.ps1 (registered in +# ~/.claude/settings.json SessionStart). Versioned here for multi-machine rollout. + +param( + [string]$ProjectDir = $env:CLAUDE_PROJECT_DIR +) + +if (-not $ProjectDir) { exit 0 } + +$inbox = Join-Path $ProjectDir '.claude-inbox' +$claudeMd = Join-Path $ProjectDir 'CLAUDE.md' + +# --- opt-in gate ----------------------------------------------------------- +$optedIn = $false +if (Test-Path $inbox) { + $optedIn = $true +} elseif (Test-Path $claudeMd) { + if (Select-String -Path $claudeMd -SimpleMatch 'inbox monitor: raise on start' -Quiet -ErrorAction SilentlyContinue) { + $optedIn = $true + } +} +if (-not $optedIn) { exit 0 } + +# Forward-slash inbox path: the Monitor poll command (Git Bash) uses this form, +# so both the sweep match and the injected command share one literal. +$inboxFwd = ($inbox -replace '\\', '/') + +# --- (a) sweep orphaned monitors of THIS inbox ----------------------------- +# Match = sentinel AND this inbox's path in the same process command line. +try { + Get-CimInstance Win32_Process -ErrorAction Stop | + Where-Object { + $_.CommandLine -and + $_.CommandLine -match 'CLAUDE_INBOX_MONITOR' -and + $_.CommandLine -like "*$inboxFwd*" + } | + ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } +} catch { } + +# --- (b) build the canonical Monitor poll command -------------------------- +# `: CLAUDE_INBOX_MONITOR` is a bash no-op carrying the sweep sentinel in the +# process command line without polluting the event stream. De-dups by filename +# so a sitting message pages once, not every 15s (a noisy monitor is auto-stopped). +$cmd = @' +: CLAUDE_INBOX_MONITOR; d='__INBOX__'; s=' '; while true; do for f in "$d"/*.md; do [ -e "$f" ] || continue; n=$(basename "$f"); case "$s" in *" $n "*) continue;; esac; s="$s$n "; echo "New inter-session message in inbox: $n - read .claude-inbox/ and handle it now"; done; sleep 15; done +'@ +$cmd = $cmd.Trim().Replace('__INBOX__', $inboxFwd) + +# --- (c) inject the raise-instruction -------------------------------------- +$ctx = @" +[session-inbox-monitor] This project participates in inter-session messaging. As your FIRST action, raise a persistent inbox watcher so messages from other sessions page you in real time. + +Use the Monitor tool with persistent: true, description "inbox watcher", and this EXACT command: + +$cmd + +Do NOT use a background Bash for this - it leaks across /clear. The Monitor tool is session-bound and tears down on its own at session end. The paired SessionStart hook already swept any orphaned watcher before this, so raise exactly one. + +If you are running headless (claude -p / non-interactive), SKIP this - the Stop-hook inbox pickup plus Notify cover delivery there. See the session-inbox-monitor skill for the full contract. +"@ + +@{ hookSpecificOutput = @{ hookEventName = 'SessionStart'; additionalContext = $ctx } } | ConvertTo-Json -Compress -Depth 5 +exit 0