feat(graceful-handoff)!: integrate with STATE.md continuity system (v3.0.0)
BREAKING: replace the NEXT-SESSION artifact + 3 hooks with a STATE.md-centric, skill-only design. /graceful-handoff now overwrites the nearest STATE.md (with the mandatory 👉 NESTE block) instead of writing a separate handover file. - Remove hooks/ entirely: Stop auto-trigger (operator choice), SessionStart loader (redundant with global session-start.sh), statusLine hint (dead — user settings win). - Invert the pipeline: the model writes STATE.md (only it has the context for 👉 NESTE); handoff-pipeline.mjs becomes a slim deterministic helper (--plan / --commit / --dry-run). - Remote-aware policy: STATE.md tracked on private remotes, local-only (gitignored) on public/open mirrors. Authoritative signal: git check-ignore STATE.md. - SKILL.md rewritten as the Session-Slutt ritual; dropped the Sonnet model pin. - Docs (README, CLAUDE.md, CHANGELOG), plugin.json 2.1.0→3.0.0, .gitignore cleanup. - Tests rewritten for --plan/--commit; no-`git add -A` regression preserved. 30/30 green. Release-cut (tag v3.0.0 + catalog ref bump) pending — separate gated action. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019SiKr4c6GAzQH5n6E6f5NA
This commit is contained in:
parent
a6f3ad4e93
commit
2c4e5e425b
18 changed files with 694 additions and 1656 deletions
333
README.md
333
README.md
|
|
@ -1,20 +1,20 @@
|
|||
# Graceful Handoff Plugin for Claude Code
|
||||
|
||||
> Auto-trigger session handoff at the context threshold so long-running work survives the next session boundary. Manual `/graceful-handoff` always works as a backup.
|
||||
> One command to end a session cleanly and hand it to the next one — `/graceful-handoff` reaches a natural stopping point and overwrites the nearest `STATE.md` with a complete, self-standing state-of-play.
|
||||
|
||||
> **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted. See [GOVERNANCE.md](GOVERNANCE.md) for the full model and what upstream provides.
|
||||
|
||||
*AI-generated: all code produced by Claude Code through dialog-driven development. [Full disclosure →](../../README.md#ai-generated-code-disclosure)*
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
A Claude Code plugin that solves a structural problem with long sessions: the context window fills (often within ~5 minutes of real work on Opus 4.7), and the user is forced to summarize, commit, and write a continuation prompt under time pressure — or skip steps and lose continuity. This plugin removes those steps from the user's hands using a deterministic JSON pipeline plus three hooks that detect the threshold, auto-execute the reversible work, and auto-load the artifact in the next session.
|
||||
A Claude Code plugin that makes session handoff a single deliberate command, fully integrated with a `STATE.md`-based continuity system. When a session is filling up — or you simply want to stop — `/graceful-handoff` reaches a natural stopping point, **overwrites the nearest `STATE.md`** with where you are, the next concrete step, and what to read, then commits per your remote policy. The next session reads that `STATE.md` and stands on its own.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -22,16 +22,17 @@ A Claude Code plugin that solves a structural problem with long sessions: the co
|
|||
|
||||
- [What Is This?](#what-is-this)
|
||||
- [The Problem](#the-problem)
|
||||
- [The STATE.md Model](#the-statemd-model)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Architecture](#architecture)
|
||||
- [How auto-trigger works](#how-auto-trigger-works)
|
||||
- [The Handoff Ritual](#the-handoff-ritual)
|
||||
- [Components](#components)
|
||||
- [Remote Policy — tracked vs local-only](#remote-policy--tracked-vs-local-only)
|
||||
- [Commands & Arguments](#commands--arguments)
|
||||
- [Workflow Examples](#workflow-examples)
|
||||
- [Safety Guarantees](#safety-guarantees)
|
||||
- [Testing](#testing)
|
||||
- [What Changed in v3.0 (BREAKING)](#what-changed-in-v30-breaking)
|
||||
- [Limitations & Open Assumptions](#limitations--open-assumptions)
|
||||
- [Version History](#version-history)
|
||||
- [License](#license)
|
||||
- [Feedback & Contributing](#feedback--contributing)
|
||||
|
||||
|
|
@ -39,36 +40,65 @@ A Claude Code plugin that solves a structural problem with long sessions: the co
|
|||
|
||||
## What Is This?
|
||||
|
||||
Three hooks plus one skill that handle session handoff for you:
|
||||
One skill — `/graceful-handoff` — plus a slim deterministic helper script. No hooks.
|
||||
|
||||
- **statusLine hint** at 60% and an urgent reminder at 70% — display only, always safe
|
||||
- **Stop-hook auto-execute** at estimated ≥70% — writes the artifact + creates a commit. Push remains user-triggered
|
||||
- **SessionStart auto-load** on `source: resume`/`compact` — handoff content is injected into the new session automatically; no `cat` needed
|
||||
- **Manual `/graceful-handoff`** — always works as a backup, with the same arguments
|
||||
Invoke it whenever you want to hand off. The session:
|
||||
|
||||
The skill itself is `disable-model-invocation: true`. The model cannot autonomously invoke handoff — only the user (via the slash command) or the Stop hook (which calls the pipeline script directly, not the skill) can trigger it. This is intentional: handoff is a moment that should be deliberate.
|
||||
1. **Reaches the first natural stopping point** — finishes the current logical unit, leaves the code working.
|
||||
2. **Overwrites the nearest `STATE.md`** with a complete state-of-play in a fixed format, headed by a mandatory `👉 NESTE — START HER` block (where we are + the next concrete step + what to read).
|
||||
3. **Commits per remote policy** — staging *only* `STATE.md` (when tracked), never `git add -A`.
|
||||
4. **Leaves push to you**, gated by your push window.
|
||||
|
||||
> [!TIP]
|
||||
> Install the plugin and forget about it. The first time the Stop hook fires, the artifact appears, a commit lands, and `git push` is yours to run when ready.
|
||||
The skill is `disable-model-invocation: true`: the model cannot trigger a handoff on its own. Handoff is a deliberate, user-initiated moment.
|
||||
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
Opus 4.7 fills the context window quickly. On real work — file reads, tool output, agent results — a session can hit 60–70% in five minutes. When it happens, three manual steps become rushed or skipped:
|
||||
Long sessions fill the context window. The classic failure mode is summarizing, committing, and writing a continuation prompt under time pressure — or skipping steps and losing continuity. Earlier versions of this plugin attacked that with auto-trigger hooks that wrote a separate `NEXT-SESSION-PROMPT` artifact.
|
||||
|
||||
1. Summarize the state of the work (commits, local changes, what was tested)
|
||||
2. Commit and push finished work (otherwise it is lost when the session ends)
|
||||
3. Write a copy-paste prompt that lets the next session continue without context loss
|
||||
That turned out to be the wrong shape for a continuity system built on `STATE.md`:
|
||||
|
||||
Doing these three things at 65% context, with the model already forgetting earlier turns, is exactly when mistakes happen. This plugin moves all three out of the critical path:
|
||||
- A separate `NEXT-SESSION` file **is exactly the kind of local handover invention** a single-source continuity system forbids. `STATE.md` already *is* the handoff.
|
||||
- The valuable part of a handoff — *where are we, what's the next step, what should I read* — **requires the session's understanding**. A deterministic `git log` snapshot can't write it; only the model can.
|
||||
- An auto-trigger hook has no session context, so it could only ever produce a poor snapshot — and would **overwrite a good, model-written `STATE.md`**.
|
||||
|
||||
- **Detection** — the Stop hook estimates context usage and fires at ≥70%
|
||||
- **Reversible execution** — the artifact is written and committed automatically
|
||||
- **Irreversible execution** — `git push` stays in your hands; the plugin will never push for you
|
||||
- **Continuation** — on the next session, the artifact is auto-loaded into context
|
||||
v3.0 fixes the shape: the model writes `STATE.md`; the script does the deterministic mechanics around it.
|
||||
|
||||
The ~10% gap between the 60% statusLine hint and the 70% Stop-hook trigger gives you a window to invoke `/graceful-handoff` manually if you want to control the slug or skip the commit.
|
||||
---
|
||||
|
||||
## The STATE.md Model
|
||||
|
||||
`STATE.md` is one layer of a three-layer continuity system:
|
||||
|
||||
| Layer | Job |
|
||||
|-------|-----|
|
||||
| **`STATE.md`** | Current state-of-play *where you work*. Overwritten (not appended) at session end. The nearest one (cwd → repo root) is auto-injected at session start by a global hook. |
|
||||
| auto-memory **`MEMORY.md`** | Durable facts about the user and preferences. |
|
||||
| **`CLAUDE.md`** | Invariants — rules, commands. |
|
||||
|
||||
Git history is the long-term log. This plugin owns the **write** side of `STATE.md` at session end. The **read** side (injecting the nearest `STATE.md` into a new session) is handled by the user's global `session-start.sh` — so the plugin ships no SessionStart loader of its own.
|
||||
|
||||
### Mandatory STATE.md format
|
||||
|
||||
```markdown
|
||||
# STATE — <name>
|
||||
_Current state-of-play. Overwritten (not appended) at session end. History → git._
|
||||
|
||||
## 👉 NESTE — START HER
|
||||
**<one line: where we are + the next concrete action>**
|
||||
1. **(you/me)** <concrete step>
|
||||
2. **(you/me)** <concrete step>
|
||||
<pointers: which files / plans / memory MUST be read to stand on its own>
|
||||
|
||||
## Mission & run-mode
|
||||
## Gotchas / inviolable rules
|
||||
## Push status
|
||||
## Repo & env
|
||||
## Session sequence (short — most of it lives in git)
|
||||
```
|
||||
|
||||
The `👉 NESTE` block is always first, right after the title — it carries the actionable part so the operator never has to hunt for it.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -77,8 +107,8 @@ The ~10% gap between the 60% statusLine hint and the 70% Stop-hook trigger gives
|
|||
### Prerequisites
|
||||
|
||||
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) v2.x+
|
||||
- Node.js (any recent LTS — required for hook and pipeline scripts)
|
||||
- Git repository (the pipeline detects detached HEAD and missing upstream and reports gracefully — it never crashes)
|
||||
- Node.js (any recent LTS — for the helper script)
|
||||
- A git repository (the helper detects detached HEAD / missing upstream and reports gracefully)
|
||||
|
||||
### Install
|
||||
|
||||
|
|
@ -96,25 +126,13 @@ Or enable directly in `~/.claude/settings.json`:
|
|||
}
|
||||
```
|
||||
|
||||
The three hooks activate immediately on install. No further configuration needed.
|
||||
|
||||
### First handoff
|
||||
|
||||
Manual:
|
||||
### Hand off
|
||||
|
||||
```
|
||||
> /graceful-handoff
|
||||
```
|
||||
|
||||
Or just keep working — when context crosses the estimated 70% threshold, the Stop hook fires automatically:
|
||||
|
||||
```
|
||||
⚠️ Auto-handoff utført ved estimert 72% [kilde: direct]:
|
||||
artefakt /path/NEXT-SESSION-PROMPT.local.md.
|
||||
Push gjenstår — kjør `git push` når du er klar.
|
||||
```
|
||||
|
||||
Start the next session with `claude --resume` and the artifact is loaded into context automatically.
|
||||
The session finishes the current unit, writes `STATE.md`, commits (if your remote policy allows), and prints a fixed closing line: **STATE.md status · next session's first action · commit/push status.** Start the next session and the nearest `STATE.md` is loaded into context automatically by your global session-start hook.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -122,59 +140,38 @@ Start the next session with `claude --resume` and the artifact is loaded into co
|
|||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Detection["Detection — display + auto-trigger"]
|
||||
direction LR
|
||||
SL["statusLine<br/>60% hint, 70% urgent"]
|
||||
SH["Stop hook<br/>≥70% estimated"]
|
||||
end
|
||||
|
||||
subgraph Pipeline["Deterministic pipeline (Node, no LLM)"]
|
||||
direction LR
|
||||
P["handoff-pipeline.mjs<br/>classify → write → stage → commit"]
|
||||
end
|
||||
|
||||
subgraph Resumption["Resumption — auto-load"]
|
||||
direction LR
|
||||
SS["SessionStart hook<br/>resume / compact only"]
|
||||
AR["Archive after read<br/>*.archived.local.md"]
|
||||
end
|
||||
|
||||
subgraph Manual["Manual fallback"]
|
||||
direction LR
|
||||
SK["SKILL.md<br/>disable-model-invocation: true"]
|
||||
end
|
||||
|
||||
SL -.display only.-> User
|
||||
SH -->|spawns| P
|
||||
SK -->|invokes| P
|
||||
P -->|writes| AR
|
||||
SS -->|reads| AR
|
||||
User((user)) -->|/graceful-handoff| SK
|
||||
User -->|git push| Done((done))
|
||||
|
||||
subgraph Skill["SKILL.md — model-driven ritual (full session context)"]
|
||||
SK["1. reach natural stopping point<br/>3. WRITE STATE.md (👉 NESTE block)<br/>7. fixed closing line"]
|
||||
end
|
||||
|
||||
subgraph Helper["handoff-pipeline.mjs — deterministic, no LLM"]
|
||||
PLAN["--plan<br/>resolve nearest STATE.md<br/>classify remote · git facts"]
|
||||
COMMIT["--commit<br/>stage ONLY STATE.md (+ --also)<br/>never git add -A"]
|
||||
end
|
||||
|
||||
SK -->|2. fetch facts| PLAN
|
||||
PLAN -->|JSON| SK
|
||||
SK -->|4. safe commit| COMMIT
|
||||
User -->|5. git push, in-window| Done((done))
|
||||
```
|
||||
|
||||
Three independent layers: **detection** (hooks watching context), **pipeline** (deterministic script that does the work), **resumption** (hook that loads the artifact in the next session). Each layer is testable in isolation. The pipeline has no LLM dependencies — `node:test` runs it against fixtures in <8 s.
|
||||
Two pieces, one responsibility each: the **skill** synthesizes `STATE.md` (only it has the context); the **helper** does the deterministic mechanics (path resolution, remote classification, safe staging). The helper has no LLM dependencies and runs under `node:test` in seconds.
|
||||
|
||||
---
|
||||
|
||||
## How auto-trigger works
|
||||
## The Handoff Ritual
|
||||
|
||||
Claude Code does not expose real-time context-percentage to hooks (Anthropic has closed feature requests [#16988](https://github.com/anthropics/claude-code/issues/16988), [#27969](https://github.com/anthropics/claude-code/issues/27969), [#34340](https://github.com/anthropics/claude-code/issues/34340)). Instead, the Stop hook uses a **4-step resolution chain** (v2.1, in `resolveContextSource()`):
|
||||
`SKILL.md` instructs the session to, in order:
|
||||
|
||||
| Step | Source | When used | Source label |
|
||||
|------|--------|-----------|--------------|
|
||||
| 1 | `payload.context_window.used_percentage` | If the field is present and > 0 | `direct` |
|
||||
| 2 | `payload.context_window.context_window_size` + transcript estimate (`chars / 3.5`) | If size > 0 but no `used_percentage` | `payload-size` |
|
||||
| 3 | `MODEL_WINDOWS[payload.model.id]` + transcript estimate | Opus 4.7 = 1 M, Sonnet 4.6 = 200 K, Haiku = 200 K | `model-map` |
|
||||
| 4 | `FALLBACK_WINDOW = 1_000_000` + transcript estimate | Last-resort default (2026-aware) | `default-1m` |
|
||||
|
||||
When the resolved percentage is ≥ 70%, the Stop hook spawns `handoff-pipeline.mjs --auto --no-push --non-interactive` synchronously (25 s timeout, fits within the 30 s Stop-hook budget). The `additionalContext` message includes `[kilde: <source>]` so the source path is always visible.
|
||||
|
||||
**Estimation drift:** Steps 2–4 use `chars / 3.5` to approximate tokens, which can drift ±10% from Claude's internal counting. The 70% threshold is conservative buffer. Step 1 (`direct`) has no drift.
|
||||
|
||||
**Lock file:** `<transcript_dir>/.handoff-lock-<session_id>` is created on first trigger to prevent repeat firing within the same session. Touch happens *before* spawning to win races on rapid Stop events.
|
||||
|
||||
**Why 70% (not 65%)?** Earlier designs targeted 65%, but estimation drift and Stop-hook latency make 70% safer. Lower thresholds risk false positives that block normal continuation.
|
||||
1. **Reach the first natural stopping point.** Finish the current logical unit; leave the code working. Don't cut mid-change; don't start new work.
|
||||
2. **`--plan`** — fetch deterministic facts (resolved `STATE.md` path, remote class, git status, recent commits).
|
||||
3. **Write / overwrite `STATE.md`** in the mandatory format (title → `👉 NESTE` block → fixed sections → short history). Max ~60 lines. Overwrite, never append.
|
||||
4. **`--commit`** — stage only `STATE.md` (when tracked) plus any explicit `--also` paths the model judges belong to the unit. Never `git add -A`.
|
||||
5. **Push** — check the window (`date '+%u %H:%M'`). In-window: push (Forgejo only, never GitHub). Out-of-window: park it and say so explicitly.
|
||||
6. **MEMORY.md check** — if it exceeds 200 lines, warn (don't auto-edit).
|
||||
7. **Fixed closing line** — STATE.md status (path) · next session's first action (= the `👉 NESTE` block) · commit/push status.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -185,160 +182,106 @@ When the resolved percentage is ≥ 70%, the Stop hook spawns `handoff-pipeline.
|
|||
```yaml
|
||||
---
|
||||
name: graceful-handoff
|
||||
description: Produser handoff-artefakt, commit+push, og copy-paste-prompt for neste sesjon.
|
||||
description: Reach a natural stopping point, overwrite the nearest STATE.md (👉 NESTE block), commit per remote policy, write the fixed closing line.
|
||||
argument-hint: "[--no-commit] [--dry-run]"
|
||||
disable-model-invocation: true
|
||||
model: claude-sonnet-4-6
|
||||
allowed-tools: Bash(git:*) Bash(jq:*) Bash(node:*) Bash(find:*) Bash(pwd:*) Read Write Glob
|
||||
allowed-tools: Bash(git:*) Bash(node:*) Bash(date:*) Read Write Edit Glob
|
||||
---
|
||||
```
|
||||
|
||||
Thin orchestration wrapper around the pipeline script. Pinned to Sonnet 4.6 to free Opus budget for the next session. `disable-model-invocation: true` prevents the model from calling the skill on its own — handoff is always user- or hook-triggered.
|
||||
No `model:` pin — the skill inherits the session model, because writing a self-standing `STATE.md` is human-facing synthesis that warrants top-tier quality.
|
||||
|
||||
> [!NOTE]
|
||||
> `allowed-tools` is *pre-approval*, not restriction. It removes permission prompts for the listed tools but does not block other tools from being invoked. For real sandboxing, use project-level `permissions.deny` rules.
|
||||
> `allowed-tools` is *pre-approval*, not a sandbox. It removes permission prompts for the listed tools but does not block others. For real sandboxing, use project-level `permissions.deny` rules.
|
||||
|
||||
### Pipeline — `scripts/handoff-pipeline.mjs`
|
||||
### Helper — `scripts/handoff-pipeline.mjs`
|
||||
|
||||
Deterministic Node script. Returns structured JSON. No LLM dependencies. Handles:
|
||||
Deterministic Node script, structured JSON output, no LLM dependencies:
|
||||
|
||||
- Classification of handoff type (`multi-sesjon` / `plugin-arbeid` / `enkelt-oppgave`) based on cwd
|
||||
- Writing the NEXT-SESSION artifact in the correct directory
|
||||
- **Explicit staging** of only the artifact (+ `REMEMBER.md` / `TODO.md` if present) — *never* `git add -A`, enforced by a regression test
|
||||
- Commit-message generation from `git diff --stat` (Conventional Commits)
|
||||
- Push (unless `--no-push`) with detached-HEAD and no-upstream detection
|
||||
- Idempotency check: 60 s cooldown on a clean tree with a recent artifact is a no-op
|
||||
- **`--plan`** (default, read-only): resolve the nearest `STATE.md` (cwd → repo root), classify the `origin` remote, gather git facts, and report whether `STATE.md` is gitignored (the authoritative commit signal) plus a `leak_warning` on misconfiguration.
|
||||
- **`--commit`**: stage `STATE.md` *only* when it is tracked, plus explicit `--also <path>` paths; commit with a generated or supplied message. **Never `git add -A`. Never pushes.** Detached-HEAD guard.
|
||||
- **`--dry-run`**: emit the plan JSON, write nothing, touch no git.
|
||||
|
||||
### Hooks — `hooks/scripts/`
|
||||
---
|
||||
|
||||
| Event | Script | What it does |
|
||||
|-------|--------|--------------|
|
||||
| `statusLine` | `statusline-monitor.mjs` | Reads `context_window.used_percentage` from payload. <60% silent, 60–69% hint, ≥70% urgent reminder. Display only — never runs git (statusLine scripts are cancellable mid-flight per official docs) |
|
||||
| `Stop` | `stop-context-monitor.mjs` | Resolves context via the 4-step chain. At ≥70% spawns the pipeline with `--auto --no-push --non-interactive`. Uses a lock file to prevent repeat firing |
|
||||
| `SessionStart` | `session-start-load-handoff.mjs` | On `source: resume` or `source: compact`, finds the most recent `NEXT-SESSION-*.local.md` (cwd + 3 levels up), injects the content via `additionalContext`, archives the file (`*.archived.local.md`) to prevent stale-load on subsequent sessions |
|
||||
## Remote Policy — tracked vs local-only
|
||||
|
||||
Registered in `hooks/hooks.json`.
|
||||
`STATE.md` must never reach a public mirror. The helper classifies `origin`:
|
||||
|
||||
| Remote | Class | STATE.md |
|
||||
|--------|-------|----------|
|
||||
| `github.com` / `github.io` | public | local-only (gitignored), never committed |
|
||||
| `…/open/<repo>` (public Forgejo mirror) | public | local-only (gitignored), never committed |
|
||||
| `…/ktg/<repo>` (private Forgejo) | private | tracked + committed |
|
||||
| no remote | none | local-only |
|
||||
|
||||
The authoritative commit decision is `git check-ignore STATE.md` — so a misclassified remote can never cause a leak; it only produces a `leak_warning` when the remote looks public but `STATE.md` is *not* gitignored. **This plugin's own repo has an `open/` remote, so its `STATE.md` is local-only.**
|
||||
|
||||
---
|
||||
|
||||
## Commands & Arguments
|
||||
|
||||
```
|
||||
/graceful-handoff [topic-slug] [flags]
|
||||
/graceful-handoff [flags]
|
||||
```
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `[topic-slug]` | Kebab-case slug. With slug: `NEXT-SESSION-<slug>.local.md`. Without: `NEXT-SESSION-PROMPT.local.md` |
|
||||
| `--no-commit` | Skip commit + push. Artifact is written; user handles git manually |
|
||||
| `--no-push` | Commit OK, but skip push (the Stop hook always uses this) |
|
||||
| `--dry-run` | No files written, no git operations; print what would happen |
|
||||
| `--auto` | Non-interactive, auto-Y on commit confirmation. Intended for hooks |
|
||||
| `--non-interactive` | Without `--auto`: error. With `--auto`: run without any prompts |
|
||||
| `--no-commit` | Write `STATE.md`, skip commit/push (you handle git manually) |
|
||||
| `--dry-run` | Write nothing, no git — show the `--plan` JSON for inspection |
|
||||
|
||||
The pipeline script accepts the same flags directly (`node scripts/handoff-pipeline.mjs ...`) — useful for debugging without going through the skill.
|
||||
|
||||
---
|
||||
|
||||
## Workflow Examples
|
||||
|
||||
### Plugin work (auto-trigger)
|
||||
|
||||
```
|
||||
cd plugins/llm-security
|
||||
# ... work until ~70% context ...
|
||||
# Stop hook fires automatically:
|
||||
# → writes plugins/llm-security/NEXT-SESSION-PROMPT.local.md
|
||||
# → stages ONLY the artifact (not other dirty files)
|
||||
# → commits with auto-generated Conventional Commits message
|
||||
# → does NOT push
|
||||
git push # when you are ready
|
||||
```
|
||||
|
||||
### Manual trigger with slug
|
||||
|
||||
```
|
||||
/graceful-handoff refactor-auth --no-commit
|
||||
# → writes plugins/<root>/NEXT-SESSION-refactor-auth.local.md
|
||||
# → no git operations
|
||||
```
|
||||
|
||||
### New session
|
||||
|
||||
```
|
||||
claude --resume
|
||||
# SessionStart hook auto-injects handoff content into context
|
||||
# Continue working immediately
|
||||
# The artifact is renamed to NEXT-SESSION-*.archived.local.md
|
||||
# so it cannot stale-load on a third session
|
||||
```
|
||||
|
||||
### Dry run before committing the workflow
|
||||
|
||||
```
|
||||
/graceful-handoff --dry-run
|
||||
# Pipeline prints the JSON it would produce — file paths, commit message,
|
||||
# next steps — without writing anything or touching git
|
||||
```
|
||||
The helper accepts the same modes directly (`node scripts/handoff-pipeline.mjs --plan` / `--commit` / `--dry-run`) — useful for debugging without going through the skill.
|
||||
|
||||
---
|
||||
|
||||
## Safety Guarantees
|
||||
|
||||
These properties are enforced by tests, not by convention:
|
||||
Enforced by tests, not convention:
|
||||
|
||||
- **Push is never automatic.** The auto-execute path always passes `--no-push`. Irreversible operations stay in the user's hands. (`stop-context-monitor.test.mjs`)
|
||||
- **Staging is explicit.** The pipeline stages *only* the handoff artifact (and `REMEMBER.md` / `TODO.md` if present). `git add -A` is never used — a regression test (`pipeline never stages unrelated dirty files`) enforces this.
|
||||
- **Pre-commit hooks are respected.** The pipeline never bypasses with `--no-verify`. If a pre-commit hook (gitleaks, pathguard) blocks, the handoff fails and the user fixes the underlying issue.
|
||||
- **Artifacts are gitignored.** All output files match `*.local.md`, which existing repos in this marketplace already gitignore via `.gitignore` patterns.
|
||||
- **No network calls.** No WebSearch, no Agent delegation, no MCP. The pipeline is fully local.
|
||||
- **Bash sub-scoped.** Skill `allowed-tools` enumerates `Bash(git:*) Bash(jq:*) Bash(node:*) Bash(find:*) Bash(pwd:*)` — pre-approval is narrow even though it is not a sandbox.
|
||||
- **Lock file scoped to transcript directory.** Lock path is based on `dirname(transcript_path)`, not `cwd`, so it survives `cd` mid-session.
|
||||
- **Staging is explicit.** `--commit` stages *only* `STATE.md` (plus explicit `--also` paths). `git add -A` is never used — a regression test (`never stages unrelated dirty files`) enforces this.
|
||||
- **STATE.md never leaks to a public mirror.** On a public/`open/` remote `STATE.md` is gitignored and the helper refuses to commit it (`local-only-skipped`), verified by test.
|
||||
- **Push is never automatic.** The helper never pushes; push stays user-triggered and window-gated.
|
||||
- **Pre-commit hooks are respected.** The helper never uses `--no-verify`.
|
||||
- **No network calls.** No WebSearch, no Agent delegation, no MCP — fully local.
|
||||
- **Bash sub-scoped.** Skill `allowed-tools` enumerates `Bash(git:*) Bash(node:*) Bash(date:*)`.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
node --test 'plugins/graceful-handoff/tests/**/*.test.mjs'
|
||||
node --test 'tests/**/*.test.mjs'
|
||||
```
|
||||
|
||||
57 tests across 6 files:
|
||||
30 tests across 3 files:
|
||||
|
||||
| File | Coverage |
|
||||
|------|----------|
|
||||
| `tests/skill-structure.test.mjs` | SKILL.md frontmatter, model pin, allowed-tools shape, removal of legacy `commands/` |
|
||||
| `tests/scripts/handoff-pipeline.test.mjs` | Pipeline JSON schema, idempotency, **no-staging-regression**, detached HEAD, no-upstream, interactive y/n |
|
||||
| `tests/hooks/statusline-monitor.test.mjs` | Threshold transitions, null payload, malformed JSON, no side effects |
|
||||
| `tests/hooks/stop-context-monitor.test.mjs` | 4-step context resolution, lock file behavior, stub-pipeline isolation, env-var failure modes |
|
||||
| `tests/hooks/session-start-load-handoff.test.mjs` | Source filter (`resume`/`compact` only), multi-level search, archive after read |
|
||||
| `tests/plugin-manifest.test.mjs` | Plugin.json schema, version pin, CHANGELOG entries |
|
||||
| `tests/skill-structure.test.mjs` | SKILL.md frontmatter (no `model:` pin, `Write` in allowed-tools, `disable-model-invocation`), `👉 NESTE` mandate, removal of `commands/` and `hooks/` |
|
||||
| `tests/scripts/handoff-pipeline.test.mjs` | `--plan`/`--commit`/`--dry-run`, nearest-STATE resolution, remote classification, **no-`git add -A` regression**, gitignored-STATE skip, detached HEAD |
|
||||
| `tests/plugin-manifest.test.mjs` | plugin.json version + description, CHANGELOG `[3.0.0]` BREAKING + preserved history |
|
||||
|
||||
Stop-hook tests use a **stub pipeline** (a fake `handoff-pipeline.mjs` written into a temp dir) so test runs do not invoke real git operations against the marketplace repo.
|
||||
The suite runs in a few seconds — no LLM involvement.
|
||||
|
||||
The pipeline runs in <8 s on a 2025 Mac. The full test suite runs in ~10 s.
|
||||
---
|
||||
|
||||
## What Changed in v3.0 (BREAKING)
|
||||
|
||||
- **Writes `STATE.md`, not a `NEXT-SESSION` artifact.** Full integration with the three-layer continuity system; the separate handover file is gone.
|
||||
- **All three hooks removed.** Stop auto-trigger, statusLine hint, and SessionStart loader deleted — redundant (global hook already injects `STATE.md`), dead (user statusLine wins), or harmful (would overwrite a good `STATE.md`).
|
||||
- **Inverted architecture.** The model writes `STATE.md`; the script is a slim deterministic helper.
|
||||
- **Remote-aware tracked/local-only policy** so `STATE.md` never leaks to a public mirror.
|
||||
- **Removed the Sonnet pin** — handoff synthesis inherits the session model.
|
||||
|
||||
Full history in [`CHANGELOG.md`](CHANGELOG.md).
|
||||
|
||||
---
|
||||
|
||||
## Limitations & Open Assumptions
|
||||
|
||||
- **Token estimation drifts ±10%** against Claude's internal counting (steps 2–4 of the resolution chain). The 70% threshold is set conservatively to absorb this. Step 1 (`direct`) has no drift but requires the payload field to be present.
|
||||
- **Stop-hook payload schema is undocumented.** It is not officially confirmed that Stop payloads include `used_percentage` or `model.id` (statusLine payloads do). If both are missing, the resolver falls through to `default-1m`. The `[kilde: <source>]` label in `additionalContext` reveals which path was actually used — first real session reveals this.
|
||||
- **statusLine placement in `hooks/hooks.json` is an open assumption.** Smoke-test before relying on it; the fallback is to move it to global `~/.claude/settings.json`.
|
||||
- **Issue [#26251](https://github.com/anthropics/claude-code/issues/26251)** — `disable-model-invocation: true` may regress and block user-invocation in some Claude Code versions. Manual smoke-test before relying on it.
|
||||
- **Auto-execute does not push.** Irreversible operations stay user-triggered, by design.
|
||||
- **Compaction events are out of scope.** PreCompact fires too late (~95%) and is not configurable. The plugin targets the 60–70% window where the user can still benefit from a clean handoff.
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Highlights |
|
||||
|---------|------|------------|
|
||||
| **2.1.0** | 2026-05-01 | **Model-aware context window detection.** Replaces 200 K fallback with 4-step resolution chain (`used_percentage` → `payload-size` → `model-map` → 1 M default). Fixes 5–7× premature firing on Opus 4.7 (1 M window). All `additionalContext` messages include `[kilde: <source>]` for transparency. 6 new tests (57 total). |
|
||||
| **2.0.0** | 2026-05-01 | **Hard cut from `commands/` to `skills/`.** New deterministic pipeline (`handoff-pipeline.mjs`), three hooks (statusLine, Stop, SessionStart), `disable-model-invocation: true`, sub-scoped `allowed-tools`, explicit staging discipline (no more `git add -A`), pinned to Sonnet 4.6 (BREAKING). |
|
||||
| **1.0.0** | 2026-04-19 | Initial release — single declarative `/graceful-handoff` command, 6-phase prose workflow, three handoff types, pre-commit hook respect, <60 s time budget. |
|
||||
|
||||
Full history in [`CHANGELOG.md`](CHANGELOG.md).
|
||||
- **`disable-model-invocation: true`** has open issue [#26251](https://github.com/anthropics/claude-code/issues/26251) — it may regress and block user-invocation in some Claude Code versions. Smoke-test after install.
|
||||
- **Remote classification is heuristic** (`open/` / `github` → public). The authoritative commit decision is `git check-ignore STATE.md`, so misclassification cannot cause a leak — only an advisory `leak_warning`.
|
||||
- **STATE.md read-side depends on the global session-start hook.** This plugin only writes; injecting the nearest `STATE.md` at session start is the user's global `~/.claude/hooks/session-start.sh`.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue