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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +15 -0
- package/agents/tp-api-surface-tracker.md +1 -1
- package/agents/tp-audit-scanner.md +1 -1
- package/agents/tp-commit-writer.md +1 -1
- package/agents/tp-context-engineer.md +1 -1
- package/agents/tp-dead-code-finder.md +1 -1
- package/agents/tp-debugger.md +1 -1
- package/agents/tp-dep-health.md +1 -1
- package/agents/tp-doc-writer.md +1 -1
- package/agents/tp-history-explorer.md +1 -1
- package/agents/tp-impact-analyzer.md +1 -1
- package/agents/tp-incident-timeline.md +1 -1
- package/agents/tp-incremental-builder.md +1 -1
- package/agents/tp-migration-scout.md +1 -1
- package/agents/tp-onboard.md +1 -1
- package/agents/tp-performance-profiler.md +1 -1
- package/agents/tp-pr-reviewer.md +1 -1
- package/agents/tp-refactor-planner.md +1 -1
- package/agents/tp-review-impact.md +1 -1
- package/agents/tp-run.md +1 -1
- package/agents/tp-session-restorer.md +1 -1
- package/agents/tp-ship-coordinator.md +1 -1
- package/agents/tp-spec-writer.md +1 -1
- package/agents/tp-test-coverage-gapper.md +1 -1
- package/agents/tp-test-triage.md +1 -1
- package/agents/tp-test-writer.md +1 -1
- package/dist/cli/typo-guard.d.ts +1 -1
- package/dist/cli/typo-guard.js +2 -0
- package/dist/hooks/installer.js +34 -5
- package/dist/hooks/subagent-stop.d.ts +83 -0
- package/dist/hooks/subagent-stop.js +149 -0
- package/dist/index.js +44 -0
- package/hooks/hooks.json +10 -0
- package/package.json +1 -1
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
12
|
+
token_pilot_version: "0.41.0"
|
|
13
13
|
token_pilot_body_hash: dd184501203fa7f3c73f419c4ffbe33c4be75400cb64a7a51733a3fe23f6e085
|
|
14
14
|
requiredMcpServers:
|
|
15
15
|
- "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.
|
|
11
|
+
token_pilot_version: "0.41.0"
|
|
12
12
|
token_pilot_body_hash: de64a406b5176de19f7422619c7de7949b1f28865f225402c9cea9255f377428
|
|
13
13
|
requiredMcpServers:
|
|
14
14
|
- "token-pilot"
|
package/agents/tp-debugger.md
CHANGED
package/agents/tp-dep-health.md
CHANGED
package/agents/tp-doc-writer.md
CHANGED
|
@@ -12,7 +12,7 @@ tools:
|
|
|
12
12
|
- mcp__token-pilot__read_symbols
|
|
13
13
|
- Read
|
|
14
14
|
model: sonnet
|
|
15
|
-
token_pilot_version: "0.
|
|
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.
|
|
11
|
+
token_pilot_version: "0.41.0"
|
|
12
12
|
token_pilot_body_hash: de5722bfea374eaab096c1ae635c37879e7a91370ee3cd0532f4240be03c91eb
|
|
13
13
|
requiredMcpServers:
|
|
14
14
|
- "token-pilot"
|
package/agents/tp-onboard.md
CHANGED
|
@@ -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.
|
|
13
|
+
token_pilot_version: "0.41.0"
|
|
14
14
|
token_pilot_body_hash: 832e95633fbc8e9b0c10f3e540a327d4be062fb4b3f17a6cce6be13f414e2927
|
|
15
15
|
requiredMcpServers:
|
|
16
16
|
- "token-pilot"
|
package/agents/tp-pr-reviewer.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
package/agents/tp-spec-writer.md
CHANGED
|
@@ -10,7 +10,7 @@ tools:
|
|
|
10
10
|
- mcp__token-pilot__test_summary
|
|
11
11
|
- Glob
|
|
12
12
|
- Grep
|
|
13
|
-
token_pilot_version: "0.
|
|
13
|
+
token_pilot_version: "0.41.0"
|
|
14
14
|
token_pilot_body_hash: be81eed53a3720d146cf89e4a14a7a56577633f7c84c234c412ab70d64c05b11
|
|
15
15
|
requiredMcpServers:
|
|
16
16
|
- "token-pilot"
|
package/agents/tp-test-triage.md
CHANGED
|
@@ -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.
|
|
11
|
+
token_pilot_version: "0.41.0"
|
|
12
12
|
token_pilot_body_hash: 362ecf4cb03b059421ea26933473700900073dc38b3a7fe271208dfb1ae14f90
|
|
13
13
|
requiredMcpServers:
|
|
14
14
|
- "token-pilot"
|
package/agents/tp-test-writer.md
CHANGED
package/dist/cli/typo-guard.d.ts
CHANGED
|
@@ -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;
|
package/dist/cli/typo-guard.js
CHANGED
|
@@ -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",
|
package/dist/hooks/installer.js
CHANGED
|
@@ -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
|
-
//
|
|
145
|
-
//
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "token-pilot",
|
|
3
|
-
"version": "0.
|
|
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",
|