claude-token-saver 3.2.1 โ†’ 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -7,7 +7,9 @@
7
7
 
8
8
  # claude-token-saver
9
9
 
10
- **Diagnose and save Claude Code tokens from a single statusline.** Zero dependencies, one-line install.
10
+ **Diagnose Claude Code token usage from a single statusline โ€” and route the easy work your expensive model keeps repeating down to cheaper models.** Zero dependencies, one-line install.
11
+
12
+ Since v3.x this is more than after-the-fact monitoring: it's a **model-fitting routing layer**. Session logs are classified into tiers (T0/T1/T2), recurring patterns get promoted to "haiku/sonnet is enough for this" delegation rules, and your main model auto-delegates them from the next session ([route-scan](#-route-scan--this-recurring-task-could-run-on-a-cheaper-tier)).
11
13
 
12
14
  ```bash
13
15
  npm i -g claude-token-saver # postinstall auto-registers the statusline + Skill
@@ -19,6 +21,7 @@ npm i -g claude-token-saver # postinstall auto-registers the statusline + Skil
19
21
 
20
22
  | | |
21
23
  |---|---|
24
+ | ๐Ÿ”€ **Model-fitting delegation** | Classifies the easy work your expensive model (opus/fable) keeps repeating into tiers (T0/T1/T2) โ†’ promotes haiku/sonnet delegation rules, auto-applied from the next session |
22
25
  | ๐Ÿ’ธ **โˆ’18.6% measured cost** | Cost per user message $2.35 โ†’ $1.91 after adopting harness+ratchet (author's logs, [details](#real-world-impact--beforeafter-report)) |
23
26
  | ๐Ÿšจ **No surprise rate limits** | Instant warning when the 5H/7D window hits 90% + `handoff` to back up your work |
24
27
  | ๐Ÿง  **Cache waste detection** | Hit rate, TTL countdown, 1M-context detection โ€” token spikes diagnosed with issue codes |
@@ -148,6 +151,21 @@ Analyzes your session history at the **episode (user request) level**, classifie
148
151
 
149
152
  Signals: call count, output tokens, **mutating tool calls (Edit/Write/Bash)**, **tool errors**, and request text. Output thresholds are auto-calibrated from your own 14-day distribution (fixed constants drift with workload).
150
153
 
154
+ <details>
155
+ <summary>Why this design โ€” research evidence (deep-research, 21 cross-verified sources + 553 locally measured episodes)</summary>
156
+
157
+ A survey of existing LLM-routing research and systems shaped each axis of the design:
158
+
159
+ - **Conservative demotion โ€” "only send down what's clearly easy" โ€” is the unexplored direction.** In RouterArena (2025), every academic and commercial router fell far short of the oracle (90.9%; best entrant 66.9), and the failure mode was consistently **over-routing to expensive models**. Per the routing-collapse work, the oracle needs the top model for under 20% of queries โ€” while 73% of the author's episodes were still on the top tier. The headroom is large.
160
+ - **Difficulty is defined by outcome, not text guessing.** RouterArena labels difficulty as "how many of 42 models actually got it right". Our logs already contain outcomes: tool errors, mutating tool calls, episode length. Local measurement confirms it โ€” error incidence splits the tiers sharply (T2 candidates 2% / T1 8% / T0 36%).
161
+ - **Thresholds calibrate to the user's own distribution.** RouteLLM's stated limitation is that fixed thresholds drift as the query distribution shifts โ€” so output-token thresholds are recomputed from your last 14 days' p25/p75 (clamped).
162
+ - **An escalation path makes demotion mistakes cheap.** The FrugalGPT cascade lesson ("start cheap, escalate on failure"). In Claude Code this falls out naturally: when a subagent gets stuck, the main model takes over โ€” and promoted rules spell out that guard.
163
+ - **Subagent `model:` is Anthropic's official cost-control mechanism.** The docs recommend routing to cheaper models like Haiku for cost control โ€” yet Claude Code itself does no automatic model routing; even trivial turns resend the full context on the session model. That gap is exactly what route-scan fills.
164
+ - **Learned classifiers (BERT/embeddings) deliberately excluded.** Even the best is 24 points off the oracle, we have no preference-pair training data, and it would break the zero-dependency principle. Regex categories + outcome signals are the current position.
165
+
166
+ Sources (RouteLLM, FrugalGPT, RouterArena, routing-survey arxiv links), the full local-measurement table, and the complete tier criteria: [docs/TIER_CRITERIA.md](./docs/TIER_CRITERIA.md).
167
+ </details>
168
+
151
169
  ```bash
152
170
  claude-token-saver route-scan # scan (24h cache) + tiered candidates
153
171
  claude-token-saver harness promote R1 --project # promote candidate R1 to a model-fitting rule
@@ -251,6 +269,16 @@ Also update `statusLine.command` in `~/.claude/settings.json` to `claude-token-s
251
269
 
252
270
  ## Release notes
253
271
 
272
+ ### v3.3.0 (2026-07-13)
273
+ - **Delegation visibility & briefing** โ€” the whole model-fitting loop is now surfaced to the user:
274
+ - The SessionStart hook injects, per candidate, **the exact rule text that would be written to ratchet-model.md** โ€” you see precisely what you're approving.
275
+ - Rules flipped to review (delegated-category error rate over 20%) are announced on **both** the statusline (`๐Ÿ…ทโš  rule-health R<N>` chip โ€” implementing the item from the design doc; precedence: session-quality warnings > rule-health > route? candidates) and the session-start briefing.
276
+ - The ratchet-model.md header now instructs the model to prefix each delegation with `๐Ÿ”€ [claude-token-saver] model-fitting: "<category>" โ†’ <agent>` โ€” making it visible which tool is saving your tokens.
277
+ - README updated to the 3.x identity (model-fitting delegation in the top pitch, collapsible research-evidence section under route-scan); fixed a stale comment in model-rules.js.
278
+
279
+ ### v3.2.2 (2026-07-13)
280
+ - **No re-proposing registered rules** โ€” (tier|category|project) combinations that already have a model-fitting rule are excluded from scan candidates (a global rule covers every project). Fixes rules registered outside the promote flow (e.g. migrations) resurfacing as candidates.
281
+
254
282
  ### v3.2.1 (2026-07-13)
255
283
  - **Instant pattern analysis on first setup** โ€” `install` (including npm postinstall) analyzes your existing session logs inline when no cache exists, so tier-delegation candidates appear from the very first Claude Code session (previously the second).
256
284
 
package/README.md CHANGED
@@ -7,7 +7,9 @@
7
7
 
8
8
  # claude-token-saver
9
9
 
10
- **Claude Code ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰์„ statusline ํ•œ ์ค„๋กœ ์ง„๋‹จํ•˜๊ณ  ์ ˆ์•ฝํ•˜๋Š” CLI.** ์˜์กด์„ฑ 0, ์„ค์น˜ ํ•œ ์ค„์ด๋ฉด ๋.
10
+ **Claude Code ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰์„ statusline ํ•œ ์ค„๋กœ ์ง„๋‹จํ•˜๊ณ , ์ƒ์œ„ ๋ชจ๋ธ์ด ๋ฐ˜๋ณต ์ฒ˜๋ฆฌํ•˜๋Š” easy ์ž‘์—…์€ ๋” ์‹ผ ๋ชจ๋ธ๋กœ ๋‚ด๋ ค๋ณด๋‚ด๋Š” CLI.** ์˜์กด์„ฑ 0, ์„ค์น˜ ํ•œ ์ค„์ด๋ฉด ๋.
11
+
12
+ v3.x๋ถ€ํ„ฐ๋Š” ์‚ฌํ›„ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ๋„˜์–ด **๋ชจ๋ธ ํ”ผํŒ… ๋ผ์šฐํŒ… ๊ณ„์ธต**์ž…๋‹ˆ๋‹ค: ์„ธ์…˜ ๋กœ๊ทธ๋ฅผ ํ‹ฐ์–ด(T0/T1/T2)๋กœ ๋ถ„๋ฅ˜ํ•ด "์ด ๋ฐ˜๋ณต ์ž‘์—…์€ haiku/sonnet์ด๋ฉด ์ถฉ๋ถ„ํ•˜๋‹ค"๋ฅผ ๋ฃฐ๋กœ ์Šน๊ฒฉํ•˜๊ณ , ๋‹ค์Œ ์„ธ์…˜๋ถ€ํ„ฐ ๋ฉ”์ธ ๋ชจ๋ธ์ด ์ž๋™ ์œ„์ž„ํ•ฉ๋‹ˆ๋‹ค ([route-scan](#-route-scan--์ด-๋ฐ˜๋ณต-์ž‘์—…-๋”-์‹ผ-ํ‹ฐ์–ด๋กœ-๋‚ด๋ ค๋„-๋ฉ๋‹ˆ๋‹ค)).
11
13
 
12
14
  ```bash
13
15
  npm i -g claude-token-saver # postinstall์ด statusline + Skill ์ž๋™ ๋“ฑ๋ก
@@ -19,6 +21,7 @@ npm i -g claude-token-saver # postinstall์ด statusline + Skill ์ž๋™ ๋“ฑ๋ก
19
21
 
20
22
  | | |
21
23
  |---|---|
24
+ | ๐Ÿ”€ **๋ชจ๋ธ ํ”ผํŒ… ์œ„์ž„** | ์ƒ์œ„ ๋ชจ๋ธ(opus/fable)์ด ๋ฐ˜๋ณต ์ฒ˜๋ฆฌํ•ด ์˜จ easy ์ž‘์—…์„ ํ‹ฐ์–ด(T0/T1/T2)๋กœ ๋ถ„๋ฅ˜ โ†’ haiku/sonnet ์œ„์ž„ ๋ฃฐ๋กœ ์Šน๊ฒฉ, ๋‹ค์Œ ์„ธ์…˜๋ถ€ํ„ฐ ์ž๋™ ์ ์šฉ |
22
25
  | ๐Ÿ’ธ **๋น„์šฉ ์‹ค์ธก โˆ’18.6%** | harness+ratchet ๋„์ž… ์ „ํ›„, ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€๋‹น ๋น„์šฉ $2.35 โ†’ $1.91 (์ €์ž ๋กœ๊ทธ, [์ƒ์„ธ](#์‹ค์ œ-ํšจ๊ณผ--๋„์ž…-์ „ํ›„-๋ฆฌํฌํŠธ)) |
23
26
  | ๐Ÿšจ **ํ•œ๋„ ์ดˆ๊ณผ ์˜ˆ๋ฐฉ** | 5H/7D rate-limit ์œˆ๋„ 90% ๋„๋‹ฌ ์‹œ ์ฆ‰์‹œ ๊ฒฝ๊ณ  + `handoff`๋กœ ์ž‘์—… ๋ฐฑ์—… |
24
27
  | ๐Ÿง  **์บ์‹œ ๋‚ญ๋น„ ๊ฐ์ง€** | ํžˆํŠธ์œจยทTTL ์นด์šดํŠธ๋‹ค์šดยท1M ์ปจํ…์ŠคํŠธ ์ž๋™ ๊ฐ์ง€ โ€” ํ† ํฐ ๊ธ‰์ฆ ์›์ธ์„ ์ฝ”๋“œ๋กœ ์ง„๋‹จ |
@@ -129,6 +132,21 @@ ratchet์˜ ๊ฐ€์น˜๋Š” **ํ•œ ๋ฐฉํ–ฅ ๋ˆ„์ **์— ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฃฐ์„ ๊ฐ€๋ณ๊ฒŒ ์ง€
129
132
 
130
133
  ํŒ์ • ์‹ ํ˜ธ๋Š” ํ˜ธ์ถœ ์ˆ˜ยท์ถœ๋ ฅ ํ† ํฐยท**๋ณ€๊ฒฝ์„ฑ ๋„๊ตฌ ์ˆ˜(Edit/Write/Bash)**ยท**๋„๊ตฌ ์—๋Ÿฌ ์ˆ˜**ยท์š”์ฒญ ํ…์ŠคํŠธ์ด๊ณ , ์ถœ๋ ฅ ์ž„๊ณ„๊ฐ’์€ ์‚ฌ์šฉ์ž ์ž์‹ ์˜ ์ตœ๊ทผ 14์ผ ๋ถ„ํฌ์—์„œ ์ž๋™ ๋ณด์ •๋ฉ๋‹ˆ๋‹ค (๊ณ ์ • ์ƒ์ˆ˜๋Š” ์›Œํฌ๋กœ๋“œ๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์–ด๊ธ‹๋‚˜๊ธฐ ๋•Œ๋ฌธ).
131
134
 
135
+ <details>
136
+ <summary>์™œ ์ด ์„ค๊ณ„์ธ๊ฐ€ โ€” ๋ฆฌ์„œ์น˜ ๊ทผ๊ฑฐ (deep-research ์†Œ์Šค 21๊ฐœ ๊ต์ฐจ๊ฒ€์ฆ + ๋กœ์ปฌ ์‹ค์ธก 553 ์—ํ”ผ์†Œ๋“œ)</summary>
137
+
138
+ ๊ธฐ์กด LLM ๋ผ์šฐํŒ… ์—ฐ๊ตฌยท์‹œ์Šคํ…œ์„ ์กฐ์‚ฌํ•œ ๊ฒฐ๊ณผ๊ฐ€ ์„ค๊ณ„์˜ ๊ฐ ์ถ•์„ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค:
139
+
140
+ - **"์‰ฌ์šด ๊ฒƒ๋งŒ ํ™•์‹คํžˆ ๋‚ด๋ฆฌ๋Š”" ๋ณด์ˆ˜์  ๊ฐ•๋“ฑ์ด ๋ฏธ๊ฐœ์ฒ™ ์ง€๋Œ€** โ€” RouterArena(2025) ํ‰๊ฐ€์—์„œ ํ•™์ˆ ยท์ƒ์šฉ ๋ผ์šฐํ„ฐ ์ „๋ถ€ ์˜ค๋ผํด(90.9%)์— ํฌ๊ฒŒ ๋ชป ๋ฏธ์ณค๊ณ (1์œ„ 66.9), ์‹คํŒจ ๋ฐฉํ–ฅ์€ ์ผ๊ด€๋˜๊ฒŒ **๋น„์‹ผ ๋ชจ๋ธ๋กœ์˜ ๊ณผ์ž‰ ๋ผ์šฐํŒ…**์ด์—ˆ์Šต๋‹ˆ๋‹ค. ์˜ค๋ผํด ๊ธฐ์ค€ ์ตœ์ƒ์œ„ ๋ชจ๋ธ์ด ์ •๋ง ํ•„์š”ํ•œ ์ฟผ๋ฆฌ๋Š” 20% ๋ฏธ๋งŒ์ธ๋ฐ(routing collapse ์—ฐ๊ตฌ), ์ €์ž ๋กœ๊ทธ์—์„  73%๊ฐ€ ์ตœ์ƒ์œ„ ํ‹ฐ์–ด์— ๋‚จ์•„ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค โ€” ํ—ค๋“œ๋ฃธ์ด ํฝ๋‹ˆ๋‹ค.
141
+ - **๋‚œ์ด๋„๋Š” ํ…์ŠคํŠธ ์ถ”์ •์ด ์•„๋‹ˆ๋ผ ๊ฒฐ๊ณผ(outcome)๋กœ ์ •์˜** โ€” RouterArena๋Š” ๋‚œ์ด๋„๋ฅผ "42๊ฐœ ๋ชจ๋ธ ์ค‘ ๋ช‡ ๊ฐœ๊ฐ€ ์‹ค์ œ๋กœ ๋งžํ˜”๋‚˜"๋กœ ๋ผ๋ฒจ๋งํ–ˆ์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ ๋กœ๊ทธ์—๋„ ๊ฒฐ๊ณผ๊ฐ€ ์ด๋ฏธ ์žˆ์Šต๋‹ˆ๋‹ค: ๋„๊ตฌ ์—๋Ÿฌยท๋ณ€๊ฒฝ์„ฑ ๋„๊ตฌ ์ˆ˜ยท์—ํ”ผ์†Œ๋“œ ๊ธธ์ด. ๋กœ์ปฌ ์‹ค์ธก์—์„œ๋„ ์—๋Ÿฌ ๊ฒฝํ—˜๋ฅ ์ด ํ‹ฐ์–ด๋ฅผ ๊ฐ•ํ•˜๊ฒŒ ๊ฐˆ๋ž์Šต๋‹ˆ๋‹ค (T2 ํ›„๋ณด 2% / T1 8% / T0 36%).
142
+ - **์ž„๊ณ„๊ฐ’์€ ์‚ฌ์šฉ์ž ๋ถ„ํฌ์—์„œ ๋ณด์ •** โ€” RouteLLM์˜ ๊ณต์‹ ํ•œ๊ณ„๊ฐ€ "๊ณ ์ • ์ž„๊ณ„๊ฐ’์€ ์ฟผ๋ฆฌ ๋ถ„ํฌ๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์–ด๊ธ‹๋‚œ๋‹ค"์ž…๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ถœ๋ ฅ ํ† ํฐ ์ž„๊ณ„๊ฐ’์„ ์ตœ๊ทผ 14์ผ p25/p75์—์„œ ์žฌ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค(ํด๋žจํ”„ ํฌํ•จ).
143
+ - **์‹คํŒจ ์‹œ ์Šน๊ธ‰ ๊ฒฝ๋กœ๊ฐ€ ๊ฐ•๋“ฑ ์˜คํŒ ๋น„์šฉ์„ ๋‚ฎ์ถค** โ€” FrugalGPT ์บ์Šค์ผ€์ด๋“œ("์‹ผ ๋ชจ๋ธ๋ถ€ํ„ฐ, ์•ˆ ๋˜๋ฉด ์œ„๋กœ")์˜ ๊ตํ›ˆ. Claude Code์—์„  ์„œ๋ธŒ์—์ด์ „ํŠธ๊ฐ€ ๋ง‰ํžˆ๋ฉด ๋ฉ”์ธ ๋ชจ๋ธ์ด ์ด์–ด๋ฐ›๋Š” ํ˜•ํƒœ๋กœ ์ž์—ฐ ๊ตฌํ˜„๋˜๊ณ , ์Šน๊ฒฉ ๋ฃฐ ํ…์ŠคํŠธ์—๋„ ์ด ๊ฐ€๋“œ๊ฐ€ ๋ช…์‹œ๋ฉ๋‹ˆ๋‹ค.
144
+ - **์„œ๋ธŒ์—์ด์ „ํŠธ `model:`์€ Anthropic ๊ณต์‹ ๋น„์šฉ ํ†ต์ œ ๋ฉ”์ปค๋‹ˆ์ฆ˜** โ€” ๊ณต์‹ ๋ฌธ์„œ๊ฐ€ "Haiku ๋“ฑ ์‹ผ ๋ชจ๋ธ๋กœ ๋ผ์šฐํŒ…ํ•ด ๋น„์šฉ ํ†ต์ œ"๋ฅผ ๊ถŒ์žฅํ•˜์ง€๋งŒ, Claude Code ๋ณธ์ฒด๋Š” ์ž๋™ ๋ชจ๋ธ ๋ผ์šฐํŒ…์„ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‚ฌ์†Œํ•œ ํ„ด๋„ ์„ธ์…˜ ๋ชจ๋ธ์ด ์ „์ฒด ์ปจํ…์ŠคํŠธ๋ฅผ ์žฌ์ „์†กํ•˜๋ฉฐ ์ฒ˜๋ฆฌํ•˜๋Š” ์ด ๊ณต๋ฐฑ์ด route-scan์ด ๋ฉ”์šฐ๋Š” ์ž๋ฆฌ์ž…๋‹ˆ๋‹ค.
145
+ - **ํ•™์Šตํ˜• ๋ถ„๋ฅ˜๊ธฐ(BERT/์ž„๋ฒ ๋”ฉ)๋Š” ์˜๋„์ ์œผ๋กœ ๋ฐฐ์ œ** โ€” ์ตœ๊ณ  ์„ฑ๋Šฅ๋„ ์˜ค๋ผํด๊ณผ 24์  ์ฐจ์ด์ธ ๋ฐ๋‹ค ํ•™์Šต ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๊ณ  zero-dep ์›์น™์— ์–ด๊ธ‹๋‚ฉ๋‹ˆ๋‹ค. ์ •๊ทœ์‹ ์นดํ…Œ๊ณ ๋ฆฌ + outcome ์‹ ํ˜ธ ์กฐํ•ฉ์ด ํ˜„์žฌ ์œ„์น˜.
146
+
147
+ ์ถœ์ฒ˜(RouteLLMยทFrugalGPTยทRouterArenaยท๋ผ์šฐํŒ… ์„œ๋ฒ ์ด arxiv ๋งํฌ)์™€ ๋กœ์ปฌ ์‹ค์ธก ์ƒ์„ธ ํ…Œ์ด๋ธ”, ํ‹ฐ์–ด ๊ธฐ์ค€ ์ „๋ฌธ์€ [docs/TIER_CRITERIA.md](./docs/TIER_CRITERIA.md) ์ฐธ๊ณ .
148
+ </details>
149
+
132
150
  ```bash
133
151
  claude-token-saver route-scan # ์Šค์บ” (24h ์บ์‹œ) + ํ‹ฐ์–ด๋ณ„ ํ›„๋ณด ์ถœ๋ ฅ
134
152
  claude-token-saver harness promote R1 --project # ํ›„๋ณด R1์„ ๋ชจ๋ธ ํ”ผํŒ… ๋ฃฐ๋กœ ๋“ฑ๋ก
@@ -208,6 +226,16 @@ npm uninstall -g claude-cache-monitor && npm i -g claude-token-saver
208
226
 
209
227
  ## ๋ฆด๋ฆฌ์Šค ๋…ธํŠธ
210
228
 
229
+ ### v3.3.0 (2026-07-13)
230
+ - **์œ„์ž„ ๊ฐ€์‹œํ™”ยท๋ธŒ๋ฆฌํ•‘ ๊ฐ•ํ™”** โ€” ๋ชจ๋ธ ํ”ผํŒ…์˜ ์ „ ๊ณผ์ •์ด ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์ด๋„๋ก:
231
+ - SessionStart ํ›…์ด ํ›„๋ณด๋งˆ๋‹ค **๋“ฑ๋ก ์‹œ ratchet-model.md์— ๊ธฐ๋ก๋  ๋ฃฐ ์›๋ฌธ**์„ ํ•จ๊ป˜ ์ฃผ์ž… โ€” ๋ฌด์—‡์ด ๋“ฑ๋ก๋ ์ง€ ์ •ํ™•ํžˆ ๋ณด๊ณ  ์Šน์ธ.
232
+ - ์—๋Ÿฌ์œจ ๊ธฐ์ค€(20%) ์ดˆ๊ณผ๋กœ review ์ƒํƒœ๊ฐ€ ๋œ ๋ฃฐ์€ **statusline `๐Ÿ…ทโš  rule-health R<N>` ์นฉ** + ์„ธ์…˜ ์‹œ์ž‘ ๋ธŒ๋ฆฌํ•‘ ์–‘์ชฝ์œผ๋กœ ํ†ต์ง€ (์„ค๊ณ„ ๋ฌธ์„œ์— ์žˆ๋˜ ๋ฏธ๊ตฌํ˜„ ํ•ญ๋ชฉ ๊ตฌํ˜„. ์šฐ์„ ์ˆœ์œ„: ์„ธ์…˜ ํ’ˆ์งˆ ๊ฒฝ๊ณ  > rule-health > route? ํ›„๋ณด).
233
+ - ratchet-model.md ํ—ค๋”์— ์œ„์ž„ ์‹คํ–‰ ์‹œ `๐Ÿ”€ [claude-token-saver] ๋ชจ๋ธ ํ”ผํŒ…: "<์œ ํ˜•>" โ†’ <agent> ์œ„์ž„` ํ•œ ์ค„์„ ๋จผ์ € ํ‘œ์‹œํ•˜๋ผ๋Š” ์ง€์‹œ ์ถ”๊ฐ€ โ€” ์–ด๋–ค ๋„๊ตฌ๊ฐ€ ํ† ํฐ์„ ์•„๋ผ๋Š”์ง€ ๊ฐ€์‹œํ™”.
234
+ - README์— 3.x ์ •์ฒด์„ฑ ๋ฐ˜์˜ (์ƒ๋‹จ ์š”์•ฝ์— ๋ชจ๋ธ ํ”ผํŒ… ์œ„์ž„ ์ถ”๊ฐ€, route-scan ์„น์…˜์— ๋ฆฌ์„œ์น˜ ๊ทผ๊ฑฐ ์ ‘์ด์‹), model-rules.js ์ฃผ์„-๊ตฌํ˜„ ๋ถˆ์ผ์น˜ ์ˆ˜์ •.
235
+
236
+ ### v3.2.2 (2026-07-13)
237
+ - **๋“ฑ๋ก ๋ฃฐ ์žฌ์ œ์•ˆ ์ฐจ๋‹จ** โ€” ์ด๋ฏธ ๋ชจ๋ธ ํ”ผํŒ… ๋ฃฐ์ด ์žˆ๋Š” (ํ‹ฐ์–ด|์นดํ…Œ๊ณ ๋ฆฌ|ํ”„๋กœ์ ํŠธ) ์กฐํ•ฉ์€ ์Šค์บ” ํ›„๋ณด์—์„œ ์ž๋™ ์ œ์™ธ (๊ธ€๋กœ๋ฒŒ ๋ฃฐ์€ ์ „ ํ”„๋กœ์ ํŠธ ์ปค๋ฒ„). promote๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๊ณ  ๋“ฑ๋ก๋œ ๋ฃฐ(๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋“ฑ)์ด ํ›„๋ณด๋กœ ๋˜์‚ด์•„๋‚˜๋˜ ๋ฌธ์ œ ์ˆ˜์ •.
238
+
211
239
  ### v3.2.1 (2026-07-13)
212
240
  - **์ตœ์ดˆ ์„ค์น˜ ์‹œ ์ฆ‰์‹œ ํŒจํ„ด ๋ถ„์„** โ€” `install`(npm postinstall ํฌํ•จ)์ด ์บ์‹œ๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ์กด ์„ธ์…˜ ๋กœ๊ทธ๋ฅผ ๊ทธ ์ž๋ฆฌ์—์„œ ๋ถ„์„ํ•ด, ์ฒซ Claude Code ์„ธ์…˜๋ถ€ํ„ฐ ํ‹ฐ์–ด ์œ„์ž„ ํ›„๋ณด๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค (๊ธฐ์กด์—๋Š” ๋‘ ๋ฒˆ์งธ ์„ธ์…˜๋ถ€ํ„ฐ).
213
241
 
package/bin/cli.js CHANGED
@@ -558,17 +558,37 @@ async function main() {
558
558
  } catch { /* refresh is best-effort; stale cache still usable below */ }
559
559
  }
560
560
  const open = rs.openCandidates(cache);
561
- if (open.length === 0) return; // silent โ€” nothing to inject
561
+ // Registered rules whose delegated-category error rate crossed the
562
+ // health threshold since promotion โ€” the user approved these, so a
563
+ // status change must be briefed, not just written into the md file.
564
+ let reviewRules = [];
565
+ try {
566
+ const mr = await import('../src/model-rules.js');
567
+ reviewRules = mr.loadModelRules().rules
568
+ .map((r, i) => ({ ...r, n: i + 1 }))
569
+ .filter((r) => r.status === 'review');
570
+ } catch { /* registry unreadable โ€” candidate briefing still goes out */ }
571
+ if (open.length === 0 && reviewRules.length === 0) return; // silent โ€” nothing to inject
562
572
  const lines = [];
563
- lines.push(`[claude-token-saver route-scan] ์ตœ๊ทผ ${cache.days}์ผ ์„ธ์…˜์—์„œ ์ƒ์œ„ ๋ชจ๋ธ(opus/fable)์ด ์ฒ˜๋ฆฌํ•œ ์œ„์ž„ ๊ฐ€๋Šฅ ๋ฐ˜๋ณต ์ž‘์—…์ด ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค:`);
564
- for (const c of open) {
565
- const tierNote = c.tier === 'T1' ? 'sonnet ์œ„์ž„(์ค‘๊ฐ„ ๋‚œ๋„)' : `${c.agent} ์œ„์ž„(๊ฒฝ๋Ÿ‰)`;
566
- lines.push(` R${c.id} [${c.tier || 'T2'}] (ร—${c.count}, ${c.project}): ${c.label} โ†’ ${tierNote} ๊ถŒ์žฅ (scope ์ œ์•ˆ: ${c.suggestedScope})`);
567
- lines.push(` ์˜ˆ์‹œ: "${c.example}"`);
573
+ if (open.length > 0) {
574
+ lines.push(`[claude-token-saver route-scan] ์ตœ๊ทผ ${cache.days}์ผ ์„ธ์…˜์—์„œ ์ƒ์œ„ ๋ชจ๋ธ(opus/fable)์ด ์ฒ˜๋ฆฌํ•œ ์œ„์ž„ ๊ฐ€๋Šฅ ๋ฐ˜๋ณต ์ž‘์—…์ด ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค:`);
575
+ for (const c of open) {
576
+ const tierNote = c.tier === 'T1' ? 'sonnet ์œ„์ž„(์ค‘๊ฐ„ ๋‚œ๋„)' : `${c.agent} ์œ„์ž„(๊ฒฝ๋Ÿ‰)`;
577
+ lines.push(` R${c.id} [${c.tier || 'T2'}] (ร—${c.count}, ${c.project}): ${c.label} โ†’ ${tierNote} ๊ถŒ์žฅ (scope ์ œ์•ˆ: ${c.suggestedScope})`);
578
+ lines.push(` ์˜ˆ์‹œ: "${c.example}"`);
579
+ lines.push(` ๋“ฑ๋ก ์‹œ ratchet-model.md์— ๊ธฐ๋ก๋  ๋ฃฐ: "${c.rule}"`);
580
+ }
581
+ lines.push('๋“ฑ๋กํ•˜๋ฉด ๋‹ค์Œ ์„ธ์…˜๋ถ€ํ„ฐ ์ž๋™ ์œ„์ž„๋ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž์—๊ฒŒ ๋“ฑ๋ก ์—ฌ๋ถ€๋ฅผ ๋ฌผ์„ ๋•Œ ์œ„ ๋ฃฐ ์›๋ฌธ์„ ๊ทธ๋Œ€๋กœ ๋ณด์—ฌ์ฃผ๊ณ , scope๊นŒ์ง€ ํ™•์ธํ•œ ๋’ค ์‹คํ–‰ํ•˜์„ธ์š”:');
582
+ lines.push(' claude-token-saver harness promote R<N> --project|--global # scope๋Š” ๋ฐ˜๋“œ์‹œ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ™•์ธ');
583
+ lines.push(' claude-token-saver route-scan dismiss <N> # ์‚ฌ์šฉ์ž๊ฐ€ ์›์น˜ ์•Š์œผ๋ฉด');
584
+ }
585
+ if (reviewRules.length > 0) {
586
+ lines.push(`[claude-token-saver rule-health] ๋“ฑ๋ก๋œ ๋ชจ๋ธ ํ”ผํŒ… ๋ฃฐ ์ค‘ ์œ„์ž„ ๋Œ€์ƒ ์œ ํ˜•์˜ ์—๋Ÿฌ์œจ์ด ๊ธฐ์ค€(20%)์„ ๋„˜์€ ๋ฃฐ์ด ์žˆ์Šต๋‹ˆ๋‹ค โ€” ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ธŒ๋ฆฌํ•‘ํ•˜๊ณ  ์กฐ๊ฑด ์ขํžˆ๊ธฐ/์ œ๊ฑฐ๋ฅผ ์ƒ์˜ํ•˜์„ธ์š”:`);
587
+ for (const r of reviewRules) {
588
+ lines.push(` #${r.n} [${r.tier}|${r.scope}] ์—๋Ÿฌ์œจ ${Math.round((r.errRate || 0) * 100)}%: ${r.rule}`);
589
+ }
590
+ lines.push(' ์ œ๊ฑฐ: claude-token-saver route-scan rules rm <N>');
568
591
  }
569
- lines.push('์ด ํŒจํ„ด์„ ๋žซ์ณ‡ ๋ฃฐ๋กœ ๋“ฑ๋กํ•˜๋ฉด ๋‹ค์Œ ์„ธ์…˜๋ถ€ํ„ฐ ์ž๋™ ์œ„์ž„๋ฉ๋‹ˆ๋‹ค. ์ ์ ˆํ•œ ์‹œ์ ์— ์‚ฌ์šฉ์ž์—๊ฒŒ ๋“ฑ๋ก ์—ฌ๋ถ€์™€ scope๋ฅผ ๋ฌผ์–ด๋ณธ ๋’ค ์‹คํ–‰ํ•˜์„ธ์š”:');
570
- lines.push(' claude-token-saver harness promote R<N> --project|--global # scope๋Š” ๋ฐ˜๋“œ์‹œ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ™•์ธ');
571
- lines.push(' claude-token-saver route-scan dismiss <N> # ์‚ฌ์šฉ์ž๊ฐ€ ์›์น˜ ์•Š์œผ๋ฉด');
572
592
  console.log(lines.join('\n'));
573
593
  return;
574
594
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "3.2.1",
3
+ "version": "3.3.0",
4
4
  "description": "Save tokens on Claude Code โ€” spike diagnosis, 1M-context detection, TTL countdown, statusline. (formerly claude-cache-monitor)",
5
5
  "type": "module",
6
6
  "bin": {
package/src/harness.js CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  appendRatchetRule,
21
21
  } from './harness-templates.js';
22
22
  import { routeWarningForStatusline } from './route-scan.js';
23
+ import { ruleHealthWarningForStatusline } from './model-rules.js';
23
24
 
24
25
  const require = createRequire(import.meta.url);
25
26
  function readHarnessState() {
@@ -419,6 +420,14 @@ export function harnessStatusForStatusline(cfg, { root } = {}) {
419
420
  else if (state.pevSkip) warning = 'PEV-skip';
420
421
  }
421
422
  }
423
+ // Below session-quality warnings: a promoted delegation rule whose
424
+ // category started failing (`rule-health R<N>`) โ€” the user approved that
425
+ // rule, so its degradation outranks a mere new-candidate nudge.
426
+ if (!warning) {
427
+ try {
428
+ warning = ruleHealthWarningForStatusline(projectRoot);
429
+ } catch { /* registry unreadable โ€” stay silent */ }
430
+ }
422
431
  // Lowest precedence: route-scan delegation candidate (`route? R<N>`).
423
432
  // Session-quality warnings above always win โ€” routing is an optimization
424
433
  // nudge, not a correctness signal. Cheap: one small cached-JSON read.
@@ -97,6 +97,10 @@ export function renderModelRatchet(rules) {
97
97
  '๋กœ๊ทธ ๊ธฐ๋ฐ˜ ํ‹ฐ์–ด ์œ„์ž„ ๋ฃฐ. ์ด ํŒŒ์ผ์€ route-scan์ด ๋งค ์Šค์บ”๋งˆ๋‹ค ํ†ต์งธ๋กœ ์žฌ์ƒ์„ฑํ•˜๋ฏ€๋กœ',
98
98
  '์ง์ ‘ ์ˆ˜์ •ํ•˜์ง€ ๋งˆ์„ธ์š” โ€” ๋ชฉ๋ก/์ œ๊ฑฐ: `claude-token-saver route-scan rules [rm <N>]`.',
99
99
  '',
100
+ '์•„๋ž˜ ๋ฃฐ์— ๋”ฐ๋ผ ์œ„์ž„์„ ์‹คํ–‰ํ•  ๋•Œ๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ๋‹ค์Œ ํ•œ ์ค„์„ ๋จผ์ € ํ‘œ์‹œํ•  ๊ฒƒ',
101
+ '(์–ด๋–ค ๋„๊ตฌ๊ฐ€ ํ† ํฐ์„ ์•„๋ผ๊ณ  ์žˆ๋Š”์ง€ ๊ฐ€์‹œํ™”):',
102
+ '`๐Ÿ”€ [claude-token-saver] ๋ชจ๋ธ ํ”ผํŒ…: "<์œ ํ˜•>" โ†’ <agent> ์œ„์ž„`',
103
+ '',
100
104
  '## Rules',
101
105
  '',
102
106
  ];
@@ -110,6 +114,23 @@ export function renderModelRatchet(rules) {
110
114
  return lines.join('\n') + '\n';
111
115
  }
112
116
 
117
+ /**
118
+ * Statusline helper (docs/TIER_CRITERIA.md ยงrule-health) โ€” cheapest possible
119
+ * check, one small JSON read. Returns `rule-health R<N>` for the first
120
+ * review-flagged rule relevant to this project (its own root, or global
121
+ * scope), else null. N is the 1-based registry index, matching the numbering
122
+ * of `route-scan rules [rm <N>]` so the fix command is one lookup away.
123
+ */
124
+ export function ruleHealthWarningForStatusline(projectRoot) {
125
+ const { rules } = loadModelRules();
126
+ for (let i = 0; i < rules.length; i++) {
127
+ const r = rules[i];
128
+ if (r.status !== 'review') continue;
129
+ if (r.scope === 'global' || r.targetRoot === projectRoot) return `rule-health R${i + 1}`;
130
+ }
131
+ return null;
132
+ }
133
+
113
134
  export function modelRatchetPathFor(scope, targetRoot) {
114
135
  return scope === 'global'
115
136
  ? join(homedir(), '.claude', 'ratchet-model.md')
@@ -150,8 +171,11 @@ export function syncAllFiles({ previousPaths = [] } = {}) {
150
171
  * for each registered rule, recompute recurrence count and the error rate
151
172
  * of episodes in its (tier-eligible) category โ€” the rule-health signal.
152
173
  *
153
- * `episodeStats`: Map "category|project" โ†’ { count, errCount, epCount }
154
- * where errCount/epCount measure post-promotion delegated-category episodes.
174
+ * `episodeStats`: Map "category|project" (plus a "category|*" wildcard key
175
+ * that global-scope rules fall back to) โ†’ { count, errCount, epCount }.
176
+ * errCount/epCount measure the scan window's shape-eligible episodes โ€” ones
177
+ * an expensive model handled directly that still look T1/T2 by shape (tier
178
+ * judged with the error signal zeroed; see route-scan's rule-health pass).
155
179
  */
156
180
  export function refreshModelRules(episodeStats, { now } = {}) {
157
181
  const data = loadModelRules();
package/src/route-scan.js CHANGED
@@ -285,12 +285,25 @@ export async function runRouteScan({ days = 14 } = {}) {
285
285
  const prev = readRouteScan();
286
286
  const resolved = new Set(prev?.resolved || []);
287
287
 
288
+ // Never re-propose a pattern that already has a registered model-fitting
289
+ // rule (a global rule covers the category in every project). Without this,
290
+ // rules that entered the registry outside the promote flow โ€” migrations,
291
+ // future imports โ€” would resurface as candidates forever.
292
+ let registered = [];
293
+ try {
294
+ const { loadModelRules } = await import('./model-rules.js');
295
+ registered = loadModelRules().rules;
296
+ } catch { /* registry unreadable โ€” candidates may repeat until promote */ }
297
+ const hasRule = (g) => registered.some((r) =>
298
+ r.tier === g.tier && r.category === g.category &&
299
+ (r.scope === 'global' || r.project === g.project));
300
+
288
301
  const ruleText = (g) => g.tier === 'T2'
289
302
  ? `"${g.label}" ์œ ํ˜•์˜ ๋‹จ์ˆœ ์š”์ฒญ(์˜ˆ: "${g.example}")์€ ${g.agent}(haiku) ์„œ๋ธŒ์—์ด์ „ํŠธ๋กœ ์œ„์ž„ํ•œ๋‹ค`
290
303
  : `"${g.label}" ์œ ํ˜•์˜ ์ค‘๊ฐ„ ๋‚œ๋„ ์š”์ฒญ(์˜ˆ: "${g.example}")์€ model: sonnet ์„œ๋ธŒ์—์ด์ „ํŠธ๋กœ ์œ„์ž„ํ•œ๋‹ค (์„ค๊ณ„ ํŒ๋‹จยท๋ฐ˜๋ณต ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋ฉ”์ธ ๋ชจ๋ธ์ด ์ด์–ด๋ฐ›์Œ)`;
291
304
 
292
305
  const candidates = [...groups.values()]
293
- .filter((g) => g.count >= MIN_RECURRENCE)
306
+ .filter((g) => g.count >= MIN_RECURRENCE && !hasRule(g))
294
307
  .sort((a, b) => b.count - a.count)
295
308
  .slice(0, 8)
296
309
  .map((g, i) => ({