claude-token-saver 3.6.3 β†’ 3.7.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
@@ -243,6 +243,12 @@ Also update `statusLine.command` in `~/.claude/settings.json` to `claude-token-s
243
243
 
244
244
  ## Release notes
245
245
 
246
+ ### v3.7.0 (2026-07-29)
247
+ - **Fixed: delegation rules named subagents that may not exist** β€” generated T2 rules hard-coded preset names like `haiku-explore` / `haiku-runner`, which live in each user's own `~/.claude/agents/` and are not shipped by this package. On a machine without them, the rule told the model to delegate to a nonexistent agent. The default phrasing is now `model: haiku` (matching T1's `model: sonnet`), and the name is added only when the agent file is really present, e.g. `haiku-explore(model: haiku)`. Project-level `.claude/agents/` counts too. `ratchet-model.md` re-renders with the new phrasing on the next `route-scan`.
248
+
249
+ ### v3.6.4 (2026-07-29)
250
+ - **Fixed: false `πŸ…·βš  ratchet-unloaded`** β€” the import check looked at a single CLAUDE.md. When the project file carried the harness block, only that file was inspected, so the common layout of block-in-project + `@` imports-in-global raised the warning even though the rules loaded fine. Claude Code loads both files, so the flags are now the union of the two, and `importSource` (`project`/`global`/`both`) says which file carries them. The warning fires only when neither file imports.
251
+
246
252
  ### v3.6.3 (2026-07-27)
247
253
  - **Fixed: approved ratchet rules never reached the session** β€” `harness promote` appended rules to `ratchet.md`, but nothing ever read that file. Claude Code loads `CLAUDE.md` (plus whatever it imports) and the harness block carried no import line, so "approved rules apply automatically from the next session" was unimplemented. The block now imports `@.claude/ratchet.md` (project) / `@~/.claude/ratchet.md` (global). Re-running `harness init` upgrades an existing block in place.
248
254
  - **`ratchet-model.md` is imported explicitly too** β€” delegation rules now travel the same declared path instead of relying on a host that happens to pick the file up. To keep the import from dangling, `harness init` seeds an empty file and `syncAllFiles` empties rather than deletes a target that loses its last rule.
package/README.md CHANGED
@@ -200,6 +200,12 @@ npm uninstall -g claude-cache-monitor && npm i -g claude-token-saver
200
200
 
201
201
  ## 릴리슀 λ…ΈνŠΈ
202
202
 
203
+ ### v3.7.0 (2026-07-29)
204
+ - **μœ„μž„ 룰이 μ—†λŠ” μ„œλΈŒμ—μ΄μ „νŠΈλ₯Ό κ°€λ¦¬ν‚€λ˜ 문제 μˆ˜μ •** β€” μƒμ„±λ˜λŠ” T2 룰이 `haiku-explore`Β·`haiku-runner` 같은 이름을 직접 μ μ—ˆλŠ”λ°, 이 preset μ—μ΄μ „νŠΈλ“€μ€ 각자의 `~/.claude/agents/`에 μžˆλŠ” 것이라 νŒ¨ν‚€μ§€κ°€ λ°°ν¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. κ·Έλž˜μ„œ ν•΄λ‹Ή 파일이 μ—†λŠ” ν™˜κ²½μ—μ„œλŠ” "μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μ—μ΄μ „νŠΈλ‘œ μœ„μž„ν•˜λΌ"λŠ” 룰이 μžλ™ μƒμ„±λμŠ΅λ‹ˆλ‹€. 이제 κΈ°λ³Έ ν‘œν˜„μ€ `model: haiku`(T1의 `model: sonnet`κ³Ό 톡일)이고, μ—μ΄μ „νŠΈ 파일이 μ‹€μ œλ‘œ μžˆμ„ λ•Œλ§Œ `haiku-explore(model: haiku)`처럼 이름을 λ³‘κΈ°ν•©λ‹ˆλ‹€. ν”„λ‘œμ νŠΈ `.claude/agents/`도 μΈμ‹ν•©λ‹ˆλ‹€. λ‹€μŒ `route-scan` λ•Œ `ratchet-model.md`κ°€ μƒˆ ν‘œν˜„μœΌλ‘œ λ‹€μ‹œ λ Œλ”λ©λ‹ˆλ‹€.
205
+
206
+ ### v3.6.4 (2026-07-29)
207
+ - **`πŸ…·βš  ratchet-unloaded`κ°€ 잘λͺ» 뜨던 버그 μˆ˜μ •** β€” import μ—¬λΆ€λ₯Ό CLAUDE.md ν•œ νŒŒμΌμ—μ„œλ§Œ ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€. ν”„λ‘œμ νŠΈ `CLAUDE.md`에 harness 블둝이 있으면 κ·Έ 파일만 보고 νŒμ •ν–ˆκΈ° λ•Œλ¬Έμ—, `@` importκ°€ κΈ€λ‘œλ²Œ `~/.claude/CLAUDE.md`에 μžˆλŠ” ν”ν•œ μ‘°ν•©μ—μ„œλŠ” 룰이 정상 λ‘œλ“œλ˜λŠ”λ°λ„ κ²½κ³ κ°€ λ–΄μŠ΅λ‹ˆλ‹€. Claude CodeλŠ” 두 νŒŒμΌμ„ λͺ¨λ‘ λ‘œλ“œν•˜λ―€λ‘œ 이제 μ–‘μͺ½μ˜ importλ₯Ό ν•©μ³μ„œ νŒμ •ν•˜κ³ , μ–΄λŠ μͺ½μ΄ λ“€κ³  μžˆλŠ”μ§€λŠ” `importSource`(`project`/`global`/`both`)둜 μ•Œλ €μ€λ‹ˆλ‹€. 두 파일 λ‹€ importκ°€ 없을 λ•Œλ§Œ κ²½κ³ ν•©λ‹ˆλ‹€.
208
+
203
209
  ### v3.6.3 (2026-07-27)
204
210
  - **μŠΉμΈν•œ ratchet 룰이 μ„Έμ…˜μ— μ „λ‹¬λ˜μ§€ μ•Šλ˜ 버그 μˆ˜μ •** β€” `harness promote`λŠ” 룰을 `ratchet.md`에 appendν–ˆμ§€λ§Œ, κ·Έ νŒŒμΌμ„ μ½λŠ” μ†ŒλΉ„μžκ°€ 어디에도 μ—†μ—ˆμŠ΅λ‹ˆλ‹€. Claude CodeλŠ” `CLAUDE.md`(와 그것이 importν•˜λŠ” 파일)만 λ‘œλ“œν•˜λŠ”λ° harness 블둝에 import 라인이 μ—†μ—ˆκΈ° λ•Œλ¬Έμ—, "승인된 룰은 λ‹€μŒ μ„Έμ…˜λΆ€ν„° μžλ™ 적용"은 사싀상 λ―Έκ΅¬ν˜„ μƒνƒœμ˜€μŠ΅λ‹ˆλ‹€. 이제 harness 블둝이 `@.claude/ratchet.md`(project) / `@~/.claude/ratchet.md`(global)λ₯Ό importν•©λ‹ˆλ‹€. `harness init`을 λ‹€μ‹œ 돌리면 κΈ°μ‘΄ 블둝도 μ œμžλ¦¬μ—μ„œ κ°±μ‹ λ©λ‹ˆλ‹€.
205
211
  - **`ratchet-model.md`도 λͺ…μ‹œμ μœΌλ‘œ import** β€” λͺ¨λΈ ν”ΌνŒ… 룰도 ν˜ΈμŠ€νŠΈκ°€ μ•Œμ•„μ„œ 읽어주길 κΈ°λŒ€ν•˜μ§€ μ•Šκ³  같은 경둜둜 μ „λ‹¬ν•©λ‹ˆλ‹€. importκ°€ λŠκΈ°μ§€ μ•Šλ„λ‘ `harness init`이 빈 νŒŒμΌμ„ 미리 λ§Œλ“€κ³ , λ§ˆμ§€λ§‰ 룰이 사라져도 `syncAllFiles`κ°€ νŒŒμΌμ„ μ§€μš°λŠ” λŒ€μ‹  λΉ„μ›λ‹ˆλ‹€.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "3.6.3",
3
+ "version": "3.7.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": {
@@ -13,4 +13,4 @@
13
13
  - μžλ™ν™” 도ꡬ가 grepΒ·μ •κ·œμ‹μœΌλ‘œ μ°ΎλŠ” 마컀 ν…μŠ€νŠΈ(ν—€λ”Β·μ•΅μ»€Β·ν‚€μ›Œλ“œ 라인)λŠ” λ¦¬νŒ©ν„°λ§Β·μ••μΆ•Β·μˆ˜μ • κΈˆμ§€ β€” μ •ν™• λ§€μΉ­ κ²€μΆœμ€ ν•œ κΈ€μžλ§Œ λ°”λ€Œμ–΄λ„ 깨짐
14
14
  - μžλ™ν™” μ„€κ³„μ—μ„œ vision/screenshot 루프가 보이면 β†’ 호좜 κ°€λŠ₯ν•œ ν•¨μˆ˜Β·APIΒ·MCP toolλΆ€ν„° μ°ΎλŠ”λ‹€. ν†΅μ œ κ°€λŠ₯ν•œ λ‚΄λΆ€ λ„κ΅¬λŠ” 무쑰건 API, ν†΅μ œ λΆˆκ°€ μ™ΈλΆ€ SaaS만 vision μ°¨μ„ μ±…. μΈν„°νŽ˜μ΄μŠ€λ₯Ό λ°”κΎΌ 뒀에 λͺ¨λΈμ„ λ‚΄λ¦°λ‹€ β€” 거꾸둜 κ°€λ©΄ 토큰 폭발
15
15
  - μž…λ ₯ 데이터(ground-truth) λ³€κ²½ μ‹œ 그에 μ˜μ‘΄ν•˜λŠ” λͺ¨λ“  νŒŒμƒ μžμ‚°μ„ λκΉŒμ§€ μž¬μƒμ„±ν•œλ‹€ β€” 쀑간 단계λ₯Ό μƒλž΅ν•˜λ©΄ stale μ‚°μΆœλ¬Όμ΄ μƒˆ μž…λ ₯ μœ„μ— μ–Ήν˜€ κ²°κ³Όκ°€ 깨짐
16
- - νƒμƒ‰Β·μ‘°νšŒΒ·μƒνƒœ 확인·λͺ…λ Ή μ‹€ν–‰ 같은 λ‹¨μˆœ μš”μ²­μ€ haiku μ„œλΈŒμ—μ΄μ „νŠΈ(haiku-exploreΒ·haiku-runner λ“±)둜 μœ„μž„ν•΄ μƒμœ„ λͺ¨λΈ 토큰을 μ•„λ‚€λ‹€
16
+ - νƒμƒ‰Β·μ‘°νšŒΒ·μƒνƒœ 확인·λͺ…λ Ή μ‹€ν–‰ 같은 λ‹¨μˆœ μš”μ²­μ€ `model: haiku` μ„œλΈŒμ—μ΄μ „νŠΈλ‘œ μœ„μž„ν•΄ μƒμœ„ λͺ¨λΈ 토큰을 μ•„λ‚€λ‹€ (읽기 μ „μš© preset μ—μ΄μ „νŠΈκ°€ 이미 있으면 κ·Έκ±Έ μ“°κ³ , μ—†μœΌλ©΄ ν‹°μ–΄λ§Œ λ‚΄λ €μ„œ μœ„μž„)
package/src/agents.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * How a delegation target is phrased in generated rules.
3
+ *
4
+ * Rules used to name concrete subagents (`haiku-explore`, `haiku-runner`, …).
5
+ * Those live in the user's own `~/.claude/agents/` and are NOT shipped by this
6
+ * package, so on any machine that never created them the generated rule told
7
+ * the model to delegate to an agent that does not exist. The model tier is the
8
+ * part that actually saves tokens and it is universal, so `model: haiku` is the
9
+ * default phrasing β€” the agent name is only added when the file is really
10
+ * there, which keeps the tool-restricted preset in play for users who have it.
11
+ */
12
+
13
+ import { existsSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { claudeUserDir } from './paths.js';
16
+
17
+ /** Tier each preset agent is pinned to, for the `model:` half of the phrase. */
18
+ const AGENT_MODEL = {
19
+ 'haiku-explore': 'haiku',
20
+ 'haiku-runner': 'haiku',
21
+ 'haiku-translate': 'haiku',
22
+ 'sonnet-worker': 'sonnet',
23
+ };
24
+
25
+ /** True when `<name>.md` exists in the project or user agents directory. */
26
+ export function agentExists(name, root = process.cwd()) {
27
+ if (!name) return false;
28
+ return existsSync(join(root, '.claude', 'agents', `${name}.md`)) ||
29
+ existsSync(join(claudeUserDir(), 'agents', `${name}.md`));
30
+ }
31
+
32
+ /**
33
+ * Korean phrase for a delegation target, e.g.
34
+ * agent present β†’ `haiku-explore(model: haiku)`
35
+ * agent absent β†’ `model: haiku`
36
+ */
37
+ export function agentPhrase(name, { model, root } = {}) {
38
+ const tier = model || AGENT_MODEL[name] || 'haiku';
39
+ return agentExists(name, root) ? `${name}(model: ${tier})` : `model: ${tier}`;
40
+ }
41
+
42
+ /**
43
+ * English phrase, including the article, e.g.
44
+ * agent present β†’ `the haiku-explore (model: haiku) subagent`
45
+ * agent absent β†’ `a model: haiku subagent`
46
+ */
47
+ export function agentPhraseEn(name, { model, root } = {}) {
48
+ const tier = model || AGENT_MODEL[name] || 'haiku';
49
+ return agentExists(name, root)
50
+ ? `the ${name} (model: ${tier}) subagent`
51
+ : `a model: ${tier} subagent`;
52
+ }
package/src/harness.js CHANGED
@@ -153,15 +153,37 @@ function statusForFile(filePath) {
153
153
  * relies on a globally-installed harness report πŸ…· 5/5 (covered by global),
154
154
  * matching reality: Claude Code loads ~/.claude/CLAUDE.md for every project.
155
155
  * The returned `source` ('project'|'global') tells callers which file was used.
156
+ *
157
+ * The `@` import flags are the union of both files, not just the source one:
158
+ * Claude Code loads ~/.claude/CLAUDE.md for every project *and* the project
159
+ * CLAUDE.md, so a project-scope block with the imports living in the global
160
+ * file still gets the ratchet rules. Checking only the source file made that
161
+ * layout report a false `ratchet-unloaded`. `importSource` says which file
162
+ * actually carries them ('project' | 'global' | 'both' | null).
156
163
  */
157
164
  export function harnessStatus(root = findProjectRoot(), { scope = 'auto' } = {}) {
158
- if (scope === 'project') return { ...statusForFile(claudeMdPath(root)), root, source: 'project' };
159
- if (scope === 'global') return { ...statusForFile(globalClaudeMdPath()), root, source: 'global' };
160
165
  const project = statusForFile(claudeMdPath(root));
161
- if (project.hasBlock) return { ...project, root, source: 'project' };
162
166
  const global = statusForFile(globalClaudeMdPath());
163
- if (global.hasBlock) return { ...global, root, source: 'global' };
164
- return { ...project, root, source: 'project' };
167
+ const pick = (s, source) => ({ ...s, ...unionImports(project, global), root, source });
168
+ if (scope === 'project') return pick(project, 'project');
169
+ if (scope === 'global') return pick(global, 'global');
170
+ if (project.hasBlock) return pick(project, 'project');
171
+ if (global.hasBlock) return pick(global, 'global');
172
+ return pick(project, 'project');
173
+ }
174
+
175
+ // Union the two files' import flags. Same file read twice (project root === ~)
176
+ // is harmless β€” OR is idempotent.
177
+ function unionImports(project, global) {
178
+ const samePath = project.file === global.file;
179
+ const g = samePath ? { hasRatchetImport: false, hasModelRatchetImport: false } : global;
180
+ const inProject = project.hasRatchetImport || project.hasModelRatchetImport;
181
+ const inGlobal = g.hasRatchetImport || g.hasModelRatchetImport;
182
+ return {
183
+ hasRatchetImport: project.hasRatchetImport || g.hasRatchetImport,
184
+ hasModelRatchetImport: project.hasModelRatchetImport || g.hasModelRatchetImport,
185
+ importSource: inProject && inGlobal ? 'both' : inProject ? 'project' : inGlobal ? 'global' : null,
186
+ };
165
187
  }
166
188
 
167
189
  /**
@@ -33,6 +33,7 @@ import { join, dirname } from 'node:path';
33
33
  import { homedir } from 'node:os';
34
34
  import { userDataDir } from './paths.js';
35
35
  import { userLanguage } from './config.js';
36
+ import { agentPhrase, agentPhraseEn } from './agents.js';
36
37
 
37
38
  // Post-promotion delegated-category error rate above this flags the rule
38
39
  // for review (rule-health). Calibrated against local T0 avg error incidence.
@@ -144,10 +145,10 @@ export function renderModelRatchet(rules, lang = userLanguage()) {
144
145
  const t1 = group.find((r) => r.tier === 'T1');
145
146
  if (t2 && t1) {
146
147
  const rule = ko
147
- ? `"${t2.label}" μœ ν˜• μš”μ²­μ€ 기본적으둜 ${t2.agent}(haiku) μ„œλΈŒμ—μ΄μ „νŠΈλ‘œ μœ„μž„ν•œλ‹€(예: "${t2.example}"). ` +
148
+ ? `"${t2.label}" μœ ν˜• μš”μ²­μ€ 기본적으둜 ${agentPhrase(t2.agent)} μ„œλΈŒμ—μ΄μ „νŠΈλ‘œ μœ„μž„ν•œλ‹€(예: "${t2.example}"). ` +
148
149
  `μ—¬λŸ¬ λ‹¨κ³„Β·μ—¬λŸ¬ 파일 μˆ˜μ •μ΄ μ–½νžŒ 쀑간 λ‚œλ„ μš”μ²­(예: "${t1.example}")은 model: sonnet μ„œλΈŒμ—μ΄μ „νŠΈλ‘œ μœ„μž„ν•œλ‹€. ` +
149
150
  `섀계 νŒλ‹¨Β·λ°°ν¬Β·μŠ€ν† μ–΄ 제좜 같은 λΉ„κ°€μ—­ μž‘μ—…μ΄ μ„žμ΄κ±°λ‚˜ μœ„μž„ 쀑 μ—λŸ¬κ°€ 반볡되면 μœ„μž„ν•˜μ§€ 말고 메인 λͺ¨λΈμ΄ 직접 μ²˜λ¦¬ν•œλ‹€`
150
- : `Delegate "${t2.labelEn || t2.label}" requests to the ${t2.agent} (haiku) subagent by default (e.g. "${t2.example}"). ` +
151
+ : `Delegate "${t2.labelEn || t2.label}" requests to ${agentPhraseEn(t2.agent)} by default (e.g. "${t2.example}"). ` +
151
152
  `Escalate moderate ones that span multiple steps or file edits (e.g. "${t1.example}") to a model: sonnet subagent. ` +
152
153
  `Do not delegate at all β€” handle it on the main model β€” when the request mixes in design judgement or irreversible work (deploy, release, store submission), or when errors repeat during delegation`;
153
154
  lines.push(`- ${rule}${healthOf(t2)}${healthOf(t1)} <!-- T2 ${statsOf(t2)} / T1 ${statsOf(t1)} -->`);
package/src/route-scan.js CHANGED
@@ -22,6 +22,7 @@ import { join } from 'node:path';
22
22
  import { userDataDir } from './paths.js';
23
23
  import { discoverSessionFiles } from './parser.js';
24
24
  import { collectSessionRecords } from './session-records.js';
25
+ import { agentPhrase, agentPhraseEn } from './agents.js';
25
26
 
26
27
  // ── Tier bands (docs/TIER_CRITERIA.md Β§3) ────────────────────────────────
27
28
  // T2 (haiku): finished in few calls, tiny output, near-zero mutation, no
@@ -373,10 +374,10 @@ export async function runRouteScan({ days = 14 } = {}) {
373
374
  // switching `language` later re-renders (and promotes) correctly without
374
375
  // waiting for a rescan.
375
376
  const ruleText = (g) => g.tier === 'T2'
376
- ? `"${g.label}" μœ ν˜•μ˜ λ‹¨μˆœ μš”μ²­(예: "${g.example}")은 ${g.agent}(haiku) μ„œλΈŒμ—μ΄μ „νŠΈλ‘œ μœ„μž„ν•œλ‹€ (섀계 νŒλ‹¨Β·λ°°ν¬Β·μŠ€ν† μ–΄ 제좜 같은 λΉ„κ°€μ—­ μž‘μ—…μ΄ μ„žμ΄λ©΄ μœ„μž„ν•˜μ§€ μ•ŠμŒ)`
377
+ ? `"${g.label}" μœ ν˜•μ˜ λ‹¨μˆœ μš”μ²­(예: "${g.example}")은 ${agentPhrase(g.agent)} μ„œλΈŒμ—μ΄μ „νŠΈλ‘œ μœ„μž„ν•œλ‹€ (섀계 νŒλ‹¨Β·λ°°ν¬Β·μŠ€ν† μ–΄ 제좜 같은 λΉ„κ°€μ—­ μž‘μ—…μ΄ μ„žμ΄λ©΄ μœ„μž„ν•˜μ§€ μ•ŠμŒ)`
377
378
  : `"${g.label}" μœ ν˜•μ˜ 쀑간 λ‚œλ„ μš”μ²­(예: "${g.example}")은 model: sonnet μ„œλΈŒμ—μ΄μ „νŠΈλ‘œ μœ„μž„ν•œλ‹€ (섀계 νŒλ‹¨Β·λΉ„κ°€μ—­ μž‘μ—…Β·λ°˜λ³΅ μ—λŸ¬ λ°œμƒ μ‹œ 메인 λͺ¨λΈμ΄ μ΄μ–΄λ°›μŒ)`;
378
379
  const ruleTextEn = (g) => g.tier === 'T2'
379
- ? `Delegate simple "${g.labelEn}" requests (e.g. "${g.example}") to the ${g.agent} (haiku) subagent β€” never when the request mixes in design judgement or irreversible work like deploy/release/submission`
380
+ ? `Delegate simple "${g.labelEn}" requests (e.g. "${g.example}") to ${agentPhraseEn(g.agent)} β€” never when the request mixes in design judgement or irreversible work like deploy/release/submission`
380
381
  : `Delegate moderate "${g.labelEn}" requests (e.g. "${g.example}") to a model: sonnet subagent β€” hand back to the main model on design judgement, irreversible work, or repeated errors`;
381
382
 
382
383
  const candidates = [...groups.values()]