claude-token-saver 3.6.2 → 3.6.3
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 +6 -0
- package/README.md +6 -0
- package/package.json +1 -1
- package/src/commands/harness.js +57 -2
- package/src/harness-templates.js +44 -2
- package/src/harness.js +96 -5
- package/src/model-rules.js +4 -2
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.6.3 (2026-07-27)
|
|
247
|
+
- **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
|
+
- **`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.
|
|
249
|
+
- **New `🅷⚠ ratchet-unloaded` warning** — all five sections can be present while the rules are dead weight, so a missing import is reported separately from `N/5`. `harness check` says the same thing and prints the fix.
|
|
250
|
+
- **New `harness prune`, plus ratchet size in `check`** — an imported ratchet costs tokens on every request. `harness check` now reports rule count and per-request tokens and warns past ~2,000. Trim with `harness prune [--tag <t>] [--older-than <months>] [--dry-run]`, which moves rules to `ratchet-archive.md` rather than deleting them. Prefix a rule with `[tags]` (`- 2026-05-08: [video] ...`) to prune by topic. (`@` imports are static, so load-time filtering is not possible — the only lever is fewer rules.)
|
|
251
|
+
|
|
246
252
|
### v3.6.2 (2026-07-26)
|
|
247
253
|
- Docs-only release — restores the missing v3.4.0–v3.5.3 release notes below. Published because the npm package page renders the README of the published version. No code changes.
|
|
248
254
|
|
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.6.3 (2026-07-27)
|
|
204
|
+
- **승인한 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
|
+
- **`ratchet-model.md`도 명시적으로 import** — 모델 피팅 룰도 호스트가 알아서 읽어주길 기대하지 않고 같은 경로로 전달합니다. import가 끊기지 않도록 `harness init`이 빈 파일을 미리 만들고, 마지막 룰이 사라져도 `syncAllFiles`가 파일을 지우는 대신 비웁니다.
|
|
206
|
+
- **`🅷⚠ ratchet-unloaded` 경고 추가** — 5개 섹션이 다 있어도 import 라인이 없으면 룰이 죽어 있는 상태라, `N/5`와 별개로 표시합니다. `harness check`도 같은 내용을 수정 명령과 함께 안내합니다.
|
|
207
|
+
- **`harness prune` 추가 + ratchet 크기 표시** — import된 ratchet은 매 요청마다 토큰을 씁니다. `harness check`가 룰 수와 요청당 토큰을 보여주고 ~2,000 토큰을 넘으면 경고합니다. 정리는 `harness prune [--tag <t>] [--older-than <months>] [--dry-run]` — 삭제가 아니라 `ratchet-archive.md`로 이동합니다. 룰 앞에 `[태그]`를 붙여두면(`- 2026-05-08: [video] ...`) 묶어서 정리할 수 있습니다. (`@` import는 정적이라 로드 시점 필터링은 불가능합니다 — 줄일 방법은 룰 자체를 줄이는 것뿐)
|
|
208
|
+
|
|
203
209
|
### v3.6.2 (2026-07-26)
|
|
204
210
|
- 문서 전용 릴리스 — 아래 v3.4.0~v3.5.3 릴리스 노트가 누락돼 있던 것을 복원했고, npm 패키지 페이지는 발행된 버전의 README를 보여주므로 이를 반영하기 위해 올립니다. 코드 변경 없음.
|
|
205
211
|
|
package/package.json
CHANGED
package/src/commands/harness.js
CHANGED
|
@@ -27,7 +27,7 @@ export async function run({ args, hasFlag }) {
|
|
|
27
27
|
}
|
|
28
28
|
return dflt;
|
|
29
29
|
};
|
|
30
|
-
const { harnessInit, harnessUninit, harnessStatus, harnessPromote, harnessPull, harnessListRules, harnessRmRule, findProjectRoot } =
|
|
30
|
+
const { harnessInit, harnessUninit, harnessStatus, harnessPromote, harnessPull, harnessListRules, harnessRmRule, harnessPrune, ratchetSizeStatus, RATCHET_TOKEN_BUDGET, findProjectRoot } =
|
|
31
31
|
await import('../harness.js');
|
|
32
32
|
const { HARNESS_SECTIONS } = await import('../harness-templates.js');
|
|
33
33
|
const { loadConfig, saveConfig, userLanguage } = await import('../config.js');
|
|
@@ -54,6 +54,29 @@ export async function run({ args, hasFlag }) {
|
|
|
54
54
|
} else {
|
|
55
55
|
console.log('All 5 harness sections present. ✅');
|
|
56
56
|
}
|
|
57
|
+
// Sections can all be present while ratchet.md still never reaches the
|
|
58
|
+
// model — blocks written before v3.6.3 have no `@` import line.
|
|
59
|
+
if (s.hasBlock && !(s.hasRatchetImport && s.hasModelRatchetImport)) {
|
|
60
|
+
const dead = [!s.hasRatchetImport && 'ratchet.md', !s.hasModelRatchetImport && 'ratchet-model.md'].filter(Boolean).join(' + ');
|
|
61
|
+
console.log(`\n⚠ ${dead} is NOT loaded into sessions — the harness block has no \`@\` import line for it.`);
|
|
62
|
+
console.log(' Those rules are being written to a file nothing reads.');
|
|
63
|
+
console.log(` Fix: claude-token-saver harness init${s.source === 'global' ? ' --global' : ''} (updates the block in place)`);
|
|
64
|
+
}
|
|
65
|
+
// Imported ratchets are charged on every request, so their size matters.
|
|
66
|
+
// Static `@` imports cannot be filtered at load time — the only lever is
|
|
67
|
+
// fewer rules, hence the prune pointer rather than a "filter" suggestion.
|
|
68
|
+
for (const sc of ['project', 'global']) {
|
|
69
|
+
const size = ratchetSizeStatus({ scope: sc });
|
|
70
|
+
if (!size.count) continue;
|
|
71
|
+
const line = `ratchet.md [${sc}]: ${size.count} rules, ~${size.tokens} tok/request`;
|
|
72
|
+
if (size.overBudget) {
|
|
73
|
+
console.log(`\n⚠ ${line} — over the ~${RATCHET_TOKEN_BUDGET} token budget.`);
|
|
74
|
+
console.log(` Trim: claude-token-saver harness prune${sc === 'global' ? ' --global' : ''} --older-than 6 --dry-run`);
|
|
75
|
+
console.log(' (project-specific rules belong in --project scope, not global.)');
|
|
76
|
+
} else {
|
|
77
|
+
console.log(`${line}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
57
80
|
return;
|
|
58
81
|
}
|
|
59
82
|
|
|
@@ -322,6 +345,38 @@ export async function run({ args, hasFlag }) {
|
|
|
322
345
|
if (wantProject) print('project');
|
|
323
346
|
if (wantGlobal) print('global');
|
|
324
347
|
console.log('Remove with: claude-token-saver harness rm [--global|--project] <N>');
|
|
348
|
+
console.log('Archive in bulk: claude-token-saver harness prune [--global] [--tag <t>] [--older-than <months>] [--dry-run]');
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (sub === 'prune') {
|
|
353
|
+
const pruneScope = parseHarnessScope(args.slice(2), 'project');
|
|
354
|
+
const argv = args.slice(2);
|
|
355
|
+
const valueOf = (flag) => {
|
|
356
|
+
const i = argv.indexOf(flag);
|
|
357
|
+
if (i !== -1 && argv[i + 1] && !argv[i + 1].startsWith('--')) return argv[i + 1];
|
|
358
|
+
const eq = argv.find((a) => a.startsWith(flag + '='));
|
|
359
|
+
return eq ? eq.slice(flag.length + 1) : null;
|
|
360
|
+
};
|
|
361
|
+
const months = valueOf('--older-than');
|
|
362
|
+
if (months !== null && !/^\d+$/.test(months)) {
|
|
363
|
+
console.error(`Invalid --older-than value: ${months} (expected a number of months)`);
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
const r = harnessPrune({
|
|
367
|
+
scope: pruneScope,
|
|
368
|
+
tag: valueOf('--tag'),
|
|
369
|
+
olderThanMonths: months ? parseInt(months, 10) : null,
|
|
370
|
+
dryRun: hasFlag('--dry-run'),
|
|
371
|
+
});
|
|
372
|
+
if (!r.ok) { console.error(r.error); process.exit(1); }
|
|
373
|
+
if (!r.pruned.length) { console.log(`Nothing matched — ${r.path} unchanged.`); return; }
|
|
374
|
+
console.log(`${r.dryRun ? 'Would prune' : 'Pruned'} ${r.pruned.length} rule(s) from ${r.path}:`);
|
|
375
|
+
for (const p of r.pruned) console.log(` #${p.index} ${p.text.slice(0, 100)}`);
|
|
376
|
+
if (!r.dryRun) {
|
|
377
|
+
console.log(`\nArchived to: ${r.archive} (backup: ${r.backup})`);
|
|
378
|
+
console.log('Archived rules are NOT loaded into sessions — paste one back into ratchet.md to restore it.');
|
|
379
|
+
}
|
|
325
380
|
return;
|
|
326
381
|
}
|
|
327
382
|
|
|
@@ -378,6 +433,6 @@ export async function run({ args, hasFlag }) {
|
|
|
378
433
|
}
|
|
379
434
|
|
|
380
435
|
console.error(`Unknown harness subcommand: ${sub}`);
|
|
381
|
-
console.error('Usage: claude-token-saver harness [check|init|uninit [--purge-ratchet]|promote "<rule>"|pull [--global|--project]|list|rm <N>|off|on]');
|
|
436
|
+
console.error('Usage: claude-token-saver harness [check|init|uninit [--purge-ratchet]|promote "<rule>"|pull [--global|--project]|list|rm <N>|prune [--tag <t>] [--older-than <months>] [--dry-run]|off|on]');
|
|
382
437
|
process.exit(1);
|
|
383
438
|
}
|
package/src/harness-templates.js
CHANGED
|
@@ -17,7 +17,36 @@ export const HARNESS_SECTIONS = [
|
|
|
17
17
|
{ id: 'safe-path', heading: '### 5. Default Safe Path — 파괴적 명령 항상 확인' },
|
|
18
18
|
];
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The `@` import that actually loads ratchet.md into the session.
|
|
22
|
+
*
|
|
23
|
+
* Without this line the promoted rules live in a file nobody reads: Claude Code
|
|
24
|
+
* only loads CLAUDE.md (plus whatever it imports), so `harness promote` was a
|
|
25
|
+
* write-only operation before v3.6.3. Project scope imports the repo-local
|
|
26
|
+
* ratchet, global scope the user-level one — mirroring resolveRatchetPath().
|
|
27
|
+
*/
|
|
28
|
+
export function ratchetImportLine(scope = 'project') {
|
|
29
|
+
return scope === 'global' ? '@~/.claude/ratchet.md' : '@.claude/ratchet.md';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The model-fitting ratchet is imported too, so the delegation rules reach the
|
|
34
|
+
* model through the same declared path as everything else instead of relying on
|
|
35
|
+
* a host that happens to pick the file up. harnessInit() seeds an empty
|
|
36
|
+
* ratchet-model.md and syncAllFiles() empties rather than deletes it, so this
|
|
37
|
+
* import never dangles.
|
|
38
|
+
*/
|
|
39
|
+
export function modelRatchetImportLine(scope = 'project') {
|
|
40
|
+
return scope === 'global' ? '@~/.claude/ratchet-model.md' : '@.claude/ratchet-model.md';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Matches either scope's import line, so completeness checks don't need to know
|
|
44
|
+
// which scope wrote the block. `(?!-)` keeps the ratchet-model import from
|
|
45
|
+
// counting as the ratchet.md one.
|
|
46
|
+
export const RATCHET_IMPORT_RE = /^@(?:~\/\.claude|\.claude)\/ratchet\.md\s*$/m;
|
|
47
|
+
export const MODEL_RATCHET_IMPORT_RE = /^@(?:~\/\.claude|\.claude)\/ratchet-model\.md\s*$/m;
|
|
48
|
+
|
|
49
|
+
export function harnessClaudeMdBlock(scope = 'project') {
|
|
21
50
|
const sections = HARNESS_SECTIONS.map((s) => s.heading).join('\n\n... (see full block below)');
|
|
22
51
|
return `${HARNESS_BLOCK_BEGIN}
|
|
23
52
|
## 🅷 Harness Rules (claude-token-saver)
|
|
@@ -36,7 +65,8 @@ ${HARNESS_SECTIONS[0].heading}
|
|
|
36
65
|
도구·환경 일반 룰이면 \`--global\`(\`~/.claude/ratchet.md\`). Bash 환경은
|
|
37
66
|
non-TTY라 CLI의 readline 프롬프트가 안 뜨므로, 호출자(LLM)가 직접 묻고
|
|
38
67
|
플래그를 명시해야 함. 묻지 않고 기본값으로 등록하지 말 것.
|
|
39
|
-
- 승인된 룰은
|
|
68
|
+
- 승인된 룰은 이 블록 맨 아래 \`@\` import로 매 세션 로드된다 — 그 import 라인을
|
|
69
|
+
지우면 ratchet.md는 컨텍스트에 들어오지 않으니 지우지 말 것.
|
|
40
70
|
- **모델 피팅 랫쳇**: \`.claude/ratchet-model.md\`(프로젝트)와
|
|
41
71
|
\`~/.claude/ratchet-model.md\`(글로벌)에 있는 티어 위임 룰도 ratchet.md와
|
|
42
72
|
동일하게 따를 것. 이 파일은 claude-token-saver가 로그 기반으로 자동
|
|
@@ -81,6 +111,13 @@ ${HARNESS_SECTIONS[4].heading}
|
|
|
81
111
|
- \`claude-token-saver harness check\` — 현재 셋업 점수
|
|
82
112
|
- \`claude-token-saver harness promote "<룰>" --project|--global\` — ratchet에 룰 추가 (scope는 사용자에게 먼저 물어볼 것)
|
|
83
113
|
- \`claude-token-saver harness off\` — statusline 표시 끄기
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
📥 ratchet 룰 로드 (이 줄들을 지우면 룰이 적용되지 않습니다):
|
|
118
|
+
|
|
119
|
+
${ratchetImportLine(scope)}
|
|
120
|
+
${modelRatchetImportLine(scope)}
|
|
84
121
|
${HARNESS_BLOCK_END}
|
|
85
122
|
`;
|
|
86
123
|
}
|
|
@@ -93,6 +130,11 @@ export function harnessRatchetMdInitial() {
|
|
|
93
130
|
\`claude-token-saver harness promote "<rule>"\`로 룰을 추가하면 자동으로
|
|
94
131
|
이 파일에 append 됩니다.
|
|
95
132
|
|
|
133
|
+
이 파일은 CLAUDE.md의 \`@\` import로 매 세션 로드됩니다 — 즉 여기 있는 모든 줄이
|
|
134
|
+
매 요청마다 토큰을 씁니다. 날짜 뒤에 \`[태그]\`를 붙여두면 나중에 묶어서 정리할 수
|
|
135
|
+
있습니다: \`- 2026-05-08: [video,tts] ...\` →
|
|
136
|
+
\`claude-token-saver harness prune --tag video\` (삭제 아니라 ratchet-archive.md로 이동).
|
|
137
|
+
|
|
96
138
|
## Rules
|
|
97
139
|
|
|
98
140
|
`;
|
package/src/harness.js
CHANGED
|
@@ -18,9 +18,11 @@ import {
|
|
|
18
18
|
harnessClaudeMdBlock,
|
|
19
19
|
harnessRatchetMdInitial,
|
|
20
20
|
appendRatchetRule,
|
|
21
|
+
RATCHET_IMPORT_RE,
|
|
22
|
+
MODEL_RATCHET_IMPORT_RE,
|
|
21
23
|
} from './harness-templates.js';
|
|
22
24
|
import { routeWarningForStatusline } from './route-scan.js';
|
|
23
|
-
import { ruleHealthWarningForStatusline } from './model-rules.js';
|
|
25
|
+
import { ruleHealthWarningForStatusline, modelRatchetPathFor, renderModelRatchet } from './model-rules.js';
|
|
24
26
|
|
|
25
27
|
const require = createRequire(import.meta.url);
|
|
26
28
|
function readHarnessState() {
|
|
@@ -96,6 +98,8 @@ function statusForFile(filePath) {
|
|
|
96
98
|
hasFile: false,
|
|
97
99
|
optOut: false,
|
|
98
100
|
custom: false,
|
|
101
|
+
hasRatchetImport: false,
|
|
102
|
+
hasModelRatchetImport: false,
|
|
99
103
|
file: filePath,
|
|
100
104
|
};
|
|
101
105
|
}
|
|
@@ -105,7 +109,7 @@ function statusForFile(filePath) {
|
|
|
105
109
|
} catch {
|
|
106
110
|
// Unreadable file (permissions, etc.) — report every section missing so
|
|
107
111
|
// `harness check` can't print "All 5 sections present ✅" over a 0/5.
|
|
108
|
-
return { configured: 0, total: HARNESS_SECTIONS.length, missing: HARNESS_SECTIONS.map((s) => s.id), hasBlock: false, hasFile: true, optOut: false, custom: false, file: filePath };
|
|
112
|
+
return { configured: 0, total: HARNESS_SECTIONS.length, missing: HARNESS_SECTIONS.map((s) => s.id), hasBlock: false, hasFile: true, optOut: false, custom: false, hasRatchetImport: false, hasModelRatchetImport: false, file: filePath };
|
|
109
113
|
}
|
|
110
114
|
const hasBlock = content.includes(HARNESS_BLOCK_BEGIN);
|
|
111
115
|
// Opt-out marker — when the user intentionally customizes the harness block
|
|
@@ -131,6 +135,11 @@ function statusForFile(filePath) {
|
|
|
131
135
|
hasFile: true,
|
|
132
136
|
optOut,
|
|
133
137
|
custom,
|
|
138
|
+
// Whether the promoted ratchet rules actually reach the model. Blocks
|
|
139
|
+
// written before v3.6.3 have all 5 sections but no import, so the rules
|
|
140
|
+
// sat in a file nothing read — worth flagging separately from N/5.
|
|
141
|
+
hasRatchetImport: RATCHET_IMPORT_RE.test(content),
|
|
142
|
+
hasModelRatchetImport: MODEL_RATCHET_IMPORT_RE.test(content),
|
|
134
143
|
file: filePath,
|
|
135
144
|
};
|
|
136
145
|
}
|
|
@@ -168,7 +177,7 @@ export function harnessInit({ root = findProjectRoot(), force = false, scope = '
|
|
|
168
177
|
const result = { wrote: [], backedUp: [], skipped: [], root, scope };
|
|
169
178
|
|
|
170
179
|
// CLAUDE.md
|
|
171
|
-
const block = harnessClaudeMdBlock();
|
|
180
|
+
const block = harnessClaudeMdBlock(scope);
|
|
172
181
|
if (existsSync(cmPath)) {
|
|
173
182
|
const existing = readFileSync(cmPath, 'utf8');
|
|
174
183
|
if (existing.includes(HARNESS_BLOCK_BEGIN) && !force) {
|
|
@@ -209,6 +218,20 @@ export function harnessInit({ root = findProjectRoot(), force = false, scope = '
|
|
|
209
218
|
result.skipped.push(rmPath + ' (already exists)');
|
|
210
219
|
}
|
|
211
220
|
|
|
221
|
+
// ratchet-model.md — tool-owned, normally written by route-scan. The block
|
|
222
|
+
// imports it, so seed an empty one now rather than ship a dangling import
|
|
223
|
+
// into every project that has not been scanned yet.
|
|
224
|
+
const mrPath = modelRatchetPathFor(scope, root);
|
|
225
|
+
if (!existsSync(mrPath)) {
|
|
226
|
+
try {
|
|
227
|
+
mkdirSync(dirname(mrPath), { recursive: true });
|
|
228
|
+
writeFileSync(mrPath, renderModelRatchet([]));
|
|
229
|
+
result.wrote.push(mrPath);
|
|
230
|
+
} catch { /* unwritable — route-scan will retry on its next sync */ }
|
|
231
|
+
} else {
|
|
232
|
+
result.skipped.push(mrPath + ' (already exists)');
|
|
233
|
+
}
|
|
234
|
+
|
|
212
235
|
return result;
|
|
213
236
|
}
|
|
214
237
|
|
|
@@ -348,10 +371,74 @@ export function harnessListRules({ root = findProjectRoot(), scope = 'project' }
|
|
|
348
371
|
// and the "## Rules" anchor are ignored. (Model-fitting rules live in a
|
|
349
372
|
// separate tool-owned file, ratchet-model.md — never listed here.)
|
|
350
373
|
if (/^\s*-\s+/.test(line)) {
|
|
351
|
-
|
|
374
|
+
const text = line.replace(/^\s*-\s+/, '');
|
|
375
|
+
rules.push({ index: rules.length + 1, lineNo: i, text, ...parseRuleMeta(text) });
|
|
352
376
|
}
|
|
353
377
|
}
|
|
354
|
-
return { path: rmPath, rules };
|
|
378
|
+
return { path: rmPath, rules, bytes: Buffer.byteLength(readFileSync(rmPath, 'utf8'), 'utf8') };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Rule metadata, parsed from the line the user already writes:
|
|
383
|
+
* "- 2026-05-08: [video,tts] 자막 렌더 시 ..."
|
|
384
|
+
* Both parts are optional — `date` is null on undated rules, `tags` empty on
|
|
385
|
+
* untagged ones. Tags exist to make pruning targeted (`prune --tag video`);
|
|
386
|
+
* they deliberately do NOT filter what gets loaded, because CLAUDE.md `@`
|
|
387
|
+
* imports are static — the file that is imported is the file that is read, so
|
|
388
|
+
* the only way to spend fewer tokens is to have fewer rules in it.
|
|
389
|
+
*/
|
|
390
|
+
export function parseRuleMeta(text) {
|
|
391
|
+
const dateM = text.match(/^(\d{4})-(\d{2})-(\d{2})\s*:/);
|
|
392
|
+
// Tags must lead the rule body (right after the date, or at the very start),
|
|
393
|
+
// so a bracketed aside later in the sentence is not mistaken for a tag list.
|
|
394
|
+
const tagM = text.match(/^(?:\d{4}-\d{2}-\d{2}\s*:\s*)?\[([^\]]+)\]/);
|
|
395
|
+
return {
|
|
396
|
+
date: dateM ? dateM[0].replace(/\s*:$/, '') : null,
|
|
397
|
+
tags: tagM ? tagM[1].split(',').map((t) => t.trim()).filter(Boolean) : [],
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Rough token cost of the imported ratchet, charged on every single request of
|
|
402
|
+
// every session. 4 bytes/token is the usual mixed ko/en approximation.
|
|
403
|
+
export const RATCHET_TOKEN_BUDGET = 2000;
|
|
404
|
+
|
|
405
|
+
export function ratchetSizeStatus({ root = findProjectRoot(), scope = 'project' } = {}) {
|
|
406
|
+
const { path, rules, bytes = 0 } = harnessListRules({ root, scope });
|
|
407
|
+
const tokens = Math.round(bytes / 4);
|
|
408
|
+
return { path, count: rules.length, bytes, tokens, overBudget: tokens > RATCHET_TOKEN_BUDGET };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* harness prune — move rules out of ratchet.md into ratchet-archive.md next to
|
|
413
|
+
* it. Selection is by tag and/or age; nothing is deleted, so a pruned rule can
|
|
414
|
+
* be pasted back. Returns the pruned rules for the CLI to echo.
|
|
415
|
+
*/
|
|
416
|
+
export function harnessPrune({ root = findProjectRoot(), scope = 'project', tag = null, olderThanMonths = null, dryRun = false } = {}) {
|
|
417
|
+
const { path: rmPath, rules } = harnessListRules({ root, scope });
|
|
418
|
+
if (!existsSync(rmPath)) return { ok: false, error: `ratchet.md not found at ${rmPath}` };
|
|
419
|
+
if (!tag && !olderThanMonths) return { ok: false, error: 'Nothing selected — pass --tag <t> and/or --older-than <months>' };
|
|
420
|
+
const cutoff = olderThanMonths ? Date.now() - olderThanMonths * 30 * 24 * 60 * 60 * 1000 : null;
|
|
421
|
+
const doomed = rules.filter((r) => {
|
|
422
|
+
if (tag && !r.tags.includes(tag)) return false;
|
|
423
|
+
// An undated rule has no age to judge, so age-based pruning leaves it be.
|
|
424
|
+
if (cutoff !== null) {
|
|
425
|
+
const t = r.date ? Date.parse(r.date) : NaN;
|
|
426
|
+
if (!Number.isFinite(t) || t >= cutoff) return false;
|
|
427
|
+
}
|
|
428
|
+
return true;
|
|
429
|
+
});
|
|
430
|
+
if (dryRun || doomed.length === 0) return { ok: true, path: rmPath, pruned: doomed, dryRun: true };
|
|
431
|
+
const content = readFileSync(rmPath, 'utf8');
|
|
432
|
+
writeFileSync(rmPath + '.bak', content);
|
|
433
|
+
const drop = new Set(doomed.map((r) => r.lineNo));
|
|
434
|
+
const kept = content.split('\n').filter((_, i) => !drop.has(i));
|
|
435
|
+
writeFileSync(rmPath, kept.join('\n'));
|
|
436
|
+
const archivePath = join(dirname(rmPath), 'ratchet-archive.md');
|
|
437
|
+
const header = existsSync(archivePath) ? '' : '# Ratchet Archive (pruned rules — not loaded into sessions)\n\n';
|
|
438
|
+
const stamp = new Date().toISOString().slice(0, 10);
|
|
439
|
+
const body = doomed.map((r) => `- ${r.text} <!-- pruned ${stamp} -->`).join('\n') + '\n';
|
|
440
|
+
writeFileSync(archivePath, (existsSync(archivePath) ? readFileSync(archivePath, 'utf8').replace(/\n*$/, '\n') : header) + body);
|
|
441
|
+
return { ok: true, path: rmPath, backup: rmPath + '.bak', archive: archivePath, pruned: doomed };
|
|
355
442
|
}
|
|
356
443
|
|
|
357
444
|
/**
|
|
@@ -420,6 +507,10 @@ export function harnessStatusForStatusline(cfg, { root } = {}) {
|
|
|
420
507
|
else if (state.pevSkip) warning = 'PEV-skip';
|
|
421
508
|
}
|
|
422
509
|
}
|
|
510
|
+
// Config defect, above the optimization nudges: the harness block is there
|
|
511
|
+
// but carries no `@` import, so every promoted ratchet rule is dead weight.
|
|
512
|
+
// One `harness init` re-run fixes it and the warning goes away for good.
|
|
513
|
+
if (!warning && status.hasBlock && !(status.hasRatchetImport && status.hasModelRatchetImport)) warning = 'ratchet-unloaded';
|
|
423
514
|
// Below session-quality warnings: a promoted delegation rule whose
|
|
424
515
|
// category started failing (`rule-health R<N>`) — the user approved that
|
|
425
516
|
// rule, so its degradation outranks a mere new-candidate nudge.
|
package/src/model-rules.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* ratchet.md.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync
|
|
31
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
32
32
|
import { join, dirname } from 'node:path';
|
|
33
33
|
import { homedir } from 'node:os';
|
|
34
34
|
import { userDataDir } from './paths.js';
|
|
@@ -205,9 +205,11 @@ export function syncAllFiles({ previousPaths = [] } = {}) {
|
|
|
205
205
|
written.push(p);
|
|
206
206
|
} catch { /* unwritable target — skip, registry stays authoritative */ }
|
|
207
207
|
}
|
|
208
|
+
// A target that lost its last rule is emptied, NOT deleted: CLAUDE.md
|
|
209
|
+
// imports this path, and a dangling `@` import is worse than an empty file.
|
|
208
210
|
for (const p of previousPaths) {
|
|
209
211
|
if (!byPath.has(p) && existsSync(p)) {
|
|
210
|
-
try {
|
|
212
|
+
try { writeFileSync(p, renderModelRatchet([])); } catch { /* stale content; regenerated next sync */ }
|
|
211
213
|
}
|
|
212
214
|
}
|
|
213
215
|
return written;
|