@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.
@@ -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.0",
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.0",
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
- single short note suggesting `/compact`.
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 note tells Claude to stay quiet mid-task
226
- and raise it at the first natural break: the hook can measure size, but only
227
- the model can see whether you're mid-refactor.
228
- - **Twice per crossing at most.** Once at the threshold, once more at **240,000**
229
- if the context keeps growing — because a note that lands mid-task can be
230
- correctly absorbed and then forgotten. Measured on one session: it fired at
231
- 166k, Claude stayed silent, and nothing spoke again until 346k. The second
232
- notice halves that blind spot; there is deliberately no third.
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. Both levels scale with the override — the
236
- second notice is always at twice the threshold.
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 tells
17
- // it to stay quiet mid-task. The phase-boundary half is the `phase-workflow`
18
- // skill's job, model-side, where judgment actually lives.
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
- // TASK-080 fallback, chosen by the PO after measurement killed the preferred
141
- // design (see userNotice()): a SECOND and final notice at twice the threshold.
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 2x and not "every +N tokens": periodic nagging would break the rule this
151
- // hook lives under — it is the only hook that ADDS tokens to the context it
152
- // exists to shrink. One extra notice per cycle is a bounded, one-off cost that
153
- // halves the worst-case blind spot; an unbounded series is the feature eating
154
- // itself. Deliberately NOT a third level.
155
- const ESCALATION_MULTIPLIER = 2;
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, 1 or 2. */
197
+ /** How many notices this session has earned at `total`: 0..LADDER.length. */
158
198
  function levelFor(total, threshold) {
159
- if (total >= threshold * ESCALATION_MULTIPLIER) return 2;
160
- if (total >= threshold) return 1;
161
- return 0;
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 TWICE per crossing (threshold, then 2x threshold), never on
169
- * every prompt above the line. A reminder costs tokens on the very prompt it
170
- * appears in, so a context-efficiency tool that nagged every turn would be
171
- * spending the thing it claims to save.
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, escalated: remind && level === 2, next: { last: total, levels: Math.max(reached, level), ts: Date.now() } };
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 stays here, because only the model can tell
211
- * whether the current thread is finished — but see the second sentence: it must
212
- * relay eventually, because staying silent forever means the user never learns.
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, { escalated = false } = {}) {
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
- 'If you are mid-task: stay quiet now, but raise it at the first natural break rather than dropping it.',
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, escalated, next } = decide(state[session], total, thresholdTokens());
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, { escalated }),
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.0",
4
- "description": "Context-efficiency configurator for Claude Code \u2014 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.",
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 one short reminder to
170
- consider `/compact`. Fires **once per crossing**, re-arming only after a
171
- compaction-sized drop: the reminder costs tokens on the prompt it appears in,
172
- so nagging every turn would spend exactly what the tool saves. It reminds on
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`.