No description
  • JavaScript 100%
Find a file
Kjell Tore Guttormsen 192d4403eb feat(graceful-handoff): remove hardcoded push window — push is now unconditional (v3.1.0)
The regime-wide push-window restriction (man–tor 08:00–17:00, fre 08:00–16:00)
was lifted permanently, so SKILL.md no longer gates the push on a weekday/time
window — it would otherwise park a push in the middle of working hours.

Two invariants are unchanged: push is Forgejo only (never GitHub), and push
stays user-triggered (the skill is disable-model-invocation: true). The pipeline
script had no window logic — only its header comment was corrected.

If a future repo needs a window again, reintroduce it as per-repo config, never
as a hardcoded default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcNX2QdgmXyLd2Bt6Z25GU
2026-06-24 11:00:10 +02:00
.claude-plugin feat(graceful-handoff): remove hardcoded push window — push is now unconditional (v3.1.0) 2026-06-24 11:00:10 +02:00
docs docs: add full-depth plugin review (2026-06-20) 2026-06-20 09:14:08 +02:00
scripts feat(graceful-handoff): remove hardcoded push window — push is now unconditional (v3.1.0) 2026-06-24 11:00:10 +02:00
skills/graceful-handoff feat(graceful-handoff): remove hardcoded push window — push is now unconditional (v3.1.0) 2026-06-24 11:00:10 +02:00
tests feat(graceful-handoff): remove hardcoded push window — push is now unconditional (v3.1.0) 2026-06-24 11:00:10 +02:00
.gitignore feat(graceful-handoff)!: integrate with STATE.md continuity system (v3.0.0) 2026-06-23 21:09:44 +02:00
CHANGELOG.md feat(graceful-handoff): remove hardcoded push window — push is now unconditional (v3.1.0) 2026-06-24 11:00:10 +02:00
CLAUDE.md feat(graceful-handoff): remove hardcoded push window — push is now unconditional (v3.1.0) 2026-06-24 11:00:10 +02:00
GOVERNANCE.md docs: introduce GOVERNANCE.md and unify fork-and-own blurb 2026-05-03 14:57:00 +02:00
LICENSE feat(graceful-handoff): initial plugin with /graceful-handoff command 2026-04-19 22:54:10 +02:00
README.md feat(graceful-handoff): remove hardcoded push window — push is now unconditional (v3.1.0) 2026-06-24 11:00:10 +02:00

Graceful Handoff Plugin for Claude Code

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 for the full model and what upstream provides.

AI-generated: all code produced by Claude Code through dialog-driven development. Full disclosure →

Version Platform Skill Hooks Pipeline Tests License

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.


Table of Contents


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.


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 readrequires 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

# 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

Prerequisites

  • 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)

Install

claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git

Or enable directly in ~/.claude/settings.json:

{
  "enabledPlugins": {
    "graceful-handoff@ktg-plugin-marketplace": true
  }
}

Hand off

> /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

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

---
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

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.


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.


Limitations & Open Assumptions

  • disable-model-invocation: true has open issue #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.

License

MIT. See LICENSE.


Feedback & Contributing

  • Bug reports + feature requests: open an issue on Forgejo
  • 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 — see the root README for related plugins.