diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ed192f2..9d02f3c 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,14 +6,14 @@ }, "metadata": { "description": "Token Pilot — save 60-90% tokens when AI reads code", - "version": "0.50.0" + "version": "0.50.1" }, "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. 25 MCP tools + 25 subagents + budget watchdog hooks.", - "version": "0.50.0", + "version": "0.50.1", "author": { "name": "Digital-Threads" }, diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 65c9f22..917bbc7 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "token-pilot", - "version": "0.50.0", + "version": "0.50.1", "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/CHANGELOG.md b/CHANGELOG.md index 9261d45..7cf07cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to Token Pilot will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.50.1] - 2026-07-26 + +### Fixed — two guard-clause defects in dispatch routing + +Both found by tp-run agents auditing the 0.50.0 change, and both in the +code that change introduced. + +**An empty description switched prompt-matching back off.** The +blank-input guard tested `description` rather than the haystack built one +line above it, so a dispatch carrying an empty description and a fully +descriptive prompt returned early with soft advice — skipping escape +detection, agent matching and blocking. That is exactly the case +prompt-matching was added to catch. The guard now tests the haystack. + +**A non-string description threw.** `description` was read with `?? ""` +while `prompt` was type-guarded. A number survives `!description`, has no +`.length`, and reached `containsEscape` as a non-string, where +`.toLowerCase()` throws. Hook input is external data; it is now +type-guarded the same way the prompt is. + ## [0.50.0] - 2026-07-26 ### Fixed — dispatch routing now enforces instead of suggesting diff --git a/agents/tp-api-surface-tracker.md b/agents/tp-api-surface-tracker.md index 31123c8..31adbc4 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: dd184501203fa7f3c73f419c4ffbe33c4be75400cb64a7a51733a3fe23f6e085 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-audit-scanner.md b/agents/tp-audit-scanner.md index d7f72c1..29bfabe 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: d172f600bf32277ea6eb4cbbee4542ddd698a986dcd96997d33930561964569b requiredMcpServers: - "token-pilot" diff --git a/agents/tp-commit-writer.md b/agents/tp-commit-writer.md index e5285e7..9793d23 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: de64a406b5176de19f7422619c7de7949b1f28865f225402c9cea9255f377428 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-context-engineer.md b/agents/tp-context-engineer.md index 1b642aa..818ecf9 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.50.0" +token_pilot_version: "0.50.1" 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 e4acfa9..d58ecfb 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: d9b7f5b7ae6f4ae21305c775361bcab097cc774370a6d976c093571d46d55021 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-debugger.md b/agents/tp-debugger.md index d7962f3..fcbbf3b 100644 --- a/agents/tp-debugger.md +++ b/agents/tp-debugger.md @@ -12,7 +12,7 @@ tools: - Read - Bash model: sonnet -token_pilot_version: "0.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: 052413de8d92377edcde6ae5c823f5378db304baccfa29e8866467f42553a500 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-dep-health.md b/agents/tp-dep-health.md index 16c6659..aabecb7 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: e14dc57493d816f8c2e017963e2ef5f66bea50fd0b805a80e8a0d97c968427e7 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-doc-writer.md b/agents/tp-doc-writer.md index cc06bb2..879b81c 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: 57d741794ab40e31a7ac49c68ea39a9088f5827cdef866ce81bfca1b7c9180cf requiredMcpServers: - "token-pilot" diff --git a/agents/tp-history-explorer.md b/agents/tp-history-explorer.md index f587721..7591bf9 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: 7b70fa76a60e3c58a1de4f56c32c0f166424137e203a0cf1c8654e7c9235d904 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-impact-analyzer.md b/agents/tp-impact-analyzer.md index fad3317..c2f009e 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: 351a987e11eba63852f5431a16d8eb53104f4f689f82fdcc5a2bf4db948ba92f requiredMcpServers: - "token-pilot" diff --git a/agents/tp-incident-timeline.md b/agents/tp-incident-timeline.md index d8bd4c0..1d754e3 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: de5722bfea374eaab096c1ae635c37879e7a91370ee3cd0532f4240be03c91eb requiredMcpServers: - "token-pilot" diff --git a/agents/tp-incremental-builder.md b/agents/tp-incremental-builder.md index 55f2a87..257f531 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: 375a824d0d847bb5453ec594c7a62ad566ee7e4d92717b0473f771f1a0477c60 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-migration-scout.md b/agents/tp-migration-scout.md index f0aa91b..cf7e035 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: 0334de1bf99b431b65359637d125cda7c44c6f780eb92c57cc538715b1939536 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-onboard.md b/agents/tp-onboard.md index 0bba0a4..8455633 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: 832e95633fbc8e9b0c10f3e540a327d4be062fb4b3f17a6cce6be13f414e2927 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-performance-profiler.md b/agents/tp-performance-profiler.md index c53f2a5..b674340 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: b61f06380d80798fa2e49d37bcba0653495bee04dd6bdbc1feff9a75607b0508 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-pr-reviewer.md b/agents/tp-pr-reviewer.md index c1c7a34..660da8b 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: f83f50d05b4f70285ae7afed2b1a406fc436df56e61a0aedbfb31edc7f2b6e66 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-refactor-planner.md b/agents/tp-refactor-planner.md index 6b5e930..5190949 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: c5f6fc122c89e16e5cf774045f92169ee3468555320b898171ba13eca5323550 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-review-impact.md b/agents/tp-review-impact.md index e77b745..2461884 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: 8ef3c3341cbfed4eb8dd130126a9683edc57e378c92ff0ca764d584fd941c55c requiredMcpServers: - "token-pilot" diff --git a/agents/tp-run.md b/agents/tp-run.md index 8ebae82..d9e9a70 100644 --- a/agents/tp-run.md +++ b/agents/tp-run.md @@ -16,7 +16,7 @@ tools: - Glob - Bash model: haiku -token_pilot_version: "0.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: 2b08618d34a61f00aafccbda9fed6d83243296dedb83440edbd2d5c28bb6dbc4 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-session-restorer.md b/agents/tp-session-restorer.md index 2a4b732..136ecba 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: 529374ed728f5eed5b758b3be3da65624783c0bf0c1a253d7d661a843eb5f767 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-ship-coordinator.md b/agents/tp-ship-coordinator.md index 73097b4..0a492fe 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: a60f6ae110eb3138064bce074e8ba26fa0ce5f4659df1624a9d9d3646803391b requiredMcpServers: - "token-pilot" diff --git a/agents/tp-spec-writer.md b/agents/tp-spec-writer.md index 4ee5547..c7715a3 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.50.0" +token_pilot_version: "0.50.1" 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 23b81c4..1f56881 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: be81eed53a3720d146cf89e4a14a7a56577633f7c84c234c412ab70d64c05b11 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-test-triage.md b/agents/tp-test-triage.md index 0a0117e..f75ea9a 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: 362ecf4cb03b059421ea26933473700900073dc38b3a7fe271208dfb1ae14f90 requiredMcpServers: - "token-pilot" diff --git a/agents/tp-test-writer.md b/agents/tp-test-writer.md index c803159..a29a0af 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.50.0" +token_pilot_version: "0.50.1" token_pilot_body_hash: 269f2fe22ff4517c277d3f56ca67d8a5527b93290ab21079a83ba7af22c1b5a9 requiredMcpServers: - "token-pilot" diff --git a/package-lock.json b/package-lock.json index c9d18c8..a43db95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "token-pilot", - "version": "0.50.0", + "version": "0.50.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "token-pilot", - "version": "0.50.0", + "version": "0.50.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 22f8dea..97b8b7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "token-pilot", - "version": "0.50.0", + "version": "0.50.1", "description": "Save up to 80% tokens when AI reads code \u2014 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/server.json b/server.json index 59de379..191bc43 100644 --- a/server.json +++ b/server.json @@ -6,12 +6,12 @@ "url": "https://github.com/Digital-Threads/token-pilot", "source": "github" }, - "version": "0.50.0", + "version": "0.50.1", "packages": [ { "registryType": "npm", "identifier": "token-pilot", - "version": "0.50.0", + "version": "0.50.1", "transport": { "type": "stdio" }, diff --git a/src/hooks/pre-task.ts b/src/hooks/pre-task.ts index c5885e5..624351b 100644 --- a/src/hooks/pre-task.ts +++ b/src/hooks/pre-task.ts @@ -109,7 +109,13 @@ export function decidePreTask( if (input.tool_name !== "Task") return { kind: "allow" }; const subagentType = input.tool_input?.subagent_type ?? ""; - const description = input.tool_input?.description ?? ""; + // Type-guarded, not just defaulted: a non-string description survives + // `!description`, has no `.length`, and reaches containsEscape as a + // non-string — which throws. Hook input is external data. + const description = + typeof input.tool_input?.description === "string" + ? input.tool_input.description + : ""; // Already a tp-* — routing intent matches catalog. Let it run. if (typeof subagentType === "string" && subagentType.startsWith("tp-")) { @@ -142,9 +148,12 @@ export function decidePreTask( typeof input.tool_input?.prompt === "string" ? input.tool_input.prompt : ""; const haystack = prompt ? `${description} ${prompt}` : description; - // No description → nothing to match against. Inject the generic - // tool-guide so the subagent still picks tp-tools (B14). - if (!description || description.length === 0) { + // Nothing to match against at all. Tests the haystack, not the + // description: a dispatch can carry an empty description and a fully + // descriptive prompt, and that is precisely the case prompt-matching + // was added for. Guarding on description alone returned soft advice + // and skipped escape detection, matching and blocking entirely. + if (haystack.trim().length === 0) { return { kind: "advise", message: SUBAGENT_TOOL_GUIDE }; } diff --git a/tests/hooks/pre-task-guards.test.ts b/tests/hooks/pre-task-guards.test.ts new file mode 100644 index 0000000..31fa444 --- /dev/null +++ b/tests/hooks/pre-task-guards.test.ts @@ -0,0 +1,92 @@ +/** + * v0.50.1 — two defects in decidePreTask's guard clauses, both found by + * tp-run agents auditing the v0.50.0 change. + * + * 1. The blank-description guard tested `description` rather than the + * haystack built one line above it. A dispatch with an empty + * description but a substantive prompt therefore returned early with + * soft advice, skipping escape detection, matching and blocking — the + * exact case prompt-matching was added to catch. + * + * 2. `description` was read with `?? ""` while `prompt` was type-guarded. + * A non-string description (say a number) survives `!description` and + * has no `.length`, so it reached the matcher as a non-string. + */ +import { describe, expect, it } from "vitest"; +import { decidePreTask, type PreTaskInput } from "../../src/hooks/pre-task.ts"; +import { parseAgent, type AgentIndex } from "../../src/core/agent-matcher.ts"; + +const index: AgentIndex = { + agents: [ + parseAgent( + "tp-pr-reviewer", + `--- +name: tp-pr-reviewer +description: PROACTIVELY use this when the user asks to review a diff, PR, commit range, or changeset ("review these changes", "look at my PR", "is this safe to merge"). +--- +`, + )!, + ], +}; + +const ctx = { mode: "deny" as const, agentIndex: index, force: false }; + +function task(tool_input: Record): PreTaskInput { + return { tool_name: "Task", tool_input: tool_input as never }; +} + +describe("decidePreTask guard clauses", () => { + it("matches on the prompt when the description is empty", () => { + const d = decidePreTask( + task({ + subagent_type: "general-purpose", + description: "", + prompt: "review these changes before merge", + }), + ctx, + ); + expect(d.kind).toBe("deny"); + if (d.kind === "deny") expect(d.reason).toContain("tp-pr-reviewer"); + }); + + it("honours an escape phrase carried only by the prompt", () => { + const d = decidePreTask( + task({ + subagent_type: "general-purpose", + description: "", + prompt: "ad-hoc: review these changes however you like", + }), + ctx, + ); + expect(d.kind).toBe("advise"); + }); + + it("still advises when both description and prompt are empty", () => { + const d = decidePreTask( + task({ subagent_type: "general-purpose", description: "", prompt: "" }), + ctx, + ); + expect(d.kind).toBe("advise"); + }); + + it("treats a non-string description as absent instead of matching on it", () => { + const d = decidePreTask( + task({ subagent_type: "general-purpose", description: 42 }), + ctx, + ); + expect(d.kind).toBe("advise"); + }); + + it("survives a non-string prompt", () => { + const d = decidePreTask( + task({ + subagent_type: "general-purpose", + description: "review these changes", + prompt: { not: "a string" }, + }), + ctx, + ); + // The description alone still carries the match; the odd prompt is dropped. + expect(d.kind).toBe("deny"); + }); +});