graceful-handoff/README.md
Kjell Tore Guttormsen 13e29723a7 docs(graceful-handoff): bring README in line with the org repo standard
Ran the repo-standard gate (plugin class): 5 ERROR + 2 WARN + 2 SKIP.
Now 0 ERROR / 0 WARN / 0 SKIP, 13 checks passing. Docs only — no plugin
code, version or behaviour changed.

- H1 is the repo name; opening line is verbatim the forge description, so
  description == catalog == README is machine-checkable.
- `## Install` promoted to a top-level heading on the first screen and given
  the CLI command `claude plugin install graceful-handoff@ktg-plugin-marketplace`
  next to `marketplace add` (the enabledPlugins JSON stays as a second form).
  Prerequisites split out as `## Requirements`; Quick Start keeps the usage.
- Added `## Non-goals` (what it deliberately does not do) and `## Changelog`.
- Dropped the static tests-30 badge: no CI runner exists on this forge, so it
  asserted a run nothing verifies. The Testing section now states the command
  and that nothing runs it automatically.
- Fixed two dead `../../README.md` references inherited from the monorepo —
  both point outside a standalone repo and 404.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019x1C8tjmB2vUCM9jE668ZF
2026-08-03 21:53:11 +02:00

319 lines
15 KiB
Markdown

# graceful-handoff
One-command session handoff into the STATE.md continuity system. Overwrites the nearest STATE.md with a complete state-of-play and commits per remote policy.
> **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. Every change is human-directed, reviewed, and validated before commit.*
![Version](https://img.shields.io/badge/version-3.1.0-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Skill](https://img.shields.io/badge/skill-1-green)
![Hooks](https://img.shields.io/badge/hooks-0-lightgrey)
![Pipeline](https://img.shields.io/badge/STATE--helper-deterministic-cyan)
![License](https://img.shields.io/badge/license-MIT-lightgrey)
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.
---
## Install
```bash
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
claude plugin install graceful-handoff@ktg-plugin-marketplace
```
Or enable directly in `~/.claude/settings.json`:
```json
{
"enabledPlugins": {
"graceful-handoff@ktg-plugin-marketplace": true
}
}
```
## Requirements
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) v2.x+
- Node.js (any recent LTS — for the helper script)
- A git repository (the helper detects detached HEAD / missing upstream and reports gracefully)
---
## Table of Contents
- [What Is This?](#what-is-this)
- [Non-goals](#non-goals)
- [The Problem](#the-problem)
- [The STATE.md Model](#the-statemd-model)
- [Quick Start](#quick-start)
- [Architecture](#architecture)
- [The Handoff Ritual](#the-handoff-ritual)
- [Components](#components)
- [Remote Policy — tracked vs local-only](#remote-policy--tracked-vs-local-only)
- [Commands & Arguments](#commands--arguments)
- [Safety Guarantees](#safety-guarantees)
- [Testing](#testing)
- [What Changed in v3.0 (BREAKING)](#what-changed-in-v30-breaking)
- [Limitations & Open Assumptions](#limitations--open-assumptions)
- [Changelog](#changelog)
- [License](#license)
- [Feedback & Contributing](#feedback--contributing)
---
## What Is This?
One skill — `/graceful-handoff` — plus a slim deterministic helper script. No hooks.
Invoke it whenever you want to hand off. The session:
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** — push to Forgejo, you-triggered (no window gate).
The skill is `disable-model-invocation: true`: the model cannot trigger a handoff on its own. Handoff is a deliberate, user-initiated moment.
---
## Non-goals
What this plugin deliberately does *not* do — read this before adopting it:
- **It never triggers itself.** No hooks, no Stop auto-trigger, no context-percentage threshold. If you want a handoff written automatically when a session fills up, this is the wrong plugin — v2 did that and v3 removed it on purpose.
- **It does not read `STATE.md` back.** Injecting the nearest `STATE.md` at session start is the job of a global `session-start.sh` hook you own. This plugin only writes.
- **It does not push.** The helper has no push path at all; pushing stays a separate, user-triggered step.
- **It is not a general-purpose commit tool.** `--commit` stages `STATE.md` plus explicitly named `--also` paths, and nothing else. Unrelated dirty files stay dirty — by design.
- **It does not edit `MEMORY.md` or `CLAUDE.md`.** It warns when `MEMORY.md` grows past 200 lines; the edit is yours.
- **It does not publish anything.** `STATE.md` is internal state-of-play, never a public artifact — on a public remote it stays gitignored and uncommitted.
---
## The Problem
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.
That turned out to be the wrong shape for a continuity system built on `STATE.md`:
- 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`**.
v3.0 fixes the shape: the model writes `STATE.md`; the script does the deterministic mechanics around it.
---
## 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.
---
## Quick Start
Installed? Then the whole workflow is one command:
```
> /graceful-handoff
```
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.
---
## Architecture
```mermaid
flowchart TB
User((user)) -->|/graceful-handoff| SK
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 (Forgejo)| Done((done))
```
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.
---
## The Handoff Ritual
`SKILL.md` instructs the session to, in order:
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** — Forgejo only, never GitHub. Push is unconditional — no window gate.
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.
---
## Components
### Skill — `skills/graceful-handoff/SKILL.md`
```yaml
---
name: graceful-handoff
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
allowed-tools: Bash(git:*) Bash(node:*) Bash(date:*) Read Write Edit Glob
---
```
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 a sandbox. It removes permission prompts for the listed tools but does not block others. For real sandboxing, use project-level `permissions.deny` rules.
### Helper — `scripts/handoff-pipeline.mjs`
Deterministic Node script, structured JSON output, no LLM dependencies:
- **`--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.
---
## Remote Policy — tracked vs local-only
`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 [flags]
```
| Argument | Description |
|----------|-------------|
| `--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 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
Enforced by tests, not convention:
- **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 (no window gate); never automatic.
- **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 'tests/**/*.test.mjs'
```
30 tests across 3 files:
| File | Coverage |
|------|----------|
| `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 |
The suite runs in a few seconds — no LLM involvement. **Nothing runs it automatically:** this forge has no CI runner, so the command above from a clean clone is the only verification. That is also why this README carries no test badge — a static one would assert a run nobody performed.
---
## 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.
---
## Limitations & Open Assumptions
- **`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`.
---
## Changelog
See [CHANGELOG.md](CHANGELOG.md).
---
## License
MIT. See [`LICENSE`](LICENSE).
---
## Feedback & Contributing
- **Bug reports + feature requests:** open an issue on [Forgejo](https://git.fromaitochitta.com/open/ktg-plugin-marketplace)
- **Pull requests:** not accepted on this repo (solo project, dialog-driven development with Claude Code). Fork freely if you need to extend.
- **Marketplace:** part of [ktg-plugin-marketplace](https://git.fromaitochitta.com/open/ktg-plugin-marketplace) — see the catalog for related plugins.