diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e487854..a09e6db 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,14 +6,14 @@ }, "metadata": { "description": "Token Pilot \u2014 save 60-90% tokens when AI reads code", - "version": "0.40.0" + "version": "0.41.0" }, "plugins": [ { "name": "token-pilot", "source": "./", "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.", - "version": "0.40.0", + "version": "0.41.0", "author": { "name": "Digital-Threads" }, diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 285ce43..26a469c 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "token-pilot", - "version": "0.40.0", + "version": "0.41.0", "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.", "author": { "name": "Digital-Threads", diff --git a/README.md b/README.md index 2082ad2..d30ff01 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,21 @@ lines instead of bouncing the call. The structural summary still rides along in `additionalContext`. Default OFF because the field is undocumented and may change. +### Experimental: SubagentStop budget feedback (CC 2.1.163+) + +Every subagent completion already lands a task-telemetry row via the +`SubagentStop` hook (that's how `stats --tasks` knows what you +dispatched). With `TOKEN_PILOT_SUBAGENT_FEEDBACK=1` the same hook also +returns `additionalContext` — when a `token-pilot workflow` fan-out is +at ≥90 % of its token ceiling, each completing agent gets a wind-down +note so a hundred-agent `/workflow` run stops before blowing the +budget. + +**Requires Claude Code 2.1.163+.** Returning `additionalContext` from +`SubagentStop` is only honoured there; older Claude Code labels it a +hook error. Default OFF for that reason — enable only once +`claude --version` reports 2.1.163 or later. + ## What's new for Claude Code 2.1.151+ These notes are about behaviour you'll see automatically once you diff --git a/agents/tp-api-surface-tracker.md b/agents/tp-api-surface-tracker.md index ffe4eb3..3b2b1f2 100644 --- a/agents/tp-api-surface-tracker.md +++ b/agents/tp-api-surface-tracker.md @@ -9,7 +9,7 @@ tools: - mcp__token-pilot__read_symbol - Bash model: haiku -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: dd184501203fa7f3c73f419c4ffbe33c4be75400cb64a7a51733a3fe23f6e085 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-audit-scanner.md b/agents/tp-audit-scanner.md index 41ed825..e3907ff 100644 --- a/agents/tp-audit-scanner.md +++ b/agents/tp-audit-scanner.md @@ -11,7 +11,7 @@ tools: - Grep - Read model: sonnet -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: d172f600bf32277ea6eb4cbbee4542ddd698a986dcd96997d33930561964569b requiredMcpServers: - "token-pilot" diff --git a/agents/tp-commit-writer.md b/agents/tp-commit-writer.md index 8173632..218dea5 100644 --- a/agents/tp-commit-writer.md +++ b/agents/tp-commit-writer.md @@ -8,7 +8,7 @@ tools: - mcp__token-pilot__test_summary - mcp__token-pilot__outline - Bash -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: de64a406b5176de19f7422619c7de7949b1f28865f225402c9cea9255f377428 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-context-engineer.md b/agents/tp-context-engineer.md index 412845a..2fc1f97 100644 --- a/agents/tp-context-engineer.md +++ b/agents/tp-context-engineer.md @@ -13,7 +13,7 @@ tools: - Edit - Glob model: sonnet -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: 68b32af2dacd82ebe52c4eec93edb903d452688274c3065218270627c564d8b0 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-dead-code-finder.md b/agents/tp-dead-code-finder.md index 85b1488..c825ac8 100644 --- a/agents/tp-dead-code-finder.md +++ b/agents/tp-dead-code-finder.md @@ -11,7 +11,7 @@ tools: - Grep - Read model: sonnet -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: d9b7f5b7ae6f4ae21305c775361bcab097cc774370a6d976c093571d46d55021 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-debugger.md b/agents/tp-debugger.md index f0136b7..acf1217 100644 --- a/agents/tp-debugger.md +++ b/agents/tp-debugger.md @@ -12,7 +12,7 @@ tools: - Read - Bash model: sonnet -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: 052413de8d92377edcde6ae5c823f5378db304baccfa29e8866467f42553a500 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-dep-health.md b/agents/tp-dep-health.md index d7f62aa..75188b0 100644 --- a/agents/tp-dep-health.md +++ b/agents/tp-dep-health.md @@ -9,7 +9,7 @@ tools: - Bash - Read model: haiku -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: e14dc57493d816f8c2e017963e2ef5f66bea50fd0b805a80e8a0d97c968427e7 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-doc-writer.md b/agents/tp-doc-writer.md index 3640198..a8fdbdb 100644 --- a/agents/tp-doc-writer.md +++ b/agents/tp-doc-writer.md @@ -13,7 +13,7 @@ tools: - Edit - Glob model: haiku -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: 57d741794ab40e31a7ac49c68ea39a9088f5827cdef866ce81bfca1b7c9180cf requiredMcpServers: - "token-pilot" diff --git a/agents/tp-history-explorer.md b/agents/tp-history-explorer.md index 186d578..8a8e6ee 100644 --- a/agents/tp-history-explorer.md +++ b/agents/tp-history-explorer.md @@ -10,7 +10,7 @@ tools: - Bash - Read model: haiku -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: 7b70fa76a60e3c58a1de4f56c32c0f166424137e203a0cf1c8654e7c9235d904 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-impact-analyzer.md b/agents/tp-impact-analyzer.md index fb9862e..e5ebf67 100644 --- a/agents/tp-impact-analyzer.md +++ b/agents/tp-impact-analyzer.md @@ -12,7 +12,7 @@ tools: - mcp__token-pilot__read_symbols - Read model: sonnet -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: 351a987e11eba63852f5431a16d8eb53104f4f689f82fdcc5a2bf4db948ba92f requiredMcpServers: - "token-pilot" diff --git a/agents/tp-incident-timeline.md b/agents/tp-incident-timeline.md index b94c6d5..8c2e918 100644 --- a/agents/tp-incident-timeline.md +++ b/agents/tp-incident-timeline.md @@ -8,7 +8,7 @@ tools: - mcp__token-pilot__read_symbol - Bash model: inherit -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: de5722bfea374eaab096c1ae635c37879e7a91370ee3cd0532f4240be03c91eb requiredMcpServers: - "token-pilot" diff --git a/agents/tp-incremental-builder.md b/agents/tp-incremental-builder.md index a929bc0..1b19a3a 100644 --- a/agents/tp-incremental-builder.md +++ b/agents/tp-incremental-builder.md @@ -13,7 +13,7 @@ tools: - Edit - Bash model: sonnet -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: 375a824d0d847bb5453ec594c7a62ad566ee7e4d92717b0473f771f1a0477c60 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-migration-scout.md b/agents/tp-migration-scout.md index b3f583f..66dbb38 100644 --- a/agents/tp-migration-scout.md +++ b/agents/tp-migration-scout.md @@ -11,7 +11,7 @@ tools: - Grep - Glob model: sonnet -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: 0334de1bf99b431b65359637d125cda7c44c6f780eb92c57cc538715b1939536 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-onboard.md b/agents/tp-onboard.md index e76bd6b..c82d930 100644 --- a/agents/tp-onboard.md +++ b/agents/tp-onboard.md @@ -10,7 +10,7 @@ tools: - mcp__token-pilot__smart_read - mcp__token-pilot__smart_read_many - mcp__token-pilot__read_section -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: 832e95633fbc8e9b0c10f3e540a327d4be062fb4b3f17a6cce6be13f414e2927 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-performance-profiler.md b/agents/tp-performance-profiler.md index dc089b6..6d6190d 100644 --- a/agents/tp-performance-profiler.md +++ b/agents/tp-performance-profiler.md @@ -11,7 +11,7 @@ tools: - Bash - Read model: sonnet -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: b61f06380d80798fa2e49d37bcba0653495bee04dd6bdbc1feff9a75607b0508 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-pr-reviewer.md b/agents/tp-pr-reviewer.md index bf5cc77..cbfacc9 100644 --- a/agents/tp-pr-reviewer.md +++ b/agents/tp-pr-reviewer.md @@ -11,7 +11,7 @@ tools: - mcp__token-pilot__read_for_edit - Read model: sonnet -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: f83f50d05b4f70285ae7afed2b1a406fc436df56e61a0aedbfb31edc7f2b6e66 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-refactor-planner.md b/agents/tp-refactor-planner.md index c71ed67..b007b3a 100644 --- a/agents/tp-refactor-planner.md +++ b/agents/tp-refactor-planner.md @@ -8,7 +8,7 @@ tools: - mcp__token-pilot__outline - mcp__token-pilot__read_symbol model: sonnet -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: c5f6fc122c89e16e5cf774045f92169ee3468555320b898171ba13eca5323550 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-review-impact.md b/agents/tp-review-impact.md index 334b877..b8899af 100644 --- a/agents/tp-review-impact.md +++ b/agents/tp-review-impact.md @@ -9,7 +9,7 @@ tools: - mcp__token-pilot__module_info - Bash model: sonnet -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: 8ef3c3341cbfed4eb8dd130126a9683edc57e378c92ff0ca764d584fd941c55c requiredMcpServers: - "token-pilot" diff --git a/agents/tp-run.md b/agents/tp-run.md index 9ea6d36..315f25c 100644 --- a/agents/tp-run.md +++ b/agents/tp-run.md @@ -16,7 +16,7 @@ tools: - Glob - Bash model: haiku -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: 2b08618d34a61f00aafccbda9fed6d83243296dedb83440edbd2d5c28bb6dbc4 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-session-restorer.md b/agents/tp-session-restorer.md index 06d08fb..3f9050f 100644 --- a/agents/tp-session-restorer.md +++ b/agents/tp-session-restorer.md @@ -9,7 +9,7 @@ tools: - mcp__token-pilot__session_budget - Bash - Read -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: 529374ed728f5eed5b758b3be3da65624783c0bf0c1a253d7d661a843eb5f767 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-ship-coordinator.md b/agents/tp-ship-coordinator.md index 78181a2..75ca796 100644 --- a/agents/tp-ship-coordinator.md +++ b/agents/tp-ship-coordinator.md @@ -11,7 +11,7 @@ tools: - Read - Grep model: sonnet -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: a60f6ae110eb3138064bce074e8ba26fa0ce5f4659df1624a9d9d3646803391b requiredMcpServers: - "token-pilot" diff --git a/agents/tp-spec-writer.md b/agents/tp-spec-writer.md index 0092e8b..e379de0 100644 --- a/agents/tp-spec-writer.md +++ b/agents/tp-spec-writer.md @@ -9,7 +9,7 @@ tools: - Read - Write model: sonnet -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: c7a4e8b39228fd5158528f389c924c5ff2d98c4b9b05ee0106d54a26c5dc1350 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-test-coverage-gapper.md b/agents/tp-test-coverage-gapper.md index a27f4f3..244c7ba 100644 --- a/agents/tp-test-coverage-gapper.md +++ b/agents/tp-test-coverage-gapper.md @@ -10,7 +10,7 @@ tools: - mcp__token-pilot__test_summary - Glob - Grep -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: be81eed53a3720d146cf89e4a14a7a56577633f7c84c234c412ab70d64c05b11 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-test-triage.md b/agents/tp-test-triage.md index da4a672..dac257c 100644 --- a/agents/tp-test-triage.md +++ b/agents/tp-test-triage.md @@ -8,7 +8,7 @@ tools: - mcp__token-pilot__find_usages - mcp__token-pilot__read_symbol model: sonnet -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: 362ecf4cb03b059421ea26933473700900073dc38b3a7fe271208dfb1ae14f90 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-test-writer.md b/agents/tp-test-writer.md index 14a2dda..10582a0 100644 --- a/agents/tp-test-writer.md +++ b/agents/tp-test-writer.md @@ -13,7 +13,7 @@ tools: - Edit - Bash model: sonnet -token_pilot_version: "0.40.0" +token_pilot_version: "0.41.0" token_pilot_body_hash: 269f2fe22ff4517c277d3f56ca67d8a5527b93290ab21079a83ba7af22c1b5a9 requiredMcpServers: - "token-pilot" diff --git a/package.json b/package.json index 73d662e..fce159a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "token-pilot", - "version": "0.40.0", + "version": "0.41.0", "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", "type": "module", "main": "dist/index.js", diff --git a/src/hooks/subagent-stop.ts b/src/hooks/subagent-stop.ts index bb7ed70..6d250d5 100644 --- a/src/hooks/subagent-stop.ts +++ b/src/hooks/subagent-stop.ts @@ -121,3 +121,64 @@ export function buildSubagentTaskEvent( code: "subagent_stop", }; } + +// ─── v0.41.0 SubagentStop feedback ─────────────────────────────────── + +export interface SubagentFeedbackContext { + /** Active workflow budget status, when a fleet workflow is running. */ + workflow?: { + workflow_id: string; + budget_tokens: number | null; + used_tokens: number; + pct: number | null; + } | null; +} + +/** + * Decide the `additionalContext` feedback to hand back from a + * SubagentStop hook. Pure — caller resolves the workflow status. + * + * Returns a short wind-down note when an active fleet workflow is at or + * past 90 % of its token ceiling, so a `/workflow`-style fan-out winds + * down before the budget is blown. Returns null otherwise — we do NOT + * nag on every completion; broad adoption nudges stay in SessionStart. + * + * Emission is the caller's responsibility and is gated behind + * TOKEN_PILOT_SUBAGENT_FEEDBACK=1 + Claude Code 2.1.163+ (older Claude + * Code labels a SubagentStop hookSpecificOutput return as a hook error). + */ +export function decideSubagentFeedback( + _input: SubagentStopInput, + ctx: SubagentFeedbackContext, +): string | null { + const wf = ctx.workflow; + if ( + wf && + wf.budget_tokens != null && + wf.budget_tokens > 0 && + wf.used_tokens >= wf.budget_tokens * 0.9 + ) { + const pct = wf.pct != null ? `${wf.pct}%` : "~90%"; + return ( + `[token-pilot] workflow ${wf.workflow_id} is at ${pct} of its ` + + `${wf.budget_tokens} token ceiling (${wf.used_tokens} used). ` + + `Wind down the fan-out: finish in-flight branches and report ` + + `rather than dispatching new agents.` + ); + } + return null; +} + +/** + * Render the SubagentStop hook JSON response carrying feedback. Returns + * null when there is nothing to say (caller writes no stdout). + */ +export function renderSubagentFeedback(message: string | null): string | null { + if (!message) return null; + return JSON.stringify({ + hookSpecificOutput: { + hookEventName: "SubagentStop", + additionalContext: message, + }, + }); +} diff --git a/src/index.ts b/src/index.ts index 0dad958..b8f5df6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -407,14 +407,45 @@ export async function main(cliArgs = process.argv.slice(2)): Promise { await runHookEntryPoint({ hook: "hook-subagent-stop" }, async () => { const stdin = readFileSync(0, "utf-8"); const input = JSON.parse(stdin); - const { buildSubagentTaskEvent } = await import( - "./hooks/subagent-stop.js" - ); + const { + buildSubagentTaskEvent, + decideSubagentFeedback, + renderSubagentFeedback, + } = await import("./hooks/subagent-stop.js"); const ev = buildSubagentTaskEvent(input, Date.now()); if (ev) { const { appendEvent } = await import("./core/event-log.js"); await appendEvent(process.cwd(), ev); } + + // v0.41.0 — optional SubagentStop feedback. Returning + // hookSpecificOutput.additionalContext from SubagentStop is a + // Claude Code 2.1.163+ feature; on older Claude Code it is + // labelled a hook error (noise). Gate strictly behind + // TOKEN_PILOT_SUBAGENT_FEEDBACK=1 so the default path (telemetry + // only) stays safe on every version. + if (process.env.TOKEN_PILOT_SUBAGENT_FEEDBACK === "1") { + const { activeWorkflowId, workflowStatus } = await import( + "./core/workflow.js" + ); + let wf = null; + const wfId = activeWorkflowId(); + if (wfId) { + const st = await workflowStatus(process.cwd(), wfId); + if (st) { + wf = { + workflow_id: st.workflow_id, + budget_tokens: st.budget_tokens, + used_tokens: st.used_tokens, + pct: st.pct, + }; + } + } + const rendered = renderSubagentFeedback( + decideSubagentFeedback(input, { workflow: wf }), + ); + if (rendered) process.stdout.write(rendered); + } }); return; } diff --git a/tests/hooks/subagent-stop.test.ts b/tests/hooks/subagent-stop.test.ts index eb0f684..8ba4069 100644 --- a/tests/hooks/subagent-stop.test.ts +++ b/tests/hooks/subagent-stop.test.ts @@ -11,6 +11,8 @@ import { tmpdir } from "node:os"; import { buildSubagentTaskEvent, tokensFromTranscript, + decideSubagentFeedback, + renderSubagentFeedback, type SubagentStopInput, } from "../../src/hooks/subagent-stop.ts"; @@ -99,3 +101,62 @@ describe("tokensFromTranscript", () => { expect(tokensFromTranscript(p)).toBe(1200); }); }); + +describe("decideSubagentFeedback (v0.41.0)", () => { + const input: SubagentStopInput = { agent_type: "general-purpose", agent_id: "x" }; + + it("warns when an active workflow is at/over 90% of its ceiling", () => { + const msg = decideSubagentFeedback(input, { + workflow: { + workflow_id: "wf-1", + budget_tokens: 1000, + used_tokens: 950, + pct: 95, + }, + }); + expect(msg).not.toBeNull(); + expect(msg).toContain("wf-1"); + expect(msg).toContain("95%"); + expect(msg).toMatch(/wind down/i); + }); + + it("stays silent below 90%", () => { + expect( + decideSubagentFeedback(input, { + workflow: { + workflow_id: "wf-1", + budget_tokens: 1000, + used_tokens: 500, + pct: 50, + }, + }), + ).toBeNull(); + }); + + it("stays silent with no workflow / no budget", () => { + expect(decideSubagentFeedback(input, { workflow: null })).toBeNull(); + expect( + decideSubagentFeedback(input, { + workflow: { + workflow_id: "wf", + budget_tokens: null, + used_tokens: 9999, + pct: null, + }, + }), + ).toBeNull(); + }); +}); + +describe("renderSubagentFeedback", () => { + it("returns null for no message", () => { + expect(renderSubagentFeedback(null)).toBeNull(); + }); + it("wraps a message in SubagentStop hookSpecificOutput", () => { + const out = renderSubagentFeedback("wind down"); + expect(out).not.toBeNull(); + const parsed = JSON.parse(out!); + expect(parsed.hookSpecificOutput.hookEventName).toBe("SubagentStop"); + expect(parsed.hookSpecificOutput.additionalContext).toBe("wind down"); + }); +});