@bglocation/tune-context 2.3.0 → 2.3.1
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 +37 -0
- package/README.md +15 -11
- package/hooks/context-reminder.mjs +86 -31
- package/package.json +2 -2
- package/skills/tune-context/SKILL.md +8 -4
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "tune-context",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Context-efficiency configurator for Claude Code: doctrine skills (token-efficiency, caveman, cdd, phase-workflow) plus a tune-context configurator that scaffolds a lean CLAUDE.md, subagents, settings and hooks.",
|
|
13
|
-
"version": "2.3.
|
|
13
|
+
"version": "2.3.1",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Szymon Walczak",
|
|
16
16
|
"url": "https://bglocation.dev"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "tune-context",
|
|
3
3
|
"displayName": "tune-context",
|
|
4
4
|
"description": "Context-efficiency configurator for Claude Code: doctrine skills (token-efficiency, caveman, cdd, phase-workflow) plus a tune-context configurator that scaffolds a lean CLAUDE.md, subagents, settings and hooks.",
|
|
5
|
-
"version": "2.3.
|
|
5
|
+
"version": "2.3.1",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Szymon Walczak",
|
|
8
8
|
"url": "https://bglocation.dev"
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,42 @@ All notable changes to `@bglocation/tune-context` are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.3.1] — 2026-07-28
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **The context reminder no longer goes quiet for 120k tokens at a stretch.** A
|
|
13
|
+
middle rung at **1.5× the threshold** (180k by default) joins the existing
|
|
14
|
+
120k and 240k, cutting the worst-case silence between notices from 120k to
|
|
15
|
+
60k. Reported from use: 120k is often too early to close a work thread, while
|
|
16
|
+
240k is far enough away that a session has to be dragged out to reach it, and
|
|
17
|
+
180k is where a phase boundary tends to land. The ladder is expressed as
|
|
18
|
+
multipliers (1×/1.5×/2×), so `TUNE_CONTEXT_REMIND_TOKENS` still scales all
|
|
19
|
+
three — a 400k threshold gives 400k/600k/800k.
|
|
20
|
+
- **Each rung is now louder than the last, instead of repeating itself.** The
|
|
21
|
+
reminder's mid-task sentence is what escalates: wait for a natural break at
|
|
22
|
+
120k, raise it at the next pause anyway at 180k, say it this turn at 240k. A
|
|
23
|
+
third identical notice would just be the same coin flipped again. This also
|
|
24
|
+
fixes a real defect the third rung would otherwise have introduced — the
|
|
25
|
+
second notice used to call itself "second and final", which on a three-rung
|
|
26
|
+
ladder is false, and Claude weighs exactly that ("nothing more is coming")
|
|
27
|
+
when deciding whether to stay quiet.
|
|
28
|
+
- **Docs describing the reminder as firing once.** `README.md` and the
|
|
29
|
+
`tune-context` skill both still said "a single short note" / "fires once per
|
|
30
|
+
crossing" — wrong since 2.1.0 added the second notice, not just since this
|
|
31
|
+
release.
|
|
32
|
+
|
|
33
|
+
### Notes
|
|
34
|
+
|
|
35
|
+
- The level-1 text is byte-identical to previous releases. A session live across
|
|
36
|
+
the upgrade whose state already recorded the old final notice earns one extra
|
|
37
|
+
notice, once, and then goes quiet — the two-rung and three-rung ladders write
|
|
38
|
+
the same number for different meanings, and a permanent ladder-version field
|
|
39
|
+
was not worth one notice.
|
|
40
|
+
- This release touches `hooks/`, so **re-run `tune-context init`** (or the
|
|
41
|
+
dev-loop sync) after upgrading — `npm i -g` does not refresh hooks already
|
|
42
|
+
copied into `~/.claude/hooks/`. `tune-context verify` reports the drift.
|
|
43
|
+
|
|
8
44
|
## [2.3.0] — 2026-07-28
|
|
9
45
|
|
|
10
46
|
### Added
|
|
@@ -428,6 +464,7 @@ action needed on upgrade.
|
|
|
428
464
|
(`explore-cheap`, `reviewer`), the PreCompact / SessionStart / PostToolUse hook
|
|
429
465
|
suite, and the adoption report (`eval/adoption-report.mjs`).
|
|
430
466
|
|
|
467
|
+
[2.3.1]: https://gitlab.com/bglocation/tune-context/-/tags/v2.3.1
|
|
431
468
|
[2.3.0]: https://gitlab.com/bglocation/tune-context/-/tags/v2.3.0
|
|
432
469
|
[2.2.0]: https://gitlab.com/bglocation/tune-context/-/tags/v2.2.0
|
|
433
470
|
[2.1.0]: https://gitlab.com/bglocation/tune-context/-/tags/v2.1.0
|
package/README.md
CHANGED
|
@@ -217,23 +217,27 @@ many tokens it is re-sending, and Claude Code has no timer or context-threshold
|
|
|
217
217
|
hook event. So `context-reminder.mjs` (registered under `UserPromptSubmit`)
|
|
218
218
|
supplies the missing measurement: it reads the tail of the session transcript,
|
|
219
219
|
sums the tokens actually re-sent, and once that passes **120,000** it injects a
|
|
220
|
-
|
|
220
|
+
short note suggesting `/compact`.
|
|
221
221
|
|
|
222
222
|
- **The note goes to Claude, not to your screen.** Hooks can only reach you
|
|
223
223
|
through the model — a `systemMessage` on this event was measured and does not
|
|
224
224
|
render — so what you actually see is Claude bringing it up in its reply.
|
|
225
|
-
- **The model picks the moment.** The
|
|
226
|
-
|
|
227
|
-
the
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
225
|
+
- **The model picks the moment — at first.** The hook can measure size, but only
|
|
226
|
+
Claude can see whether you're mid-refactor, so the first note tells it to stay
|
|
227
|
+
quiet mid-task and raise it at the next natural break. That deference runs out:
|
|
228
|
+
see the ladder below. A session that never reaches a "natural break" is exactly
|
|
229
|
+
the one paying the most for the wait.
|
|
230
|
+
- **Three notes per crossing at most — 120,000 / 180,000 / 240,000.** A note that
|
|
231
|
+
lands mid-task can be correctly absorbed and then forgotten. Measured on one
|
|
232
|
+
session: it fired at 166k, Claude stayed silent, and nothing spoke again until
|
|
233
|
+
346k. Each rung halves that blind spot, and each one is *louder* than the last:
|
|
234
|
+
at 120k Claude is told to wait for a natural break, at 180k to raise it at the
|
|
235
|
+
next pause anyway, at 240k to say it this turn whatever it is doing. There is
|
|
236
|
+
deliberately no fourth — the reminder costs tokens on the prompt it appears in.
|
|
233
237
|
- **Raise it for a large context window:** `export TUNE_CONTEXT_REMIND_TOKENS=400000`.
|
|
234
238
|
120k is ~60% of a standard 200k window; the transcript doesn't record the window
|
|
235
|
-
size, so this can't be auto-scaled.
|
|
236
|
-
|
|
239
|
+
size, so this can't be auto-scaled. The whole ladder scales with the override —
|
|
240
|
+
the rungs are always 1×, 1.5× and 2× the threshold (400k/600k/800k here).
|
|
237
241
|
- **Turn it off:** delete the `UserPromptSubmit` entry from `settings.json`.
|
|
238
242
|
|
|
239
243
|
Depth lives in the `phase-workflow` and `token-efficiency` skills (both installed
|
|
@@ -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.3.
|
|
4
|
-
"description": "Context-efficiency configurator for Claude Code
|
|
3
|
+
"version": "2.3.1",
|
|
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`.
|