aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-02 20:36:18 -0500
committerCraig Jennings <c@cjennings.net>2026-06-02 20:36:18 -0500
commit526df6dd7872890e525b9af33917169cfdf705d8 (patch)
tree0db8e87a7099f525a45d4592269368109b70e615
parent4a5a30280c541006be04f47ebb45c8aab28e9f3c (diff)
downloadrulesets-526df6dd7872890e525b9af33917169cfdf705d8.tar.gz
rulesets-526df6dd7872890e525b9af33917169cfdf705d8.zip
feat(flush): add /flush skill and SessionStart(clear) resume hook
Flush is the checkpoint half of the wrap/restart rhythm. It refreshes the session-context anchor in place, the user runs /clear, and the session resumes from the anchor instead of starting cold. One logical session stays alive across a /clear boundary without the archive-and-commit of wrap-it-up or the full cold boot of startup, which buys cheaper tokens and a sharper context window. The mechanism splits into two halves around /clear, which wipes the conversation so nothing runs straight through it. The /flush skill is the pre-clear half: dump live state, refresh the anchor's Summary, append a dated flush marker, verify the write landed, then prompt the user to /clear. The agent can initiate at a clean task boundary on its own judgment, but /clear is user-only, so the agent does the work and the user supplies the single keystroke. The session-clear-resume.sh hook is the post-clear half, a SessionStart matcher=clear hook that points the fresh session at the anchor to resume, or at startup when no anchor exists. I packaged the pre-clear half as a skill rather than a project-workflow doc so both halves are global. The hook was already global, so /flush is now callable by name from any project with no per-project sync. The hook is canonicalized under hooks/ and symlinked into ~/.claude/hooks/, matching precompact-priorities.sh. settings.json wires the SessionStart entry, and settings-snippet.json carries it so a fresh machine wires the hook on make install-hooks.
-rw-r--r--.claude/settings.json11
-rw-r--r--flush/SKILL.md84
-rwxr-xr-xhooks/session-clear-resume.sh55
-rw-r--r--hooks/settings-snippet.json8
4 files changed, 158 insertions, 0 deletions
diff --git a/.claude/settings.json b/.claude/settings.json
index 0f76d18..4734d45 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -28,6 +28,17 @@
}
]
}
+ ],
+ "SessionStart": [
+ {
+ "matcher": "clear",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "~/.claude/hooks/session-clear-resume.sh"
+ }
+ ]
+ }
]
},
"enabledPlugins": {
diff --git a/flush/SKILL.md b/flush/SKILL.md
new file mode 100644
index 0000000..d46c18b
--- /dev/null
+++ b/flush/SKILL.md
@@ -0,0 +1,84 @@
+---
+name: flush
+description: Mid-session context flush — the checkpoint half of the wrap/restart rhythm. Refresh the session-context anchor in place, prompt the user to /clear, then resume the same logical session from the anchor without re-running startup. Cheaper tokens and a sharper context window without fragmenting the session into archive files. Agent-callable and agent-initiated: the agent may run the pre-clear checkpoint on its own judgment at a clean task boundary, but /clear is user-only — the agent does all the work, then prompts for the single /clear keystroke. Use when the current task has a clean boundary and the context window is large enough that a reset would sharpen the work. Do NOT use for end-of-day or done-for-now (use wrap-it-up, which archives to .ai/sessions/ and commits), or for a genuine fresh start after being away or on another machine (use startup, which pulls + syncs + surfaces inbox).
+---
+
+# /flush — Mid-Session Context Checkpoint
+
+Flush keeps one logical session alive across a `/clear` boundary. It refreshes the session-context anchor in place, hands the `/clear` to the user, and lets the post-clear session resume from the anchor — same goal, same anchor file, no fresh-start overhead.
+
+This is the checkpoint half of the wrap/restart rhythm. It is distinct from two neighbors:
+
+- **wrap-it-up** archives the session to `.ai/sessions/` and commits — the single end-of-session boundary. Flush does neither; it keeps the session going.
+- **startup** pulls rulesets + project, syncs templates, and surfaces inbox + reminders for a genuine fresh start. Flush does none of that — it is a resume, not a cold boot.
+
+## Usage
+
+```
+/flush
+```
+
+The skill is agent-callable. The agent may also **initiate** a flush on its own judgment when the rhythm calls for it (see below) — it runs the pre-clear checkpoint, then prompts the user to type `/clear`.
+
+## The hard constraint
+
+`/clear` is a user-only command. The agent **cannot** execute it. "Agent-initiated" means the agent runs the pre-clear checkpoint (refresh the anchor + verify the write landed) on its own, then **prompts** the user: "checkpoint saved, type /clear to reset." The agent proposes and does all the work; the user supplies the single `/clear` keystroke. Never design or imply a flow where the agent self-triggers `/clear`.
+
+## When the agent should initiate
+
+Propose a flush — don't wait to be asked — when **both** hold:
+
+1. The current task is at a clean boundary (a unit of work just finished, nothing is half-edited, no command is mid-flight).
+2. The context window has grown large enough that a reset would sharpen the work — the accumulated conversation is now more noise than signal for what comes next.
+
+That is the wrap/restart rhythm. When both conditions hold, run Phase 1 and end on the `/clear` prompt. When in doubt, it is fine to ask the user whether to flush rather than initiating.
+
+## Phase 1 — Pre-clear checkpoint (this agent)
+
+`/clear` wipes the conversation, so this half runs entirely before the clear. Nothing the live session knows survives the clear except what lands in the anchor — so the gate at step 5 is not optional.
+
+1. **Dump everything not yet on disk.** Before touching the anchor, capture the live working state: decisions reached this turn, `file:line` targets, the exact next action, and any finding that would be expensive to reconstruct. The whole point of the flush is that nothing is lost across the clear.
+
+2. **Resolve the anchor path.**
+
+ ```bash
+ sc=$(.ai/scripts/session-context-path 2>/dev/null || echo .ai/session-context.org)
+ ```
+
+ Create the file with a skeletal structure (`#+TITLE`, `* Summary` with empty subsections, `* Session Log`) if it does not exist yet.
+
+3. **Refresh the `* Summary` in place.** Write or overwrite the subsections so the anchor reflects current state:
+ - **Active Goal** — the overall goal of the session, in one or two sentences.
+ - **Decisions** — what has been settled so far.
+ - **Data Collected / Findings** — load-bearing facts, with exact identifiers.
+ - **Files Modified** — what changed on disk this session.
+ - **Next Steps** — the immediate next action, specific enough to resume blind.
+
+ Do NOT archive and do NOT start a new file. Refresh the existing anchor.
+
+4. **Append a flush marker to the `* Session Log`.**
+
+ ```bash
+ date "+%Y-%m-%d %a @ %H:%M:%S %z"
+ ```
+
+ Add `** <timestamp> — flushed` with a one-line note on what is in flight, so the log stays a continuous narrative across clears.
+
+5. **Verify the write landed — gate before clearing.** Re-read the anchor and confirm the Active Goal and Next Steps are present and current. If the write did not land, STOP and fix it — do not tell the user to `/clear`. There is no recovering the conversation after `/clear`.
+
+6. **Hand off the clear.** Tell the user the checkpoint is saved, name the anchor path, and prompt: type `/clear` now, then send any message to resume.
+
+## Phase 2 — Post-clear resume (hook-driven)
+
+This half is driven by the `SessionStart(clear)` hook, not by this skill — but it is documented here so the loop is legible.
+
+1. The user types `/clear`. The hook `~/.claude/hooks/session-clear-resume.sh` fires and injects "read the anchor, reply 'flushed.', resume" into the fresh context.
+2. The user sends any message (e.g. "go", "resume"). The injected context loads at `/clear` but the model waits for the user's next message before acting — that one keystroke is expected, not a bug.
+3. The fresh agent reads the anchor, replies `flushed.` on its own line, restates the Active Goal and immediate Next Step, and continues. It does NOT run startup or re-sync anything — this is a resume, not a fresh start.
+
+If the anchor is absent (a stray `/clear` with no prior flush), the hook instead points the fresh session at the startup workflow.
+
+## Notes
+
+- The anchor persists across as many flushes as a day needs. Only wrap-it-up archives it to `.ai/sessions/` and commits — that is the single end-of-session boundary.
+- The hook is global (`~/.claude/settings.json` → `SessionStart` matcher `clear`), so the post-clear half works in any project that has a session-context anchor. This skill makes the pre-clear half global too, so `/flush` is callable by name from any project with no per-project workflow-doc sync.
diff --git a/hooks/session-clear-resume.sh b/hooks/session-clear-resume.sh
new file mode 100755
index 0000000..6692f54
--- /dev/null
+++ b/hooks/session-clear-resume.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+# SessionStart(clear) hook: after /clear, point the fresh context at the
+# session-context anchor so a "flush" resumes the same logical session
+# instead of starting cold.
+#
+# Pairs with the /flush skill (rulesets flush/SKILL.md): the skill's
+# pre-clear half refreshes .ai/session-context.org in place; this hook is the
+# post-clear half that /clear's blank session can't run itself.
+#
+# General by design — not flush-specific:
+# - anchor present -> resume (read it, say "flushed.", continue)
+# - anchor absent -> run the startup workflow (covers a stray /clear)
+#
+# SessionStart hooks reach the model ONLY through
+# hookSpecificOutput.additionalContext — plain stdout goes to the debug log.
+# After /clear the injected context loads but the model still waits for the
+# user's next message before it acts; that one keystroke is expected.
+#
+# Wire in ~/.claude/settings.json:
+#
+# {
+# "hooks": {
+# "SessionStart": [
+# {
+# "matcher": "clear",
+# "hooks": [
+# { "type": "command",
+# "command": "~/.claude/hooks/session-clear-resume.sh" }
+# ]
+# }
+# ]
+# }
+# }
+set -euo pipefail
+
+# Resolve the AI_AGENT_ID-aware anchor path — mirror of
+# .ai/scripts/session-context-path, evaluated against the project cwd the
+# hook inherits. Fall back to the singleton path when the id is unset.
+id="${AI_AGENT_ID:-}"
+if [ -n "$id" ]; then
+ safe=$(printf '%s' "$id" | tr -c 'A-Za-z0-9._-' '_')
+ sc=".ai/session-context.d/${safe}.org"
+else
+ sc=".ai/session-context.org"
+fi
+
+if [ -f "$sc" ]; then
+ ctx="A context flush just happened (/clear). The session-context anchor is at ${sc}. Read it now — the * Summary (Active Goal, Next Steps) plus the most recent * Session Log entries — then reply with 'flushed.' on its own line, followed by one line restating the Active Goal and the immediate Next Step, and resume that work. This is a mid-session resume, NOT a fresh start: do not run the startup workflow, do not re-pull rulesets or re-sync templates."
+else
+ ctx="Session started with no session-context anchor present (.ai/session-context.org absent). Run the startup workflow at .ai/workflows/startup.org."
+fi
+
+# Build the whole object in python3 so the additionalContext string is
+# JSON-escaped correctly regardless of its content.
+python3 -c 'import json,sys; print(json.dumps({"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":sys.argv[1]}}))' "$ctx"
diff --git a/hooks/settings-snippet.json b/hooks/settings-snippet.json
index 11459f2..1be5f00 100644
--- a/hooks/settings-snippet.json
+++ b/hooks/settings-snippet.json
@@ -1,5 +1,13 @@
{
"hooks": {
+ "SessionStart": [
+ {
+ "matcher": "clear",
+ "hooks": [
+ { "type": "command", "command": "~/.claude/hooks/session-clear-resume.sh" }
+ ]
+ }
+ ],
"PreCompact": [
{
"hooks": [