From 41c06d4b973fdb06d7457c8712cf75fb18b458b3 Mon Sep 17 00:00:00 2001 From: cdeust Date: Wed, 22 Jul 2026 15:22:48 +0200 Subject: [PATCH 1/2] chore(adoption): single-source hook registration, English statusline docs, CI for existing suites Co-Authored-By: Claude Fable 5 --- .claude-plugin/plugin.json | 38 +-------------- .github/workflows/ci.yml | 43 +++++++++++++++++ README-statusline.fr.md | 64 +++++++++++++++++++++++++ README-statusline.md | 98 +++++++++++++++++++------------------- README.md | 4 +- hooks/hooks.json | 12 +++++ statusline-budget.json | 6 +-- 7 files changed, 175 insertions(+), 90 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 README-statusline.fr.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 4deddc9..33f50cf 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -19,41 +19,5 @@ "prompt-engineering", "refine", "intent-binding" - ], - "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 - } - ] - } - ], - "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/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..49735fa --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + sudo apt-get update + sudo apt-get install -y shellcheck + + - name: Refine gate contract tests + run: pytest tests/test_refine_gate.py -v + + - name: Subagent tracker tests + run: pytest tests/test_subagent_usage.py -v + + - name: Statusline unit tests + run: bash tests/statusline/test_heat_rgb.sh + + # Scoped to --severity=error: the script currently carries 3 benign + # warnings (2x SC1083 on git's literal @{u} syntax, 1x SC2034) that + # would fail a full-severity run. + - name: Shellcheck statusline script + run: shellcheck --severity=error statusline-command.sh + + - name: Validate hook and plugin JSON + run: | + python -m json.tool .claude-plugin/plugin.json > /dev/null + python -m json.tool hooks/hooks.json > /dev/null diff --git a/README-statusline.fr.md b/README-statusline.fr.md new file mode 100644 index 0000000..dcc0872 --- /dev/null +++ b/README-statusline.fr.md @@ -0,0 +1,64 @@ +> English version: [README-statusline.md](README-statusline.md) + +# Statusline zététique — Claude Code + +Statusline multi-lignes (Catppuccin Mocha) avec barres en dégradé RGB, suivi +de coûts mensuels et jauges d'objectifs par personne. + +## Fichiers + +| 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. | + +## Segments + +- **Identité** : modèle, effort, thinking 💡, dossier. +- **Git** : branche 🌿 + dirty `✗`, `↑n ↓n` (avance/retard vs upstream), `⚠n` + (conflits), décompo `!M +A ✘D ?U` (m+). Repli `@repo` sur le sous-repo le plus + récent quand le cwd n'est pas un dépôt. +- **Session** : barre contexte 🧠, tokens, `💰` coût, `⏱` durée, rate-limits + 🚀/🌟, churn ✏️. +- **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. +- **Quota** (l+) : jauges 🎯 `🚀 5h` et `🌟 7d` = % du quota rate-limit Pro/Max + consommé (la vraie contrainte « ne pas dépasser » ; 100 % = lockout), avec + reset. Couleurs : vert < 50, jaune 50–79, rouge ≥ 80. Au preset `m`, version + inline compacte sur la ligne session. Suivi d'une ligne **référence coût** + (informative, pas un plafond) : `💰 $/mois · 🤖 $/run`. + +## Installation + +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. + +## Tailles d'affichage (presets) + +`xs` (1 ligne) · `s` (2) · `m` (3) · `l` (5, défaut) · `xl` (5, barres larges + moyenne/mo). + +Réglage : variable d'env `STATUSLINE_SIZE`, ou champ `"size"` de `statusline-budget.json`. + +## Notes techniques + +- `.rate_limits.{five_hour,seven_day}` (comptes Pro/Max) : `used_percentage` est + déjà un ratio du quota → pilote directement les jauges 🎯 ; `resets_at` = epoch + 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 + scans. JSONL append-only ⇒ le compte de compactions est incrémental (scan des + octets ajoutés `[prev_size, size)` uniquement). +- `cache_ttl_min` : 5 (défaut Pro) ou 60 (Max) — source : docs Anthropic + prompt-caching (TTL 5 min par défaut). Inspirations : `CCometixLine` + (git ahead/behind + conflits), `claude-hud` (tok/s, compactions, cache TTL). diff --git a/README-statusline.md b/README-statusline.md index 2b8bd80..5e92126 100644 --- a/README-statusline.md +++ b/README-statusline.md @@ -1,62 +1,64 @@ -# Statusline zététique — Claude Code +> Version française : [README-statusline.fr.md](README-statusline.fr.md) -Statusline multi-lignes (Catppuccin Mocha) avec barres en dégradé RGB, suivi -de coûts mensuels et jauges d'objectifs par personne. +# Zetetic statusline — Claude Code -## Fichiers +Multi-line statusline (Catppuccin Mocha) with RGB-gradient bars, monthly +cost tracking, and per-person target gauges. -| Fichier | Rôle | +## Files + +| File | Role | |---|---| -| `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-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. | ## Segments -- **Identité** : modèle, effort, thinking 💡, dossier. -- **Git** : branche 🌿 + dirty `✗`, `↑n ↓n` (avance/retard vs upstream), `⚠n` - (conflits), décompo `!M +A ✘D ?U` (m+). Repli `@repo` sur le sous-repo le plus - récent quand le cwd n'est pas un dépôt. -- **Session** : barre contexte 🧠, tokens, `💰` coût, `⏱` durée, rate-limits +- **Identity**: model, effort, thinking 💡, folder. +- **Git**: branch 🌿 + dirty `✗`, `↑n ↓n` (ahead/behind vs upstream), `⚠n` + (conflicts), breakdown `!M +A ✘D ?U` (m+). Falls back to `@repo` on the most + recently touched sub-repo when the cwd is not a repository. +- **Session**: context bar 🧠, tokens, `💰` cost, `⏱` duration, rate limits 🚀/🌟, churn ✏️. -- **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. -- **Quota** (l+) : jauges 🎯 `🚀 5h` et `🌟 7d` = % du quota rate-limit Pro/Max - consommé (la vraie contrainte « ne pas dépasser » ; 100 % = lockout), avec - reset. Couleurs : vert < 50, jaune 50–79, rouge ≥ 80. Au preset `m`, version - inline compacte sur la ligne session. Suivi d'une ligne **référence coût** - (informative, pas un plafond) : `💰 $/mois · 🤖 $/run`. +- **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. +- **Quota** (l+): 🎯 gauges `🚀 5h` and `🌟 7d` = % of the Pro/Max rate-limit + quota consumed (the real "do not exceed" constraint; 100% = lockout), with + reset time. Colors: green < 50, yellow 50–79, red ≥ 80. At the `m` preset, + a compact inline version on the session line. Followed by a **cost + reference** line (informative, not a cap): `💰 $/month · 🤖 $/run`. ## Installation -1. Copier les 4 fichiers dans `~/.claude/`. -2. Déclarer la statusline dans `~/.claude/settings.json` : +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. Adapter `statusline-budget.json` à ses propres objectifs. - -## Tailles d'affichage (presets) - -`xs` (1 ligne) · `s` (2) · `m` (3) · `l` (5, défaut) · `xl` (5, barres larges + moyenne/mo). - -Réglage : variable d'env `STATUSLINE_SIZE`, ou champ `"size"` de `statusline-budget.json`. - -## Notes techniques - -- `.rate_limits.{five_hour,seven_day}` (comptes Pro/Max) : `used_percentage` est - déjà un ratio du quota → pilote directement les jauges 🎯 ; `resets_at` = epoch - 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 - scans. JSONL append-only ⇒ le compte de compactions est incrémental (scan des - octets ajoutés `[prev_size, size)` uniquement). -- `cache_ttl_min` : 5 (défaut Pro) ou 60 (Max) — source : docs Anthropic - prompt-caching (TTL 5 min par défaut). Inspirations : `CCometixLine` - (git ahead/behind + conflits), `claude-hud` (tok/s, compactions, cache TTL). +3. Adapt `statusline-budget.json` to your own targets. + +## Display sizes (presets) + +`xs` (1 line) · `s` (2) · `m` (3) · `l` (5, default) · `xl` (5, wide bars + monthly average). + +Setting: `STATUSLINE_SIZE` env variable, or the `"size"` field of `statusline-budget.json`. + +## Technical notes + +- `.rate_limits.{five_hour,seven_day}` (Pro/Max accounts): `used_percentage` is + already a ratio of the quota → drives the 🎯 gauges directly; `resets_at` = + 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 + between two scans. JSONL is append-only ⇒ the compaction count is incremental + (scans only the appended bytes `[prev_size, size)`). +- `cache_ttl_min`: 5 (Pro default) or 60 (Max) — source: Anthropic + prompt-caching docs (5 min TTL by default). Inspirations: `CCometixLine` + (git ahead/behind + conflicts), `claude-hud` (tok/s, compactions, cache TTL). diff --git a/README.md b/README.md index d5c932b..245006e 100644 --- a/README.md +++ b/README.md @@ -244,8 +244,8 @@ python3 tools/subagent_usage.py --json # machine-readable ### Install -Add the `SubagentStop` entry from `hooks/hooks.json` (or the plugin's -`plugin.json`) to your settings; `subagent-tracker.py` imports the shared core +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 diff --git a/hooks/hooks.json b/hooks/hooks.json index dbcac1e..39b0715 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -21,6 +21,18 @@ } ] } + ], + "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/statusline-budget.json b/statusline-budget.json index 981d76d..5eb1647 100644 --- a/statusline-budget.json +++ b/statusline-budget.json @@ -1,7 +1,7 @@ { - "_comment": "Les jauges 'target' 🎯 suivent désormais les quotas de rate-limit Pro/Max (fenêtres 5h + 7j) fournis par Claude Code — la vraie contrainte 'ne pas dépasser'. Plus aucun budget mensuel absolu à régler ici.", - "_size": "Verbosité de la statusline : xs (1 ligne) | s (2) | m (3) | l (défaut) | xl (barres larges). Surchargeable par la variable d'env STATUSLINE_SIZE.", - "_cache_ttl_min": "Durée de vie du cache de prompt pour le compte à rebours ❄ : 5 (défaut Pro) ou 60 (Max). Source : docs.anthropic.com prompt-caching (TTL 5 min par défaut).", + "_comment": "The 'target' 🎯 gauges now track the Pro/Max rate-limit quotas (5h + 7d windows) provided by Claude Code — the real 'do not exceed' constraint. No absolute monthly budget to set here anymore.", + "_size": "Statusline verbosity: xs (1 line) | s (2) | m (3) | l (default) | xl (wide bars). Can be overridden by the STATUSLINE_SIZE env variable.", + "_cache_ttl_min": "Prompt-cache lifetime for the ❄ countdown: 5 (Pro default) or 60 (Max). Source: docs.anthropic.com prompt-caching (5 min TTL by default).", "cache_ttl_min": 5, "size": "l" } From b24024958e9eb1e3a95975d80ea3d3f5957f4e18 Mon Sep 17 00:00:00 2001 From: cdeust Date: Wed, 22 Jul 2026 15:51:42 +0200 Subject: [PATCH 2/2] fix(lint): justify shellcheck suppressions inline, run CI shellcheck at full severity SC1083: @{u} is git upstream-revision syntax, not brace expansion. SC2034: tok_cur deliberately sinks the 4th jq field so read's remainder rule cannot corrupt cost_agent. With both documented at the use site, CI drops --severity=error scoping. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 5 +---- statusline-command.sh | 3 +++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49735fa..0a76785 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,11 +31,8 @@ jobs: - name: Statusline unit tests run: bash tests/statusline/test_heat_rgb.sh - # Scoped to --severity=error: the script currently carries 3 benign - # warnings (2x SC1083 on git's literal @{u} syntax, 1x SC2034) that - # would fail a full-severity run. - name: Shellcheck statusline script - run: shellcheck --severity=error statusline-command.sh + run: shellcheck statusline-command.sh - name: Validate hook and plugin JSON run: | diff --git a/statusline-command.sh b/statusline-command.sh index ee82190..21439ad 100755 --- a/statusline-command.sh +++ b/statusline-command.sh @@ -237,6 +237,7 @@ branch_of() { # echo "branch dirty" for repo root $1 # Pure git; one rev-list + one status call. Sets the git_* / n* globals above. compute_git_extra() { local root="$1" lr line xy + # shellcheck disable=SC1083 # @{u} is git revision syntax (upstream), not a brace expansion lr=$(git -C "$root" -c core.useBuiltinFSMonitor=false \ rev-list --left-right --count HEAD...@{u} 2>/dev/null) if [ -n "$lr" ]; then @@ -296,6 +297,8 @@ fi cost_cur=""; cost_avg=""; cost_agent=""; tok_cur="" if [ -r "$COST_CACHE" ]; then + # shellcheck disable=SC2034 # tok_cur is a field sink: it absorbs the 4th jq + # field so cost_agent cannot swallow the rest of the line via read's remainder rule read -r cost_cur cost_avg cost_agent tok_cur < <( jq -r '"\(.current_month // "") \(.avg_month // "") \(.avg_per_agent // "") \(.current_month_tokens // "")"' "$COST_CACHE" 2>/dev/null ) || true