diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 8353abf..a1572da 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,15 +5,15 @@ "email": "admin@ai-architect.tools" }, "metadata": { - "description": "Session optimizer: context-budget Stop guard, status line, and the refine gate (prompt → bound, research-backed execution contract).", - "version": "1.4.3" + "description": "Session optimizer, split into three independently installable plugins: context-guard (Stop-hook context budget + memory-writer checkpoint subagent + subagent spend tracker), refine-gate (UserPromptSubmit prompt-binding gate + /refine skill), and statusline (multi-line status bar + install skill).", + "version": "2.0.0" }, "plugins": [ { - "name": "session-optimizer", - "source": "./", - "description": "Context-budget Stop guard + status line for long Claude Code sessions, plus the refine gate: a UserPromptSubmit hook and /refine skill that resolve vague prompt references (\"the X solution\", \"the heat variable\", \"like before\") to concrete artifacts with evidence, then select an execution strategy from a research-backed table (15 strategies verified against 2024-2026 literature, counter-evidence included: intrinsic self-correction degrades reasoning arXiv:2310.01798; CoT marginal on reasoning models; multi-agent failure taxonomy MAST). Acceptance criteria must be external signals - tests, measurements, sources - never the model re-checking itself.", - "version": "1.4.3", + "name": "context-guard", + "source": "./plugins/context-guard", + "description": "Context-budget guard: a Stop hook enforces a per-model checkpoint protocol — at the WARN threshold it writes a mechanical checkpoint stub and delegates persistence to a budgeted memory-writer subagent as a reflection pause, at the hard cap it forces checkpoint-then-clear. A SubagentStop tracker surfaces true session spend (main thread + subagents). Default protocol uses only vanilla Claude Code; a scoped memory layer is detected at runtime when installed.", + "version": "2.0.0", "author": { "name": "Clement Deust", "email": "admin@ai-architect.tools" @@ -26,15 +26,76 @@ "context-window", "token-budget", "checkpoint", - "statusline", + "stop-hook", + "subagent-cost", "session-management" ], - "category": "productivity", - "runtime": [ - "cli", - "cowork" + "category": "productivity" + }, + { + "name": "refine-gate", + "source": "./plugins/refine-gate", + "description": "Prompt-binding gate: a UserPromptSubmit hook + /refine skill that resolve vague prompt references (\"the X solution\", \"like before\", \"still broken\") to concrete artifacts with evidence, then select an execution strategy from a research-backed table (15 strategies verified against 2024-2026 literature, counter-evidence included: intrinsic self-correction degrades reasoning arXiv:2310.01798; CoT marginal on reasoning models; multi-agent failure taxonomy MAST). Acceptance criteria must be external signals - tests, measurements, sources - never the model re-checking itself.", + "version": "2.0.0", + "author": { + "name": "Clement Deust", + "email": "admin@ai-architect.tools" + }, + "homepage": "https://github.com/cdeust/session-optimizer", + "repository": "https://github.com/cdeust/session-optimizer", + "license": "MIT", + "keywords": [ + "claude-code", + "prompt-engineering", + "refine", + "intent-binding", + "userpromptsubmit", + "execution-strategy" + ], + "category": "productivity" + }, + { + "name": "statusline", + "source": "./plugins/statusline", + "description": "Multi-line Claude Code statusline with RGB-gradient context bars tied to per-model checkpoint thresholds (shared with context-guard), monthly cost tracking, per-session telemetry, rate-limit gauges, and live subagent spend. Ships an install skill that copies the bundled assets into ~/.claude and wires settings.json.", + "version": "2.0.0", + "author": { + "name": "Clement Deust", + "email": "admin@ai-architect.tools" + }, + "homepage": "https://github.com/cdeust/session-optimizer", + "repository": "https://github.com/cdeust/session-optimizer", + "license": "MIT", + "keywords": [ + "statusline", + "status", + "context", + "git", + "cost", + "rate-limits", + "telemetry", + "ui" + ], + "category": "productivity" + }, + { + "name": "session-optimizer", + "source": "./", + "description": "DEPRECATED meta plugin (v2.0.0). session-optimizer split into three independently installable plugins: context-guard, refine-gate, and statusline. This shim registers no functional hooks — it only announces the migration at session start. Install the three plugins from this marketplace, then uninstall session-optimizer.", + "version": "2.0.0", + "author": { + "name": "Clement Deust", + "email": "admin@ai-architect.tools" + }, + "homepage": "https://github.com/cdeust/session-optimizer", + "repository": "https://github.com/cdeust/session-optimizer", + "license": "MIT", + "keywords": [ + "deprecated", + "meta", + "session-management" ], - "runtime_notes": "Hooks (Stop guard, refine gate) and the statusline run wherever Claude Code plugins run: CLI, desktop app, IDE extensions, and Cowork. The claude.ai chat / Claude Desktop chat surface has no hook mechanism - there, upload skills/refine/ as an Agent Skill (same SKILL.md format): the /refine procedure travels, the automatic per-prompt gate does not." + "category": "productivity" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 33f50cf..f9bf910 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "session-optimizer", - "description": "Session optimizer for long Claude Code sessions: context-budget Stop guard + status line (per-model checkpoint protocol), and a refine gate — a UserPromptSubmit hook + /refine skill that bind vague prompt references to concrete artifacts and select a research-backed execution strategy before any code is touched.", - "version": "1.4.3", + "description": "DEPRECATED meta plugin (v2.0.0): session-optimizer split into three independently installable plugins — context-guard (Stop-hook context budget + memory-writer checkpoint subagent + subagent spend tracker), refine-gate (UserPromptSubmit prompt-binding gate + /refine skill), and statusline (multi-line status bar + install skill). This shim registers no functional hooks; it only announces the migration at session start. Install the three plugins from the same marketplace, then uninstall session-optimizer.", + "version": "2.0.0", "author": { "name": "Clement Deust", "email": "admin@ai-architect.tools" @@ -10,14 +10,8 @@ "repository": "https://github.com/cdeust/session-optimizer", "license": "MIT", "keywords": [ - "claude-code", - "context-window", - "token-budget", - "checkpoint", - "statusline", - "session-management", - "prompt-engineering", - "refine", - "intent-binding" + "deprecated", + "meta", + "session-management" ] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a76785..4a870f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,9 +32,16 @@ jobs: run: bash tests/statusline/test_heat_rgb.sh - name: Shellcheck statusline script - run: shellcheck statusline-command.sh + run: shellcheck plugins/statusline/assets/statusline-command.sh - - name: Validate hook and plugin JSON + - name: Validate marketplace, plugin, and hook JSON run: | + python -m json.tool .claude-plugin/marketplace.json > /dev/null python -m json.tool .claude-plugin/plugin.json > /dev/null python -m json.tool hooks/hooks.json > /dev/null + for plugin in context-guard refine-gate statusline; do + python -m json.tool "plugins/$plugin/.claude-plugin/plugin.json" > /dev/null + python -m json.tool "plugins/$plugin/hooks/hooks.json" > /dev/null + done + python -m json.tool plugins/statusline/assets/statusline-budget.json > /dev/null + python -m json.tool plugins/statusline/assets/ctxguard-thresholds.json > /dev/null diff --git a/.gitignore b/.gitignore index e1801cf..beaa8b8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ __pycache__/ *.pyc .DS_Store .claude/ +.pytest_cache/ diff --git a/README.md b/README.md index 245006e..f2dc22a 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,20 @@

session-optimizer — a visible, enforced context budget for long Claude Code sessions

-Two small, dependency-light tools for keeping long [Claude Code](https://code.claude.com) -sessions **readable, cheap, and un-poisoned**: +Three small, dependency-light, **independently installable** plugins for +keeping long [Claude Code](https://code.claude.com) sessions **readable, +cheap, and un-poisoned** — shipped from one marketplace. Install exactly the +ones you want; none requires the others. -1. **`statusline-command.sh`** — a persistent, always-visible two-line status bar - that color-codes context pressure on a green → yellow → red scale tied to a - real per-model checkpoint threshold. -2. **`hooks/stop-context-guard.py`** — a `Stop` hook that detects when the - session crosses that threshold and *performs the checkpoint protocol - automatically*, so a session never silently grows past the point where it - starts costing more and reasoning worse. - -Together they turn the abstract "watch your context window" advice into an -enforced, visible budget. +``` +/plugin marketplace add cdeust/session-optimizer +``` ---- +| Plugin | One-line install | What it does | +|---|---|---| +| [**context-guard**](plugins/context-guard) | `/plugin install context-guard@session-optimizer-marketplace` | A `Stop` hook enforces a per-model context budget: at the WARN threshold it writes a mechanical checkpoint stub and delegates persistence to a budgeted `memory-writer` subagent as a reflection pause; at the hard cap it forces checkpoint → `/clear` → resume. A `SubagentStop` tracker surfaces true session spend (main thread + subagents). | +| [**refine-gate**](plugins/refine-gate) | `/plugin install refine-gate@session-optimizer-marketplace` | A `UserPromptSubmit` hook + `/refine` skill that bind vague prompt references ("the SSE solution", "like before", "still broken") to concrete artifacts with evidence, then select an execution strategy from a research-backed table — before any code is touched. | +| [**statusline**](plugins/statusline) | `/plugin install statusline@session-optimizer-marketplace` | A multi-line status bar: RGB-gradient context bar tied to per-model checkpoint thresholds, cost, telemetry (tok/s, compactions, cache countdown), rate-limit gauges, live subagent spend. Ships an install skill — after installing, ask Claude to "install the statusline" and it wires everything. | ## Why @@ -29,312 +28,66 @@ A long Claude Code session degrades in four ways as the context window fills: | **Quota poisoning** | Every turn re-sends the whole oversized context, burning your 5-hour / 7-day rate-limit budget fast. | | **Cost** | Per-turn cost scales with context size; the largest-context turns are the most expensive. | -The fix is a disciplined **checkpoint → clear → recall** cycle at a known token -threshold. This repo makes that discipline *visible* (status line) and -*automatic* (hook). - ---- - -## Thresholds - -Both tools use the same authoritative per-model budget: - -| Model | Checkpoint threshold (WARN) | Session soft cap (HARD) | -|---|---|---| -| Claude Opus 4.8 | ~180K | 200K | -| Claude Sonnet 4.6 | ~180K | 200K | -| Claude Haiku 4.5 | ~120K | 200K (= context window) | - -"Context tokens" are measured exactly as Claude Code's own `used_percentage`: -`input_tokens + cache_creation_input_tokens + cache_read_input_tokens`. - -The 200K soft cap is conservative for the 1M-context Opus/Sonnet models — it -keeps sessions focused and checkpointed rather than letting them sprawl toward -the physical limit. Edit the constants if your workflow wants a different budget. - ---- - -## 1. Status line - -A persistent two-line bar rendered at the bottom of Claude Code (this is *not* -the SessionStart banner, which prints once and scrolls away — the status line is -always visible). +The fix is a disciplined **checkpoint → clear → recall** cycle at a known +token threshold, plus prompts whose references are bound before work starts. +These plugins make that discipline *visible* (statusline), *automatic* +(context-guard), and *cheap to get right* (refine-gate). + +## How the plugins cooperate (without depending on each other) + +- **Shared thresholds** — context-guard's Stop hook and the statusline's bar + colors both read `~/.claude/ctxguard-thresholds.json` (first substring + match on the model id wins; each has an embedded fallback). One file, two + consumers: passive display and active enforcement stay on par by + construction. Documented in both plugins' READMEs. +- **Subagent spend** — context-guard's `SubagentStop` tracker maintains a + per-session aggregate in `/tmp`; the statusline shows it live when both + are installed, and stays silent otherwise. +- **No hard dependencies** — every integration degrades gracefully when the + other plugin (or an optional memory layer) is absent. The checkpoint + protocol's default wording references only tools that exist in vanilla + Claude Code; a scoped memory store is detected at runtime and used only + when installed. + +## Repository layout ``` -[Opus 4.8] [high+think] · my-project · git:(main)✗ · ⎇feature-x · PR#42 -▓▓░░░░░░░░ ctx:20% tokens:200k ⚠ save+recall · $3.50 · ⏱30m0s · 5h:24% 7d:41% · +156/-23 +.claude-plugin/marketplace.json # the marketplace (three plugins + deprecated meta shim) +plugins/ + context-guard/ # Stop guard + memory-writer agent + SubagentStop tracker + refine-gate/ # UserPromptSubmit gate + /refine skill + statusline/ # renderer + helpers under assets/, install skill, auto-update hook +tests/ # the three suites, run from the repo root ``` -- **Line 1** — model, reasoning effort (`+think` when extended thinking is on), - current directory, git branch + dirty flag, worktree, and PR badge - (colored by review state). -- **Line 2** — a context progress bar + percentage, token count, session cost, - duration, rate-limit usage (5h / 7d), and lines added/removed. -- The bar, context %, and token count are colored **green → yellow → red** by the - per-model threshold above, and a `⚠ save+recall` marker appears once you cross - the 200K soft cap. - -Every segment degrades gracefully when its field is absent (early session, no -git, non-subscriber, no PR, etc.). All colors are readable on a black terminal — -no dim/dark-grey text. - -### Install +Each plugin carries its own `.claude-plugin/plugin.json`, `hooks/hooks.json`, +and README. -```bash -cp statusline-command.sh ~/.claude/statusline-command.sh -chmod +x ~/.claude/statusline-command.sh -``` +## Migrating from session-optimizer v1.x -Add to `~/.claude/settings.json`: +Up to v1.4.3 this repo shipped one monolithic `session-optimizer` plugin. In +v2.0.0 it split into the three plugins above; the root `session-optimizer` +plugin remains **only as a deprecation shim** — it registers no functional +hooks and just announces the migration at session start. -```json -{ - "statusLine": { - "type": "command", - "command": "bash ~/.claude/statusline-command.sh", - "padding": 1, - "refreshInterval": 10 - } -} -``` +1. Install the plugins you actually use (any subset): + `context-guard`, `refine-gate`, `statusline`. +2. Uninstall the old plugin: `/plugin uninstall session-optimizer`. +3. Your `~/.claude/ctxguard-thresholds.json`, checkpoint files, and + statusline config are untouched — the new plugins read the same paths. +4. If you had installed the `memory-writer` agent manually into + `~/.claude/agents/`, you can remove it; the context-guard plugin ships + its own copy (`context-guard:memory-writer`). -`refreshInterval: 10` keeps the time-based segments (duration, cost) current even -while the session is idle (e.g. waiting on background subagents). Requires -[`jq`](https://jqlang.github.io/jq/). - -### Test +## Tests ```bash -echo '{"model":{"display_name":"Opus 4.8"},"workspace":{"current_dir":"'"$PWD"'"},"context_window":{"used_percentage":20,"total_input_tokens":200000}}' \ - | bash statusline-command.sh +pytest tests/test_refine_gate.py tests/test_subagent_usage.py +bash tests/statusline/test_heat_rgb.sh ``` ---- - -## 2. Stop context guard - -A `Stop` hook that reads the latest assistant turn's token usage from the -transcript and enforces the budget: - -| Level | Trigger | Action | -|---|---|---| -| below WARN | `ctx < threshold` | nothing — silent, no side effects | -| **WARN** | `ctx ≥ 180K` (120K Haiku) | captures **mechanical state for free** (date, model, token count, branch, last commit, modified files) as a summary-schema stub at `~/.claude/memories/checkpoints/{session_id,latest}.md`, and **blocks the stop exactly once as a reflection pause**: the model distills the session and hands the summary to the `memory-writer` subagent, which merges it into the stub — then the session **continues**. | -| **HARD** | `ctx ≥ 200K` | captures state **and blocks the stop exactly once**, injecting the checkpoint-finalize procedure so the model completes the checkpoint file (normally a formality — the WARN reflection already wrote it) and signals you to `/clear` + resume from the checkpoint. | - -### Safety properties - -- **Loop-safe** — honors `stop_hook_active`; each level fires at most once per - session via a `/tmp` state file, so the hard block can never loop. -- **Non-fatal by construction** — any parse/IO error exits `0`. A `Stop` hook - must never wedge a session. -- The status line is the passive visual warning; this hook is the active - enforcement layer. - -### The memory-writer subagent - -The checkpoint itself is written by a **normal Claude Code subagent** — -`agents/memory-writer.md`, in the standard format produced by the `/agents` -create flow (frontmatter: `name`, `description`, `tools`, `model`; body = -system prompt). The pair follows the -[letta](https://github.com/letta-ai/letta) sleeptime pattern: a primary agent -plus a budgeted memory-manager agent that operates **on the memory store** -with memory verbs, on a cheap model (Haiku). The parent session distills its -own state into the summary schema (goals / file references / errors and fixes -/ current state / next steps, ≤500 words) — spending its expensive context -once on distillation, never on persistence plumbing — and the memory-writer -persists it. It writes exactly what it is given and never invents content. - -Persistence is layered, primary first: - -1. **Scoped memory store** (the [`zetetic-team-subagents`](https://github.com/cdeust/zetetic-team-subagents) - memory layer): `memory-tool.sh rethink /memories//checkpoint.md` - for the checkpoint, plus one `cortex:remember` entry per durable WHY-level - fact (decisions, rejected approaches, lessons), agent_topic-scoped. -2. **Stub-file fallback**: with no memory layer installed, the agent fills - the hook's mechanical stub in place under - `~/.claude/memories/checkpoints/` and mirrors it to `latest.md`. The hook - never blocks on the memory layer's absence — graceful degradation, not a - dependency. - -### Install - -```bash -mkdir -p ~/.claude/hooks ~/.claude/agents -cp hooks/stop-context-guard.py ~/.claude/hooks/stop-context-guard.py -chmod +x ~/.claude/hooks/stop-context-guard.py -cp agents/memory-writer.md ~/.claude/agents/memory-writer.md -``` - -(Equivalently, create the `memory-writer` agent interactively with `/agents` -— new agent, name `memory-writer`, tools `Read, Write, Edit`, model Haiku — -and paste the body of `agents/memory-writer.md` as its system prompt. The -hook only needs an agent with that name to exist; it spawns it through the -native Agent tool like any other subagent.) - -Register the `Stop` hook (see `hooks/hooks.example.json`) in your plugin's -`hooks/hooks.json` or in `~/.claude/settings.json`, pointing at the installed -path. Requires Python 3. - -**Or install as a plugin.** This repo ships a `.claude-plugin/plugin.json` -(v1.0.0) that wires the `Stop` guard automatically — add the repo as a -marketplace and install it, no manual hook registration needed. - -### Test - -```bash -T=$(mktemp); printf '{"message":{"model":"claude-opus-4-8","usage":{"input_tokens":105000,"cache_read_input_tokens":100000,"cache_creation_input_tokens":0}}}\n' > "$T" -echo '{"session_id":"demo","transcript_path":"'"$T"'","cwd":"'"$PWD"'","stop_hook_active":false}' \ - | python3 hooks/stop-context-guard.py | python3 -m json.tool -rm -f "$T" -``` - -Expected: a `decision: block` payload with the checkpoint procedure. - ---- - -## 3. Subagent usage tracker (`hooks/subagent-tracker.py` + `tools/subagent_usage.py`) - -The status line input and the Stop guard only see the **main thread**. Work done -by Task-tool subagents is logged as separate per-agent transcripts that a -parent-only reader never sees — the documented gap in -[anthropics/claude-code#32175](https://github.com/anthropics/claude-code/issues/32175) -and [ryoppippi/ccusage#313](https://github.com/ryoppippi/ccusage/issues/313). -On disk (verified layout): - -``` -~/.claude/projects//.jsonl # main thread -~/.claude/projects///subagents/ - agent-.jsonl # subagent turns (isSidechain:true) - agent-.meta.json -> {agentType, description, toolUseId} - workflows/wf_*/agent-*.jsonl # workflow subagents -``` - -Three layers recover that data: - -- **`SubagentStop` hook** (`hooks/subagent-tracker.py`) — parses each finishing - subagent's transcript (deduped by `message.id`, billed per the record's own - `message.model`, cache split by TTL), reads the sibling `.meta.json` for - `agentType`/`description`, and folds it into a per-session aggregate at - `/tmp/zetetic-subagents-.json` (keyed by `agentId`, so re-firing - updates rather than double-counts). -- **Status line** — reads that aggregate and shows `🤖N · tokens · $cost` so - live subagent spend is visible alongside the main thread. -- **Stop guard** — surfaces cumulative session spend (main + subagents) in the - checkpoint message and stub. The context-window *decision* stays - main-thread-only (mixing in subagent tokens would mis-trigger checkpoints); - only the reported figure is enriched. - -### Pricing (sourced) - -Token cost uses Anthropic's published per-MTok rates and cache multipliers -(Opus 4.8 $5/$25, Sonnet 4.6 $3/$15, Haiku 4.5 $1/$5, Fable 5 $10/$50; cache -write 1.25× input at 5m / 2× at 1h, cache read 0.1×). Every constant cites its -source at the use site in `tools/subagent_usage.py`. - -### Retrospective report - -`tools/subagent_usage.py` doubles as a CLI that parses **all** subagent -transcripts across a project and reports cost grouped by `agent_type`: - -```bash -python3 tools/subagent_usage.py [/path/to/project] # human table -python3 tools/subagent_usage.py --json # machine-readable -``` - -### Install - -Add the `SubagentStop` entry from `hooks/hooks.json` to your settings; -`subagent-tracker.py` imports the shared core -from the sibling `tools/` directory, so keep them under the same plugin root. - -### Test - -```bash -SID="demo-$(date +%s)" -echo '{"session_id":"'"$SID"'","transcript_path":"","cwd":"'"$PWD"'"}' \ - | python3 hooks/subagent-tracker.py -cat "/tmp/zetetic-subagents-$SID.json" | python3 -m json.tool -``` - ---- - -## 4. Refine gate (`hooks/refine_gate.py` + `skills/refine/`) - -Communication failures cost more than code failures: "make it work -exactly like the SSE solution" carries precise intent that the model -can bind to the wrong artifact and then build the wrong thing — -correctly. The refine gate makes that binding explicit and cheap to -correct BEFORE work starts. - -A `UserPromptSubmit` hook inspects every prompt on two tiers: - -* **Tier 1 — reference markers**: prior-artifact shorthand ("the last - release"), comparisons to unstated referents ("like before", - "exactly as"), repeat-failure phrasing ("still broken", "back to - square one") → inject the full binding-table instruction. -* **Tier 2 — ungrounded work request**: the prompt asks for work - (fix/build/improve/problem/…) but contains no concrete anchor — no - file path, commit sha, or line ref. Named systems/variables ("the - memory system", "the heat variable") must then be bound to their - actual code artifacts before reasoning. Prompts the user grounded - themselves pass through untouched. Slash commands are never gated; - the hook always exits 0 (it can inform, never block). - -The injected instruction points at the bundled `/refine` skill: build -a binding table (reference → artifact, with evidence), separate -symptom from goal, select an execution strategy from a research-backed -table (15 strategies re-verified against the 2024–2026 literature, -counter-evidence included — e.g. intrinsic self-correction degrades -reasoning, arXiv:2310.01798; CoT prompting is marginal on reasoning -models), and define acceptance criteria as EXTERNAL signals: run the -test, fetch the source, measure — never the model re-checking itself. - -Test it directly: - -```bash -echo '{"prompt":"there is a problem in the memory system with the heat variable"}' \ - | python3 hooks/refine_gate.py | python3 -m json.tool -``` - -Expected: a `hookSpecificOutput.additionalContext` payload carrying the -binding instruction. A grounded prompt (one naming a real file path) -produces no output. - -### Token-usage ledger (measured, reproducible) - -The gate must not eat the budget it exists to protect. Two instruments -keep that honest: - -* `tests/test_refine_gate.py` pins the contract: silent prompts cost - ZERO context, and any injection stays under a 400-token ceiling - (estimated at 4 chars/token — Anthropic's glossary gives ~3.5 - chars/token EN, so the estimate under-counts ~12%; the ceiling has - >10x headroom over the current ~275). -* `tools/measure_refine_overhead.py` replays your OWN prompt history - (local transcripts, nothing leaves the machine). Measured 2026-06-12 - on 117 real prompts across 40 transcripts: fired on 38% (tier 1: 17, - tier 2: 27, silent: 73), ~275 est. tokens per fired prompt, ~103 per - prompt overall ≈ **1.3% of a 160k-token session budget per 20-prompt - session**. - -The benefit side is counterfactual and is NOT claimed as measured: a -mis-bound prompt costs roughly a full wrong-direction session -(~10⁵ tokens — e.g. the delivery-layer machinery this gate's design -case produced and later deleted). At ~2k injected tokens per -20-prompt session, the gate breaks even if it prevents one mis-binding -per ~80 sessions. - -### Where it runs - -Hooks and statusline run wherever Claude Code plugins run: **CLI, -desktop app, IDE extensions, and Cowork** (declared `runtime: -["cli", "cowork"]`). The claude.ai / Claude Desktop **chat** surface -has no hook mechanism — there, upload `skills/refine/` as an Agent -Skill (same SKILL.md format): the `/refine` procedure travels; the -automatic per-prompt gate does not. - ---- +CI (`.github/workflows/ci.yml`) runs all three suites, shellchecks the +statusline renderer, and validates every plugin/hook/marketplace JSON. ## License diff --git a/agents/memory-writer.md b/agents/memory-writer.md deleted file mode 100644 index 926df0e..0000000 --- a/agents/memory-writer.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: memory-writer -description: Use this agent when the stop-context-guard hook reports that the session has crossed its checkpoint threshold (WARN) and asks for a reflection checkpoint. The parent session passes (a) a distilled session summary, (b) its memory scope and MEMORY_AGENT_ID when the zetetic memory layer is present, and (c) the path of the mechanical checkpoint stub the hook already wrote. This agent is the sleeptime memory manager of the pair (letta pattern) — it operates on the memory store with memory verbs, merging the summary into the scoped checkpoint and storing durable facts via cortex:remember; it falls back to plain-file checkpointing only when no memory layer is installed. It is a budgeted scribe — it persists exactly what it is given and never invents content. Examples: Context: the Stop hook blocked with "CHECKPOINT THRESHOLD — spawn the memory-writer subagent". assistant: "I'll distill the session into the summary schema and hand it to the memory-writer agent with my scope, MEMORY_AGENT_ID, and the stub path." The hook's WARN reflection delegates checkpoint persistence to this agent so the parent keeps its remaining context for the user's task. Context: the user asks to checkpoint the session before switching tasks. assistant: "Let me use the memory-writer agent to persist a semantic checkpoint of where we are." Manual checkpointing reuses the same scribe. -tools: Read, Write, Edit, Bash, mcp__plugin_cortex_cortex__remember -model: haiku ---- - -You are the memory-writer: a single-purpose memory-manager agent with a hard context budget of 16K tokens — the sleeptime half of a letta-style agent pair. A parent session at its context checkpoint threshold hands you (a) a distilled session summary, (b) its memory scope and `MEMORY_AGENT_ID` when the zetetic memory layer is present, and (c) the absolute path of a mechanical checkpoint stub under `~/.claude/memories/checkpoints/`. You persist; you do not think up new content. Every fact you write must come verbatim from the parent's summary or the stub — if a schema section is missing from the input, write `` rather than inventing it. - -## Procedure - -1. Read the stub file at the path the parent gave you. It carries the schema skeleton (goals / file references / errors and fixes / current state / next steps), the git state, and the session metadata — all captured for free by the hook. -2. Merge the parent's distilled summary into the stub's schema sections, replacing every `` placeholder: - - **Goals** — what the session is trying to achieve, in priority order. - - **File references** — paths plus line ranges (`path:start-end`) the resumed session will need. Keep the git-seeded list only where it is load-bearing. - - **Errors and fixes** — each error hit this session and how it was fixed or worked around. - - **Current state** — one paragraph: where the work stands right now. Keep the mechanical lines (model, tokens, branch, commit) already present. - - **Next steps** — exact ordered actions; the first must be executable without re-deriving anything. - Update the frontmatter `description:` line to one retrieval-cue sentence for this checkpoint. Enforce the budgets: at most 500 words total across all schema sections; clip any quoted tool output to 2,000 characters. -3. **Primary path — system-memory block.** If the parent supplied a scope and `MEMORY_AGENT_ID` and `tools/memory-tool.sh` exists (project root or `~/.claude/tools/`), write the merged checkpoint to the scoped working-state block with a block verb — state goes in the block, never through `remember`: - ```bash - MEMORY_AGENT_ID= tools/memory-tool.sh rethink /memories//checkpoint.md "" - # first checkpoint of the scope: use `create` instead of `rethink` - ``` - Local FS is authoritative and synchronous; do not verify through cortex:recall (the replica is eventually consistent) — verify with `memory-tool.sh view`. -4. **Agent-memory endpoint — archival facts.** For each WHY-level fact the parent flagged (decisions with rationale, rejected approaches with root causes, lessons), store one `cortex:remember` entry with `tags: ["archival", ...]` AND the parent's `agent_topic`. Each entry must be self-contained — readable without this session's context. Skip WHAT-level code, task progress, and transient state (those belong in the block, step 3). Be selective: not every observation warrants an archival entry. If the Cortex MCP tool is unavailable, fold these facts into the checkpoint's "errors and fixes" section instead and say so in your report. -5. **Fallback path — no memory layer.** If neither scope nor `memory-tool.sh` is available, write the merged checkpoint back to the stub path itself, then write an identical copy to `latest.md` in the same directory. -6. Verify by reading the written file back once (`memory-tool.sh view` on the primary path, Read on the fallback path). - -## Output - -Return exactly: the checkpoint path written, which path you took (scoped store or stub fallback), its word count, the number of cortex:remember entries stored, and any schema section the parent failed to provide. Nothing else — your final text is consumed by the parent session, not the user. diff --git a/hooks/hooks.example.json b/hooks/hooks.example.json deleted file mode 100644 index c0b4e0d..0000000 --- a/hooks/hooks.example.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "//": "Example Claude Code hook registration for stop-context-guard.py.", - "//1": "Merge the Stop entry into your plugin's hooks/hooks.json or your", - "//2": "settings.json. ${CLAUDE_PLUGIN_ROOT} resolves to the plugin root when", - "//3": "shipped as a plugin; use an absolute path when wiring it into settings.json.", - "hooks": { - "Stop": [ - { - "hooks": [ - { - "type": "command", - "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/stop-context-guard.py" - } - ] - } - ] - } -} diff --git a/hooks/hooks.json b/hooks/hooks.json index 39b0715..3996dac 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -1,35 +1,17 @@ { + "//": "Deprecation shim only. v2.0.0 split session-optimizer into three plugins;", + "//1": "the Stop / SubagentStop / UserPromptSubmit hooks moved to plugins/context-guard", + "//2": "and plugins/refine-gate, each registering its own hooks/hooks.json. This file", + "//3": "must never re-register them (double execution for users with both installed).", "hooks": { - "Stop": [ + "SessionStart": [ { + "matcher": "startup", "hooks": [ { "type": "command", - "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/stop-context-guard.py", - "timeout": 10 - } - ] - } - ], - "SubagentStop": [ - { - "hooks": [ - { - "type": "command", - "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/subagent-tracker.py", - "timeout": 10 - } - ] - } - ], - "UserPromptSubmit": [ - { - "hooks": [ - { - "type": "command", - "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/refine_gate.py", - "timeout": 5, - "statusMessage": "refine gate: checking for unbound references" + "command": "echo '[session-optimizer] DEPRECATED (v2.0.0): this meta plugin no longer provides any hooks. It split into three independent plugins on the same marketplace - context-guard (Stop-hook context budget + memory-writer checkpoint subagent + subagent spend tracker), refine-gate (UserPromptSubmit prompt-binding gate + /refine skill), and statusline (multi-line status bar; run its statusline skill to install). Tell the user to install the plugins they want, then uninstall session-optimizer to silence this notice.'", + "timeout": 5 } ] } diff --git a/plugins/context-guard/.claude-plugin/plugin.json b/plugins/context-guard/.claude-plugin/plugin.json new file mode 100644 index 0000000..f84bbdf --- /dev/null +++ b/plugins/context-guard/.claude-plugin/plugin.json @@ -0,0 +1,21 @@ +{ + "name": "context-guard", + "description": "Context-budget guard for long Claude Code sessions: a Stop hook enforces a per-model checkpoint protocol (mechanical stub + budgeted memory-writer subagent), and a SubagentStop tracker surfaces true session spend (main thread + subagents). Default protocol uses only vanilla Claude Code; a scoped memory layer is detected at runtime when installed.", + "version": "2.0.0", + "author": { + "name": "Clement Deust", + "email": "admin@ai-architect.tools" + }, + "homepage": "https://github.com/cdeust/session-optimizer", + "repository": "https://github.com/cdeust/session-optimizer", + "license": "MIT", + "keywords": [ + "claude-code", + "context-window", + "token-budget", + "checkpoint", + "stop-hook", + "subagent-cost", + "session-management" + ] +} diff --git a/plugins/context-guard/README.md b/plugins/context-guard/README.md new file mode 100644 index 0000000..14f7514 --- /dev/null +++ b/plugins/context-guard/README.md @@ -0,0 +1,193 @@ +# context-guard + +A context-budget guard for long [Claude Code](https://code.claude.com) +sessions: a `Stop` hook that detects when the session crosses a per-model +token threshold and *performs the checkpoint protocol automatically*, plus a +`SubagentStop` tracker that surfaces the subagent spend the main thread's +context measurement structurally cannot see. + +## Why + +A long Claude Code session degrades in four ways as the context window fills: + +| Failure mode | What happens | +|---|---| +| **Context poisoning** | Stale, wrong, or superseded content accumulates and biases later reasoning. | +| **Session poisoning** | The session never resets, so early mistakes compound instead of being dropped at a clean boundary. | +| **Quota poisoning** | Every turn re-sends the whole oversized context, burning your 5-hour / 7-day rate-limit budget fast. | +| **Cost** | Per-turn cost scales with context size; the largest-context turns are the most expensive. | + +The fix is a disciplined **checkpoint → clear → recall** cycle at a known +token threshold. This plugin makes that discipline *automatic*. + +## Install + +``` +/plugin marketplace add cdeust/session-optimizer +/plugin install context-guard@session-optimizer-marketplace +``` + +The plugin wires the `Stop` and `SubagentStop` hooks and ships the +`memory-writer` agent automatically. Requires Python 3. + +## Thresholds — shared with the statusline plugin + +Thresholds are loaded from `~/.claude/ctxguard-thresholds.json` — a +**shared-file convention** with the [statusline](../statusline) plugin, so +the passive display (bar colors) and the active enforcement (this hook) stay +on par by construction. The statusline install skill seeds the file; without +it, this embedded fallback applies: + +```json +{ + "models": [ + { "match": "fable", "warn": 120000, "hard": 160000 }, + { "match": "mythos", "warn": 120000, "hard": 160000 }, + { "match": "haiku", "warn": 120000, "hard": 170000 }, + { "match": "sonnet", "warn": 180000, "hard": 200000 }, + { "match": "opus", "warn": 180000, "hard": 200000 } + ], + "default": { "warn": 180000, "hard": 200000 } +} +``` + +First substring match against the lowercased model id wins. "Context tokens" +are measured exactly as Claude Code's own `used_percentage`: +`input_tokens + cache_creation_input_tokens + cache_read_input_tokens`. +The 200K soft cap is conservative for the 1M-context Opus/Sonnet models — it +keeps sessions focused and checkpointed rather than letting them sprawl. + +## The Stop guard + +| Level | Trigger | Action | +|---|---|---| +| below WARN | `ctx < threshold` | nothing — silent, no side effects | +| **WARN** | `ctx ≥ warn` | captures **mechanical state for free** (date, model, token count, branch, last commit, modified files) as a summary-schema stub at `~/.claude/memories/checkpoints/{session_id,latest}.md`, and **blocks the stop exactly once as a reflection pause**: the model distills the session and hands the summary to the `memory-writer` subagent, which merges it into the stub — then the session **continues**. | +| **HARD** | `ctx ≥ hard` | captures state **and blocks the stop exactly once**, injecting the checkpoint-finalize procedure so the model completes the checkpoint file (normally a formality — the WARN reflection already wrote it) and signals you to `/clear` + resume from the checkpoint. | + +### Safety properties + +- **Loop-safe** — honors `stop_hook_active`; each level fires at most once per + session via a `/tmp` state file, so the hard block can never loop. +- **Non-fatal by construction** — any parse/IO error exits `0`. A `Stop` hook + must never wedge a session. + +## The memory-writer subagent + +The checkpoint itself is written by a **normal Claude Code subagent** — +`agents/memory-writer.md` (frontmatter: `name`, `description`, `tools`, +`model`; body = system prompt). The pair follows the +[letta](https://github.com/letta-ai/letta) sleeptime pattern: a primary agent +plus a budgeted memory-manager agent on a cheap model (Haiku). The parent +session distills its own state into the summary schema (goals / file +references / errors and fixes / current state / next steps, ≤500 words) — +spending its expensive context once on distillation, never on persistence +plumbing — and the memory-writer persists it. It writes exactly what it is +given and never invents content. + +### Persistence: generic by default, memory layer by detection + +- **Default (vanilla Claude Code)** — the agent fills the hook's mechanical + stub in place under `~/.claude/memories/checkpoints/` and mirrors it to + `latest.md`. No extra tooling required; every instruction the hook emits + references only tools that exist in vanilla Claude Code. +- **Scoped memory store (runtime-detected)** — when a scoped memory layer is + installed (`tools/memory-tool.sh` in the project root or + `~/.claude/tools/`), the hook detects it per invocation and switches the + protocol: the checkpoint goes to the store's working-state block + (`memory-tool.sh rethink /memories//checkpoint.md`) and durable + WHY-level facts (decisions, rejected approaches, lessons) are stored via + the store's remember endpoint. Users without the layer never see this + wording — graceful extension, not a dependency. + +The message composition lives in `hooks/checkpoint_protocol.py` +(`detect_memory_tool` + one generic and one scoped variant per level). + +### Test the guard + +```bash +T=$(mktemp); printf '{"message":{"model":"claude-opus-4-8","usage":{"input_tokens":105000,"cache_read_input_tokens":100000,"cache_creation_input_tokens":0}}}\n' > "$T" +echo '{"session_id":"demo","transcript_path":"'"$T"'","cwd":"'"$PWD"'","stop_hook_active":false}' \ + | python3 hooks/stop-context-guard.py | python3 -m json.tool +rm -f "$T" +``` + +Expected: a `decision: block` payload with the checkpoint procedure. + +## Subagent usage tracker + +The `Stop` guard (and any statusline) only sees the **main thread**. Work +done by Task-tool subagents is logged as separate per-agent transcripts that +a parent-only reader never sees — the documented gap in +[anthropics/claude-code#32175](https://github.com/anthropics/claude-code/issues/32175) +and [ryoppippi/ccusage#313](https://github.com/ryoppippi/ccusage/issues/313). +On disk (verified layout): + +``` +~/.claude/projects//.jsonl # main thread +~/.claude/projects///subagents/ + agent-.jsonl # subagent turns (isSidechain:true) + agent-.meta.json -> {agentType, description, toolUseId} + workflows/wf_*/agent-*.jsonl # workflow subagents +``` + +Three layers recover that data: + +- **`SubagentStop` hook** (`hooks/subagent-tracker.py`) — parses each finishing + subagent's transcript (deduped by `message.id`, billed per the record's own + `message.model`, cache split by TTL), reads the sibling `.meta.json` for + `agentType`/`description`, and folds it into a per-session aggregate at + `/tmp/zetetic-subagents-.json` (keyed by `agentId`, so re-firing + updates rather than double-counts). +- **The [statusline](../statusline) plugin** — reads that aggregate and shows + `🤖N · tokens · $cost` so live subagent spend is visible alongside the main + thread (another shared-file convention between the two plugins). +- **Stop guard** — surfaces cumulative session spend (main + subagents) in the + checkpoint message and stub. The context-window *decision* stays + main-thread-only (mixing in subagent tokens would mis-trigger checkpoints); + only the reported figure is enriched. + +### Pricing (sourced) + +Token cost uses Anthropic's published per-MTok rates and cache multipliers +(Opus 4.8 $5/$25, Sonnet 4.6 $3/$15, Haiku 4.5 $1/$5, Fable 5 $10/$50; cache +write 1.25× input at 5m / 2× at 1h, cache read 0.1×). Every constant cites its +source at the use site in `tools/subagent_usage.py`. + +### Retrospective report + +`tools/subagent_usage.py` doubles as a CLI that parses **all** subagent +transcripts across a project and reports cost grouped by `agent_type`: + +```bash +python3 tools/subagent_usage.py [/path/to/project] # human table +python3 tools/subagent_usage.py --json # machine-readable +``` + +### Test the tracker + +```bash +SID="demo-$(date +%s)" +echo '{"session_id":"'"$SID"'","transcript_path":"","cwd":"'"$PWD"'"}' \ + | python3 hooks/subagent-tracker.py +cat "/tmp/zetetic-subagents-$SID.json" | python3 -m json.tool +``` + +## Manual install (without the plugin system) + +```bash +mkdir -p ~/.claude/hooks ~/.claude/agents +cp hooks/stop-context-guard.py hooks/checkpoint_protocol.py ~/.claude/hooks/ +chmod +x ~/.claude/hooks/stop-context-guard.py +cp agents/memory-writer.md ~/.claude/agents/memory-writer.md +``` + +`stop-context-guard.py` imports `checkpoint_protocol.py` from its own +directory and `subagent-tracker.py` imports the shared core from the sibling +`tools/` directory — keep each pair together. Then register the `Stop` / +`SubagentStop` entries (see `hooks/hooks.json`) in `~/.claude/settings.json`, +pointing at the installed paths. + +## License + +[MIT](../../LICENSE) © Clement Deust diff --git a/plugins/context-guard/agents/memory-writer.md b/plugins/context-guard/agents/memory-writer.md new file mode 100644 index 0000000..fd565d1 --- /dev/null +++ b/plugins/context-guard/agents/memory-writer.md @@ -0,0 +1,33 @@ +--- +name: memory-writer +description: >- + Use this agent when the stop-context-guard hook reports that the session has crossed its checkpoint threshold (WARN) and asks for a reflection checkpoint. The parent session passes (a) a distilled session summary and (b) the path of the mechanical checkpoint stub the hook already wrote; when a scoped memory layer is installed, it additionally passes its memory scope and MEMORY_AGENT_ID. This agent is the sleeptime memory manager of the pair (letta pattern) — a budgeted scribe that merges the summary into the checkpoint and persists it. By default it fills the stub file in place (vanilla Claude Code, no extra tooling); it uses a scoped memory store only when one is detected at runtime. It persists exactly what it is given and never invents content. Examples: Context: the Stop hook blocked with "CHECKPOINT THRESHOLD — spawn the memory-writer subagent". assistant: "I'll distill the session into the summary schema and hand it to the memory-writer agent with the stub path." The hook's WARN reflection delegates checkpoint persistence to this agent so the parent keeps its remaining context for the user's task. Context: the user asks to checkpoint the session before switching tasks. assistant: "Let me use the memory-writer agent to persist a semantic checkpoint of where we are." Manual checkpointing reuses the same scribe. +tools: Read, Write, Edit, Bash, mcp__plugin_cortex_cortex__remember +model: haiku +--- + +You are the memory-writer: a single-purpose memory-manager agent with a hard context budget of 16K tokens — the sleeptime half of a letta-style agent pair. A parent session at its context checkpoint threshold hands you (a) a distilled session summary and (b) the absolute path of a mechanical checkpoint stub under `~/.claude/memories/checkpoints/`; when a scoped memory layer is installed it also hands you its memory scope and `MEMORY_AGENT_ID`. You persist; you do not think up new content. Every fact you write must come verbatim from the parent's summary or the stub — if a schema section is missing from the input, write `` rather than inventing it. + +## Procedure + +1. Read the stub file at the path the parent gave you. It carries the schema skeleton (goals / file references / errors and fixes / current state / next steps), the git state, and the session metadata — all captured for free by the hook. +2. Merge the parent's distilled summary into the stub's schema sections, replacing every `` placeholder: + - **Goals** — what the session is trying to achieve, in priority order. + - **File references** — paths plus line ranges (`path:start-end`) the resumed session will need. Keep the git-seeded list only where it is load-bearing. + - **Errors and fixes** — each error hit this session and how it was fixed or worked around. + - **Current state** — one paragraph: where the work stands right now. Keep the mechanical lines (model, tokens, branch, commit) already present. + - **Next steps** — exact ordered actions; the first must be executable without re-deriving anything. + Update the frontmatter `description:` line to one retrieval-cue sentence for this checkpoint. Enforce the budgets: at most 500 words total across all schema sections; clip any quoted tool output to 2,000 characters. +3. **Detect the persistence path at runtime.** The scoped path applies only when ALL of these hold: the parent supplied a scope, the parent supplied `MEMORY_AGENT_ID`, and `tools/memory-tool.sh` exists (project root or `~/.claude/tools/`). Anything less → the default path. +4. **Default path — stub file (vanilla Claude Code).** Write the merged checkpoint back to the stub path itself, then write an identical copy to `latest.md` in the same directory. This needs nothing beyond your Read/Write tools. +5. **Scoped path — memory store (only when detected in step 3).** Write the merged checkpoint to the scoped working-state block with a block verb — state goes in the block, never through a remember endpoint: + ```bash + MEMORY_AGENT_ID= tools/memory-tool.sh rethink /memories//checkpoint.md "" + # first checkpoint of the scope: use `create` instead of `rethink` + ``` + Local FS is authoritative and synchronous; verify with `memory-tool.sh view`, not through the store's recall endpoint (its replica is eventually consistent). Additionally, for each WHY-level fact the parent flagged (decisions with rationale, rejected approaches with root causes, lessons), store one entry via the store's remember tool (e.g. `cortex:remember`) with `tags: ["archival", ...]` AND the parent's `agent_topic`. Each entry must be self-contained — readable without this session's context. Skip WHAT-level code, task progress, and transient state (those belong in the block). Be selective: not every observation warrants an archival entry. If the remember tool is unavailable, fold these facts into the checkpoint's "errors and fixes" section instead and say so in your report. +6. Verify by reading the written file back once (Read on the default path, `memory-tool.sh view` on the scoped path). + +## Output + +Return exactly: the checkpoint path written, which path you took (stub default or scoped store), its word count, the number of archival memory entries stored (0 on the default path), and any schema section the parent failed to provide. Nothing else — your final text is consumed by the parent session, not the user. diff --git a/plugins/context-guard/hooks/checkpoint_protocol.py b/plugins/context-guard/hooks/checkpoint_protocol.py new file mode 100644 index 0000000..50e86a0 --- /dev/null +++ b/plugins/context-guard/hooks/checkpoint_protocol.py @@ -0,0 +1,162 @@ +"""checkpoint_protocol.py — compose the Stop guard's checkpoint instructions. + +stop-context-guard.py measures context usage and decides WHEN to act; this +module owns WHAT the model is told. Each level (warn / hard) has two +variants, chosen at runtime by `detect_memory_tool`: + + * generic (default) — references only tools that exist in vanilla + Claude Code: the memory-writer subagent fills the mechanical stub + file the hook already wrote. + * scoped — emitted only when a scoped memory layer is installed + (`tools/memory-tool.sh` in the project root or ~/.claude/tools/): + the checkpoint routes through the store's block verbs and durable + facts through its remember endpoint. + +Detection runs per invocation, so an outside user without the memory +layer never sees store-specific wording (memory verbs, MEMORY_AGENT_ID, +cortex:remember). +""" + +import os + +SCHEMA = ( + "goals / file references (paths + line ranges) / errors and fixes / " + "current state / next steps — <=500 words total, quoted tool outputs " + "clipped to 2,000 chars" +) + +_MEMORY_TOOL_RELPATH = os.path.join("tools", "memory-tool.sh") + + +def detect_memory_tool(cwd: str): + """Return the scoped memory layer's CLI path when installed, else None. + + Same detection the memory-writer agent performs: `tools/memory-tool.sh` + in the project root, then in ~/.claude/tools/. + """ + candidates = ( + os.path.join(cwd or "", _MEMORY_TOOL_RELPATH), + os.path.join(os.path.expanduser("~"), ".claude", _MEMORY_TOOL_RELPATH), + ) + for path in candidates: + if os.path.isfile(path): + return path + return None + + +def _warn_header(ctx: int, warn: int, hard: int) -> str: + return ( + f"⚠ CHECKPOINT THRESHOLD — {ctx:,} input tokens " + f"(≥ {warn:,} for this model; hard stop at {hard:,}).\n" + f"This is a reflection pause, NOT the end of the session. While you still " + f"have headroom, persist the semantic checkpoint via a budgeted subagent, " + f"then resume the user's task:\n\n" + f"1. Distill the session into the summary schema ({SCHEMA}).\n" + ) + + +def _warn_footer(hard: int) -> str: + return ( + f"3. CONTINUE the user's task in this session. The hard cap at {hard:,} " + f"still applies; thanks to this reflection it should be a formality." + ) + + +_SPAWN = ( + "2. Spawn the memory-writer subagent (Agent tool, subagent_type " + "\"memory-writer\", or \"context-guard:memory-writer\" if only the " + "plugin copy is installed). " +) + +_SPAWN_FALLBACK = ( + "Do not write the checkpoint yourself unless the spawn fails (agent not " + "installed — it ships in the context-guard plugin under " + "agents/memory-writer.md; install with /agents or copy it to " + "~/.claude/agents/).\n" +) + + +def warn_reason(ctx: int, stub_path: str, warn: int, hard: int) -> str: + """WARN instructions, generic variant: stub-file protocol only.""" + return ( + _warn_header(ctx, warn, hard) + + _SPAWN + + f"Pass it: your distilled summary and the checkpoint stub path " + f"{stub_path or 'n/a'}. It merges the summary into the stub's schema " + f"sections in place and mirrors it to latest.md in the same " + f"directory. " + + _SPAWN_FALLBACK + + _warn_footer(hard) + ) + + +def warn_reason_scoped(ctx: int, stub_path: str, warn: int, hard: int) -> str: + """WARN instructions when a scoped memory layer is installed.""" + return ( + _warn_header(ctx, warn, hard) + + _SPAWN + + f"Pass it: your distilled summary, your memory scope + " + f"MEMORY_AGENT_ID, and the checkpoint stub path {stub_path or 'n/a'}. " + f"It merges the summary into the schema and persists it to " + f"/memories//checkpoint.md plus durable WHY-level facts " + f"via the store's remember endpoint (agent_topic-scoped); if the " + f"scoped store is unreachable it falls back to filling the stub file " + f"in place. " + + _SPAWN_FALLBACK + + _warn_footer(hard) + ) + + +def _block_header(ctx: int, hard: int) -> str: + return ( + f"⚠ CONTEXT SOFT CAP REACHED — {ctx:,} input tokens " + f"(≥ {hard:,} session budget for this model).\n" + f"Continuing in this session now risks context poisoning, quota burn, and " + f"escalating per-turn cost. Execute the checkpoint protocol before yielding:\n\n" + ) + + +_BLOCK_FOOTER = ( + "3. End your response with exactly:\n" + " CHECKPOINT — context cleared.\n" + " Resume from: \n" + " Next action: \n" + "Then instruct the user to run /clear and resume. Resume contract: the " + "next session reads the checkpoint + at most ONE targeted search — it " + "must NOT re-read files the checkpoint already summarizes.\n" + "Do NOT start new substantive work in this session." +) + + +def block_reason(ctx: int, stub_path: str, hard: int) -> str: + """HARD instructions, generic variant: stub-file protocol only.""" + return ( + _block_header(ctx, hard) + + f"1. Write (or update) your semantic checkpoint ({SCHEMA}) by filling " + f"the stub file at " + f"{stub_path or '~/.claude/memories/checkpoints/latest.md'} in place. " + f"If the WARN-time memory-writer already wrote it, update only what " + f"changed since.\n" + f"2. If important decisions are not yet durable, fold them into the " + f"checkpoint's errors-and-fixes section.\n" + + _BLOCK_FOOTER + ) + + +def block_reason_scoped(ctx: int, stub_path: str, hard: int) -> str: + """HARD instructions when a scoped memory layer is installed.""" + return ( + _block_header(ctx, hard) + + f"1. Write (or update) your semantic checkpoint ({SCHEMA}): " + f"MEMORY_AGENT_ID= tools/memory-tool.sh rethink " + f"/memories//checkpoint.md — if the scoped store is " + f"unreachable, fill the stub file at " + f"{stub_path or '~/.claude/memories/checkpoints/latest.md'} in place. " + f"If the WARN-time memory-writer already wrote it, update only what " + f"changed since.\n" + f"2. If important decisions are not yet durable, persist them now via " + f"the store's remember endpoint (scoped to your agent_topic); " + f"otherwise fold them into the checkpoint's errors-and-fixes " + f"section.\n" + + _BLOCK_FOOTER + ) diff --git a/plugins/context-guard/hooks/hooks.json b/plugins/context-guard/hooks/hooks.json new file mode 100644 index 0000000..dbcac1e --- /dev/null +++ b/plugins/context-guard/hooks/hooks.json @@ -0,0 +1,26 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/stop-context-guard.py", + "timeout": 10 + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/subagent-tracker.py", + "timeout": 10 + } + ] + } + ] + } +} diff --git a/hooks/stop-context-guard.py b/plugins/context-guard/hooks/stop-context-guard.py similarity index 80% rename from hooks/stop-context-guard.py rename to plugins/context-guard/hooks/stop-context-guard.py index 9bf4b05..27eaf7c 100755 --- a/hooks/stop-context-guard.py +++ b/plugins/context-guard/hooks/stop-context-guard.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """stop-context-guard.py — Stop hook: enforce the token-budget checkpoint protocol. -Contract (source of truth: agents/orchestrator.md ): +Contract: | Model | Checkpoint (warn) | Hard cap (block) | Why | |--------------------|-------------------|------------------|--------------------------------------| @@ -27,15 +27,14 @@ - WARN <= ctx < HARD -> write a free mechanical checkpoint stub (summary schema) AND block the stop exactly once: the model is instructed to spawn the `memory-writer` subagent - (a normal Claude Code agent installed in - ~/.claude/agents/, shipped in this repo under - agents/) that persists the model's distilled - summary into the memory store — scoped checkpoint - via memory-tool.sh + durable facts via - cortex:remember when the zetetic memory layer is - present, stub-file fallback otherwise — then - RESUME the user's task in-session (reflection, - not a stop). + (a normal Claude Code agent, shipped in this + plugin under agents/) that persists the model's + distilled summary — by default into the stub file + itself (vanilla Claude Code, no extra tooling); + when a scoped memory layer is detected at runtime + (checkpoint_protocol.detect_memory_tool), into + that store instead — then RESUME the user's task + in-session (reflection, not a stop). - ctx >= HARD -> write the stub AND block the stop exactly once, injecting the checkpoint-finalize procedure so the model persists the semantic checkpoint and signals @@ -65,6 +64,16 @@ import sys from datetime import datetime, timezone +# The protocol text (generic vs scoped-memory-layer variants) lives in the +# sibling checkpoint_protocol module, shipped in the same hooks/ directory. +# Named failure mode: a manual install copied only this script. A Stop hook +# must never fail hard, so degrade to inert rather than erroring every stop. +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +try: + import checkpoint_protocol +except ImportError: + sys.exit(0) + # --- Thresholds (tokens) ----------------------------------------------------- # Single source of truth shared with statusline-command.sh. First substring # match against the lowercased model id wins; "default" applies otherwise. @@ -374,65 +383,6 @@ def _save_level(session_id: str, level: str) -> None: pass -SCHEMA = ( - "goals / file references (paths + line ranges) / errors and fixes / " - "current state / next steps — <=500 words total, quoted tool outputs " - "clipped to 2,000 chars" -) - - -def _warn_reason(ctx: int, stub_path: str, warn: int, hard: int) -> str: - return ( - f"⚠ CHECKPOINT THRESHOLD — {ctx:,} input tokens " - f"(≥ {warn:,} for this model; hard stop at {hard:,}).\n" - f"This is a reflection pause, NOT the end of the session. While you still " - f"have headroom, persist the semantic checkpoint via a budgeted subagent, " - f"then resume the user's task:\n\n" - f"1. Distill the session into the summary schema ({SCHEMA}).\n" - f"2. Spawn the memory-writer subagent (Agent tool, subagent_type " - f"\"memory-writer\", or \"session-optimizer:memory-writer\" if only the " - f"plugin copy is installed). Pass it: your distilled summary, your " - f"memory scope + MEMORY_AGENT_ID (when the zetetic memory layer is " - f"present), and the checkpoint stub path {stub_path or 'n/a'}. It merges " - f"the summary into the schema and persists it to " - f"/memories//checkpoint.md plus durable WHY-level facts via " - f"cortex:remember (agent_topic-scoped); without a memory layer it falls " - f"back to filling the stub file in place. Do not write the checkpoint " - f"yourself unless the spawn fails (agent not installed — it ships in " - f"session-optimizer under agents/memory-writer.md; install with /agents " - f"or copy it to ~/.claude/agents/).\n" - f"3. CONTINUE the user's task in this session. The hard cap at {hard:,} " - f"still applies; thanks to this reflection it should be a formality." - ) - - -def _block_reason(ctx: int, stub_path: str, hard: int) -> str: - return ( - f"⚠ CONTEXT SOFT CAP REACHED — {ctx:,} input tokens " - f"(≥ {hard:,} session budget for this model).\n" - f"Continuing in this session now risks context poisoning, quota burn, and " - f"escalating per-turn cost. Execute the checkpoint protocol before yielding:\n\n" - f"1. Write (or update) your semantic checkpoint ({SCHEMA}): with the " - f"zetetic memory layer, MEMORY_AGENT_ID= " - f"tools/memory-tool.sh rethink /memories//checkpoint.md — " - f"otherwise fill the stub file at " - f"{stub_path or '~/.claude/memories/checkpoints/latest.md'} in place. " - f"If the WARN-time memory-writer already wrote it, update only what " - f"changed since.\n" - f"2. If important decisions are not yet durable, persist them now " - f"(cortex:remember, scoped to your agent_topic; without Cortex, fold " - f"them into the checkpoint's errors-and-fixes section).\n" - f"3. End your response with exactly:\n" - f" CHECKPOINT — context cleared.\n" - f" Resume from: \n" - f" Next action: \n" - f"Then instruct the user to run /clear and resume. Resume contract: the " - f"next session reads the checkpoint + at most ONE targeted search — it " - f"must NOT re-read files the checkpoint already summarizes.\n" - f"Do NOT start new substantive work in this session." - ) - - def main(): try: data = json.load(sys.stdin) @@ -468,10 +418,21 @@ def main(): _save_level(session_id, level) sub_line = _subagent_line(session_id) + # Runtime detection: the scoped-memory-layer wording is emitted only when + # the layer is actually installed; everyone else gets the stub-file + # protocol, which references vanilla Claude Code tools only. + scoped = checkpoint_protocol.detect_memory_tool(cwd) is not None + if scoped: + warn_reason = checkpoint_protocol.warn_reason_scoped + block_reason = checkpoint_protocol.block_reason_scoped + else: + warn_reason = checkpoint_protocol.warn_reason + block_reason = checkpoint_protocol.block_reason + if level == "hard": _exit({ "decision": "block", - "reason": _block_reason(ctx, stub_path, hard) + sub_line, + "reason": block_reason(ctx, stub_path, hard) + sub_line, "systemMessage": ( f"[context-guard] {ctx:,} tokens ≥ {hard:,} soft cap " f"({model_id or 'model'}) — forcing a checkpoint before the " @@ -481,7 +442,7 @@ def main(): else: # warn — one-time reflection block: persist memory while headroom remains _exit({ "decision": "block", - "reason": _warn_reason(ctx, stub_path, warn, hard) + sub_line, + "reason": warn_reason(ctx, stub_path, warn, hard) + sub_line, "systemMessage": ( f"[context-guard] {ctx:,} tokens ≥ {warn:,} checkpoint threshold " f"({(model_id or 'model')}) — spawning the memory-writer subagent to " diff --git a/hooks/subagent-tracker.py b/plugins/context-guard/hooks/subagent-tracker.py similarity index 100% rename from hooks/subagent-tracker.py rename to plugins/context-guard/hooks/subagent-tracker.py diff --git a/tools/subagent_usage.py b/plugins/context-guard/tools/subagent_usage.py similarity index 100% rename from tools/subagent_usage.py rename to plugins/context-guard/tools/subagent_usage.py diff --git a/plugins/refine-gate/.claude-plugin/plugin.json b/plugins/refine-gate/.claude-plugin/plugin.json new file mode 100644 index 0000000..57ecee3 --- /dev/null +++ b/plugins/refine-gate/.claude-plugin/plugin.json @@ -0,0 +1,20 @@ +{ + "name": "refine-gate", + "description": "Prompt-binding gate for Claude Code: a UserPromptSubmit hook detects prompts carrying unbound references (\"the SSE solution\", \"like before\", \"still broken\") or ungrounded work requests, and injects the /refine procedure — build a binding table with evidence, separate symptom from goal, select an execution strategy from a research-backed table (15 strategies verified against 2024-2026 literature, counter-evidence included), and define acceptance criteria as external signals.", + "version": "2.0.0", + "author": { + "name": "Clement Deust", + "email": "admin@ai-architect.tools" + }, + "homepage": "https://github.com/cdeust/session-optimizer", + "repository": "https://github.com/cdeust/session-optimizer", + "license": "MIT", + "keywords": [ + "claude-code", + "prompt-engineering", + "refine", + "intent-binding", + "userpromptsubmit", + "execution-strategy" + ] +} diff --git a/plugins/refine-gate/README.md b/plugins/refine-gate/README.md new file mode 100644 index 0000000..6c3e7ce --- /dev/null +++ b/plugins/refine-gate/README.md @@ -0,0 +1,95 @@ +# refine-gate + +A prompt-binding gate for [Claude Code](https://code.claude.com): +a `UserPromptSubmit` hook (`hooks/refine_gate.py`) plus the `/refine` skill +(`skills/refine/`). + +Communication failures cost more than code failures: "make it work +exactly like the SSE solution" carries precise intent that the model +can bind to the wrong artifact and then build the wrong thing — +correctly. The refine gate makes that binding explicit and cheap to +correct BEFORE work starts. + +## Install + +``` +/plugin marketplace add cdeust/session-optimizer +/plugin install refine-gate@session-optimizer-marketplace +``` + +The plugin wires the `UserPromptSubmit` hook and registers the `/refine` +skill automatically. Requires Python 3. + +## How it works + +The hook inspects every prompt on two tiers: + +* **Tier 1 — reference markers**: prior-artifact shorthand ("the last + release"), comparisons to unstated referents ("like before", + "exactly as"), repeat-failure phrasing ("still broken", "back to + square one") → inject the full binding-table instruction. +* **Tier 2 — ungrounded work request**: the prompt asks for work + (fix/build/improve/problem/…) but contains no concrete anchor — no + file path, commit sha, or line ref. Named systems/variables ("the + memory system", "the heat variable") must then be bound to their + actual code artifacts before reasoning. Prompts the user grounded + themselves pass through untouched. Slash commands are never gated; + the hook always exits 0 (it can inform, never block). + +The injected instruction points at the bundled `/refine` skill: build +a binding table (reference → artifact, with evidence from grep, git +history, and your memory layer's recall tool when one is installed), +separate symptom from goal, select an execution strategy from a +research-backed table (15 strategies re-verified against the 2024–2026 +literature, counter-evidence included — e.g. intrinsic self-correction +degrades reasoning, arXiv:2310.01798; CoT prompting is marginal on +reasoning models), and define acceptance criteria as EXTERNAL signals: +run the test, fetch the source, measure — never the model re-checking +itself. + +Test it directly: + +```bash +echo '{"prompt":"there is a problem in the memory system with the heat variable"}' \ + | python3 hooks/refine_gate.py | python3 -m json.tool +``` + +Expected: a `hookSpecificOutput.additionalContext` payload carrying the +binding instruction. A grounded prompt (one naming a real file path) +produces no output. + +## Token-usage ledger (measured, reproducible) + +The gate must not eat the budget it exists to protect. Two instruments +keep that honest: + +* `tests/test_refine_gate.py` (repo root) pins the contract: silent + prompts cost ZERO context, and any injection stays under a 400-token + ceiling (estimated at 4 chars/token — Anthropic's glossary gives ~3.5 + chars/token EN, so the estimate under-counts ~12%; the ceiling has + >10x headroom over the current ~275). +* `tools/measure_refine_overhead.py` replays your OWN prompt history + (local transcripts, nothing leaves the machine). Measured 2026-06-12 + on 117 real prompts across 40 transcripts: fired on 38% (tier 1: 17, + tier 2: 27, silent: 73), ~275 est. tokens per fired prompt, ~103 per + prompt overall ≈ **1.3% of a 160k-token session budget per 20-prompt + session**. + +The benefit side is counterfactual and is NOT claimed as measured: a +mis-bound prompt costs roughly a full wrong-direction session +(~10⁵ tokens — e.g. the delivery-layer machinery this gate's design +case produced and later deleted). At ~2k injected tokens per +20-prompt session, the gate breaks even if it prevents one mis-binding +per ~80 sessions. + +## Where it runs + +Hooks run wherever Claude Code plugins run: **CLI, desktop app, IDE +extensions, and Cowork**. The claude.ai / Claude Desktop **chat** surface +has no hook mechanism — there, upload `skills/refine/` as an Agent +Skill (same SKILL.md format): the `/refine` procedure travels; the +automatic per-prompt gate does not. + +## License + +[MIT](../../LICENSE) © Clement Deust diff --git a/plugins/refine-gate/hooks/hooks.json b/plugins/refine-gate/hooks/hooks.json new file mode 100644 index 0000000..e2c3d53 --- /dev/null +++ b/plugins/refine-gate/hooks/hooks.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/refine_gate.py", + "timeout": 5, + "statusMessage": "refine gate: checking for unbound references" + } + ] + } + ] + } +} diff --git a/hooks/refine_gate.py b/plugins/refine-gate/hooks/refine_gate.py similarity index 90% rename from hooks/refine_gate.py rename to plugins/refine-gate/hooks/refine_gate.py index 4dc0982..e202cdb 100755 --- a/hooks/refine_gate.py +++ b/plugins/refine-gate/hooks/refine_gate.py @@ -2,9 +2,10 @@ """UserPromptSubmit gate: inject the /refine binding-table instruction when the prompt contains unresolved-reference markers. -Part of the session-optimizer plugin: ships with the /refine skill -(skills/refine/SKILL.md) and is wired via plugin.json's UserPromptSubmit -hook. The skill only runs when invoked; this hook automates its core +Part of the refine-gate plugin: ships with the /refine skill +(skills/refine/SKILL.md) and is wired via the plugin's hooks/hooks.json +UserPromptSubmit entry. The skill only runs when invoked; this hook +automates its core discipline for prompts that look like they carry unbound references — shorthand for prior work ("the SSE solution", "like before"), repeat-failure markers ("still broken", @@ -77,10 +78,11 @@ _INSTRUCTION_REFS = ( "This prompt contains references that may be unbound " "(matched: {matched}). Before executing, apply the /refine " - "procedure (the session-optimizer /refine skill): (1) build the " + "procedure (the refine-gate /refine skill): (1) build the " "binding table — resolve EACH vague reference to a concrete " "artifact (file/commit/memory/process) with evidence from " - "cortex:recall, git log, or grep; (2) state symptom vs goal vs " + "git log, grep, or your memory layer's recall tool when one is " + "installed; (2) state symptom vs goal vs " "non-goals; (3) select the execution strategy from the skill's " "research-backed table (/refine skill §5) — match the task's " "characteristics, never stack scaffolding a task doesn't exhibit " @@ -96,11 +98,12 @@ _INSTRUCTION_GROUND = ( "This prompt requests work but names no concrete " "artifact (no file path, commit, or line ref). Before executing, " - "apply the /refine procedure (the session-optimizer /refine skill): " + "apply the /refine procedure (the refine-gate /refine skill): " "(1) bind every named system, component, variable, or concept to " "its actual code artifact (which module IS 'the X system'? which " - "field/code path IS that variable?) using cortex:recall, grep, " - "and the knowledge graph — a name can map to several artifacts, " + "field/code path IS that variable?) using grep, git history, and " + "your memory layer's recall tool when one is installed " + "— a name can map to several artifacts, " "and picking the wrong one solves the wrong problem; (2) recall " "prior decisions and failed attempts on those artifacts; (3) " "state symptom vs goal vs non-goals; (4) select the execution " diff --git a/skills/refine/SKILL.md b/plugins/refine-gate/skills/refine/SKILL.md similarity index 94% rename from skills/refine/SKILL.md rename to plugins/refine-gate/skills/refine/SKILL.md index addc8f9..6f070e5 100644 --- a/skills/refine/SKILL.md +++ b/plugins/refine-gate/skills/refine/SKILL.md @@ -23,7 +23,7 @@ output: > ## Automation A `UserPromptSubmit` hook (`hooks/refine_gate.py`, wired by this -plugin's `plugin.json`) applies this skill automatically on two +plugin's `hooks/hooks.json`) applies this skill automatically on two tiers: * **Tier 1 — reference markers**: prior-artifact shorthand, "exactly @@ -144,14 +144,14 @@ knowledge?) and pick the matching row(s) — usually 1–2, never all: | recursive_refinement | hard reasoning, high precision | DeepSeek-R1 arXiv:2501.12948; Snell arXiv:2408.03314; survey arXiv:2512.02008: no test-time strategy universally optimal (confirmed, native) | extended thinking / higher effort on the hard subtask only | | self_consistency | one answer, several plausible derivations | Wang arXiv:2203.11171; still effective as parallel test-time scaling (arXiv:2512.02008, ST-BoN arXiv:2503.01422) (confirmed) | N independent attempts + majority/judge via parallel subagents | | verified_reasoning | accuracy-critical claims | CoVe arXiv:2309.11495 (Meta AI, not Stanford/Anthropic); BUT intrinsic self-correction degrades reasoning: Huang ICLR'24 arXiv:2310.01798, Self-Correction Bench arXiv:2507.02778 (caveated) | verify against EXTERNAL ground truth — run the test, fetch the source, measure — never by re-asking the model to double-check itself | -| reflexion | retrying after failed attempts | Shinn arXiv:2303.11366; reflection without external signal is mostly confirmatory, rarely flips the answer ("First Try Matters" arXiv:2510.08308) (caveated) | feed REAL feedback — test failures, benchmark deltas, recalled failures (cortex:recall first, cortex:remember after) | +| reflexion | retrying after failed attempts | Shinn arXiv:2303.11366; reflection without external signal is mostly confirmatory, rarely flips the answer ("First Try Matters" arXiv:2510.08308) (caveated) | feed REAL feedback — test failures, benchmark deltas, recalled failures (recall past attempts first, store the lesson after, via your memory layer when one is installed) | | tree_of_thoughts | genuinely branching solution space | Yao arXiv:2305.10601; ~100× compute vs one CoT on Game-of-24, gains shown on puzzle-class tasks (arXiv:2401.14295) (caveated: cost) | judge panel of N approaches ONLY when designs genuinely diverge; otherwise skip | | graph_of_thoughts | decompose-and-merge structure | Besta arXiv:2308.09687 (confirmed, niche) | build the dependency graph explicitly (query_workflow_graph / get_impact) before editing | | meta_prompting | task spans expert roles | Suzgun arXiv:2401.12954; BUT multi-agent often ≤ single agent — 14 failure modes, spec issues + misalignment + weak verification (MAST arXiv:2503.13657) (caveated) | route to ONE specialist with a complete role spec + verification step; don't fan out by default | | plan_and_solve | ordering-heavy multi-step work | Wang arXiv:2305.04091 (confirmed, absorbed into agentic planning) | plan mode / written plan with checkable steps | | react | interleaved reasoning + tool use | Yao arXiv:2210.03629 (native) | the agent loop IS ReAct; just act | | many_shot (was few_shot) | output format/style must match examples | Brown arXiv:2005.14165 → Many-Shot ICL arXiv:2404.11018 (NeurIPS'24 spotlight): more real examples keep helping with long context (confirmed, evolved) | paste real examples from THIS repo; scale count with context budget | -| generate_knowledge | missing domain facts | Liu arXiv:2110.08387 (weakened: parametric generation hallucinates; superseded by retrieval) | RETRIEVE, don't generate: cortex:recall, read the actual paper/source — knowledge from a vacuum is the failure mode §8 of coding-standards forbids | +| generate_knowledge | missing domain facts | Liu arXiv:2110.08387 (weakened: parametric generation hallucinates; superseded by retrieval) | RETRIEVE, don't generate: your memory layer's recall (when installed), read the actual paper/source — knowledge from a vacuum is the failure mode §8 of coding-standards forbids | | multimodal_cot | visual evidence | Zhang arXiv:2302.00923 (native multimodality) | read the actual image/screenshot; verify UI claims by looking | | chain_of_thought | explicit audit trail wanted | Wei-era CoT now marginal on reasoning models: +2.9–3.1% at 20–80% time cost (Wharton GAIL 2025); HARMS some task classes, −36.3% o1-preview on implicit statistical learning (arXiv:2410.21333); models reason unprompted (arXiv:2402.10200) (weakened) | don't inject "think step by step"; write the derivation out only when the USER needs the audit trail | | problem_analysis | tangled symptom, unclear decomposition | engine-internal (no arXiv); closest sourced umbrella: decomposition class in The Prompt Report arXiv:2406.06608 (weakest row) | §§2–3 of this skill: binding + symptom/goal split IS the decomposition | @@ -196,11 +196,13 @@ the end and report pass/fail explicitly. ### 8. Close the loop -After execution (or after the user corrects a binding), store via -`cortex:remember` (tags: ["archival", "lesson", "prompt-binding"], -agent_topic scoped): the raw phrase → correct binding pair, so the -next session binds it instantly. Mis-bindings the user had to correct -are the highest-value memories this skill produces. +After execution (or after the user corrects a binding), store the raw +phrase → correct binding pair via your memory layer's remember tool +when one is installed (e.g. `cortex:remember`, tags: ["archival", +"lesson", "prompt-binding"], agent_topic scoped), so the next session +binds it instantly; without a memory layer, note it in the project's +own records (CLAUDE.md or a docs note). Mis-bindings the user had to +correct are the highest-value memories this skill produces. ## Failure modes this skill exists to prevent diff --git a/tools/measure_refine_overhead.py b/plugins/refine-gate/tools/measure_refine_overhead.py similarity index 100% rename from tools/measure_refine_overhead.py rename to plugins/refine-gate/tools/measure_refine_overhead.py diff --git a/plugins/statusline/.claude-plugin/plugin.json b/plugins/statusline/.claude-plugin/plugin.json new file mode 100644 index 0000000..5713f2c --- /dev/null +++ b/plugins/statusline/.claude-plugin/plugin.json @@ -0,0 +1,22 @@ +{ + "name": "statusline", + "description": "Multi-line Claude Code statusline (Catppuccin Mocha) with RGB-gradient context bars tied to per-model checkpoint thresholds, monthly cost tracking, per-session telemetry (tok/s, compactions, prompt-cache countdown), rate-limit gauges, and live subagent spend. Ships an install skill that copies the bundled assets into ~/.claude and wires settings.json.", + "version": "2.0.0", + "author": { + "name": "Clement Deust", + "email": "admin@ai-architect.tools" + }, + "homepage": "https://github.com/cdeust/session-optimizer", + "repository": "https://github.com/cdeust/session-optimizer", + "license": "MIT", + "keywords": [ + "statusline", + "status", + "context", + "git", + "cost", + "rate-limits", + "telemetry", + "ui" + ] +} diff --git a/README-statusline.fr.md b/plugins/statusline/README.fr.md similarity index 57% rename from README-statusline.fr.md rename to plugins/statusline/README.fr.md index dcc0872..882ade4 100644 --- a/README-statusline.fr.md +++ b/plugins/statusline/README.fr.md @@ -1,18 +1,49 @@ -> English version: [README-statusline.md](README-statusline.md) +> English version: [README.md](README.md) -# Statusline zététique — Claude Code +# statusline — Claude Code Statusline multi-lignes (Catppuccin Mocha) avec barres en dégradé RGB, suivi -de coûts mensuels et jauges d'objectifs par personne. +de coûts mensuels, télémétrie par session et jauges de rate-limit. -## Fichiers +## Installation + +``` +/plugin marketplace add cdeust/session-optimizer +/plugin install statusline@session-optimizer-marketplace +``` + +Puis demander à Claude d'**« installer la statusline »** — le skill +`statusline` embarqué copie les fichiers dans `~/.claude/` et déclare +`statusLine` dans `~/.claude/settings.json` (sauvegardes incluses, fichiers +de config jamais écrasés). Redémarrer Claude Code pour activer. Requiert +`jq` et `python3`. + +Un hook `SessionStart` maintient les fichiers de code à jour après +`plugin update` ; `statusline-budget.json` et `ctxguard-thresholds.json` +ne sont jamais touchés automatiquement. + +
+Installation manuelle (sans le système de plugins) + +1. Copier les 5 fichiers de `assets/` dans `~/.claude/` puis + `chmod +x ~/.claude/statusline-command.sh`. +2. Déclarer la statusline dans `~/.claude/settings.json` : + ```json + { "statusLine": { "type": "command", "command": "bash ~/.claude/statusline-command.sh", "padding": 1, "refreshInterval": 10 } } + ``` +3. Adapter `statusline-budget.json` à ses propres préférences. + +
+ +## Fichiers (embarqués sous `assets/`) | Fichier | Rôle | |---|---| | `statusline-command.sh` | Script de rendu (appelé par Claude Code à chaque refresh). | | `statusline-costs.py` | Agrégateur de coûts (scan `~/.claude/projects/**/*.jsonl`, cache 1 h). | | `statusline-transcript.py` | Télémétrie par session (tok/s, compactions, âge réponse, last_ts) — reverse-tail + scan incrémental, cache court (15 s, en arrière-plan). | -| `statusline-budget.json` | Config **personnelle** : objectifs mensuels, TTL cache, taille d'affichage. | +| `statusline-budget.json` | Config **personnelle** : TTL cache, taille d'affichage. | +| `ctxguard-thresholds.json` | Seuils de contexte par modèle — **partagés** avec le plugin context-guard (voir plus bas). | ## Segments @@ -22,6 +53,9 @@ de coûts mensuels et jauges d'objectifs par personne. récent quand le cwd n'est pas un dépôt. - **Session** : barre contexte 🧠, tokens, `💰` coût, `⏱` durée, rate-limits 🚀/🌟, churn ✏️. +- **Sous-agents** : dépense en direct `🤖N · tokens · $coût`, lue depuis + l'agrégat maintenu par le tracker `SubagentStop` du plugin context-guard + quand il est installé (segment vide sinon). - **Télémétrie** (m+) : `⚡ t/s` (débit du dernier tour — wall-clock, inclut la latence outils ⇒ borne basse), `🕑` âge dernière réponse, `❄` compte à rebours du cache de prompt (rouge = `cold`), `🗜` compactions de contexte. @@ -31,14 +65,15 @@ de coûts mensuels et jauges d'objectifs par personne. inline compacte sur la ligne session. Suivi d'une ligne **référence coût** (informative, pas un plafond) : `💰 $/mois · 🤖 $/run`. -## Installation +## Seuils partagés avec context-guard -1. Copier les 4 fichiers dans `~/.claude/`. -2. Déclarer la statusline dans `~/.claude/settings.json` : - ```json - { "statusLine": { "type": "command", "command": "~/.claude/statusline-command.sh" } } - ``` -3. Adapter `statusline-budget.json` à ses propres objectifs. +L'échelle vert → jaune → rouge de la barre de contexte est pilotée par +`~/.claude/ctxguard-thresholds.json` — une **convention de fichier partagé** +avec le hook Stop du plugin [context-guard](../context-guard). Un fichier, +deux consommateurs : la statusline est l'alerte visuelle passive, le garde +Stop l'application active ; éditer le fichier déplace les deux d'un coup, +donc ils restent alignés par construction. Le skill d'installation crée le +fichier s'il est absent et n'écrase jamais une copie existante. ## Tailles d'affichage (presets) @@ -53,7 +88,6 @@ Réglage : variable d'env `STATUSLINE_SIZE`, ou champ `"size"` de `statusline-bu en **secondes**. Pas de budget mensuel absolu : sur un forfait flat-rate, la contrainte est le quota, pas une dépense en $/tokens. - Barres : interpolation RGB continue par cellule (`grad_rgb`) vert→jaune→pêche→rouge. -- Seuils de contexte : `~/.claude/ctxguard-thresholds.json` (partagés avec le hook stop-context-guard). - Télémétrie : le `.py` tourne en arrière-plan (lock + TTL 15 s) et écrit un cache par session (clé = `transcript_path`) ; `🕑` et `❄` sont recalculés en direct à chaque refresh depuis `last_ts`, donc le décompte reste à la seconde entre deux diff --git a/README-statusline.md b/plugins/statusline/README.md similarity index 57% rename from README-statusline.md rename to plugins/statusline/README.md index 5e92126..aefb883 100644 --- a/README-statusline.md +++ b/plugins/statusline/README.md @@ -1,18 +1,48 @@ -> Version française : [README-statusline.fr.md](README-statusline.fr.md) +> Version française : [README.fr.md](README.fr.md) -# Zetetic statusline — Claude Code +# statusline — Claude Code Multi-line statusline (Catppuccin Mocha) with RGB-gradient bars, monthly -cost tracking, and per-person target gauges. +cost tracking, per-session telemetry, and rate-limit gauges. -## Files +## Install + +``` +/plugin marketplace add cdeust/session-optimizer +/plugin install statusline@session-optimizer-marketplace +``` + +Then ask Claude to **"install the statusline"** — the bundled `statusline` +skill copies the assets into `~/.claude/` and wires `statusLine` in +`~/.claude/settings.json` (backups included, config files never +overwritten). Restart Claude Code to activate. Requires `jq` and `python3`. + +A `SessionStart` hook keeps the code assets current after `plugin update`; +your `statusline-budget.json` and `ctxguard-thresholds.json` are never +touched automatically. + +
+Manual install (without the plugin system) + +1. Copy the 5 files from `assets/` into `~/.claude/` and + `chmod +x ~/.claude/statusline-command.sh`. +2. Declare the statusline in `~/.claude/settings.json`: + ```json + { "statusLine": { "type": "command", "command": "bash ~/.claude/statusline-command.sh", "padding": 1, "refreshInterval": 10 } } + ``` +3. Adapt `statusline-budget.json` to your own preferences. + +
+ +## Files (bundled under `assets/`) | File | Role | |---|---| | `statusline-command.sh` | Rendering script (called by Claude Code on every refresh). | | `statusline-costs.py` | Cost aggregator (scans `~/.claude/projects/**/*.jsonl`, 1 h cache). | | `statusline-transcript.py` | Per-session telemetry (tok/s, compactions, response age, last_ts) — reverse-tail + incremental scan, short cache (15 s, in the background). | -| `statusline-budget.json` | **Personal** config: monthly targets, cache TTL, display size. | +| `statusline-budget.json` | **Personal** config: cache TTL, display size. | +| `ctxguard-thresholds.json` | Per-model context thresholds — **shared** with the context-guard plugin (see below). | ## Segments @@ -22,6 +52,9 @@ cost tracking, and per-person target gauges. recently touched sub-repo when the cwd is not a repository. - **Session**: context bar 🧠, tokens, `💰` cost, `⏱` duration, rate limits 🚀/🌟, churn ✏️. +- **Subagents**: live spend `🤖N · tokens · $cost`, read from the aggregate + maintained by the context-guard plugin's `SubagentStop` tracker when that + plugin is installed (segment stays empty otherwise). - **Telemetry** (m+): `⚡ t/s` (throughput of the last turn — wall-clock, includes tool latency ⇒ lower bound), `🕑` age of the last response, `❄` prompt-cache countdown (red = `cold`), `🗜` context compactions. @@ -31,14 +64,15 @@ cost tracking, and per-person target gauges. a compact inline version on the session line. Followed by a **cost reference** line (informative, not a cap): `💰 $/month · 🤖 $/run`. -## Installation +## Shared thresholds with context-guard -1. Copy the 4 files into `~/.claude/`. -2. Declare the statusline in `~/.claude/settings.json`: - ```json - { "statusLine": { "type": "command", "command": "~/.claude/statusline-command.sh" } } - ``` -3. Adapt `statusline-budget.json` to your own targets. +The context bar's green → yellow → red scale is driven by +`~/.claude/ctxguard-thresholds.json` — a **shared-file convention** with the +[context-guard](../context-guard) plugin's Stop hook. One file, two +consumers: the statusline is the passive visual warning, the Stop guard the +active enforcement, and editing the file moves both at once so they stay on +par by construction. The install skill seeds the file if absent and never +overwrites an existing copy. ## Display sizes (presets) @@ -53,7 +87,6 @@ Setting: `STATUSLINE_SIZE` env variable, or the `"size"` field of `statusline-bu epoch in **seconds**. No absolute monthly budget: on a flat-rate plan, the constraint is the quota, not a spend in $/tokens. - Bars: continuous per-cell RGB interpolation (`grad_rgb`) green→yellow→peach→red. -- Context thresholds: `~/.claude/ctxguard-thresholds.json` (shared with the stop-context-guard hook). - Telemetry: the `.py` runs in the background (lock + 15 s TTL) and writes a per-session cache (key = `transcript_path`); `🕑` and `❄` are recomputed live on every refresh from `last_ts`, so the countdown stays second-accurate diff --git a/ctxguard-thresholds.json b/plugins/statusline/assets/ctxguard-thresholds.json similarity index 100% rename from ctxguard-thresholds.json rename to plugins/statusline/assets/ctxguard-thresholds.json diff --git a/statusline-budget.json b/plugins/statusline/assets/statusline-budget.json similarity index 100% rename from statusline-budget.json rename to plugins/statusline/assets/statusline-budget.json diff --git a/statusline-command.sh b/plugins/statusline/assets/statusline-command.sh similarity index 100% rename from statusline-command.sh rename to plugins/statusline/assets/statusline-command.sh diff --git a/statusline-costs.py b/plugins/statusline/assets/statusline-costs.py similarity index 100% rename from statusline-costs.py rename to plugins/statusline/assets/statusline-costs.py diff --git a/statusline-transcript.py b/plugins/statusline/assets/statusline-transcript.py similarity index 100% rename from statusline-transcript.py rename to plugins/statusline/assets/statusline-transcript.py diff --git a/plugins/statusline/hooks/hooks.json b/plugins/statusline/hooks/hooks.json new file mode 100644 index 0000000..bb7eb40 --- /dev/null +++ b/plugins/statusline/hooks/hooks.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "SessionStart": [ + { + "matcher": "startup|resume", + "hooks": [ + { + "type": "command", + "command": "echo '[statusline auto-update] Bundled statusline assets are in ${CLAUDE_PLUGIN_ROOT}/assets . Only if this statusline is already installed (~/.claude/statusline-command.sh exists): quietly compare the three CODE assets - statusline-command.sh, statusline-costs.py, statusline-transcript.py - against the copies in ~/.claude using your Read and Write tools; when any differ, copy the bundled version over, backing up each changed file first as name.bak.timestamp, then set the execute bit on statusline-command.sh. NEVER overwrite statusline-budget.json or ctxguard-thresholds.json - those hold user-tuned configuration. If no statusline is installed yet, do nothing. Report only if you actually updated a file.'", + "timeout": 10 + } + ] + } + ] + } +} diff --git a/plugins/statusline/skills/statusline/SKILL.md b/plugins/statusline/skills/statusline/SKILL.md new file mode 100644 index 0000000..942c633 --- /dev/null +++ b/plugins/statusline/skills/statusline/SKILL.md @@ -0,0 +1,122 @@ +--- +name: statusline +description: Install or update the session-optimizer statusline for Claude Code. Use when the user asks to "install the statusline", "set up the status bar", "configure the statusline", "update the statusline", or wants git/context/cost/rate-limit/telemetry info in the Claude Code status bar. +version: 2.0.0 +--- + +# Statusline install + +Installs the multi-line statusline that shows model + git context, an +RGB-gradient context bar tied to per-model checkpoint thresholds, session +cost and duration, 5h/7d rate-limit gauges, per-session telemetry (tok/s, +compactions, prompt-cache countdown), and live subagent spend. + +The plugin **bundles** its assets under `assets/`: + +| File | Role | Update policy | +|---|---|---| +| `statusline-command.sh` | Renderer (called by Claude Code every refresh) | overwrite on update (with backup) | +| `statusline-costs.py` | Cost aggregator (scans `~/.claude/projects/**/*.jsonl`, 1 h cache) | overwrite on update (with backup) | +| `statusline-transcript.py` | Per-session telemetry, backgrounded on a 15 s TTL | overwrite on update (with backup) | +| `statusline-budget.json` | **Personal** config: display size, cache TTL | copy only if absent — never overwrite | +| `ctxguard-thresholds.json` | Per-model checkpoint thresholds, **shared** with the context-guard plugin | copy only if absent — never overwrite | + +A `SessionStart` hook (`hooks/hooks.json`) injects a short maintenance +instruction each session start so Claude reconciles the three CODE assets +into `~/.claude` when the plugin updates — the two config files are never +touched automatically. + +## Requirements + +- `jq` — JSON parsing in the renderer (**required**) +- `python3` — cost aggregation and telemetry (**required**) +- `git` — repository context (optional; segment degrades gracefully) + +## Instructions for Claude + +When the user asks to install, update, or set up the statusline, follow +these steps **in order**: + +### Step 1 — Pre-flight checks + +```bash +preflight_ok=true +for cmd in jq python3; do + command -v "$cmd" >/dev/null 2>&1 || { echo "BLOCKING: '$cmd' not found"; preflight_ok=false; } +done +command -v git >/dev/null 2>&1 || echo "OPTIONAL: 'git' not found — git segment will stay empty" +mkdir -p ~/.claude 2>/dev/null +if ! touch ~/.claude/.write-test 2>/dev/null; then + echo "BLOCKING: cannot write to ~/.claude/"; preflight_ok=false +else + rm -f ~/.claude/.write-test +fi +if [ -f ~/.claude/settings.json ] && [ ! -w ~/.claude/settings.json ]; then + echo "BLOCKING: ~/.claude/settings.json not writable"; preflight_ok=false +fi +echo "preflight_ok=$preflight_ok" +``` + +| Failure | Fix | +|---|---| +| `jq` missing | `brew install jq` (macOS) · `apt install jq` (Linux/WSL) | +| `python3` missing | `brew install python` · `apt install python3` | +| `~/.claude/` not writable | check ownership: `ls -la ~/` | + +If any **BLOCKING** check fails, stop and tell the user what to fix. + +### Step 2 — Place the assets + +Do this with your **Read/Write tools**: + +1. Locate the bundled `assets/` dir: `find ~/.claude/plugins -type d -path '*/statusline/*/assets' 2>/dev/null | head -1` (dev checkout of this repo: `plugins/statusline/assets/` directly). +2. **Code assets** (`statusline-command.sh`, `statusline-costs.py`, `statusline-transcript.py`): for each, if a copy already exists in `~/.claude` and differs, back it up as `~/.claude/.bak.`, then write the bundled version to `~/.claude/`. +3. **Config assets** (`statusline-budget.json`, `ctxguard-thresholds.json`): copy to `~/.claude/` **only if the file does not exist yet** — these hold user-tuned values and must never be overwritten. +4. Set the execute bit: `chmod +x ~/.claude/statusline-command.sh`. + +### Step 3 — Configure settings + +Use the Edit tool to set `statusLine` in `~/.claude/settings.json`: + +```json +{ + "statusLine": { + "type": "command", + "command": "bash ~/.claude/statusline-command.sh", + "padding": 1, + "refreshInterval": 10 + } +} +``` + +`refreshInterval: 10` keeps the time-based segments (duration, cost) +current while the session is idle. + +### Step 4 — Post-install verification + +```bash +for f in statusline-command.sh statusline-costs.py statusline-transcript.py statusline-budget.json ctxguard-thresholds.json; do + [ -f ~/.claude/$f ] && echo "OK: $f present" || echo "ERROR: $f missing" +done +[ -x ~/.claude/statusline-command.sh ] && echo "OK: renderer executable" || echo "ERROR: renderer not executable" +jq -e '.statusLine.command' ~/.claude/settings.json >/dev/null 2>&1 \ + && echo "OK: statusLine registered in settings.json" \ + || echo "ERROR: statusLine not found in settings.json" +echo '{"model":{"display_name":"Opus 4.8"},"workspace":{"current_dir":"'"$PWD"'"},"context_window":{"used_percentage":20,"total_input_tokens":200000}}' \ + | bash ~/.claude/statusline-command.sh >/dev/null && echo "OK: renderer runs" || echo "ERROR: renderer failed" +``` + +If any check fails, diagnose and fix before telling the user to restart. + +### Step 5 — Tell the user to restart Claude Code + +Summarize what was done (installed/updated, backups created, config files +seeded or preserved) and ask them to restart Claude Code. Mention: + +- display size is tunable via `STATUSLINE_SIZE` (`xs`/`s`/`m`/`l`/`xl`) or + the `"size"` field of `~/.claude/statusline-budget.json`; +- `~/.claude/ctxguard-thresholds.json` is shared with the **context-guard** + plugin — editing it moves both the bar's color thresholds and the Stop + guard's checkpoint triggers, so the two layers stay on par by construction; +- future code updates apply automatically via the SessionStart hook when + the plugin version increases. diff --git a/tests/statusline/test_heat_rgb.sh b/tests/statusline/test_heat_rgb.sh index fdb041b..76700de 100755 --- a/tests/statusline/test_heat_rgb.sh +++ b/tests/statusline/test_heat_rgb.sh @@ -10,7 +10,7 @@ # trap EXIT). Execution order is randomized (shuf) on every run. All fixture # data is synthetic (percentages 0-100); no production data, no PII. set -uo pipefail -SCRIPT_UNDER_TEST="${SCRIPT_UNDER_TEST:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/statusline-command.sh}" +SCRIPT_UNDER_TEST="${SCRIPT_UNDER_TEST:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/plugins/statusline/assets/statusline-command.sh}" # Expected palette values — asserted independently of the script's own HEAT_* # constants (except HEAT_3, whose exact terracotta-attenuated RGB is derived diff --git a/tests/test_refine_gate.py b/tests/test_refine_gate.py index b1fcc61..d54a2f1 100644 --- a/tests/test_refine_gate.py +++ b/tests/test_refine_gate.py @@ -23,7 +23,10 @@ import sys from pathlib import Path -HOOK = Path(__file__).resolve().parent.parent / "hooks" / "refine_gate.py" +HOOK = ( + Path(__file__).resolve().parent.parent + / "plugins" / "refine-gate" / "hooks" / "refine_gate.py" +) # 4 chars/token heuristic — see module docstring for source + bias note. CHARS_PER_TOKEN = 4 diff --git a/tests/test_subagent_usage.py b/tests/test_subagent_usage.py index 9395f48..3d38215 100644 --- a/tests/test_subagent_usage.py +++ b/tests/test_subagent_usage.py @@ -1,10 +1,12 @@ -"""Tests for tools/subagent_usage.py — parsing, pricing, dedup, discovery.""" +"""Tests for plugins/context-guard/tools/subagent_usage.py — parsing, pricing, +dedup, discovery.""" import json import os import sys -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "tools")) +sys.path.insert(0, os.path.join( + os.path.dirname(__file__), "..", "plugins", "context-guard", "tools")) import subagent_usage as su # noqa: E402