Skip to content

Commit af2b0dc

Browse files
Shahinyanmclaude
andcommitted
fix(profile): recommender stops pushing trims + doctor flags explicit trimmed profile (26b)
Root of the recurring 'read_range/read_for_edit doesn't exist' pain: the profile-recommender suggested TOKEN_PILOT_PROFILE=nav (or edit) for read-heavy usage and printed an 'apply to .mcp.json' snippet. Users applied it; the next edit session then hit trimmed edit-tools the rules/hooks still demand → dead 'No such tool available' → Read fallback. Past usage doesn't predict future edits, and this contradicts the v0.45.0 full default (trimming is penny-wise-pound-foolish). - recommendProfile now always recommends ; the nav/edit reasons explain why trimming is risky (minimal stays a self-serve, warned opt-in). - formatRecommendation no longer prints the 'apply nav/edit to .mcp.json' trap. - token-pilot doctor v0.44.0 Install mode: npm / npx Node.js: v20.9.0 ✓ config: /Users/mhershahinyan/www/token-pilot/.token-pilot.json ✓ git repo: yes ✓ ── token-pilot ── installed: 0.44.0 latest: 0.44.0 ✓ (up to date) ── ast-index ── installed: 3.47.0 (bundled: /Users/mhershahinyan/.nvm/versions/node/v20.9.0/lib/node_modules/@ast-index/cli/bin/ast-index) latest: 3.47.1 (update available!) run: npx token-pilot install-ast-index auto-update: disabled (set updates.autoUpdate=true in .token-pilot.json) ── context-mode ── detected: no setup: npx token-pilot init ── Claude Code env knobs (savings tips) ── ⚠ CLAUDE_CODE_SUBAGENT_MODEL not set to haiku — add `"CLAUDE_CODE_SUBAGENT_MODEL": "haiku"` under `env` in ~/.claude/settings.json to route subagents to Haiku (~80% cheaper). ⚠ MAX_THINKING_TOKENS unset (defaults to 32000) — add `"MAX_THINKING_TOKENS": "10000"` under `env` in ~/.claude/settings.json to cap hidden reasoning tokens (~70% saving). ⚠ CLAUDE_AUTOCOMPACT_PCT_OVERRIDE unset — add `"CLAUDE_AUTOCOMPACT_PCT_OVERRIDE": "50"` under `env` in ~/.claude/settings.json to compact context earlier (healthier sessions). ── .claudeignore ── not present — run `npx token-pilot init` to add sensible defaults ── CLAUDE.md hygiene ── ⚠ /Users/mhershahinyan/www/token-pilot/CLAUDE.md has 72 non-empty lines (threshold: 60). This file loads into every Claude Code message — splitting into docs/*.md and loading on-demand saves tokens per turn. ── ecosystem coverage ── ✓ caveman Output compression (terse-speak skill) — cuts ~75% of Claude's response prose ✓ context-mode Sandbox executor — runs shell/python/js, only stdout enters context ○ cavemem missing — Cross-session memory — remember context across restarts install: see https://github.com/JuliusBrussee/cavemem token-pilot owns INPUT tokens. Each tool above owns a different half of a session — they do not overlap. See docs/ecosystem.md. ── statusline badge ── ⚠ statusline points at tp-statusline.sh directly but caveman is installed — switch to statusline-chain.sh so both badges show. ── runtime health ── errors: 3 recent (top codes): 3× unknown drill-in: token-pilot errors --tail=20 stale hooks: none ✓ tp-* agents: 25/25 ✓ cwd: /Users/mhershahinyan/www/token-pilot ✓ now LOUDLY flags an explicit TOKEN_PILOT_PROFILE=trim, names the hidden tools, and tells the user to remove it / set full. Live-verified: doctor warns on nav. 1391 tests (recommender tests rewritten). tsc clean. No version bump — 0.45.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 67adf44 commit af2b0dc

3 files changed

Lines changed: 62 additions & 34 deletions

File tree

src/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,6 +1520,31 @@ export async function handleDoctor() {
15201520
/* ignore */
15211521
}
15221522

1523+
// ── explicit trimmed-profile warning (v0.45.0, token-pilot-26b) ──
1524+
// An explicit TOKEN_PILOT_PROFILE=nav|edit|minimal hides tools the rules and
1525+
// the pre-edit hook still reference (read_for_edit / read_range / batch),
1526+
// trapping edit sessions on "No such tool available". This recurred for two
1527+
// users. Surface it loudly so they can remove it.
1528+
try {
1529+
const raw = process.env.TOKEN_PILOT_PROFILE;
1530+
if (raw && raw.trim()) {
1531+
const { parseProfileEnv } = await import("./server/tool-profiles.js");
1532+
if (parseProfileEnv(raw) !== "full") {
1533+
console.log(
1534+
`⚠ TOKEN_PILOT_PROFILE=${raw.trim()} is set — a TRIMMED tool surface.\n` +
1535+
` It hides read_for_edit / read_range / batch reads (and code_audit /\n` +
1536+
` find_unused / test_summary) that the rules + pre-edit hook still name →\n` +
1537+
` calls to them fail with "No such tool available" and the agent falls\n` +
1538+
` back to raw Read/Bash.\n` +
1539+
` Fix: remove "TOKEN_PILOT_PROFILE" from your .mcp.json env block (or set\n` +
1540+
` it to "full"), then restart. Full is the default since v0.45.0.\n`,
1541+
);
1542+
}
1543+
}
1544+
} catch {
1545+
/* doctor must never crash over an optional check */
1546+
}
1547+
15231548
// ── profile recommendation ──
15241549
// v0.26.4 — data-driven. Reads cumulative tool-calls.jsonl and suggests
15251550
// the narrowest TOKEN_PILOT_PROFILE that wouldn't hide any tool the

src/server/profile-recommender.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,21 @@ export function recommendProfile(
6565
};
6666
}
6767

68+
// v0.45.0 (token-pilot-26b) — we NO LONGER recommend trimming to nav/edit.
69+
// Past tool usage doesn't predict future edits, and a trimmed profile hides
70+
// read_for_edit / read_range / batch reads that the rules and the pre-edit
71+
// hook still reference — so the agent calls them, hits "No such tool
72+
// available", and falls back to raw Read/Bash. That recurring trap cost two
73+
// users whole sessions. Full is the recommendation; minimal stays a
74+
// self-serve, clearly-warned opt-in for context-critical work only.
6875
const allInNav = [...used].every((t) => NAV_TOOLS.has(t));
6976
if (allInNav) {
7077
return {
71-
recommended: "nav",
72-
reason: `Every tool you've used (${uniqueToolsSeen} distinct) is part of the nav subset. You're a read-only explorer.`,
78+
recommended: "full",
79+
reason: `You've used only nav-subset tools so far (${uniqueToolsSeen} distinct), but read_for_edit / read_range / batch reads — named by the rules and the pre-edit hook — live in edit/full. Stay on full so an edit doesn't hit "No such tool available". Set TOKEN_PILOT_PROFILE=minimal yourself ONLY if context is critically tight (it hides edit tools).`,
7380
uniqueToolsSeen,
7481
totalCalls,
75-
wouldHide: [
76-
...[...EDIT_EXTRAS].filter((t) => !used.has(t)),
77-
/* full-only — we don't enumerate here, keep the list short */
78-
],
82+
wouldHide: [],
7983
lowConfidence: false,
8084
};
8185
}
@@ -85,8 +89,8 @@ export function recommendProfile(
8589
);
8690
if (allInEditOrBelow) {
8791
return {
88-
recommended: "edit",
89-
reason: `You use edit-preparation tools (read_for_edit, batch reads) but never reach for full-only tools like code_audit/test_summary/find_unused.`,
92+
recommended: "full",
93+
reason: `You use edit-prep tools but haven't reached for full-only ones (code_audit/test_summary/find_unused) yet. Stay on full — they cost ~1k tokens to advertise but trimming hides them the moment you need one, and dead calls cost more.`,
9094
uniqueToolsSeen,
9195
totalCalls,
9296
wouldHide: [],
@@ -120,20 +124,16 @@ export function formatRecommendation(rec: ProfileRecommendation): string {
120124
);
121125
lines.push(` recommend: TOKEN_PILOT_PROFILE=${rec.recommended}`);
122126
lines.push(` why: ${rec.reason}`);
123-
if (rec.recommended !== "full") {
124-
lines.push(
125-
` savings: ~${rec.recommended === "nav" ? "2200 tokens (−54%)" : "1000 tokens (−25%)"} on every tools/list response`,
126-
);
127-
lines.push(
128-
` apply: add "env": { "TOKEN_PILOT_PROFILE": "${rec.recommended}" } to your token-pilot entry in .mcp.json`,
129-
);
130-
} else if (rec.lowConfidence) {
127+
// v0.45.0 (26b) — we no longer print an "apply nav/edit to .mcp.json"
128+
// snippet. recommendProfile always returns `full`; the old snippet trapped
129+
// users into trimming, which hid edit tools the rules reference.
130+
if (rec.lowConfidence) {
131131
lines.push(
132-
` action: keep default (full). Re-run \`token-pilot doctor\` after a few real sessions for a data-backed suggestion.`,
132+
` action: keep default (full). Re-run \`token-pilot doctor\` after a few real sessions for a data-backed view.`,
133133
);
134134
} else {
135135
lines.push(
136-
` action: keep default (full). You're using what you have.`,
136+
` action: keep default (full). Trim to minimal yourself only for context-critical, read-only work.`,
137137
);
138138
}
139139
return lines.join("\n");

tests/server/profile-recommender.test.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,23 @@ describe("recommendProfile", () => {
4141
expect(r.reason).toMatch(/too small|20|Only/i);
4242
});
4343

44-
it("recommends `nav` when every used tool is a nav tool", () => {
45-
// 30 calls, only nav tools used
44+
it("recommends `full` even for nav-only usage — no trim trap (v0.45.0, 26b)", () => {
45+
// 30 calls, only nav tools used — old behaviour recommended `nav`, which
46+
// hid edit tools the rules reference and trapped the next edit session.
4647
const events = [
4748
...many("smart_read", 15),
4849
...many("outline", 8),
4950
...many("find_usages", 7),
5051
];
5152
const r = recommendProfile(events);
52-
expect(r.recommended).toBe("nav");
53+
expect(r.recommended).toBe("full");
5354
expect(r.lowConfidence).toBe(false);
5455
expect(r.uniqueToolsSeen).toBe(3);
5556
expect(r.totalCalls).toBe(30);
56-
expect(r.reason).toMatch(/read-only|nav/i);
57+
expect(r.reason).toMatch(/No such tool|stay on full|edit/i);
5758
});
5859

59-
it("recommends `edit` when user calls edit-prep tools but never full-only", () => {
60+
it("recommends `full` for edit-prep usage (does not push the edit trim)", () => {
6061
// 25 calls: nav + read_for_edit + read_symbols, no code_audit etc
6162
const events = [
6263
...many("smart_read", 10),
@@ -65,9 +66,9 @@ describe("recommendProfile", () => {
6566
...many("smart_diff", 2),
6667
];
6768
const r = recommendProfile(events);
68-
expect(r.recommended).toBe("edit");
69+
expect(r.recommended).toBe("full");
6970
expect(r.lowConfidence).toBe(false);
70-
expect(r.reason).toMatch(/edit|read_for_edit|batch/i);
71+
expect(r.reason).toMatch(/full|audit|edit-prep/i);
7172
});
7273

7374
it("recommends `full` when user touches any full-only tool", () => {
@@ -94,10 +95,10 @@ describe("recommendProfile", () => {
9495
});
9596

9697
it("exactly at MIN_SAMPLE_CALLS boundary behaves correctly", () => {
97-
// Exactly 20 calls, all nav → should recommend nav (not lowConfidence)
98+
// Exactly 20 calls, all nav → enough data, but recommends full (no trim trap)
9899
const events = many("smart_read", 20);
99100
const r = recommendProfile(events);
100-
expect(r.recommended).toBe("nav");
101+
expect(r.recommended).toBe("full");
101102
expect(r.lowConfidence).toBe(false);
102103
});
103104

@@ -111,22 +112,24 @@ describe("recommendProfile", () => {
111112
});
112113

113114
describe("formatRecommendation", () => {
114-
it("nav recommendation includes env snippet to paste", () => {
115+
it("never prints an apply-trim snippet — even for nav-only usage (26b)", () => {
115116
const rec = recommendProfile(many("smart_read", 30));
116117
const out = formatRecommendation(rec);
117-
expect(out).toMatch(/TOKEN_PILOT_PROFILE=nav/);
118-
expect(out).toMatch(/\.mcp\.json/);
119-
expect(out).toMatch(/savings:/);
118+
expect(out).toMatch(/TOKEN_PILOT_PROFILE=full/);
119+
// The old trap: "apply: add env TOKEN_PILOT_PROFILE=nav to .mcp.json".
120+
expect(out).not.toMatch(/TOKEN_PILOT_PROFILE=nav/);
121+
expect(out).not.toMatch(/apply:/);
122+
expect(out).not.toMatch(/savings:/);
120123
});
121124

122-
it("edit recommendation advertises the ~1000 token savings", () => {
125+
it("edit-prep usage recommends full, not a trim", () => {
123126
const rec = recommendProfile([
124127
...many("smart_read", 15),
125128
...many("read_for_edit", 10),
126129
]);
127130
const out = formatRecommendation(rec);
128-
expect(out).toMatch(/TOKEN_PILOT_PROFILE=edit/);
129-
expect(out).toMatch(/1000 tokens|25%/);
131+
expect(out).toMatch(/TOKEN_PILOT_PROFILE=full/);
132+
expect(out).not.toMatch(/TOKEN_PILOT_PROFILE=edit/);
130133
});
131134

132135
it("low-confidence reco tells user to come back later", () => {

0 commit comments

Comments
 (0)