@bglocation/tune-context 2.3.0 → 2.4.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +115 -0
- package/README.md +126 -17
- package/bin/tune-context.mjs +241 -15
- package/cli/experiment.mjs +674 -0
- package/cli/managed-block.mjs +24 -0
- package/cli/verify.mjs +51 -4
- package/eval/adoption-report.mjs +8 -6
- package/eval/arm.mjs +242 -0
- package/eval/pricing.mjs +430 -0
- package/eval/prompt-metrics.mjs +718 -0
- package/hooks/context-reminder.mjs +86 -31
- package/package.json +2 -2
- package/skills/tune-context/SKILL.md +8 -4
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
// boundary": a hook is a plain script with no model access, so it cannot know
|
|
14
14
|
// that a work thread just ended. Size is measurable; a phase boundary is a
|
|
15
15
|
// judgment. This hook therefore supplies the measurement the model can't see,
|
|
16
|
-
// and leaves the timing judgment to the model — the reminder explicitly
|
|
17
|
-
// it to stay quiet mid-task.
|
|
18
|
-
//
|
|
16
|
+
// and leaves the timing judgment to the model — the first reminder explicitly
|
|
17
|
+
// tells it to stay quiet mid-task. That deference is finite, though: it is
|
|
18
|
+
// exactly what the LADDER below withdraws rung by rung, because a session that
|
|
19
|
+
// never finds a "natural break" is the one paying the most. The phase-boundary
|
|
20
|
+
// half is the `phase-workflow` skill's job, model-side, where judgment lives.
|
|
19
21
|
//
|
|
20
22
|
// The number is real, not a proxy: Claude Code records per-request `usage` in
|
|
21
23
|
// the transcript, and input_tokens + cache_read_input_tokens +
|
|
@@ -137,43 +139,93 @@ function pruneState(state, now) {
|
|
|
137
139
|
return state;
|
|
138
140
|
}
|
|
139
141
|
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
+
// The notice ladder, as multipliers of the threshold: 120k / 180k / 240k by
|
|
143
|
+
// default (TASK-080 added the second rung, TASK-084 the middle one).
|
|
142
144
|
//
|
|
143
145
|
// The problem it solves is measured, not hypothetical. Fire-once is consumed
|
|
144
146
|
// when the model reads the reminder, and the reminder itself tells the model to
|
|
145
147
|
// stay quiet mid-task — correctly, since only it can see whether a refactor is
|
|
146
148
|
// half-finished. In session c9329adb that combination cost the user everything
|
|
147
149
|
// between ~166k and ~346k: one firing, silently absorbed, and no re-arm until a
|
|
148
|
-
// compaction that nobody had suggested.
|
|
150
|
+
// compaction that nobody had suggested. Each rung halves the worst-case blind
|
|
151
|
+
// spot: one rung left 120k of silence, two left 120k above the threshold, three
|
|
152
|
+
// leave 60k.
|
|
149
153
|
//
|
|
150
|
-
// Why
|
|
151
|
-
// hook lives under — it is the only hook that ADDS tokens to the
|
|
152
|
-
// exists to shrink.
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
|
|
154
|
+
// Why a fixed ladder and not "every +N tokens": periodic nagging would break the
|
|
155
|
+
// rule this hook lives under — it is the only hook that ADDS tokens to the
|
|
156
|
+
// context it exists to shrink. Three notices per cycle is O(1) and measurable:
|
|
157
|
+
// the text is ~600 chars ≈ 150 tokens, so the third rung costs ~0.1% of the
|
|
158
|
+
// context it appears in. An unbounded series is the feature eating itself; a
|
|
159
|
+
// bounded rung is not. (PO decision 2026-07-28, TASK-084 — the 1.5x point comes
|
|
160
|
+
// from use: 120k is often too early to close a thread, 240k so far that a
|
|
161
|
+
// session has to be dragged out to reach it.)
|
|
162
|
+
//
|
|
163
|
+
// Multipliers rather than absolute numbers so TUNE_CONTEXT_REMIND_TOKENS scales
|
|
164
|
+
// the whole ladder, as the README promises. A hardcoded 180_000 would fall
|
|
165
|
+
// BELOW the first rung the moment someone raises the threshold for a 1M window.
|
|
166
|
+
//
|
|
167
|
+
// Each rung carries its own wording, in ONE structure, deliberately. The
|
|
168
|
+
// escalation IS that sentence tightening — repeating the level-1 text three
|
|
169
|
+
// times would just be the same coin flipped again, which is what would make a
|
|
170
|
+
// third rung not worth its tokens. Splitting trigger from wording into two
|
|
171
|
+
// parallel structures would let them drift: a fourth multiplier with no fourth
|
|
172
|
+
// sentence falls back to the level-1 text, so the LOUDEST rung would print the
|
|
173
|
+
// SOFTEST wording and nothing would fail. One array makes that unrepresentable.
|
|
174
|
+
//
|
|
175
|
+
// The graded sentence REPLACES the mid-task line rather than riding on top of
|
|
176
|
+
// it, and that is a budget fact rather than taste: the rest of the reminder is
|
|
177
|
+
// 490 chars against a <700 ceiling, and the old append-a-sentence shape had
|
|
178
|
+
// already spent that down to 21 chars of headroom (measured 592 / 679 before
|
|
179
|
+
// TASK-084). Swapping keeps every rung inside budget — 592 / 615 / 606.
|
|
180
|
+
const LADDER = [
|
|
181
|
+
{
|
|
182
|
+
at: 1,
|
|
183
|
+
timing: 'If you are mid-task: stay quiet now, but raise it at the first natural break rather than dropping it.',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
at: 1.5,
|
|
187
|
+
timing:
|
|
188
|
+
'If you are mid-task: raise it at the next pause anyway — this is no longer early, and silence now costs the user every turn.',
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
at: 2,
|
|
192
|
+
timing:
|
|
193
|
+
'Final automatic notice — say it this turn whatever you are in the middle of; nothing more fires until a compaction.',
|
|
194
|
+
},
|
|
195
|
+
];
|
|
156
196
|
|
|
157
|
-
/** How many notices this session has earned at `total`: 0
|
|
197
|
+
/** How many notices this session has earned at `total`: 0..LADDER.length. */
|
|
158
198
|
function levelFor(total, threshold) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
199
|
+
let level = 0;
|
|
200
|
+
for (const rung of LADDER) {
|
|
201
|
+
if (total >= threshold * rung.at) level++;
|
|
202
|
+
}
|
|
203
|
+
return level;
|
|
162
204
|
}
|
|
163
205
|
|
|
164
206
|
/**
|
|
165
207
|
* Pure decision: given the previous reading for this session and the current
|
|
166
208
|
* one, should a reminder fire, and what is the session's next state?
|
|
167
209
|
*
|
|
168
|
-
* Fires at most
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
210
|
+
* Fires at most once per rung of LEVEL_MULTIPLIERS, never on every prompt above
|
|
211
|
+
* the line. A reminder costs tokens on the very prompt it appears in, so a
|
|
212
|
+
* context-efficiency tool that nagged every turn would be spending the thing it
|
|
213
|
+
* claims to save.
|
|
214
|
+
*
|
|
215
|
+
* `level` rides out with the decision because the TEXT is graded by it — see
|
|
216
|
+
* reminderText(). Crossing several rungs in one turn (a 100k tool result) fires
|
|
217
|
+
* once, at the rung actually reached: no level is owed retroactively.
|
|
172
218
|
*/
|
|
173
219
|
export function decide(prev, total, threshold) {
|
|
174
220
|
// `fired: true` is how state written before TASK-080 recorded a single
|
|
175
221
|
// firing; read it as level 1 so an in-flight session whose hook is upgraded
|
|
176
222
|
// mid-run is not re-notified from scratch.
|
|
223
|
+
//
|
|
224
|
+
// A pre-TASK-084 `levels: 2` means "crossed 2x", which on the new ladder is
|
|
225
|
+
// level 3 — so a session live across that upgrade earns exactly ONE extra
|
|
226
|
+
// notice, then goes quiet for good. Accepted rather than fixed: telling old-2
|
|
227
|
+
// from new-2 needs a ladder version in the state, a permanent cost for one
|
|
228
|
+
// notice in sessions that happen to span an upgrade.
|
|
177
229
|
let reached = typeof prev?.levels === 'number' ? prev.levels : prev?.fired === true ? 1 : 0;
|
|
178
230
|
// A large drop means the context was compacted/cleared: arm again so the
|
|
179
231
|
// NEXT time it grows past the threshold the user hears about it.
|
|
@@ -181,7 +233,7 @@ export function decide(prev, total, threshold) {
|
|
|
181
233
|
|
|
182
234
|
const level = levelFor(total, threshold);
|
|
183
235
|
const remind = level > reached;
|
|
184
|
-
return { remind,
|
|
236
|
+
return { remind, level, next: { last: total, levels: Math.max(reached, level), ts: Date.now() } };
|
|
185
237
|
}
|
|
186
238
|
|
|
187
239
|
/**
|
|
@@ -207,11 +259,15 @@ export function userNotice(total) {
|
|
|
207
259
|
|
|
208
260
|
/**
|
|
209
261
|
* The channel that actually works (`additionalContext`): the model reads it and
|
|
210
|
-
* relays it. The timing judgement
|
|
211
|
-
* whether the current thread is finished — but
|
|
212
|
-
*
|
|
262
|
+
* relays it. The timing judgement is the model's on the FIRST rung, because only
|
|
263
|
+
* it can tell whether the current thread is finished — but that exemption is
|
|
264
|
+
* what LADDER spends: by the last rung the text asks it to speak regardless,
|
|
265
|
+
* since staying silent forever means the user never learns at all.
|
|
266
|
+
*
|
|
267
|
+
* `level` defaults to 1, so the level-1 text is byte-identical to every release
|
|
268
|
+
* since TASK-079 and a caller that does not care about rungs keeps working.
|
|
213
269
|
*/
|
|
214
|
-
export function reminderText(total, {
|
|
270
|
+
export function reminderText(total, { level = 1 } = {}) {
|
|
215
271
|
const k = Math.round(total / 1000);
|
|
216
272
|
return [
|
|
217
273
|
`[tune-context] This session is now carrying ~${k}k tokens of context, and every further turn re-sends all of it.`,
|
|
@@ -220,11 +276,10 @@ export function reminderText(total, { escalated = false } = {}) {
|
|
|
220
276
|
// heard about the cost at ~346k.
|
|
221
277
|
'The user CANNOT see this reminder — it reaches you alone, so if you never mention it they will never know.',
|
|
222
278
|
'If the current work thread is finished: suggest /compact (or /clear when switching to unrelated work).',
|
|
223
|
-
|
|
279
|
+
// Position matters: the graded sentence stays 4th of 5, so level 1 is
|
|
280
|
+
// unchanged from what shipped.
|
|
281
|
+
(LADDER[level - 1] ?? LADDER[0]).timing,
|
|
224
282
|
'The PreCompact hook preserves branch, dirty files and recent commits across a compaction — judgment calls and open threads are NOT preserved, so write those down first.',
|
|
225
|
-
// Kept short on purpose: it rides ON TOP of the full reminder, inside the
|
|
226
|
-
// same <700 char budget.
|
|
227
|
-
...(escalated ? ['This is the second and final automatic notice — nothing more fires until a compaction.'] : []),
|
|
228
283
|
].join(' ');
|
|
229
284
|
}
|
|
230
285
|
|
|
@@ -245,7 +300,7 @@ function main() {
|
|
|
245
300
|
const session = input.session_id || 'unknown';
|
|
246
301
|
const path = stateFile();
|
|
247
302
|
const state = readState(path);
|
|
248
|
-
const { remind,
|
|
303
|
+
const { remind, level, next } = decide(state[session], total, thresholdTokens());
|
|
249
304
|
|
|
250
305
|
state[session] = next;
|
|
251
306
|
try {
|
|
@@ -264,7 +319,7 @@ function main() {
|
|
|
264
319
|
systemMessage: userNotice(total),
|
|
265
320
|
hookSpecificOutput: {
|
|
266
321
|
hookEventName: 'UserPromptSubmit',
|
|
267
|
-
additionalContext: reminderText(total, {
|
|
322
|
+
additionalContext: reminderText(total, { level }),
|
|
268
323
|
},
|
|
269
324
|
}),
|
|
270
325
|
);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bglocation/tune-context",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Context-efficiency configurator for Claude Code
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "Context-efficiency configurator for Claude Code — token-saving doctrine packaged as skills, plus a tune-context configurator that scaffolds a lean CLAUDE.md, doctrine skills, subagents, settings, hooks and MCP wiring.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Szymon Walczak",
|
|
@@ -166,10 +166,14 @@ covering the detected build/test/lint commands.
|
|
|
166
166
|
It reads the **tail** of the session transcript (`transcript_path`, present on
|
|
167
167
|
every hook payload), sums `input_tokens + cache_read + cache_creation` — the
|
|
168
168
|
context actually re-sent — and once that crosses a PO-frozen threshold
|
|
169
|
-
(120k, override `TUNE_CONTEXT_REMIND_TOKENS`) injects
|
|
170
|
-
consider `/compact`. Fires **
|
|
171
|
-
|
|
172
|
-
|
|
169
|
+
(120k, override `TUNE_CONTEXT_REMIND_TOKENS`) injects a short reminder to
|
|
170
|
+
consider `/compact`. Fires at most **three times per crossing**, on a ladder
|
|
171
|
+
of 1×/1.5×/2× the threshold (120k/180k/240k), re-arming only after a
|
|
172
|
+
compaction-sized drop. Each rung tightens the wording — wait for a natural
|
|
173
|
+
break, then raise it at the next pause, then say it this turn — because a
|
|
174
|
+
note absorbed mid-task is otherwise never repeated. Bounded at three on
|
|
175
|
+
purpose: the reminder costs tokens on the prompt it appears in, so nagging
|
|
176
|
+
every turn would spend exactly what the tool saves. It reminds on
|
|
173
177
|
SIZE, not on "phase boundary" — size is measurable from the transcript, a
|
|
174
178
|
phase boundary is a judgment a hook (no model access) cannot make, so the
|
|
175
179
|
reminder hands that timing call to the model and `phase-workflow`.
|