token-pilot 0.44.0 → 0.45.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.
Files changed (44) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +87 -0
  4. package/README.md +2 -0
  5. package/agents/tp-api-surface-tracker.md +1 -1
  6. package/agents/tp-audit-scanner.md +1 -1
  7. package/agents/tp-commit-writer.md +1 -1
  8. package/agents/tp-context-engineer.md +1 -1
  9. package/agents/tp-dead-code-finder.md +1 -1
  10. package/agents/tp-debugger.md +1 -1
  11. package/agents/tp-dep-health.md +1 -1
  12. package/agents/tp-doc-writer.md +1 -1
  13. package/agents/tp-history-explorer.md +1 -1
  14. package/agents/tp-impact-analyzer.md +1 -1
  15. package/agents/tp-incident-timeline.md +1 -1
  16. package/agents/tp-incremental-builder.md +1 -1
  17. package/agents/tp-migration-scout.md +1 -1
  18. package/agents/tp-onboard.md +1 -1
  19. package/agents/tp-performance-profiler.md +1 -1
  20. package/agents/tp-pr-reviewer.md +1 -1
  21. package/agents/tp-refactor-planner.md +1 -1
  22. package/agents/tp-review-impact.md +1 -1
  23. package/agents/tp-run.md +1 -1
  24. package/agents/tp-session-restorer.md +1 -1
  25. package/agents/tp-ship-coordinator.md +1 -1
  26. package/agents/tp-spec-writer.md +1 -1
  27. package/agents/tp-test-coverage-gapper.md +1 -1
  28. package/agents/tp-test-triage.md +1 -1
  29. package/agents/tp-test-writer.md +1 -1
  30. package/dist/core/event-log.d.ts +11 -0
  31. package/dist/core/validation.d.ts +17 -0
  32. package/dist/core/validation.js +42 -0
  33. package/dist/hooks/session-start.d.ts +9 -0
  34. package/dist/hooks/session-start.js +21 -1
  35. package/dist/hooks/subagent-stop.d.ts +7 -0
  36. package/dist/hooks/subagent-stop.js +3 -0
  37. package/dist/index.d.ts +15 -0
  38. package/dist/index.js +80 -13
  39. package/dist/server/profile-recommender.js +18 -15
  40. package/dist/server/tool-definitions.d.ts +2 -2
  41. package/dist/server/tool-definitions.js +3 -3
  42. package/dist/server/tool-profiles.js +11 -3
  43. package/dist/server.js +35 -4
  44. package/package.json +1 -1
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Token Pilot — save 60-90% tokens when AI reads code",
9
- "version": "0.44.0"
9
+ "version": "0.45.1"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "token-pilot",
14
14
  "source": "./",
15
15
  "description": "Reduces token consumption by 60-90% via AST-aware lazy file reading, structural symbol navigation, and cross-session tool-usage analytics. 23 MCP tools + 25 subagents + budget watchdog hooks.",
16
- "version": "0.44.0",
16
+ "version": "0.45.1",
17
17
  "author": {
18
18
  "name": "Digital-Threads"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-pilot",
3
- "version": "0.44.0",
3
+ "version": "0.45.1",
4
4
  "description": "Saves 60-90% tokens on AI code reading. AST-aware lazy reads, symbol navigation, find_usages, structural git diff/log, edit-safety guard, Task-routing matcher, cross-session telemetry (errors + diagnostics), 25 tp-* subagents tiered to haiku/sonnet/opus with budget watchdog.",
5
5
  "author": {
6
6
  "name": "Digital-Threads",
package/CHANGELOG.md CHANGED
@@ -5,6 +5,93 @@ All notable changes to Token Pilot will be documented in this file.
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
+ ## [0.45.1] - 2026-06-11
9
+
10
+ ### Fixed — refuse a multi-repo workspace parent (cross-project index bleed)
11
+
12
+ `start.sh` always passes an explicit project root (`${CLAUDE_PROJECT_DIR:-$USER_CWD}`)
13
+ to the server, so `startServer`'s git-root **narrowing only runs in the
14
+ `!explicitRoot` branch — which is never taken**. When the session is launched
15
+ from a non-git workspace parent that nests several project repos (e.g.
16
+ `/work/loom` holding `token-pilot`, `loom-host`, `aimux`, …), the raw parent was
17
+ used verbatim and ast-index indexed **every** sibling into one index. Symbol
18
+ lookups then bled across projects — `find_usages` / `read_symbol` returning
19
+ matches from the wrong repo, or `symbol not found`. `isDangerousRoot` only
20
+ caught system/home dirs, so the parent slipped through.
21
+
22
+ New guard `isMultiRepoParent(root)` (in `core/validation.ts`) detects a non-git
23
+ directory with ≥2 immediate child git repos. When the resolved root matches,
24
+ ast-index is disabled (`skipAstIndex`) and a warning tells the user to set
25
+ `CLAUDE_PROJECT_DIR` to the specific project — fail safe instead of bleeding.
26
+ Wired into `startServer` and the `server.ts` MCP-roots auto-detect. Single-repo
27
+ roots, monorepos, and roots that are themselves a git repo are unaffected.
28
+
29
+
30
+
31
+ ### Changed — default tool profile is now `full` (adoption fix)
32
+
33
+ The advice surface (rules, SessionStart/PostToolUse banners, the pre-edit hook)
34
+ references `read_for_edit`, batch reads, `test_summary` etc. unconditionally,
35
+ but the old default (`edit`) and any trimmed profile hide some of those — so the
36
+ model calls a hidden tool, hits `No such tool available`, and falls back to raw
37
+ `Read`/`Bash`. Those dead round-trips cost far more than the ~2k tokens the trim
38
+ saved. Default is now `full` (advertise everything); trimmed profiles stay
39
+ opt-in via `TOKEN_PILOT_PROFILE=nav|edit|minimal`. When a trimmed profile is
40
+ active the SessionStart banner now prepends a caveat naming what's hidden.
41
+
42
+ The profile **recommender** no longer pushes a trim: it used to suggest
43
+ `TOKEN_PILOT_PROFILE=nav` for read-heavy usage and print an "apply to
44
+ `.mcp.json`" snippet — users applied it, then the next edit session hit the
45
+ trimmed-away `read_for_edit` / `read_range` / batch reads. It now always
46
+ recommends `full`. And `token-pilot doctor` loudly flags an explicit trimmed
47
+ profile, names the hidden tools, and tells you to remove it.
48
+
49
+ ### Added — tool failures are logged (no more silent breakage)
50
+
51
+ `createServer`'s tool dispatch now writes handler exceptions / validation errors
52
+ (and unknown-tool names that reach the server) to `~/.token-pilot/hook-errors.jsonl`,
53
+ visible via `token-pilot errors`. Previously tp breakage vanished while telemetry
54
+ reported "all ok". (`No such tool available` is rejected at the Claude Code layer
55
+ before reaching us and stays invisible by design — the full default removes its
56
+ main source.)
57
+
58
+ ### Fixed — `read_section` is docs-only
59
+
60
+ Clarified that `read_section` reads Markdown/YAML/JSON/CSV by heading/key/row —
61
+ **not** code by line/symbol (use `read_range` / `read_symbol`). Removed its
62
+ misleading placement under "Batch variants" in the SessionStart banner.
63
+
64
+ ### Added — bounded-read leak closed (gate on read span, not bound presence)
65
+
66
+ `PreToolUse:Read` passed *any* `offset`/`limit` Read straight through, so
67
+ `Read(file, limit=2000)` (Claude Code's default page) pulled a whole big file
68
+ hook-free **and** un-counted in the adaptive burn signal — the #1 invisible
69
+ leak. The hook now measures the span a Read actually pulls
70
+ (`effectiveReadSpanLines`) and applies the same deny threshold: a default-page
71
+ or offset-no-limit read of a big file denies with a structural summary, while a
72
+ genuinely narrow slice (`limit < threshold`) still passes. Cost estimates are
73
+ scaled by the span so bounded denies don't over-report savings.
74
+
75
+ ### Added — `parent_session_id` capture in SubagentStop (groundwork)
76
+
77
+ A subagent's MCP server runs with `CLAUDE_CODE_SESSION_ID` = the *agent*
78
+ session, so subagent savings get tagged with that id and the statusline's
79
+ main-session badge drops them (savings look flat when subagents do the reading).
80
+ SubagentStop now captures `parent_session_id` (which CC ships in the payload),
81
+ enabling a future child→parent rollup in the badge. Additive/no-op when absent.
82
+
83
+ ### Security — `vitest` 3.2.4 → 3.2.6
84
+
85
+ Patches GHSA-5xrq-8626-4rwp (Vitest UI arbitrary file read/exec, critical).
86
+ Dev-only dependency; shipped runtime deps unchanged. The other 32 Dependabot
87
+ alerts were already resolved (installed transitive versions at/above the
88
+ patched version) and auto-close on re-scan.
89
+
90
+ ### Docs
91
+
92
+ Fable-5 economic positioning in the README — savings are in tokens, value is in
93
+ tokens × price; keep the premium thread lean.
94
+
8
95
  ## [0.44.0] - 2026-06-10
9
96
 
10
97
  ### Changed — adaptive deny threshold ON by default
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  **Token-efficient AI coding, enforced.** Cuts context consumption in AI coding assistants by up to **90%** without changing the way you work.
4
4
 
5
+ > **Why it matters more now:** as frontier models move up in price — Claude's Fable 5 is the most capable (and most expensive-per-token) tier yet — the tokens you *don't* spend reading code are worth more, not less. The savings are in tokens; the value is in tokens × price. Token Pilot keeps the expensive main thread lean so the premium model spends its budget on reasoning, not on re-reading files.
6
+
5
7
  Three layers, each useful on its own, stronger together:
6
8
 
7
9
  1. **MCP tools** — structural reads (`smart_read`, `read_symbol`, `read_for_edit`, …). Ask for an outline or load one function by name instead of the whole file.
@@ -9,7 +9,7 @@ tools:
9
9
  - mcp__token-pilot__read_symbol
10
10
  - Bash
11
11
  model: haiku
12
- token_pilot_version: "0.44.0"
12
+ token_pilot_version: "0.45.1"
13
13
  token_pilot_body_hash: dd184501203fa7f3c73f419c4ffbe33c4be75400cb64a7a51733a3fe23f6e085
14
14
  requiredMcpServers:
15
15
  - "token-pilot"
@@ -11,7 +11,7 @@ tools:
11
11
  - Grep
12
12
  - Read
13
13
  model: sonnet
14
- token_pilot_version: "0.44.0"
14
+ token_pilot_version: "0.45.1"
15
15
  token_pilot_body_hash: d172f600bf32277ea6eb4cbbee4542ddd698a986dcd96997d33930561964569b
16
16
  requiredMcpServers:
17
17
  - "token-pilot"
@@ -8,7 +8,7 @@ tools:
8
8
  - mcp__token-pilot__test_summary
9
9
  - mcp__token-pilot__outline
10
10
  - Bash
11
- token_pilot_version: "0.44.0"
11
+ token_pilot_version: "0.45.1"
12
12
  token_pilot_body_hash: de64a406b5176de19f7422619c7de7949b1f28865f225402c9cea9255f377428
13
13
  requiredMcpServers:
14
14
  - "token-pilot"
@@ -13,7 +13,7 @@ tools:
13
13
  - Edit
14
14
  - Glob
15
15
  model: sonnet
16
- token_pilot_version: "0.44.0"
16
+ token_pilot_version: "0.45.1"
17
17
  token_pilot_body_hash: 68b32af2dacd82ebe52c4eec93edb903d452688274c3065218270627c564d8b0
18
18
  requiredMcpServers:
19
19
  - "token-pilot"
@@ -11,7 +11,7 @@ tools:
11
11
  - Grep
12
12
  - Read
13
13
  model: sonnet
14
- token_pilot_version: "0.44.0"
14
+ token_pilot_version: "0.45.1"
15
15
  token_pilot_body_hash: d9b7f5b7ae6f4ae21305c775361bcab097cc774370a6d976c093571d46d55021
16
16
  requiredMcpServers:
17
17
  - "token-pilot"
@@ -12,7 +12,7 @@ tools:
12
12
  - Read
13
13
  - Bash
14
14
  model: sonnet
15
- token_pilot_version: "0.44.0"
15
+ token_pilot_version: "0.45.1"
16
16
  token_pilot_body_hash: 052413de8d92377edcde6ae5c823f5378db304baccfa29e8866467f42553a500
17
17
  requiredMcpServers:
18
18
  - "token-pilot"
@@ -9,7 +9,7 @@ tools:
9
9
  - Bash
10
10
  - Read
11
11
  model: haiku
12
- token_pilot_version: "0.44.0"
12
+ token_pilot_version: "0.45.1"
13
13
  token_pilot_body_hash: e14dc57493d816f8c2e017963e2ef5f66bea50fd0b805a80e8a0d97c968427e7
14
14
  requiredMcpServers:
15
15
  - "token-pilot"
@@ -13,7 +13,7 @@ tools:
13
13
  - Edit
14
14
  - Glob
15
15
  model: haiku
16
- token_pilot_version: "0.44.0"
16
+ token_pilot_version: "0.45.1"
17
17
  token_pilot_body_hash: 57d741794ab40e31a7ac49c68ea39a9088f5827cdef866ce81bfca1b7c9180cf
18
18
  requiredMcpServers:
19
19
  - "token-pilot"
@@ -10,7 +10,7 @@ tools:
10
10
  - Bash
11
11
  - Read
12
12
  model: haiku
13
- token_pilot_version: "0.44.0"
13
+ token_pilot_version: "0.45.1"
14
14
  token_pilot_body_hash: 7b70fa76a60e3c58a1de4f56c32c0f166424137e203a0cf1c8654e7c9235d904
15
15
  requiredMcpServers:
16
16
  - "token-pilot"
@@ -12,7 +12,7 @@ tools:
12
12
  - mcp__token-pilot__read_symbols
13
13
  - Read
14
14
  model: sonnet
15
- token_pilot_version: "0.44.0"
15
+ token_pilot_version: "0.45.1"
16
16
  token_pilot_body_hash: 351a987e11eba63852f5431a16d8eb53104f4f689f82fdcc5a2bf4db948ba92f
17
17
  requiredMcpServers:
18
18
  - "token-pilot"
@@ -8,7 +8,7 @@ tools:
8
8
  - mcp__token-pilot__read_symbol
9
9
  - Bash
10
10
  model: inherit
11
- token_pilot_version: "0.44.0"
11
+ token_pilot_version: "0.45.1"
12
12
  token_pilot_body_hash: de5722bfea374eaab096c1ae635c37879e7a91370ee3cd0532f4240be03c91eb
13
13
  requiredMcpServers:
14
14
  - "token-pilot"
@@ -13,7 +13,7 @@ tools:
13
13
  - Edit
14
14
  - Bash
15
15
  model: sonnet
16
- token_pilot_version: "0.44.0"
16
+ token_pilot_version: "0.45.1"
17
17
  token_pilot_body_hash: 375a824d0d847bb5453ec594c7a62ad566ee7e4d92717b0473f771f1a0477c60
18
18
  requiredMcpServers:
19
19
  - "token-pilot"
@@ -11,7 +11,7 @@ tools:
11
11
  - Grep
12
12
  - Glob
13
13
  model: sonnet
14
- token_pilot_version: "0.44.0"
14
+ token_pilot_version: "0.45.1"
15
15
  token_pilot_body_hash: 0334de1bf99b431b65359637d125cda7c44c6f780eb92c57cc538715b1939536
16
16
  requiredMcpServers:
17
17
  - "token-pilot"
@@ -10,7 +10,7 @@ tools:
10
10
  - mcp__token-pilot__smart_read
11
11
  - mcp__token-pilot__smart_read_many
12
12
  - mcp__token-pilot__read_section
13
- token_pilot_version: "0.44.0"
13
+ token_pilot_version: "0.45.1"
14
14
  token_pilot_body_hash: 832e95633fbc8e9b0c10f3e540a327d4be062fb4b3f17a6cce6be13f414e2927
15
15
  requiredMcpServers:
16
16
  - "token-pilot"
@@ -11,7 +11,7 @@ tools:
11
11
  - Bash
12
12
  - Read
13
13
  model: sonnet
14
- token_pilot_version: "0.44.0"
14
+ token_pilot_version: "0.45.1"
15
15
  token_pilot_body_hash: b61f06380d80798fa2e49d37bcba0653495bee04dd6bdbc1feff9a75607b0508
16
16
  requiredMcpServers:
17
17
  - "token-pilot"
@@ -11,7 +11,7 @@ tools:
11
11
  - mcp__token-pilot__read_for_edit
12
12
  - Read
13
13
  model: sonnet
14
- token_pilot_version: "0.44.0"
14
+ token_pilot_version: "0.45.1"
15
15
  token_pilot_body_hash: f83f50d05b4f70285ae7afed2b1a406fc436df56e61a0aedbfb31edc7f2b6e66
16
16
  requiredMcpServers:
17
17
  - "token-pilot"
@@ -8,7 +8,7 @@ tools:
8
8
  - mcp__token-pilot__outline
9
9
  - mcp__token-pilot__read_symbol
10
10
  model: sonnet
11
- token_pilot_version: "0.44.0"
11
+ token_pilot_version: "0.45.1"
12
12
  token_pilot_body_hash: c5f6fc122c89e16e5cf774045f92169ee3468555320b898171ba13eca5323550
13
13
  requiredMcpServers:
14
14
  - "token-pilot"
@@ -9,7 +9,7 @@ tools:
9
9
  - mcp__token-pilot__module_info
10
10
  - Bash
11
11
  model: sonnet
12
- token_pilot_version: "0.44.0"
12
+ token_pilot_version: "0.45.1"
13
13
  token_pilot_body_hash: 8ef3c3341cbfed4eb8dd130126a9683edc57e378c92ff0ca764d584fd941c55c
14
14
  requiredMcpServers:
15
15
  - "token-pilot"
package/agents/tp-run.md CHANGED
@@ -16,7 +16,7 @@ tools:
16
16
  - Glob
17
17
  - Bash
18
18
  model: haiku
19
- token_pilot_version: "0.44.0"
19
+ token_pilot_version: "0.45.1"
20
20
  token_pilot_body_hash: 2b08618d34a61f00aafccbda9fed6d83243296dedb83440edbd2d5c28bb6dbc4
21
21
  requiredMcpServers:
22
22
  - "token-pilot"
@@ -9,7 +9,7 @@ tools:
9
9
  - mcp__token-pilot__session_budget
10
10
  - Bash
11
11
  - Read
12
- token_pilot_version: "0.44.0"
12
+ token_pilot_version: "0.45.1"
13
13
  token_pilot_body_hash: 529374ed728f5eed5b758b3be3da65624783c0bf0c1a253d7d661a843eb5f767
14
14
  requiredMcpServers:
15
15
  - "token-pilot"
@@ -11,7 +11,7 @@ tools:
11
11
  - Read
12
12
  - Grep
13
13
  model: sonnet
14
- token_pilot_version: "0.44.0"
14
+ token_pilot_version: "0.45.1"
15
15
  token_pilot_body_hash: a60f6ae110eb3138064bce074e8ba26fa0ce5f4659df1624a9d9d3646803391b
16
16
  requiredMcpServers:
17
17
  - "token-pilot"
@@ -9,7 +9,7 @@ tools:
9
9
  - Read
10
10
  - Write
11
11
  model: sonnet
12
- token_pilot_version: "0.44.0"
12
+ token_pilot_version: "0.45.1"
13
13
  token_pilot_body_hash: c7a4e8b39228fd5158528f389c924c5ff2d98c4b9b05ee0106d54a26c5dc1350
14
14
  requiredMcpServers:
15
15
  - "token-pilot"
@@ -10,7 +10,7 @@ tools:
10
10
  - mcp__token-pilot__test_summary
11
11
  - Glob
12
12
  - Grep
13
- token_pilot_version: "0.44.0"
13
+ token_pilot_version: "0.45.1"
14
14
  token_pilot_body_hash: be81eed53a3720d146cf89e4a14a7a56577633f7c84c234c412ab70d64c05b11
15
15
  requiredMcpServers:
16
16
  - "token-pilot"
@@ -8,7 +8,7 @@ tools:
8
8
  - mcp__token-pilot__find_usages
9
9
  - mcp__token-pilot__read_symbol
10
10
  model: sonnet
11
- token_pilot_version: "0.44.0"
11
+ token_pilot_version: "0.45.1"
12
12
  token_pilot_body_hash: 362ecf4cb03b059421ea26933473700900073dc38b3a7fe271208dfb1ae14f90
13
13
  requiredMcpServers:
14
14
  - "token-pilot"
@@ -13,7 +13,7 @@ tools:
13
13
  - Edit
14
14
  - Bash
15
15
  model: sonnet
16
- token_pilot_version: "0.44.0"
16
+ token_pilot_version: "0.45.1"
17
17
  token_pilot_body_hash: 269f2fe22ff4517c277d3f56ca67d8a5527b93290ab21079a83ba7af22c1b5a9
18
18
  requiredMcpServers:
19
19
  - "token-pilot"
@@ -38,6 +38,17 @@ export interface HookEvent {
38
38
  * never populate it; events stay shape-compatible.
39
39
  */
40
40
  parent_agent_id?: string | null;
41
+ /**
42
+ * v0.45.0 — root/parent SESSION id (distinct from agent_id). Claude Code
43
+ * exposes `parent_session_id` in subagent hook payloads (binary:
44
+ * `if(D.parentSessionId) X.parent_session_id = D.parentSessionId`). A
45
+ * subagent's MCP server is spawned with CLAUDE_CODE_SESSION_ID = the AGENT
46
+ * session, so its tool-call and denial savings get tagged with that id and
47
+ * the statusline's main-session filter drops them. Capturing the parent lets
48
+ * the badge roll subagent savings up to the conversation that spawned them.
49
+ * Optional — absent on the main thread and on older Claude Code.
50
+ */
51
+ parent_session_id?: string | null;
41
52
  /**
42
53
  * v0.38.0 — id of the token-pilot workflow this event belongs to,
43
54
  * when one is active (TOKEN_PILOT_WORKFLOW_ID set). Lets fleet-level
@@ -197,4 +197,21 @@ export declare function validateReadSectionArgs(args: unknown): {
197
197
  };
198
198
  /** Detect roots that would cause ast-index to scan the entire filesystem */
199
199
  export declare function isDangerousRoot(root: string): boolean;
200
+ /**
201
+ * Detect a non-git workspace parent that nests multiple sibling git
202
+ * repos. Handing such a directory to ast-index would index every
203
+ * sibling into one index, bleeding symbols across unrelated projects
204
+ * (find_usages / read_symbol returning matches from the wrong repo).
205
+ *
206
+ * Returns true only when BOTH hold:
207
+ * - `root` itself is NOT a git repo (no `.git` entry), AND
208
+ * - `root` has >= 2 immediate child directories that each contain a
209
+ * `.git` entry — a directory for a normal repo, a file for a
210
+ * submodule / worktree.
211
+ *
212
+ * Fail-open: a missing path, an unreadable directory, a single child
213
+ * repo, or a root that is itself a repo all return false, so legitimate
214
+ * single-project and monorepo layouts are never disabled.
215
+ */
216
+ export declare function isMultiRepoParent(root: string): boolean;
200
217
  //# sourceMappingURL=validation.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { resolve, relative } from "node:path";
2
+ import { existsSync, readdirSync } from "node:fs";
2
3
  /**
3
4
  * v0.33.0 (B9) — coerce an `unknown` argument value to an integer.
4
5
  *
@@ -615,4 +616,45 @@ export function isDangerousRoot(root) {
615
616
  return true;
616
617
  return false;
617
618
  }
619
+ /**
620
+ * Detect a non-git workspace parent that nests multiple sibling git
621
+ * repos. Handing such a directory to ast-index would index every
622
+ * sibling into one index, bleeding symbols across unrelated projects
623
+ * (find_usages / read_symbol returning matches from the wrong repo).
624
+ *
625
+ * Returns true only when BOTH hold:
626
+ * - `root` itself is NOT a git repo (no `.git` entry), AND
627
+ * - `root` has >= 2 immediate child directories that each contain a
628
+ * `.git` entry — a directory for a normal repo, a file for a
629
+ * submodule / worktree.
630
+ *
631
+ * Fail-open: a missing path, an unreadable directory, a single child
632
+ * repo, or a root that is itself a repo all return false, so legitimate
633
+ * single-project and monorepo layouts are never disabled.
634
+ */
635
+ export function isMultiRepoParent(root) {
636
+ if (!root)
637
+ return false;
638
+ let entries;
639
+ try {
640
+ // A root that is itself a git repo is a single project — vendored
641
+ // repos or submodules underneath are intentional, not a parent.
642
+ if (existsSync(resolve(root, ".git")))
643
+ return false;
644
+ entries = readdirSync(root, { withFileTypes: true });
645
+ }
646
+ catch {
647
+ return false;
648
+ }
649
+ let repoChildren = 0;
650
+ for (const entry of entries) {
651
+ if (!entry.isDirectory())
652
+ continue;
653
+ if (existsSync(resolve(root, entry.name, ".git"))) {
654
+ if (++repoChildren >= 2)
655
+ return true;
656
+ }
657
+ }
658
+ return false;
659
+ }
618
660
  //# sourceMappingURL=validation.js.map
@@ -8,6 +8,7 @@
8
8
  * Output contract: one JSON line on stdout, or exit 0 silent.
9
9
  */
10
10
  import { type HookEvent } from "../core/event-log.js";
11
+ import { type ToolProfile } from "../server/tool-profiles.js";
11
12
  export declare function buildSubagentAdoptionNudge(events: HookEvent[], now: number, windowDays?: number, minSample?: number, threshold?: number): string | null;
12
13
  export interface AgentEntry {
13
14
  name: string;
@@ -43,5 +44,13 @@ export declare function buildReminderMessage(agents: AgentEntry[], maxReminderTo
43
44
  * Returns the JSON string to write to stdout, or null for silent exit.
44
45
  * Never throws — any error → null (fail-safe pass-through).
45
46
  */
47
+ /**
48
+ * v0.45.0 (token-pilot-2fd part 2) — when a trimmed TOOL profile is active,
49
+ * the banner still names tools that profile hides. The full default advertises
50
+ * everything, but an explicit nav/edit/minimal does not, so warn the agent
51
+ * before it calls a hidden tool, hits "No such tool available", and falls back
52
+ * to raw Read/Bash. Empty string for the default `full` profile.
53
+ */
54
+ export declare function profileBannerNote(profile: ToolProfile): string;
46
55
  export declare function handleSessionStart(opts: HandleSessionStartOptions): Promise<string | null>;
47
56
  //# sourceMappingURL=session-start.d.ts.map
@@ -11,6 +11,7 @@ import { readdir, readFile } from "node:fs/promises";
11
11
  import { join, basename } from "node:path";
12
12
  import { loadLatestSnapshot } from "./../handlers/session-snapshot-persist.js";
13
13
  import { loadEvents } from "../core/event-log.js";
14
+ import { parseProfileEnv } from "../server/tool-profiles.js";
14
15
  const SNAPSHOT_FRESH_MS = 2 * 3600 * 1000; // 2h — enough to cover compaction/restart, tight enough that a new day's unrelated work doesn't inherit yesterday's thread
15
16
  // ─── subagent adoption nudge (v0.32.0) ──────────────────────────────
16
17
  // Pure function: takes the event log + current time, returns either a
@@ -138,7 +139,8 @@ MANDATORY — use these BEFORE raw Read / Grep / git:
138
139
  smart_log(path?) — git log with symbol context (INSTEAD of raw git log)
139
140
  test_summary(command) — test runs without dumping full output
140
141
  project_overview — unfamiliar repo top-level map (first step)
141
- Batch variants (prefer over loops): read_symbols, smart_read_many, read_section.
142
+ Batch variants (prefer over loops): read_symbols, smart_read_many.
143
+ read_section — Markdown/YAML/JSON/CSV ONLY (by heading/key/row); for CODE use read_range / read_symbol.
142
144
  Also available: read_range, read_diff, module_info, related_files, explore_area,
143
145
  code_audit, find_unused, session_snapshot, session_budget, session_analytics.
144
146
  Raw Read/Grep allowed only with offset/limit / narrow regex / non-code files,
@@ -235,6 +237,20 @@ export function buildReminderMessage(agents, maxReminderTokens) {
235
237
  * Returns the JSON string to write to stdout, or null for silent exit.
236
238
  * Never throws — any error → null (fail-safe pass-through).
237
239
  */
240
+ /**
241
+ * v0.45.0 (token-pilot-2fd part 2) — when a trimmed TOOL profile is active,
242
+ * the banner still names tools that profile hides. The full default advertises
243
+ * everything, but an explicit nav/edit/minimal does not, so warn the agent
244
+ * before it calls a hidden tool, hits "No such tool available", and falls back
245
+ * to raw Read/Bash. Empty string for the default `full` profile.
246
+ */
247
+ export function profileBannerNote(profile) {
248
+ if (profile === "full")
249
+ return "";
250
+ return (`⚠ TOKEN_PILOT_PROFILE=${profile} — trimmed tool surface active. Some tools named below are NOT advertised this session ` +
251
+ `(test_summary / code_audit / find_unused always; read_for_edit / read_range / read_diff / batch reads on nav & minimal). ` +
252
+ `Calling them returns "No such tool available" — use the listed alternatives or unset TOKEN_PILOT_PROFILE to advertise all.\n\n`);
253
+ }
238
254
  export async function handleSessionStart(opts) {
239
255
  try {
240
256
  if (!opts.sessionStartConfig.enabled) {
@@ -242,6 +258,10 @@ export async function handleSessionStart(opts) {
242
258
  }
243
259
  const agents = await scanAgents(opts.projectRoot, opts.homeDir);
244
260
  let message = buildReminderMessage(agents, opts.sessionStartConfig.maxReminderTokens);
261
+ // Prepend a profile caveat when a trimmed surface hides referenced tools.
262
+ message =
263
+ profileBannerNote(parseProfileEnv(process.env.TOKEN_PILOT_PROFILE)) +
264
+ message;
245
265
  // TP-340: surface a fresh snapshot so the new session can resume.
246
266
  const snap = await loadLatestSnapshot(opts.projectRoot);
247
267
  if (snap && snap.ageMs < SNAPSHOT_FRESH_MS) {
@@ -38,6 +38,13 @@ export interface SubagentStopInput {
38
38
  last_assistant_message?: string;
39
39
  session_id?: string;
40
40
  parent_agent_id?: string;
41
+ /**
42
+ * v0.45.0 — root/parent session id. CC ships this in the SubagentStop
43
+ * payload (`X.parent_session_id`). Lets the statusline roll a subagent's
44
+ * savings (tagged with the agent's own session id) up to the parent
45
+ * conversation. Absent on older CC → field simply not written.
46
+ */
47
+ parent_session_id?: string;
41
48
  }
42
49
  /**
43
50
  * Best-effort token total from a subagent transcript (JSONL of CC
@@ -90,6 +90,9 @@ export function buildSubagentTaskEvent(input, now, tokensOverride) {
90
90
  agent_type: input.agent_type ?? null,
91
91
  agent_id: input.agent_id ?? null,
92
92
  ...(input.parent_agent_id ? { parent_agent_id: input.parent_agent_id } : {}),
93
+ ...(input.parent_session_id
94
+ ? { parent_session_id: input.parent_session_id }
95
+ : {}),
93
96
  event: "task",
94
97
  file: "",
95
98
  lines: 0,
package/dist/index.d.ts CHANGED
@@ -39,6 +39,21 @@ export declare function handleHookRead(filePathArg?: string, mode?: HookMode, de
39
39
  * wrapping.
40
40
  */
41
41
  export declare function runHookReadDispatch(filePathArg: string | undefined, mode: HookMode, denyThresholdArg?: number, projectRootArg?: string, adaptive?: HookReadAdaptiveOptions): Promise<string | null>;
42
+ /**
43
+ * v0.45.0 (token-pilot-xg9) — how many lines a Read actually pulls.
44
+ *
45
+ * An unbounded Read (no offset/limit) pulls the whole file. A bounded Read
46
+ * pulls `limit` lines starting at `offset` — but Claude Code's Read defaults
47
+ * to a 2000-line page, so `Read(file, limit=2000)` or an offset with no limit
48
+ * drags a whole big file through. The old hook passed ANY bounded Read
49
+ * straight through (`hasOffset || hasLimit → return null`), which is the leak:
50
+ * the model bounds with a large/default limit and reads everything hook-free
51
+ * AND un-counted in the adaptive burn signal. Comparing the *span* against the
52
+ * deny threshold closes that while still letting a genuinely narrow slice pass.
53
+ *
54
+ * `offset` / `limit` are null when the field is absent on the tool call.
55
+ */
56
+ export declare function effectiveReadSpanLines(totalLines: number, offset: number | null, limit: number | null): number;
42
57
  /**
43
58
  * PreToolUse:Edit / MultiEdit / Write enforcement.
44
59
  *
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ import { appendDiagnostic } from "./core/event-log.js";
30
30
  import { startWorkflow, endWorkflow, listWorkflows, workflowStatus, formatWorkflowStatus, formatWorkflowList, } from "./core/workflow.js";
31
31
  import { findBinary, installBinary, checkBinaryUpdate, isNewerVersion, } from "./ast-index/binary-manager.js";
32
32
  import { loadConfig } from "./config/loader.js";
33
- import { isDangerousRoot } from "./core/validation.js";
33
+ import { isDangerousRoot, isMultiRepoParent } from "./core/validation.js";
34
34
  import { runSummaryPipeline } from "./hooks/summary-pipeline.js";
35
35
  import { formatDenyMessage } from "./hooks/format-deny-message.js";
36
36
  import { isPathWithinProject } from "./hooks/path-safety.js";
@@ -678,6 +678,18 @@ export async function startServer(cliArgs = process.argv.slice(2)) {
678
678
  ` Fix: pass project path explicitly — token-pilot /path/to/project\n` +
679
679
  ` Or configure mcpServers with "args": ["/path/to/project"]`);
680
680
  }
681
+ // Guard: refuse a non-git workspace parent that nests multiple sibling
682
+ // git repos. start.sh always passes an explicit root, so the git-detect
683
+ // narrowing above is skipped; without this guard a parent like
684
+ // `/work/loom` (holding several project repos) would be indexed whole,
685
+ // bleeding symbols across unrelated projects.
686
+ const multiRepoParent = !isDangerousRoot(projectRoot) && isMultiRepoParent(projectRoot);
687
+ if (multiRepoParent) {
688
+ console.error(`[token-pilot] WARNING: project root "${projectRoot}" contains multiple git repos.\n` +
689
+ ` ast-index will be disabled to avoid cross-project index bleed.\n` +
690
+ ` Fix: set CLAUDE_PROJECT_DIR to the specific project, or\n` +
691
+ ` configure mcpServers with "args": ["/path/to/project"].`);
692
+ }
681
693
  // Non-blocking update check for all components (logs to stderr, never blocks startup)
682
694
  const config = await loadConfig(projectRoot);
683
695
  const binaryStatus = await findBinary(config.astIndex.binaryPath);
@@ -731,7 +743,7 @@ export async function startServer(cliArgs = process.argv.slice(2)) {
731
743
  /* ignore — not Claude Code or no .claude dir */
732
744
  });
733
745
  const server = await createServer(projectRoot, {
734
- skipAstIndex: isDangerousRoot(projectRoot),
746
+ skipAstIndex: isDangerousRoot(projectRoot) || multiRepoParent,
735
747
  enforcementMode: parseEnforcementMode(process.env.TOKEN_PILOT_MODE),
736
748
  });
737
749
  const transport = new StdioServerTransport();
@@ -767,14 +779,36 @@ export async function runHookReadDispatch(filePathArg, mode, denyThresholdArg, p
767
779
  const projectRoot = projectRootArg ?? process.cwd();
768
780
  return runHookReadDispatchImpl(filePathArg, mode, denyThreshold, projectRoot, adaptive);
769
781
  }
782
+ /**
783
+ * v0.45.0 (token-pilot-xg9) — how many lines a Read actually pulls.
784
+ *
785
+ * An unbounded Read (no offset/limit) pulls the whole file. A bounded Read
786
+ * pulls `limit` lines starting at `offset` — but Claude Code's Read defaults
787
+ * to a 2000-line page, so `Read(file, limit=2000)` or an offset with no limit
788
+ * drags a whole big file through. The old hook passed ANY bounded Read
789
+ * straight through (`hasOffset || hasLimit → return null`), which is the leak:
790
+ * the model bounds with a large/default limit and reads everything hook-free
791
+ * AND un-counted in the adaptive burn signal. Comparing the *span* against the
792
+ * deny threshold closes that while still letting a genuinely narrow slice pass.
793
+ *
794
+ * `offset` / `limit` are null when the field is absent on the tool call.
795
+ */
796
+ export function effectiveReadSpanLines(totalLines, offset, limit) {
797
+ if (offset == null && limit == null)
798
+ return totalLines;
799
+ const DEFAULT_READ_PAGE = 2000;
800
+ const start = offset != null && offset > 0 ? offset : 0;
801
+ const page = limit != null && limit >= 0 ? limit : DEFAULT_READ_PAGE;
802
+ return Math.max(0, Math.min(page, totalLines - start));
803
+ }
770
804
  async function runHookReadDispatchImpl(filePathArg, mode, denyThreshold, projectRoot, adaptive = {}) {
771
805
  if (mode === "off")
772
806
  return null;
773
807
  // Parse stdin to get tool_input + session/agent metadata, unless a
774
808
  // filePath was supplied directly (tests, --filePath invocation).
775
809
  let filePath = filePathArg;
776
- let hasOffset = false;
777
- let hasLimit = false;
810
+ let offsetVal = null;
811
+ let limitVal = null;
778
812
  let sessionId = "";
779
813
  let agentType = null;
780
814
  let agentId = null;
@@ -783,8 +817,14 @@ async function runHookReadDispatchImpl(filePathArg, mode, denyThreshold, project
783
817
  const stdin = readFileSync(0, "utf-8");
784
818
  const input = JSON.parse(stdin);
785
819
  filePath = input?.tool_input?.file_path;
786
- hasOffset = input?.tool_input?.offset != null;
787
- hasLimit = input?.tool_input?.limit != null;
820
+ offsetVal =
821
+ typeof input?.tool_input?.offset === "number"
822
+ ? input.tool_input.offset
823
+ : null;
824
+ limitVal =
825
+ typeof input?.tool_input?.limit === "number"
826
+ ? input.tool_input.limit
827
+ : null;
788
828
  sessionId = typeof input?.session_id === "string" ? input.session_id : "";
789
829
  agentType =
790
830
  typeof input?.agent_type === "string" ? input.agent_type : null;
@@ -799,9 +839,6 @@ async function runHookReadDispatchImpl(filePathArg, mode, denyThreshold, project
799
839
  const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
800
840
  if (!CODE_EXTENSIONS.has(ext))
801
841
  return null;
802
- // Bounded Reads are always passed through — the agent already narrowed scope.
803
- if (hasOffset || hasLimit)
804
- return null;
805
842
  // Path safety: refuse to summarise any file outside the project root
806
843
  // (traversal, symlinks pointing outside). Pass-through on failure so the
807
844
  // agent is never blocked by a safety reject.
@@ -829,13 +866,20 @@ async function runHookReadDispatchImpl(filePathArg, mode, denyThreshold, project
829
866
  try {
830
867
  fileContent = readFileSync(filePath, "utf-8");
831
868
  lineCount = fileContent.split("\n").length;
832
- if (lineCount <= effectiveThreshold)
833
- return null;
834
869
  }
835
870
  catch {
836
871
  return null;
837
872
  }
838
- const charEst = Math.ceil(fileContent.length / 4);
873
+ // v0.45.0 (token-pilot-xg9) — measure the span the Read actually pulls, not
874
+ // just the file size. A narrow bounded slice (< threshold) still passes; a
875
+ // `limit=2000` / offset-no-limit read of a big file no longer slips through.
876
+ const spanLines = effectiveReadSpanLines(lineCount, offsetVal, limitVal);
877
+ if (spanLines <= effectiveThreshold)
878
+ return null;
879
+ // Cost estimate reflects what the read would pull (the span), so a bounded
880
+ // deny doesn't over-report savings vs the whole-file figure.
881
+ const spanRatio = lineCount > 0 ? Math.min(1, spanLines / lineCount) : 1;
882
+ const charEst = Math.ceil((fileContent.length * spanRatio) / 4);
839
883
  const wsRatio = (fileContent.match(/\s/g)?.length ?? 0) / fileContent.length;
840
884
  const estTokens = Math.ceil(charEst * (1 - wsRatio * 0.3));
841
885
  // Legacy telemetry (hook-denied.jsonl) — retained for backward compatibility
@@ -873,7 +917,7 @@ async function runHookReadDispatchImpl(filePathArg, mode, denyThreshold, project
873
917
  if (mode === "advisory") {
874
918
  const reason = `File "${filePath}" has ${lineCount} lines. Use mcp__token-pilot__smart_read("${filePath}") ` +
875
919
  `for a structural overview, or mcp__token-pilot__read_for_edit("${filePath}", symbol="<name>") ` +
876
- `for edit context. Bounded Read with offset/limit is still allowed.`;
920
+ `for edit context. A narrow bounded Read (small limit) is still allowed.`;
877
921
  await writeEvent("denied", Math.ceil(reason.length / 4));
878
922
  return JSON.stringify({
879
923
  hookSpecificOutput: {
@@ -1282,6 +1326,29 @@ export async function handleDoctor() {
1282
1326
  catch {
1283
1327
  /* ignore */
1284
1328
  }
1329
+ // ── explicit trimmed-profile warning (v0.45.0, token-pilot-26b) ──
1330
+ // An explicit TOKEN_PILOT_PROFILE=nav|edit|minimal hides tools the rules and
1331
+ // the pre-edit hook still reference (read_for_edit / read_range / batch),
1332
+ // trapping edit sessions on "No such tool available". This recurred for two
1333
+ // users. Surface it loudly so they can remove it.
1334
+ try {
1335
+ const raw = process.env.TOKEN_PILOT_PROFILE;
1336
+ if (raw && raw.trim()) {
1337
+ const { parseProfileEnv } = await import("./server/tool-profiles.js");
1338
+ if (parseProfileEnv(raw) !== "full") {
1339
+ console.log(`⚠ TOKEN_PILOT_PROFILE=${raw.trim()} is set — a TRIMMED tool surface.\n` +
1340
+ ` It hides read_for_edit / read_range / batch reads (and code_audit /\n` +
1341
+ ` find_unused / test_summary) that the rules + pre-edit hook still name →\n` +
1342
+ ` calls to them fail with "No such tool available" and the agent falls\n` +
1343
+ ` back to raw Read/Bash.\n` +
1344
+ ` Fix: remove "TOKEN_PILOT_PROFILE" from your .mcp.json env block (or set\n` +
1345
+ ` it to "full"), then restart. Full is the default since v0.45.0.\n`);
1346
+ }
1347
+ }
1348
+ }
1349
+ catch {
1350
+ /* doctor must never crash over an optional check */
1351
+ }
1285
1352
  // ── profile recommendation ──
1286
1353
  // v0.26.4 — data-driven. Reads cumulative tool-calls.jsonl and suggests
1287
1354
  // the narrowest TOKEN_PILOT_PROFILE that wouldn't hide any tool the
@@ -41,25 +41,29 @@ export function recommendProfile(events) {
41
41
  lowConfidence: true,
42
42
  };
43
43
  }
44
+ // v0.45.0 (token-pilot-26b) — we NO LONGER recommend trimming to nav/edit.
45
+ // Past tool usage doesn't predict future edits, and a trimmed profile hides
46
+ // read_for_edit / read_range / batch reads that the rules and the pre-edit
47
+ // hook still reference — so the agent calls them, hits "No such tool
48
+ // available", and falls back to raw Read/Bash. That recurring trap cost two
49
+ // users whole sessions. Full is the recommendation; minimal stays a
50
+ // self-serve, clearly-warned opt-in for context-critical work only.
44
51
  const allInNav = [...used].every((t) => NAV_TOOLS.has(t));
45
52
  if (allInNav) {
46
53
  return {
47
- recommended: "nav",
48
- reason: `Every tool you've used (${uniqueToolsSeen} distinct) is part of the nav subset. You're a read-only explorer.`,
54
+ recommended: "full",
55
+ 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).`,
49
56
  uniqueToolsSeen,
50
57
  totalCalls,
51
- wouldHide: [
52
- ...[...EDIT_EXTRAS].filter((t) => !used.has(t)),
53
- /* full-only — we don't enumerate here, keep the list short */
54
- ],
58
+ wouldHide: [],
55
59
  lowConfidence: false,
56
60
  };
57
61
  }
58
62
  const allInEditOrBelow = [...used].every((t) => NAV_TOOLS.has(t) || EDIT_EXTRAS.has(t));
59
63
  if (allInEditOrBelow) {
60
64
  return {
61
- recommended: "edit",
62
- 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.`,
65
+ recommended: "full",
66
+ 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.`,
63
67
  uniqueToolsSeen,
64
68
  totalCalls,
65
69
  wouldHide: [],
@@ -87,15 +91,14 @@ export function formatRecommendation(rec) {
87
91
  lines.push(` data: ${rec.totalCalls} calls, ${rec.uniqueToolsSeen} distinct tools`);
88
92
  lines.push(` recommend: TOKEN_PILOT_PROFILE=${rec.recommended}`);
89
93
  lines.push(` why: ${rec.reason}`);
90
- if (rec.recommended !== "full") {
91
- lines.push(` savings: ~${rec.recommended === "nav" ? "2200 tokens (−54%)" : "1000 tokens (−25%)"} on every tools/list response`);
92
- lines.push(` apply: add "env": { "TOKEN_PILOT_PROFILE": "${rec.recommended}" } to your token-pilot entry in .mcp.json`);
93
- }
94
- else if (rec.lowConfidence) {
95
- lines.push(` action: keep default (full). Re-run \`token-pilot doctor\` after a few real sessions for a data-backed suggestion.`);
94
+ // v0.45.0 (26b) — we no longer print an "apply nav/edit to .mcp.json"
95
+ // snippet. recommendProfile always returns `full`; the old snippet trapped
96
+ // users into trimming, which hid edit tools the rules reference.
97
+ if (rec.lowConfidence) {
98
+ lines.push(` action: keep default (full). Re-run \`token-pilot doctor\` after a few real sessions for a data-backed view.`);
96
99
  }
97
100
  else {
98
- lines.push(` action: keep default (full). You're using what you have.`);
101
+ lines.push(` action: keep default (full). Trim to minimal yourself only for context-critical, read-only work.`);
99
102
  }
100
103
  return lines.join("\n");
101
104
  }
@@ -8,8 +8,8 @@
8
8
  *
9
9
  * minimal — 5 core tools, minimal context overhead
10
10
  * nav — 10 exploration tools, no editing
11
- * edit — nav + 6 edit-prep tools (DEFAULT)
12
- * full — everything including audit tools
11
+ * edit — nav + 6 edit-prep tools
12
+ * full — everything including audit tools (DEFAULT since v0.45.0)
13
13
  */
14
14
  import type { ToolProfile } from "./tool-profiles.js";
15
15
  /**
@@ -42,7 +42,7 @@ const MCP_INSTRUCTIONS_NAV = [
42
42
  "• Explore: project_overview → explore_area → smart_read → read_symbol",
43
43
  ].join("\n");
44
44
  // ---------------------------------------------------------------------------
45
- // Edit profile — nav + batch reads + edit-prep (DEFAULT)
45
+ // Edit profile — nav + batch reads + edit-prep
46
46
  // ---------------------------------------------------------------------------
47
47
  const MCP_INSTRUCTIONS_EDIT = [
48
48
  "Token Pilot — token-efficient code reading (saves 60-80% tokens). ALWAYS prefer these tools over Read/cat/grep.",
@@ -87,7 +87,7 @@ const MCP_INSTRUCTIONS_EDIT = [
87
87
  "• Long session: session_snapshot → compact context → continue with minimal state",
88
88
  ].join("\n");
89
89
  // ---------------------------------------------------------------------------
90
- // Full profile — all tools including audit (code_audit, find_unused, test_summary)
90
+ // Full profile — all tools including audit (code_audit, find_unused, test_summary) — DEFAULT since v0.45.0
91
91
  // ---------------------------------------------------------------------------
92
92
  const MCP_INSTRUCTIONS_FULL = [
93
93
  "Token Pilot — token-efficient code reading (saves 60-80% tokens). ALWAYS prefer these tools over Read/cat/grep.",
@@ -297,7 +297,7 @@ export const TOOL_DEFINITIONS = [
297
297
  },
298
298
  {
299
299
  name: "read_section",
300
- description: "Read a specific section from Markdown, YAML, JSON, or CSV files. Markdown: by heading name. YAML/JSON: by top-level key. CSV: by row range (rows:1-50). Much cheaper than reading the whole file.",
300
+ description: "Read a specific section from Markdown, YAML, JSON, or CSV files. Markdown: by heading name. YAML/JSON: by top-level key. CSV: by row range (rows:1-50). Much cheaper than reading the whole file. DOCS/DATA ONLY — `heading` is required; this does NOT read code by line/symbol. For source files use read_range (line range) or read_symbol (one symbol).",
301
301
  inputSchema: {
302
302
  type: "object",
303
303
  properties: {
@@ -117,8 +117,16 @@ export function filterToolsByProfile(tools, profile) {
117
117
  * silently apply a guess.
118
118
  */
119
119
  export function parseProfileEnv(envValue, warn = () => { }) {
120
+ // v0.45.0 — default is now `full` (was `edit`). Trimming the advertised
121
+ // tools/list saved ~2k tokens but created a mismatch: the rules, the
122
+ // SessionStart/PostToolUse banners and the pre-edit hook all reference tools
123
+ // (read_for_edit, test_summary, batch reads) that a trimmed profile hides —
124
+ // so the model calls them, hits "No such tool available", and falls back to
125
+ // raw Read/Bash. Those dead round-trips cost far more than the 2k saved.
126
+ // Advertise everything by default; users who truly need the smaller surface
127
+ // opt in with TOKEN_PILOT_PROFILE=nav|edit|minimal.
120
128
  if (!envValue)
121
- return "edit";
129
+ return "full";
122
130
  const lower = envValue.trim().toLowerCase();
123
131
  if (lower === "full" ||
124
132
  lower === "nav" ||
@@ -126,7 +134,7 @@ export function parseProfileEnv(envValue, warn = () => { }) {
126
134
  lower === "minimal") {
127
135
  return lower;
128
136
  }
129
- warn(`[token-pilot] Unknown TOKEN_PILOT_PROFILE="${envValue}". Expected full|nav|edit|minimal. Falling back to edit.`);
130
- return "edit";
137
+ warn(`[token-pilot] Unknown TOKEN_PILOT_PROFILE="${envValue}". Expected full|nav|edit|minimal. Falling back to full.`);
138
+ return "full";
131
139
  }
132
140
  //# sourceMappingURL=tool-profiles.js.map
package/dist/server.js CHANGED
@@ -4,6 +4,7 @@ import { AstIndexClient } from "./ast-index/client.js";
4
4
  import { FileCache } from "./core/file-cache.js";
5
5
  import { ContextRegistry } from "./core/context-registry.js";
6
6
  import { SessionRegistryManager } from "./core/session-registry.js";
7
+ import { appendError, classifyError } from "./core/error-log.js";
7
8
  import { SymbolResolver } from "./core/symbol-resolver.js";
8
9
  import { SessionAnalytics, } from "./core/session-analytics.js";
9
10
  import { classifyIntent } from "./core/intent-classifier.js";
@@ -13,7 +14,7 @@ import { loadConfig } from "./config/loader.js";
13
14
  import { readFileSync } from "node:fs";
14
15
  import { dirname, resolve } from "node:path";
15
16
  import { execFile } from "node:child_process";
16
- import { isDangerousRoot } from "./core/validation.js";
17
+ import { isDangerousRoot, isMultiRepoParent } from "./core/validation.js";
17
18
  import { promisify } from "node:util";
18
19
  import { GitWatcher } from "./git/watcher.js";
19
20
  const execFilePromise = promisify(execFile);
@@ -150,7 +151,9 @@ export async function createServer(projectRoot, options) {
150
151
  for (const root of roots) {
151
152
  if (root.uri.startsWith("file://")) {
152
153
  const rootPath = decodeURIComponent(new URL(root.uri).pathname);
153
- if (rootPath && !isDangerousRoot(rootPath)) {
154
+ if (rootPath &&
155
+ !isDangerousRoot(rootPath) &&
156
+ !isMultiRepoParent(rootPath)) {
154
157
  await applyDetectedRoot(rootPath, "MCP roots");
155
158
  return;
156
159
  }
@@ -277,8 +280,8 @@ export async function createServer(projectRoot, options) {
277
280
  instructions: getMcpInstructions(activeProfile),
278
281
  });
279
282
  const advertisedTools = filterToolsByProfile(TOOL_DEFINITIONS, activeProfile);
280
- if (activeProfile !== "edit") {
281
- process.stderr.write(`[token-pilot] Profile: ${activeProfile} — advertising ${advertisedTools.length}/${TOOL_DEFINITIONS.length} tools. Set TOKEN_PILOT_PROFILE=edit for the default set.\n`);
283
+ if (activeProfile !== "full") {
284
+ process.stderr.write(`[token-pilot] Profile: ${activeProfile} — advertising ${advertisedTools.length}/${TOOL_DEFINITIONS.length} tools (full is the default). A trimmed profile hides tools the rules/hooks still reference; unset TOKEN_PILOT_PROFILE to advertise all.\n`);
282
285
  }
283
286
  server.setRequestHandler(ListToolsRequestSchema, () => ({
284
287
  tools: advertisedTools,
@@ -1057,6 +1060,17 @@ export async function createServer(projectRoot, options) {
1057
1060
  return { content: budgetResult.content };
1058
1061
  }
1059
1062
  default:
1063
+ // token-pilot-m68 — an unknown tool NAME that still reached the
1064
+ // server (a forwarded call CC didn't reject). Log it so the gap is
1065
+ // visible via `token-pilot errors`.
1066
+ void appendError({
1067
+ ts: Date.now(),
1068
+ hook: `mcp-tool:${name}`,
1069
+ level: "warn",
1070
+ code: "unknown_tool",
1071
+ msg: `Unknown tool: ${name}`,
1072
+ input: { tool: name },
1073
+ });
1060
1074
  return {
1061
1075
  content: [{ type: "text", text: `Unknown tool: ${name}` }],
1062
1076
  isError: true,
@@ -1065,6 +1079,23 @@ export async function createServer(projectRoot, options) {
1065
1079
  }
1066
1080
  catch (err) {
1067
1081
  const message = err instanceof Error ? err.message : String(err);
1082
+ // v0.45.0 (token-pilot-m68) — surface tool failures that REACH the
1083
+ // server (validation errors, handler exceptions) in the same
1084
+ // ~/.token-pilot/hook-errors.jsonl the hooks use, so `token-pilot errors`
1085
+ // shows them. Previously these vanished — telemetry reported "saving
1086
+ // tokens, all ok" while broken tp calls were invisible. Best-effort;
1087
+ // appendError never throws. NOTE: "No such tool available" rejections
1088
+ // happen at the Claude Code layer BEFORE the call reaches us, so those
1089
+ // remain invisible by CC design (mitigated by the full-tool default).
1090
+ void appendError({
1091
+ ts: Date.now(),
1092
+ hook: `mcp-tool:${name}`,
1093
+ level: "error",
1094
+ code: classifyError(err),
1095
+ msg: message,
1096
+ stack: err instanceof Error ? err.stack : undefined,
1097
+ input: { tool: name },
1098
+ });
1068
1099
  return {
1069
1100
  content: [{ type: "text", text: `Error: ${message}` }],
1070
1101
  isError: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-pilot",
3
- "version": "0.44.0",
3
+ "version": "0.45.1",
4
4
  "description": "Save up to 80% tokens when AI reads code — MCP server for token-efficient code navigation, AST-aware structural reading instead of dumping full files into context window",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",