token-pilot 0.39.3 → 0.41.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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Token Pilot \u2014 save 60-90% tokens when AI reads code",
9
- "version": "0.39.3"
9
+ "version": "0.41.0"
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. 22 MCP tools + 19 subagents + budget watchdog hooks.",
16
- "version": "0.39.3",
16
+ "version": "0.41.0",
17
17
  "author": {
18
18
  "name": "Digital-Threads"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-pilot",
3
- "version": "0.39.3",
3
+ "version": "0.41.0",
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/README.md CHANGED
@@ -232,6 +232,21 @@ lines instead of bouncing the call. The structural summary still
232
232
  rides along in `additionalContext`. Default OFF because the field
233
233
  is undocumented and may change.
234
234
 
235
+ ### Experimental: SubagentStop budget feedback (CC 2.1.163+)
236
+
237
+ Every subagent completion already lands a task-telemetry row via the
238
+ `SubagentStop` hook (that's how `stats --tasks` knows what you
239
+ dispatched). With `TOKEN_PILOT_SUBAGENT_FEEDBACK=1` the same hook also
240
+ returns `additionalContext` — when a `token-pilot workflow` fan-out is
241
+ at ≥90 % of its token ceiling, each completing agent gets a wind-down
242
+ note so a hundred-agent `/workflow` run stops before blowing the
243
+ budget.
244
+
245
+ **Requires Claude Code 2.1.163+.** Returning `additionalContext` from
246
+ `SubagentStop` is only honoured there; older Claude Code labels it a
247
+ hook error. Default OFF for that reason — enable only once
248
+ `claude --version` reports 2.1.163 or later.
249
+
235
250
  ## What's new for Claude Code 2.1.151+
236
251
 
237
252
  These notes are about behaviour you'll see automatically once you
@@ -9,7 +9,7 @@ tools:
9
9
  - mcp__token-pilot__read_symbol
10
10
  - Bash
11
11
  model: haiku
12
- token_pilot_version: "0.39.3"
12
+ token_pilot_version: "0.41.0"
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.39.3"
14
+ token_pilot_version: "0.41.0"
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.39.3"
11
+ token_pilot_version: "0.41.0"
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.39.3"
16
+ token_pilot_version: "0.41.0"
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.39.3"
14
+ token_pilot_version: "0.41.0"
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.39.3"
15
+ token_pilot_version: "0.41.0"
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.39.3"
12
+ token_pilot_version: "0.41.0"
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.39.3"
16
+ token_pilot_version: "0.41.0"
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.39.3"
13
+ token_pilot_version: "0.41.0"
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.39.3"
15
+ token_pilot_version: "0.41.0"
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.39.3"
11
+ token_pilot_version: "0.41.0"
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.39.3"
16
+ token_pilot_version: "0.41.0"
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.39.3"
14
+ token_pilot_version: "0.41.0"
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.39.3"
13
+ token_pilot_version: "0.41.0"
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.39.3"
14
+ token_pilot_version: "0.41.0"
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.39.3"
14
+ token_pilot_version: "0.41.0"
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.39.3"
11
+ token_pilot_version: "0.41.0"
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.39.3"
12
+ token_pilot_version: "0.41.0"
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.39.3"
19
+ token_pilot_version: "0.41.0"
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.39.3"
12
+ token_pilot_version: "0.41.0"
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.39.3"
14
+ token_pilot_version: "0.41.0"
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.39.3"
12
+ token_pilot_version: "0.41.0"
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.39.3"
13
+ token_pilot_version: "0.41.0"
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.39.3"
11
+ token_pilot_version: "0.41.0"
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.39.3"
16
+ token_pilot_version: "0.41.0"
17
17
  token_pilot_body_hash: 269f2fe22ff4517c277d3f56ca67d8a5527b93290ab21079a83ba7af22c1b5a9
18
18
  requiredMcpServers:
19
19
  - "token-pilot"
@@ -17,7 +17,7 @@
17
17
  * Everything else passes through untouched — a real project root like
18
18
  * `/home/user/my-project` or `./subdir` goes to startServer as before.
19
19
  */
20
- export declare const KNOWN_COMMANDS: readonly ["hook-read", "hook-edit", "hook-pre-bash", "hook-pre-grep", "hook-pre-task", "hook-post-bash", "hook-post-task", "hook-session-start", "hook-bootstrap", "install-hook", "uninstall-hook", "install-ast-index", "doctor", "bless-agents", "unbless-agents", "install-agents", "uninstall-agents", "stats", "tool-audit", "save-doc", "list-docs", "init", "migrate-hooks", "errors", "workflow", "--version", "-v", "--help", "-h"];
20
+ export declare const KNOWN_COMMANDS: readonly ["hook-read", "hook-edit", "hook-pre-bash", "hook-pre-grep", "hook-pre-task", "hook-post-bash", "hook-post-task", "hook-session-start", "hook-bootstrap", "hook-subagent-stop", "install-hook", "uninstall-hook", "install-ast-index", "doctor", "bless-agents", "unbless-agents", "install-agents", "uninstall-agents", "stats", "tool-audit", "save-doc", "list-docs", "init", "migrate-hooks", "errors", "workflow", "--version", "-v", "--help", "-h"];
21
21
  export interface TypoGuardResult {
22
22
  kind: "pass-through" | "typo";
23
23
  suggestion?: string;
@@ -35,6 +35,8 @@ export const KNOWN_COMMANDS = [
35
35
  // v0.35.0 — one-shot project setup hook (uses Claude Code's
36
36
  // undocumented `once: true` SessionStart flag).
37
37
  "hook-bootstrap",
38
+ // v0.40.0 — canonical subagent-completion capture
39
+ "hook-subagent-stop",
38
40
  "install-hook",
39
41
  "uninstall-hook",
40
42
  "install-ast-index",
@@ -140,13 +140,21 @@ function createHookConfig(options) {
140
140
  // v0.39.2 — post-task MUST run synchronously. It writes the
141
141
  // `event:"task"` record via appendEvent (mkdir + stat +
142
142
  // appendFile). Under `async: true` Claude Code fires the hook
143
- // detached and may reap the process before those writes flush
144
- // the suspected cause of persistently zero task events in
145
- // hook-events.jsonl despite subagents being dispatched.
146
- // Telemetry integrity > the ~5ms saved on a non-hot-path hook.
143
+ // detached and may reap the process before those writes flush.
144
+ // Kept as a secondary path; v0.39.3 probe showed it does not
145
+ // fire on current Claude Code (see SubagentStop below).
147
146
  hooks: [hookEntry("hook-post-task", options)],
148
147
  },
149
148
  ],
149
+ // v0.40.0 — SubagentStop is the canonical, reliably-firing
150
+ // subagent-completion event. PostToolUse:Task proved non-firing
151
+ // for the dispatch tool; SubagentStop is where the task adoption
152
+ // signal is actually captured. Synchronous (writes telemetry).
153
+ SubagentStop: [
154
+ {
155
+ hooks: [hookEntry("hook-subagent-stop", options)],
156
+ },
157
+ ],
150
158
  },
151
159
  };
152
160
  }
@@ -276,6 +284,17 @@ export async function installHook(projectRoot, options) {
276
284
  settings.hooks.PostToolUse.push(hookDef);
277
285
  }
278
286
  }
287
+ // v0.40.0 — SubagentStop (canonical subagent-completion capture).
288
+ // Installed idempotently, same pattern as SessionStart.
289
+ if (Array.isArray(hookConfig.hooks.SubagentStop)) {
290
+ if (!Array.isArray(settings.hooks.SubagentStop)) {
291
+ settings.hooks.SubagentStop = [];
292
+ }
293
+ const hasSubagentStop = settings.hooks.SubagentStop.some(isTokenPilotHook);
294
+ if (!hasSubagentStop) {
295
+ settings.hooks.SubagentStop.push(...hookConfig.hooks.SubagentStop);
296
+ }
297
+ }
279
298
  await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n");
280
299
  return {
281
300
  installed: true,
@@ -303,7 +322,11 @@ export async function uninstallHook(projectRoot) {
303
322
  const hasPreToolUse = !!settings.hooks?.PreToolUse;
304
323
  const hasSessionStart = !!settings.hooks?.SessionStart;
305
324
  const hasPostToolUse = !!settings.hooks?.PostToolUse;
306
- if (!hasPreToolUse && !hasSessionStart && !hasPostToolUse) {
325
+ const hasSubagentStop = !!settings.hooks?.SubagentStop;
326
+ if (!hasPreToolUse &&
327
+ !hasSessionStart &&
328
+ !hasPostToolUse &&
329
+ !hasSubagentStop) {
307
330
  return { removed: false, fatal: false, message: "No hooks to remove." };
308
331
  }
309
332
  const isTokenPilotHook = (h) => h.hooks?.some((hook) => hook.command?.includes("token-pilot"));
@@ -325,6 +348,12 @@ export async function uninstallHook(projectRoot) {
325
348
  delete settings.hooks.PostToolUse;
326
349
  }
327
350
  }
351
+ if (Array.isArray(settings.hooks?.SubagentStop)) {
352
+ settings.hooks.SubagentStop = settings.hooks.SubagentStop.filter((h) => !isTokenPilotHook(h));
353
+ if (settings.hooks.SubagentStop.length === 0) {
354
+ delete settings.hooks.SubagentStop;
355
+ }
356
+ }
328
357
  if (settings.hooks && Object.keys(settings.hooks).length === 0) {
329
358
  delete settings.hooks;
330
359
  }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * v0.40.0 — SubagentStop task-completion capture.
3
+ *
4
+ * Why this exists (grounded, not speculative):
5
+ *
6
+ * PostToolUse:Task was supposed to record one `event:"task"` per
7
+ * subagent dispatch. A clean restarted-session probe on v0.39.3
8
+ * proved it does not fire: a real subagent dispatch produced ZERO
9
+ * events, while a Read-deny in the SAME session wrote its `denied`
10
+ * event fine (so appendEvent + the hook runtime are healthy). That
11
+ * is years of "0 task events" explained — the parent-side PostToolUse
12
+ * hook simply never lands for the dispatch tool on this Claude Code.
13
+ *
14
+ * SubagentStop is Claude Code's canonical subagent-completion event
15
+ * (confirmed in the 2.1.131/2.1.161 bundle schema:
16
+ * literal("SubagentStop"), stop_hook_active, agent_id,
17
+ * agent_transcript_path, agent_type, last_assistant_message ).
18
+ * It fires once per subagent completion by definition, so it is the
19
+ * reliable place to record the adoption signal the whole v0.30 goal
20
+ * depends on: WAS a subagent used, and was it a tp-* or not.
21
+ *
22
+ * Tokens: SubagentStop carries `agent_transcript_path`. We make a
23
+ * best-effort read of the transcript's cumulative usage; on any
24
+ * failure we record estTokens:0 — the agent_type signal is the
25
+ * primary value, tokens are secondary.
26
+ *
27
+ * Routing-miss detection (was general-purpose picked where a tp-*
28
+ * fit?) stays in the PreToolUse:Task diagnostic, which has the task
29
+ * description SubagentStop lacks. This hook records the ACTUAL
30
+ * completion; pre-task records the ADVICE.
31
+ */
32
+ import type { HookEvent } from "../core/event-log.js";
33
+ export interface SubagentStopInput {
34
+ hook_event_name?: string;
35
+ agent_id?: string;
36
+ agent_type?: string;
37
+ agent_transcript_path?: string;
38
+ last_assistant_message?: string;
39
+ session_id?: string;
40
+ parent_agent_id?: string;
41
+ }
42
+ /**
43
+ * Best-effort token total from a subagent transcript (JSONL of CC
44
+ * messages). Sums `usage.output_tokens` across assistant messages, or
45
+ * takes a cumulative `usage.total_tokens` when present. Returns 0 on
46
+ * any read/parse failure — never throws.
47
+ */
48
+ export declare function tokensFromTranscript(path: string | undefined): number;
49
+ /**
50
+ * Build the `event:"task"` record from a SubagentStop payload. Pure —
51
+ * no I/O except the optional transcript token read, which the caller
52
+ * can pre-resolve for tests via `tokensOverride`.
53
+ */
54
+ export declare function buildSubagentTaskEvent(input: SubagentStopInput, now: number, tokensOverride?: number): HookEvent | null;
55
+ export interface SubagentFeedbackContext {
56
+ /** Active workflow budget status, when a fleet workflow is running. */
57
+ workflow?: {
58
+ workflow_id: string;
59
+ budget_tokens: number | null;
60
+ used_tokens: number;
61
+ pct: number | null;
62
+ } | null;
63
+ }
64
+ /**
65
+ * Decide the `additionalContext` feedback to hand back from a
66
+ * SubagentStop hook. Pure — caller resolves the workflow status.
67
+ *
68
+ * Returns a short wind-down note when an active fleet workflow is at or
69
+ * past 90 % of its token ceiling, so a `/workflow`-style fan-out winds
70
+ * down before the budget is blown. Returns null otherwise — we do NOT
71
+ * nag on every completion; broad adoption nudges stay in SessionStart.
72
+ *
73
+ * Emission is the caller's responsibility and is gated behind
74
+ * TOKEN_PILOT_SUBAGENT_FEEDBACK=1 + Claude Code 2.1.163+ (older Claude
75
+ * Code labels a SubagentStop hookSpecificOutput return as a hook error).
76
+ */
77
+ export declare function decideSubagentFeedback(_input: SubagentStopInput, ctx: SubagentFeedbackContext): string | null;
78
+ /**
79
+ * Render the SubagentStop hook JSON response carrying feedback. Returns
80
+ * null when there is nothing to say (caller writes no stdout).
81
+ */
82
+ export declare function renderSubagentFeedback(message: string | null): string | null;
83
+ //# sourceMappingURL=subagent-stop.d.ts.map
@@ -0,0 +1,149 @@
1
+ /**
2
+ * v0.40.0 — SubagentStop task-completion capture.
3
+ *
4
+ * Why this exists (grounded, not speculative):
5
+ *
6
+ * PostToolUse:Task was supposed to record one `event:"task"` per
7
+ * subagent dispatch. A clean restarted-session probe on v0.39.3
8
+ * proved it does not fire: a real subagent dispatch produced ZERO
9
+ * events, while a Read-deny in the SAME session wrote its `denied`
10
+ * event fine (so appendEvent + the hook runtime are healthy). That
11
+ * is years of "0 task events" explained — the parent-side PostToolUse
12
+ * hook simply never lands for the dispatch tool on this Claude Code.
13
+ *
14
+ * SubagentStop is Claude Code's canonical subagent-completion event
15
+ * (confirmed in the 2.1.131/2.1.161 bundle schema:
16
+ * literal("SubagentStop"), stop_hook_active, agent_id,
17
+ * agent_transcript_path, agent_type, last_assistant_message ).
18
+ * It fires once per subagent completion by definition, so it is the
19
+ * reliable place to record the adoption signal the whole v0.30 goal
20
+ * depends on: WAS a subagent used, and was it a tp-* or not.
21
+ *
22
+ * Tokens: SubagentStop carries `agent_transcript_path`. We make a
23
+ * best-effort read of the transcript's cumulative usage; on any
24
+ * failure we record estTokens:0 — the agent_type signal is the
25
+ * primary value, tokens are secondary.
26
+ *
27
+ * Routing-miss detection (was general-purpose picked where a tp-*
28
+ * fit?) stays in the PreToolUse:Task diagnostic, which has the task
29
+ * description SubagentStop lacks. This hook records the ACTUAL
30
+ * completion; pre-task records the ADVICE.
31
+ */
32
+ import { readFileSync } from "node:fs";
33
+ /**
34
+ * Best-effort token total from a subagent transcript (JSONL of CC
35
+ * messages). Sums `usage.output_tokens` across assistant messages, or
36
+ * takes a cumulative `usage.total_tokens` when present. Returns 0 on
37
+ * any read/parse failure — never throws.
38
+ */
39
+ export function tokensFromTranscript(path) {
40
+ if (!path || typeof path !== "string")
41
+ return 0;
42
+ let raw;
43
+ try {
44
+ raw = readFileSync(path, "utf-8");
45
+ }
46
+ catch {
47
+ return 0;
48
+ }
49
+ let out = 0;
50
+ let lastTotal = 0;
51
+ for (const line of raw.split("\n")) {
52
+ if (!line.trim())
53
+ continue;
54
+ let rec;
55
+ try {
56
+ rec = JSON.parse(line);
57
+ }
58
+ catch {
59
+ continue;
60
+ }
61
+ const msg = rec
62
+ .message;
63
+ const usage = msg?.usage ?? rec.usage;
64
+ if (usage && typeof usage === "object") {
65
+ const o = usage.output_tokens;
66
+ if (typeof o === "number")
67
+ out += o;
68
+ const t = usage.total_tokens;
69
+ if (typeof t === "number")
70
+ lastTotal = t;
71
+ }
72
+ }
73
+ // Prefer summed output tokens; fall back to the last cumulative total.
74
+ return out > 0 ? out : lastTotal;
75
+ }
76
+ /**
77
+ * Build the `event:"task"` record from a SubagentStop payload. Pure —
78
+ * no I/O except the optional transcript token read, which the caller
79
+ * can pre-resolve for tests via `tokensOverride`.
80
+ */
81
+ export function buildSubagentTaskEvent(input, now, tokensOverride) {
82
+ const agentType = typeof input.agent_type === "string" ? input.agent_type : "";
83
+ // No agent_type → nothing meaningful to record.
84
+ if (!agentType)
85
+ return null;
86
+ const est = tokensOverride ?? tokensFromTranscript(input.agent_transcript_path);
87
+ return {
88
+ ts: now,
89
+ session_id: input.session_id ?? "",
90
+ agent_type: input.agent_type ?? null,
91
+ agent_id: input.agent_id ?? null,
92
+ ...(input.parent_agent_id ? { parent_agent_id: input.parent_agent_id } : {}),
93
+ event: "task",
94
+ file: "",
95
+ lines: 0,
96
+ estTokens: est,
97
+ summaryTokens: 0,
98
+ savedTokens: 0,
99
+ subagent_type: agentType,
100
+ // SubagentStop has no task description, so no heuristic match here.
101
+ // Routing-miss detection lives in the PreToolUse:Task diagnostic.
102
+ matched_tp_agent: null,
103
+ // Mark the source so a future revival of PostToolUse:Task can be
104
+ // deduped in stats rather than double-counted.
105
+ code: "subagent_stop",
106
+ };
107
+ }
108
+ /**
109
+ * Decide the `additionalContext` feedback to hand back from a
110
+ * SubagentStop hook. Pure — caller resolves the workflow status.
111
+ *
112
+ * Returns a short wind-down note when an active fleet workflow is at or
113
+ * past 90 % of its token ceiling, so a `/workflow`-style fan-out winds
114
+ * down before the budget is blown. Returns null otherwise — we do NOT
115
+ * nag on every completion; broad adoption nudges stay in SessionStart.
116
+ *
117
+ * Emission is the caller's responsibility and is gated behind
118
+ * TOKEN_PILOT_SUBAGENT_FEEDBACK=1 + Claude Code 2.1.163+ (older Claude
119
+ * Code labels a SubagentStop hookSpecificOutput return as a hook error).
120
+ */
121
+ export function decideSubagentFeedback(_input, ctx) {
122
+ const wf = ctx.workflow;
123
+ if (wf &&
124
+ wf.budget_tokens != null &&
125
+ wf.budget_tokens > 0 &&
126
+ wf.used_tokens >= wf.budget_tokens * 0.9) {
127
+ const pct = wf.pct != null ? `${wf.pct}%` : "~90%";
128
+ return (`[token-pilot] workflow ${wf.workflow_id} is at ${pct} of its ` +
129
+ `${wf.budget_tokens} token ceiling (${wf.used_tokens} used). ` +
130
+ `Wind down the fan-out: finish in-flight branches and report ` +
131
+ `rather than dispatching new agents.`);
132
+ }
133
+ return null;
134
+ }
135
+ /**
136
+ * Render the SubagentStop hook JSON response carrying feedback. Returns
137
+ * null when there is nothing to say (caller writes no stdout).
138
+ */
139
+ export function renderSubagentFeedback(message) {
140
+ if (!message)
141
+ return null;
142
+ return JSON.stringify({
143
+ hookSpecificOutput: {
144
+ hookEventName: "SubagentStop",
145
+ additionalContext: message,
146
+ },
147
+ });
148
+ }
149
+ //# sourceMappingURL=subagent-stop.js.map
package/dist/index.js CHANGED
@@ -326,6 +326,50 @@ export async function main(cliArgs = process.argv.slice(2)) {
326
326
  });
327
327
  return;
328
328
  }
329
+ case "hook-subagent-stop": {
330
+ // v0.40.0 — canonical subagent-completion capture. PostToolUse:Task
331
+ // proved non-firing for the dispatch tool (clean v0.39.3 probe:
332
+ // a real dispatch wrote 0 events while a Read-deny in the same
333
+ // session wrote fine). SubagentStop fires once per subagent by
334
+ // definition, so this is the reliable source for the task
335
+ // adoption signal. Synchronous (writes telemetry — never async).
336
+ await runHookEntryPoint({ hook: "hook-subagent-stop" }, async () => {
337
+ const stdin = readFileSync(0, "utf-8");
338
+ const input = JSON.parse(stdin);
339
+ const { buildSubagentTaskEvent, decideSubagentFeedback, renderSubagentFeedback, } = await import("./hooks/subagent-stop.js");
340
+ const ev = buildSubagentTaskEvent(input, Date.now());
341
+ if (ev) {
342
+ const { appendEvent } = await import("./core/event-log.js");
343
+ await appendEvent(process.cwd(), ev);
344
+ }
345
+ // v0.41.0 — optional SubagentStop feedback. Returning
346
+ // hookSpecificOutput.additionalContext from SubagentStop is a
347
+ // Claude Code 2.1.163+ feature; on older Claude Code it is
348
+ // labelled a hook error (noise). Gate strictly behind
349
+ // TOKEN_PILOT_SUBAGENT_FEEDBACK=1 so the default path (telemetry
350
+ // only) stays safe on every version.
351
+ if (process.env.TOKEN_PILOT_SUBAGENT_FEEDBACK === "1") {
352
+ const { activeWorkflowId, workflowStatus } = await import("./core/workflow.js");
353
+ let wf = null;
354
+ const wfId = activeWorkflowId();
355
+ if (wfId) {
356
+ const st = await workflowStatus(process.cwd(), wfId);
357
+ if (st) {
358
+ wf = {
359
+ workflow_id: st.workflow_id,
360
+ budget_tokens: st.budget_tokens,
361
+ used_tokens: st.used_tokens,
362
+ pct: st.pct,
363
+ };
364
+ }
365
+ }
366
+ const rendered = renderSubagentFeedback(decideSubagentFeedback(input, { workflow: wf }));
367
+ if (rendered)
368
+ process.stdout.write(rendered);
369
+ }
370
+ });
371
+ return;
372
+ }
329
373
  case "hook-session-start": {
330
374
  await runHookEntryPoint({ hook: "hook-session-start" }, async () => {
331
375
  const cfg = await loadConfig(process.cwd());
package/hooks/hooks.json CHANGED
@@ -96,6 +96,16 @@
96
96
  }
97
97
  ]
98
98
  }
99
+ ],
100
+ "SubagentStop": [
101
+ {
102
+ "hooks": [
103
+ {
104
+ "type": "command",
105
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/dist/index.js hook-subagent-stop"
106
+ }
107
+ ]
108
+ }
99
109
  ]
100
110
  }
101
111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-pilot",
3
- "version": "0.39.3",
3
+ "version": "0.41.0",
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",