diff --git a/plugins/graceful-handoff/.claude-plugin/plugin.json b/plugins/graceful-handoff/.claude-plugin/plugin.json index 632d90b..49119ea 100644 --- a/plugins/graceful-handoff/.claude-plugin/plugin.json +++ b/plugins/graceful-handoff/.claude-plugin/plugin.json @@ -1,11 +1,10 @@ { "name": "graceful-handoff", - "version": "1.0.0", - "description": "Produce session-handoff artifacts, commit and push pending work, and print a copy-paste prompt for the next session. Designed for context-constrained models like Opus 4.7 where sessions fill fast.", + "version": "2.0.0", + "description": "Auto-trigger session handoff at context-threshold (Stop hook + statusLine hint), with manual /graceful-handoff fallback. Skill-architecture (disable-model-invocation: true) + JSON pipeline + auto-load on session resume.", "author": { "name": "Kjell Tore Guttormsen" }, - "auto_discover": true, "license": "MIT", "repository": "https://git.fromaitochitta.com/open/ktg-plugin-marketplace", "keywords": [ @@ -14,6 +13,8 @@ "context-management", "opus-4.7", "git", - "workflow" + "workflow", + "auto-trigger", + "skills" ] } diff --git a/plugins/graceful-handoff/CHANGELOG.md b/plugins/graceful-handoff/CHANGELOG.md index 94e48d5..c942536 100644 --- a/plugins/graceful-handoff/CHANGELOG.md +++ b/plugins/graceful-handoff/CHANGELOG.md @@ -4,6 +4,45 @@ All notable changes to graceful-handoff are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [2.0.0] - 2026-05-01 + +### BREAKING + +- **Hard cut from `commands/` to `skills/`.** The plugin now ships a single SKILL.md at `skills/graceful-handoff/SKILL.md` with `disable-model-invocation: true` and `model: claude-sonnet-4-6`. The legacy `commands/graceful-handoff.md` is deleted. User-invocation `/graceful-handoff` works as before. +- **Architecture rewrite.** The 6-phase prose workflow is replaced by a deterministic Node script `scripts/handoff-pipeline.mjs` that returns structured JSON. SKILL.md is now a thin orchestration wrapper. Tests run directly against the pipeline without LLM involvement. +- Removed `auto_discover: true` from `plugin.json` (not in documented schema; silently ignored anyway per research/05). + +### Added + +- **Auto-trigger via Stop hook (`hooks/scripts/stop-context-monitor.mjs`).** Estimates context usage from transcript size; at estimated ≥70%, auto-writes the artifact and creates a commit. Push remains user-triggered (separates reversible from irreversible). Lock file at `/.handoff-lock-` prevents repeat firing within a session. +- **Context hint via statusLine (`hooks/scripts/statusline-monitor.mjs`).** Reads `context_window.used_percentage` from payload; prints a hint at 60% and an urgent reminder at 70%. Display-only — never runs git (unsafe per research/03). +- **Auto-load via SessionStart hook (`hooks/scripts/session-start-load-handoff.mjs`).** On `source: resume` or `source: compact`, finds `NEXT-SESSION-*.local.md` (cwd + 3 levels up), injects content via `additionalContext`, archives the file (`*.archived.local.md`) to prevent stale-load. +- Commit-message confirmation gate: pipeline prints message to stderr, reads `y/n` from stdin (interactive). `--auto` flag bypasses for hook-driven invocations. +- New flags: `--no-push` (commit but don't push), `--auto` (non-interactive auto-Y), `--non-interactive`. +- Pipeline robustness: detached HEAD detection, no-upstream detection, idempotency check (60s cooldown on clean tree), pre-commit hook respect. +- 36 unit tests across 5 test files (skill-structure, pipeline, statusline-monitor, stop-context-monitor, session-start-load-handoff). + +### Changed + +- **Pipeline staging discipline (CRITICAL).** Pipeline now stages ONLY the handoff artifact (and REMEMBER.md/TODO.md if present). Previously used `git add -A` which scoops up unrelated work-in-progress. The new behavior is enforced by a regression test. +- `allowed-tools` is now Bash sub-scoped (`Bash(git:*) Bash(node:*) Bash(jq:*) ...`) instead of an open `Bash`. Note: per research/02, this is pre-approval (not restriction) — to actually block tools, project-level deny rules are needed. +- Plugin model is pinned to `claude-sonnet-4-6` (was: inherit from session). Frees Opus 4.7 budget for the next session that the user is actually entering. + +### Known limitations + +- statusLine placement in `hooks/hooks.json` is an open assumption (research/03 confirmed statusLine config exists, but exact placement vs `settings.json` is unverified). Smoke-test required. +- Token estimation in Stop hook uses `chars/3.5` heuristic — may drift ±10% from Claude's internal counting. The 70% threshold is conservatively set. +- `disable-model-invocation: true` has open issue [#26251](https://github.com/anthropics/claude-code/issues/26251); manual smoke-test recommended before relying on it. +- Auto-execute does not push: irreversible operations remain user-triggered. + +### Migration from v1.0.0 + +There is no automatic migration. v2.0.0 is a breaking change. + +1. Reinstall the plugin to pick up `skills/` and remove `commands/`. +2. The `/graceful-handoff` slash command works identically from the user's perspective. +3. The new auto-trigger features activate automatically when the plugin's hooks are loaded. + ## [1.0.0] - 2026-04-19 ### Added diff --git a/plugins/graceful-handoff/tests/plugin-manifest.test.mjs b/plugins/graceful-handoff/tests/plugin-manifest.test.mjs new file mode 100644 index 0000000..1ba3967 --- /dev/null +++ b/plugins/graceful-handoff/tests/plugin-manifest.test.mjs @@ -0,0 +1,46 @@ +// plugin-manifest.test.mjs — verify plugin.json schema for v2.0 + +import { test } from 'node:test'; +import { strict as assert } from 'node:assert'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const MANIFEST = join(__dirname, '..', '.claude-plugin', 'plugin.json'); +const CHANGELOG = join(__dirname, '..', 'CHANGELOG.md'); + +test('plugin.json version is 2.0.0', () => { + const m = JSON.parse(readFileSync(MANIFEST, 'utf-8')); + assert.equal(m.version, '2.0.0'); +}); + +test('plugin.json does NOT include auto_discover (not in documented schema)', () => { + const m = JSON.parse(readFileSync(MANIFEST, 'utf-8')); + assert.ok(!('auto_discover' in m), 'auto_discover field should be removed'); +}); + +test('plugin.json description mentions auto-trigger or context-threshold', () => { + const m = JSON.parse(readFileSync(MANIFEST, 'utf-8')); + assert.match(m.description, /auto-trigger|context-threshold/i); +}); + +test('CHANGELOG has [2.0.0] entry', () => { + const c = readFileSync(CHANGELOG, 'utf-8'); + assert.match(c, /## \[2\.0\.0\]/); +}); + +test('CHANGELOG [2.0.0] entry has BREAKING section', () => { + const c = readFileSync(CHANGELOG, 'utf-8'); + // Get content from [2.0.0] until next ## or end + const match = c.match(/## \[2\.0\.0\][\s\S]*?(?=## \[1\.0\.0\]|$)/); + assert.ok(match, '[2.0.0] section missing'); + assert.match(match[0], /### BREAKING/); +}); + +test('No source files reference version 1.0.0', () => { + const m = JSON.parse(readFileSync(MANIFEST, 'utf-8')); + // Manifest is the canonical source — check it doesn't accidentally still say 1.0.0 + const raw = readFileSync(MANIFEST, 'utf-8'); + assert.doesNotMatch(raw, /"version":\s*"1\.0\.0"/); +});