@bglocation/tune-context 2.3.1 → 2.5.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.
@@ -24,3 +24,27 @@ export function upsertManagedBlock(existing, innerBlock) {
24
24
  const next = `${existing}${sep}\n${block}\n`;
25
25
  return { content: next, changed: true };
26
26
  }
27
+
28
+ /**
29
+ * The inverse of upsertManagedBlock: drop the block AND its markers, keep every
30
+ * byte outside them. Used only by the experiment switch (TASK-087) to reach the
31
+ * `tc off` arm — init never removes the block, it replaces it.
32
+ *
33
+ * Deliberately conservative about what it deletes: only the marker span, plus
34
+ * the blank lines the removal would otherwise leave stacked at the seam. What
35
+ * the file's owner wrote below the closing marker is what an off arm has to
36
+ * keep — losing it would be the TASK-082 defect with a different trigger.
37
+ *
38
+ * @param {string|null} existing - current file content, or null if there is no file
39
+ * @returns {{content: string|null, changed: boolean}} content unchanged when there is no block
40
+ */
41
+ export function removeManagedBlock(existing) {
42
+ if (existing == null) return { content: null, changed: false };
43
+ const startIdx = existing.indexOf(START_MARKER);
44
+ const endIdx = existing.indexOf(END_MARKER);
45
+ if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) return { content: existing, changed: false };
46
+ const head = existing.slice(0, startIdx);
47
+ const tail = existing.slice(endIdx + END_MARKER.length).replace(/^\n+/, '');
48
+ const next = head.trim() === '' ? tail : `${head.trimEnd()}\n\n${tail}`;
49
+ return { content: next, changed: next !== existing };
50
+ }
package/cli/verify.mjs CHANGED
@@ -158,10 +158,19 @@ function freshnessChecks(claudeDir, pkgRoot) {
158
158
 
159
159
  /**
160
160
  * @param {string} [pkgRoot] - installed package root, for the staleness check
161
+ * @param {{tc: 'on'|'off', rag: 'on'|'off'}} [arm] - the experiment arm currently set
162
+ * through `tune-context experiment` (TASK-087), read from the ledger state — NOT
163
+ * detected here. Under `tc: 'off'` the doctrine is *supposed* to be absent, so the
164
+ * expectations below are inverted rather than dropped: a missing registration is a
165
+ * tick, and one that is still there is a RED, because an off arm that is not off
166
+ * invalidates the measurement just as surely as a broken install invalidates a
167
+ * session. Reporting "broken install" for a correctly disabled doctrine would be
168
+ * the EPIC-006 false-red regression, only pointing the other way.
161
169
  * @returns {{ok: boolean, checks: {name: string, ok: boolean, detail?: string, unknown?: boolean}[]}}
162
170
  */
163
- export function verify({ claudeDir, userSettingsPath, expectedHookCommands, projectSettingsPath, expectedPermissions, pkgRoot }) {
171
+ export function verify({ claudeDir, userSettingsPath, expectedHookCommands, projectSettingsPath, expectedPermissions, pkgRoot, arm }) {
164
172
  const checks = [];
173
+ const tcOff = arm?.tc === 'off';
165
174
 
166
175
  // Cross-check, not two independent presence checks. The old code asked
167
176
  // "does some command contain the script name?" and, separately, "does the
@@ -175,6 +184,19 @@ export function verify({ claudeDir, userSettingsPath, expectedHookCommands, proj
175
184
  const allCommands = entries.flatMap((g) => (g.hooks || []).map((h) => h.command)).filter(Boolean);
176
185
  const installedPath = resolve(join(claudeDir, 'hooks', scriptName));
177
186
 
187
+ if (tcOff) {
188
+ // Same looseness as the switch's own removal rule: any command naming the
189
+ // script counts, whatever dir it points at. A stricter test here would let
190
+ // a stray registration keep firing the hook while verify called the arm clean.
191
+ const still = allCommands.filter((c) => c.includes(scriptName));
192
+ checks.push({
193
+ name: `hooks.${event}: ${scriptName} unregistered (experiment arm tc=off)`,
194
+ ok: still.length === 0,
195
+ detail: still.length ? `still registered — the off arm is NOT in effect: ${still.join(', ')}` : undefined,
196
+ });
197
+ continue;
198
+ }
199
+
178
200
  // Classify by DESTINATION, not by spelling: a command that resolves to
179
201
  // the very file we installed is the CLI registration, whatever its path
180
202
  // happens to look like. Without this, PLUGIN_PATH_HINT's loose
@@ -254,7 +276,24 @@ export function verify({ claudeDir, userSettingsPath, expectedHookCommands, proj
254
276
  const scriptPath = join(claudeDir, 'hooks', scriptName);
255
277
  const exists = existsSync(scriptPath);
256
278
  const executable = exists && Boolean(statSync(scriptPath).mode & 0o111);
257
- checks.push({ name: `${scriptName} installed + executable`, ok: exists && executable, detail: !exists ? 'missing' : !executable ? 'not executable' : undefined });
279
+ // Under an off arm a missing script is not a broken install: the switch
280
+ // parks REGISTRATIONS, never the scripts, so their absence just means the
281
+ // doctrine was never installed on this machine — which is what the arm asks
282
+ // for anyway. Reported as `?`, so it neither passes nor turns the arm red
283
+ // (epic criterion 4). Present-but-not-executable stays red in both arms: the
284
+ // arm did not cause it, and it breaks the moment the doctrine goes back on.
285
+ checks.push({
286
+ name: `${scriptName} installed + executable`,
287
+ ok: exists && executable,
288
+ unknown: tcOff && !exists,
289
+ detail: !exists
290
+ ? tcOff
291
+ ? 'not installed — this arm parks hook registrations, not the scripts themselves'
292
+ : 'missing'
293
+ : !executable
294
+ ? 'not executable'
295
+ : undefined,
296
+ });
258
297
  }
259
298
 
260
299
  if (projectSettingsPath && expectedPermissions?.length) {
@@ -268,8 +307,16 @@ export function verify({ claudeDir, userSettingsPath, expectedHookCommands, proj
268
307
 
269
308
  const skillsDir = join(claudeDir, 'skills');
270
309
  for (const skill of ['token-efficiency', 'caveman', 'cdd', 'phase-workflow']) {
271
- const ok = existsSync(join(skillsDir, skill, 'SKILL.md'));
272
- checks.push({ name: `skill ${skill} installed`, ok, detail: ok ? undefined : `missing ${join(skillsDir, skill, 'SKILL.md')}` });
310
+ const present = existsSync(join(skillsDir, skill, 'SKILL.md'));
311
+ if (tcOff) {
312
+ checks.push({
313
+ name: `skill ${skill} parked (experiment arm tc=off)`,
314
+ ok: !present,
315
+ detail: present ? `still installed at ${join(skillsDir, skill)} — the off arm is NOT in effect` : undefined,
316
+ });
317
+ continue;
318
+ }
319
+ checks.push({ name: `skill ${skill} installed`, ok: present, detail: present ? undefined : `missing ${join(skillsDir, skill, 'SKILL.md')}` });
273
320
  }
274
321
 
275
322
  checks.push(...freshnessChecks(claudeDir, pkgRoot));
@@ -6,9 +6,10 @@
6
6
  // levers show adoption, which don't. See tasks/TASK-062.
7
7
  //
8
8
  // Honest scope (printed in the report too):
9
- // • Cost is NOT here. Claude Code exposes token cost only via OTEL
10
- // (`claude_code.cost.usage`, needs a collector) — never a hook. This report
11
- // measures *behavior* (a proxy), not dollars. OTEL is the opt-in complement.
9
+ // • Cost is NOT here. This report reads the hook log, which carries no
10
+ // tokens — it measures *behavior* (a proxy), not dollars. For dollars see
11
+ // eval/prompt-metrics.mjs + eval/pricing.mjs (TASK-085/086), which price the
12
+ // transcript. OTEL stays the independent route to Anthropic's own figure.
12
13
  // • caveman can't be measured: it's an output style, not a tool call.
13
14
  // • RAG availability (the `~` line in the verdict) rests on a project's
14
15
  // `.mcp.json` declaring a semantic-search server that predates the sessions
@@ -75,7 +76,7 @@ Options:
75
76
  -h, --help Show this help
76
77
 
77
78
  The log is written by the adoption-log.mjs PostToolUse hook (installed by
78
- tune-context). Cost in dollars is NOT here — that needs OTEL (see report footer).
79
+ tune-context). Cost in dollars is NOT here — see eval/prompt-metrics.mjs.
79
80
  `.trim();
80
81
 
81
82
  export function parseLog(text) {
@@ -494,8 +495,9 @@ export function formatReport(a) {
494
495
  }
495
496
  }
496
497
  L.push('');
497
- L.push('Cost (dollars) is not measurable from hooks. For hard cost, enable OTEL:');
498
- L.push(' CLAUDE_CODE_ENABLE_TELEMETRY=1 + an OTLP collector →');
498
+ L.push('Cost (dollars) is not in the hook log. Two independent routes to it:');
499
+ L.push(' eval/prompt-metrics.mjs — prices the transcript, works with hooks OFF too.');
500
+ L.push(' OTEL: CLAUDE_CODE_ENABLE_TELEMETRY=1 + an OTLP collector →');
499
501
  L.push(' metric `claude_code.cost.usage` (direct) / `claude_code.token.usage`.');
500
502
  L.push(' caveman (output style) leaves no tool call — not measured here.');
501
503
 
@@ -537,6 +539,6 @@ function main() {
537
539
  process.stdout.write(`${formatReport(aggregate(records))}\n`);
538
540
  }
539
541
 
540
- if (process.argv[1] && process.argv[1].endsWith('adoption-report.mjs')) {
542
+ if (process.argv[1]?.endsWith('adoption-report.mjs')) {
541
543
  main();
542
544
  }