token-pilot 0.29.0 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -4
  3. package/CHANGELOG.md +35 -0
  4. package/README.md +57 -384
  5. package/agents/tp-api-surface-tracker.md +1 -1
  6. package/agents/tp-audit-scanner.md +1 -1
  7. package/agents/tp-commit-writer.md +1 -1
  8. package/agents/tp-context-engineer.md +1 -1
  9. package/agents/tp-dead-code-finder.md +1 -1
  10. package/agents/tp-debugger.md +1 -1
  11. package/agents/tp-dep-health.md +1 -1
  12. package/agents/tp-doc-writer.md +1 -1
  13. package/agents/tp-history-explorer.md +1 -1
  14. package/agents/tp-impact-analyzer.md +1 -1
  15. package/agents/tp-incident-timeline.md +1 -1
  16. package/agents/tp-incremental-builder.md +1 -1
  17. package/agents/tp-migration-scout.md +1 -1
  18. package/agents/tp-onboard.md +1 -1
  19. package/agents/tp-performance-profiler.md +1 -1
  20. package/agents/tp-pr-reviewer.md +1 -1
  21. package/agents/tp-refactor-planner.md +1 -1
  22. package/agents/tp-review-impact.md +1 -1
  23. package/agents/tp-run.md +1 -1
  24. package/agents/tp-session-restorer.md +1 -1
  25. package/agents/tp-ship-coordinator.md +1 -1
  26. package/agents/tp-spec-writer.md +1 -1
  27. package/agents/tp-test-coverage-gapper.md +1 -1
  28. package/agents/tp-test-triage.md +1 -1
  29. package/agents/tp-test-writer.md +1 -1
  30. package/dist/ast-index/client.d.ts +17 -2
  31. package/dist/ast-index/client.js +233 -107
  32. package/dist/cli/tool-audit.d.ts +5 -0
  33. package/dist/cli/tool-audit.js +9 -1
  34. package/dist/core/edit-prep-state.d.ts +42 -0
  35. package/dist/core/edit-prep-state.js +108 -0
  36. package/dist/core/policy-engine.d.ts +1 -5
  37. package/dist/core/policy-engine.js +9 -24
  38. package/dist/handlers/explore-area.js +6 -1
  39. package/dist/handlers/read-for-edit.d.ts +5 -5
  40. package/dist/handlers/read-for-edit.js +188 -110
  41. package/dist/hooks/installer.js +18 -0
  42. package/dist/hooks/pre-bash.d.ts +11 -1
  43. package/dist/hooks/pre-bash.js +51 -1
  44. package/dist/hooks/pre-edit.d.ts +69 -0
  45. package/dist/hooks/pre-edit.js +104 -0
  46. package/dist/hooks/pre-grep.d.ts +12 -1
  47. package/dist/hooks/pre-grep.js +39 -1
  48. package/dist/index.d.ts +30 -0
  49. package/dist/index.js +87 -22
  50. package/dist/server/enforcement-mode.d.ts +47 -0
  51. package/dist/server/enforcement-mode.js +59 -0
  52. package/dist/server/tool-definitions.d.ts +20 -0
  53. package/dist/server/tool-definitions.js +127 -12
  54. package/dist/server/tool-profiles.d.ts +19 -1
  55. package/dist/server/tool-profiles.js +38 -4
  56. package/dist/server.d.ts +2 -0
  57. package/dist/server.js +89 -21
  58. package/docs/agents.md +82 -0
  59. package/docs/configuration.md +117 -0
  60. package/docs/hooks.md +99 -0
  61. package/docs/installation.md +169 -0
  62. package/docs/tools.md +61 -0
  63. package/hooks/hooks.json +18 -0
  64. package/package.json +2 -2
  65. package/start.sh +19 -9
@@ -1,6 +1,26 @@
1
1
  /**
2
2
  * MCP tool definitions and system instructions.
3
3
  * Pure static data — no runtime dependencies.
4
+ *
5
+ * v0.30.0 — Profile-specific instructions. Each profile advertises only
6
+ * the tools it includes; instructions are trimmed to match so the agent
7
+ * doesn't hallucinate tools that aren't in tools/list.
8
+ *
9
+ * minimal — 5 core tools, minimal context overhead
10
+ * nav — 10 exploration tools, no editing
11
+ * edit — nav + 6 edit-prep tools (DEFAULT)
12
+ * full — everything including audit tools
13
+ */
14
+ import type { ToolProfile } from "./tool-profiles.js";
15
+ /**
16
+ * Select MCP instructions for the given tool profile.
17
+ * Each profile only mentions tools that are actually advertised in its
18
+ * tools/list — prevents the agent from calling tools it can't see.
19
+ */
20
+ export declare function getMcpInstructions(profile: ToolProfile): string;
21
+ /**
22
+ * @deprecated Use getMcpInstructions(profile) instead.
23
+ * Kept for backward-compat — resolves to the full profile instructions.
4
24
  */
5
25
  export declare const MCP_INSTRUCTIONS: string;
6
26
  export declare const TOOL_DEFINITIONS: ({
@@ -1,10 +1,58 @@
1
- /**
2
- * MCP tool definitions and system instructions.
3
- * Pure static data — no runtime dependencies.
4
- */
5
- export const MCP_INSTRUCTIONS = [
1
+ // ---------------------------------------------------------------------------
2
+ // Minimal profile 5 essential tools, near-zero instructions overhead
3
+ // ---------------------------------------------------------------------------
4
+ const MCP_INSTRUCTIONS_MINIMAL = [
5
+ "Token Pilot token-efficient code reading. ALWAYS prefer these tools over Read/cat/grep.",
6
+ "",
7
+ "TOOLS:",
8
+ "• smart_read(path) — read a code file (NOT cat/Read — returns structure, 60-80% fewer tokens)",
9
+ "• read_symbol(path, symbol) — read ONE function/class body (NOT the whole file)",
10
+ "• find_usages(symbol) — find where a symbol is defined, imported, or used",
11
+ "• smart_diff — review git changes mapped to functions/classes (NOT git diff)",
12
+ "• smart_log — structured commit history (NOT git log)",
13
+ "",
14
+ "USE Read/Grep ONLY for: non-code configs (JSON, YAML, markdown), regex patterns.",
15
+ ].join("\n");
16
+ // ---------------------------------------------------------------------------
17
+ // Nav profile — exploration only, no edit-prep tools
18
+ // ---------------------------------------------------------------------------
19
+ const MCP_INSTRUCTIONS_NAV = [
20
+ "Token Pilot — token-efficient code reading (saves 60-80% tokens). ALWAYS prefer these tools over Read/cat/grep.",
21
+ "",
22
+ "DECISION RULES — pick the first match:",
23
+ "1. New codebase / unfamiliar project → project_overview",
24
+ "2. Starting work on a directory → explore_area (outline + imports + tests + git log in one call)",
25
+ "3. Need to read a code file → smart_read (NOT Read/cat — returns structure, 60-80% fewer tokens)",
26
+ ' - For navigation/browsing: smart_read(scope="nav") — names + lines only, 2-3x smaller',
27
+ ' - For public API overview: smart_read(scope="exports")',
28
+ "4. Need one function/class body → read_symbol (loads only that symbol, NOT the whole file)",
29
+ "5. Find where a symbol is used → find_usages (semantic: definitions + imports + usages)",
30
+ ' - For initial discovery: find_usages(mode="list") — file:line only, 5-10x smaller',
31
+ "6. Understand file dependencies → related_files (imports, importers, tests — ranked by relevance)",
32
+ "7. List all symbols in a directory → outline (classes, functions, methods in one call)",
33
+ "8. Review git changes → smart_diff (NOT git diff — maps changes to functions/classes)",
34
+ "9. Commit history → smart_log (NOT git log — structured with categories)",
35
+ "10. Module architecture → module_info (deps, dependents, public API)",
36
+ "11. Read markdown/yaml/json/csv section → read_section (loads one heading/key/row-range, NOT the whole file)",
37
+ "12. Long session / before compaction → session_snapshot (<200 token state capture)",
38
+ "",
39
+ "USE Read/Grep ONLY for: regex text search → Grep | exact raw content → Read",
40
+ "",
41
+ "WORKFLOW:",
42
+ "• Explore: project_overview → explore_area → smart_read → read_symbol",
43
+ ].join("\n");
44
+ // ---------------------------------------------------------------------------
45
+ // Edit profile — nav + batch reads + edit-prep (DEFAULT)
46
+ // ---------------------------------------------------------------------------
47
+ const MCP_INSTRUCTIONS_EDIT = [
6
48
  "Token Pilot — token-efficient code reading (saves 60-80% tokens). ALWAYS prefer these tools over Read/cat/grep.",
7
49
  "",
50
+ "MANDATORY EDIT SAFETY — before ANY Edit/Write tool call on an existing code file:",
51
+ " → FIRST call read_for_edit(path, symbol=<target>) to obtain the exact old_string.",
52
+ " → NEVER build Edit's old_string from a smart_read / Read snippet — whitespace and",
53
+ " line-number prefixes diverge from disk and Edit silently mismatches.",
54
+ " → For a brand-new file, Write is fine; read_for_edit is only required for edits.",
55
+ "",
8
56
  "DECISION RULES — pick the first match:",
9
57
  "1. New codebase / unfamiliar project → project_overview",
10
58
  "2. Starting work on a directory → explore_area (outline + imports + tests + git log in one call)",
@@ -14,7 +62,52 @@ export const MCP_INSTRUCTIONS = [
14
62
  "4. Need one function/class body → read_symbol (loads only that symbol, NOT the whole file)",
15
63
  " - Preparing edit? Add include_edit_context=true to skip separate read_for_edit call",
16
64
  "5. Need MULTIPLE function/class bodies from same file → read_symbols (batch — one call instead of N)",
17
- "6. Preparing an edit → read_for_edit (returns exact text for Edit old_string)",
65
+ "6. Preparing an Edit → read_for_edit MANDATORY, not optional. Returns exact old_string.",
66
+ "7. Verify edits after editing → read_diff (only changed hunks — REQUIRES smart_read BEFORE editing)",
67
+ "8. Multiple files at once → smart_read_many (batch up to 20 files)",
68
+ "9. Find where a symbol is used → find_usages (semantic: definitions + imports + usages)",
69
+ ' - For initial discovery: find_usages(mode="list") — file:line only, 5-10x smaller',
70
+ "10. Understand file dependencies → related_files (imports, importers, tests — ranked by relevance)",
71
+ "11. List all symbols in a directory → outline (classes, functions, methods in one call)",
72
+ "12. Review git changes → smart_diff (NOT git diff — maps changes to functions/classes)",
73
+ "13. Commit history → smart_log (NOT git log — structured with categories)",
74
+ "14. Module architecture → module_info (deps, dependents, public API)",
75
+ "15. Read markdown/yaml/json/csv section → read_section (loads one heading/key/row-range, NOT the whole file)",
76
+ ' - For editing sections: read_for_edit(path, section="Section Name")',
77
+ "16. Long session / before compaction → session_snapshot (capture goal, decisions, confirmed facts, files, next step as <200 token block)",
78
+ " - Budget-constrained? Use smart_read(max_tokens=N) to auto-downgrade output size",
79
+ "",
80
+ "USE Read/Grep ONLY for: regex text search → Grep | exact raw content → Read",
81
+ "",
82
+ "WORKFLOWS:",
83
+ "• Explore: project_overview → explore_area → smart_read → read_symbol",
84
+ "• Edit (mandatory): smart_read (to pick target) → read_for_edit → Edit → read_diff",
85
+ "• Docs: smart_read (outline) → read_section → read_for_edit(section=) → Edit → read_diff",
86
+ "• Refactor: find_usages → read_symbols → read_for_edit → Edit",
87
+ "• Long session: session_snapshot → compact context → continue with minimal state",
88
+ ].join("\n");
89
+ // ---------------------------------------------------------------------------
90
+ // Full profile — all tools including audit (code_audit, find_unused, test_summary)
91
+ // ---------------------------------------------------------------------------
92
+ const MCP_INSTRUCTIONS_FULL = [
93
+ "Token Pilot — token-efficient code reading (saves 60-80% tokens). ALWAYS prefer these tools over Read/cat/grep.",
94
+ "",
95
+ "MANDATORY EDIT SAFETY — before ANY Edit/Write tool call on an existing code file:",
96
+ " → FIRST call read_for_edit(path, symbol=<target>) to obtain the exact old_string.",
97
+ " → NEVER build Edit's old_string from a smart_read / Read snippet — whitespace and",
98
+ " line-number prefixes diverge from disk and Edit silently mismatches.",
99
+ " → For a brand-new file, Write is fine; read_for_edit is only required for edits.",
100
+ "",
101
+ "DECISION RULES — pick the first match:",
102
+ "1. New codebase / unfamiliar project → project_overview",
103
+ "2. Starting work on a directory → explore_area (outline + imports + tests + git log in one call)",
104
+ "3. Need to read a code file → smart_read (NOT Read/cat — returns structure, 60-80% fewer tokens)",
105
+ ' - For navigation/browsing: smart_read(scope="nav") — names + lines only, 2-3x smaller',
106
+ ' - For public API overview: smart_read(scope="exports")',
107
+ "4. Need one function/class body → read_symbol (loads only that symbol, NOT the whole file)",
108
+ " - Preparing edit? Add include_edit_context=true to skip separate read_for_edit call",
109
+ "5. Need MULTIPLE function/class bodies from same file → read_symbols (batch — one call instead of N)",
110
+ "6. Preparing an Edit → read_for_edit — MANDATORY, not optional. Returns exact old_string.",
18
111
  "7. Verify edits after editing → read_diff (only changed hunks — REQUIRES smart_read BEFORE editing)",
19
112
  "8. Multiple files at once → smart_read_many (batch up to 20 files)",
20
113
  "9. Find where a symbol is used → find_usages (semantic: definitions + imports + usages)",
@@ -28,20 +121,42 @@ export const MCP_INSTRUCTIONS = [
28
121
  "16. Dead code → find_unused (unreferenced symbols across project)",
29
122
  "17. Module architecture → module_info (deps, dependents, public API)",
30
123
  "18. Read markdown/yaml/json/csv section → read_section (loads one heading/key/row-range, NOT the whole file)",
31
- ' - For editing sections: read_for_edit(path, section="Section Name")',
124
+ ' - For editing sections: read_for_edit(path, section="Section Name")',
32
125
  "19. Long session / before compaction → session_snapshot (capture goal, decisions, confirmed facts, files, next step as <200 token block)",
33
- " - Budget-constrained? Use smart_read(max_tokens=N) to auto-downgrade output size",
126
+ " - Budget-constrained? Use smart_read(max_tokens=N) to auto-downgrade output size",
34
127
  "",
35
- "USE DEFAULT TOOLS ONLY FOR: regex text search → Grep | exact raw content → Read | non-code configs → Read",
128
+ "USE Read/Grep ONLY for: regex text search → Grep | exact raw content → Read | non-code configs → Read",
36
129
  "",
37
130
  "WORKFLOWS:",
38
131
  "• Explore: project_overview → explore_area → smart_read → read_symbol",
39
- "• Edit: smart_read read_symbol(include_edit_context=true) → Edit → read_diff",
132
+ "• Edit (mandatory): smart_read (to pick target) → read_for_edit → Edit → read_diff",
40
133
  "• Docs: smart_read (outline) → read_section → read_for_edit(section=) → Edit → read_diff",
41
134
  "• Refactor: find_usages → read_symbols → read_for_edit → Edit → test_summary",
42
135
  "• Audit: code_audit + find_unused + Grep (for regex patterns)",
43
136
  "• Long session: session_snapshot → compact context → continue with minimal state",
44
137
  ].join("\n");
138
+ /**
139
+ * Select MCP instructions for the given tool profile.
140
+ * Each profile only mentions tools that are actually advertised in its
141
+ * tools/list — prevents the agent from calling tools it can't see.
142
+ */
143
+ export function getMcpInstructions(profile) {
144
+ switch (profile) {
145
+ case "minimal":
146
+ return MCP_INSTRUCTIONS_MINIMAL;
147
+ case "nav":
148
+ return MCP_INSTRUCTIONS_NAV;
149
+ case "edit":
150
+ return MCP_INSTRUCTIONS_EDIT;
151
+ case "full":
152
+ return MCP_INSTRUCTIONS_FULL;
153
+ }
154
+ }
155
+ /**
156
+ * @deprecated Use getMcpInstructions(profile) instead.
157
+ * Kept for backward-compat — resolves to the full profile instructions.
158
+ */
159
+ export const MCP_INSTRUCTIONS = MCP_INSTRUCTIONS_FULL;
45
160
  export const TOOL_DEFINITIONS = [
46
161
  // --- Core reading tools ---
47
162
  {
@@ -477,7 +592,7 @@ export const TOOL_DEFINITIONS = [
477
592
  },
478
593
  {
479
594
  name: "explore_area",
480
- description: "One-call exploration of a directory: outline (all symbols), imports (external deps + who imports this area), tests (matching test files), recent git changes. Use INSTEAD OF separate outline + related_files + git log calls.",
595
+ description: "One-call exploration of a directory: outline (all symbols), imports (external deps + who imports this area), tests (matching test files), recent git changes. Use INSTEAD OF separate outline + related_files + git log calls. Default since v0.30.0 returns only outline+changes — telemetry showed the all-4 default producing negative token reduction for small areas. Opt into imports/tests explicitly via `include` when you need them.",
481
596
  inputSchema: {
482
597
  type: "object",
483
598
  properties: {
@@ -491,7 +606,7 @@ export const TOOL_DEFINITIONS = [
491
606
  type: "string",
492
607
  enum: ["outline", "imports", "tests", "changes"],
493
608
  },
494
- description: "Sections to include (default: all)",
609
+ description: 'Sections to include. Default: ["outline","changes"]. Add "imports" for dep graph, "tests" to map test files — both can be heavy on large areas.',
495
610
  },
496
611
  },
497
612
  required: ["path"],
@@ -21,7 +21,7 @@
21
21
  * Selection: TOKEN_PILOT_PROFILE=nav|edit|full env var. Unknown values
22
22
  * fall back to full with a stderr warning. Silent on missing env.
23
23
  */
24
- export type ToolProfile = "full" | "nav" | "edit";
24
+ export type ToolProfile = "full" | "nav" | "edit" | "minimal";
25
25
  export declare const PROFILE_NAMES: readonly ToolProfile[];
26
26
  /**
27
27
  * Meta-tools — diagnostic / self-observation tools that must be visible
@@ -30,6 +30,12 @@ export declare const PROFILE_NAMES: readonly ToolProfile[];
30
30
  * would you trust the savings number?
31
31
  */
32
32
  export declare const META_TOOLS: ReadonlySet<string>;
33
+ /**
34
+ * Minimal profile — 5 core tools for emergency / context-constrained sessions.
35
+ * Token overhead: tools/list is tiny; instructions are ~80 tokens vs ~350 for full.
36
+ * Use TOKEN_PILOT_PROFILE=minimal when the agent's context budget is nearly full.
37
+ */
38
+ export declare const MINIMAL_TOOLS: ReadonlySet<string>;
33
39
  /** Minimum nav profile — exploration only, no editing support. */
34
40
  export declare const NAV_TOOLS: ReadonlySet<string>;
35
41
  /** Edit profile adds batch reads + edit-preparation tools. */
@@ -48,5 +54,17 @@ export declare function filterToolsByProfile<T extends {
48
54
  * Parse the TOKEN_PILOT_PROFILE env value. Unknown values get a warning
49
55
  * and fall back to full — we never silently apply a guess.
50
56
  */
57
+ /**
58
+ * Parse the TOKEN_PILOT_PROFILE env value.
59
+ *
60
+ * Default changed in v0.30.0: full → edit.
61
+ * Rationale: 'full' was exposing 22 tools + full instruction set on every
62
+ * session, burning ~3 k tokens before any work. 'edit' covers 99% of
63
+ * development workflows (reading + writing code). Switch to 'full' only
64
+ * when you need audit tools (code_audit, find_unused, test_summary).
65
+ *
66
+ * Unknown values fall back to 'edit' with a stderr warning — we never
67
+ * silently apply a guess.
68
+ */
51
69
  export declare function parseProfileEnv(envValue: string | undefined, warn?: (msg: string) => void): ToolProfile;
52
70
  //# sourceMappingURL=tool-profiles.d.ts.map
@@ -25,6 +25,7 @@ export const PROFILE_NAMES = [
25
25
  "full",
26
26
  "nav",
27
27
  "edit",
28
+ "minimal",
28
29
  ];
29
30
  /**
30
31
  * Meta-tools — diagnostic / self-observation tools that must be visible
@@ -37,6 +38,18 @@ export const META_TOOLS = new Set([
37
38
  "session_budget",
38
39
  "session_snapshot",
39
40
  ]);
41
+ /**
42
+ * Minimal profile — 5 core tools for emergency / context-constrained sessions.
43
+ * Token overhead: tools/list is tiny; instructions are ~80 tokens vs ~350 for full.
44
+ * Use TOKEN_PILOT_PROFILE=minimal when the agent's context budget is nearly full.
45
+ */
46
+ export const MINIMAL_TOOLS = new Set([
47
+ "smart_read",
48
+ "read_symbol",
49
+ "find_usages",
50
+ "smart_diff",
51
+ "smart_log",
52
+ ]);
40
53
  /** Minimum nav profile — exploration only, no editing support. */
41
54
  export const NAV_TOOLS = new Set([
42
55
  "smart_read",
@@ -49,6 +62,7 @@ export const NAV_TOOLS = new Set([
49
62
  "explore_area",
50
63
  "smart_log",
51
64
  "smart_diff",
65
+ "read_section", // v0.30.0: section reading is nav-class (read-only, no edit prep)
52
66
  ]);
53
67
  /** Edit profile adds batch reads + edit-preparation tools. */
54
68
  export const EDIT_EXTRAS = new Set([
@@ -73,6 +87,11 @@ export function filterToolsByProfile(tools, profile) {
73
87
  // session_snapshot are the instruments for verifying the profile is
74
88
  // doing its job. Hiding them would turn "did this save tokens?" into
75
89
  // a guess.
90
+ if (profile === "minimal") {
91
+ // Minimal: 5 core tools only. META excluded — keep the footprint tiny.
92
+ // The agent can still call session_analytics by name if it knows it.
93
+ return tools.filter((t) => MINIMAL_TOOLS.has(t.name));
94
+ }
76
95
  if (profile === "nav") {
77
96
  return tools.filter((t) => NAV_TOOLS.has(t.name) || META_TOOLS.has(t.name));
78
97
  }
@@ -85,14 +104,29 @@ export function filterToolsByProfile(tools, profile) {
85
104
  * Parse the TOKEN_PILOT_PROFILE env value. Unknown values get a warning
86
105
  * and fall back to full — we never silently apply a guess.
87
106
  */
107
+ /**
108
+ * Parse the TOKEN_PILOT_PROFILE env value.
109
+ *
110
+ * Default changed in v0.30.0: full → edit.
111
+ * Rationale: 'full' was exposing 22 tools + full instruction set on every
112
+ * session, burning ~3 k tokens before any work. 'edit' covers 99% of
113
+ * development workflows (reading + writing code). Switch to 'full' only
114
+ * when you need audit tools (code_audit, find_unused, test_summary).
115
+ *
116
+ * Unknown values fall back to 'edit' with a stderr warning — we never
117
+ * silently apply a guess.
118
+ */
88
119
  export function parseProfileEnv(envValue, warn = () => { }) {
89
120
  if (!envValue)
90
- return "full";
121
+ return "edit";
91
122
  const lower = envValue.trim().toLowerCase();
92
- if (lower === "full" || lower === "nav" || lower === "edit") {
123
+ if (lower === "full" ||
124
+ lower === "nav" ||
125
+ lower === "edit" ||
126
+ lower === "minimal") {
93
127
  return lower;
94
128
  }
95
- warn(`[token-pilot] Unknown TOKEN_PILOT_PROFILE="${envValue}". Expected full|nav|edit. Falling back to full.`);
96
- return "full";
129
+ warn(`[token-pilot] Unknown TOKEN_PILOT_PROFILE="${envValue}". Expected full|nav|edit|minimal. Falling back to edit.`);
130
+ return "edit";
97
131
  }
98
132
  //# sourceMappingURL=tool-profiles.js.map
package/dist/server.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { type EnforcementMode } from "./server/enforcement-mode.js";
2
3
  export declare function createServer(projectRoot: string, options?: {
3
4
  skipAstIndex?: boolean;
5
+ enforcementMode?: EnforcementMode;
4
6
  }): Promise<Server<{
5
7
  method: string;
6
8
  params?: {
package/dist/server.js CHANGED
@@ -45,11 +45,13 @@ import { detectContextMode } from "./integration/context-mode-detector.js";
45
45
  import { estimateTokens } from "./core/token-estimator.js";
46
46
  import { checkPolicy, isFullReadTool } from "./core/policy-engine.js";
47
47
  import { appendToolCall } from "./core/tool-call-log.js";
48
- import { MCP_INSTRUCTIONS, TOOL_DEFINITIONS, } from "./server/tool-definitions.js";
48
+ import { getMcpInstructions, TOOL_DEFINITIONS, } from "./server/tool-definitions.js";
49
49
  import { filterToolsByProfile, parseProfileEnv, } from "./server/tool-profiles.js";
50
+ import { STRICT_SMART_READ_MAX_TOKENS, STRICT_EXPLORE_AREA_INCLUDE, } from "./server/enforcement-mode.js";
50
51
  import { createTokenEstimates } from "./server/token-estimates.js";
51
52
  import { validateSmartReadArgs, validateReadSymbolArgs, validateReadSymbolsArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, validateCodeAuditArgs, validateProjectOverviewArgs, validateModuleInfoArgs, validateSmartDiffArgs, validateExploreAreaArgs, validateSmartLogArgs, validateTestSummaryArgs, validateReadSectionArgs, } from "./core/validation.js";
52
53
  export async function createServer(projectRoot, options) {
54
+ const mode = options?.enforcementMode ?? "deny";
53
55
  const config = await loadConfig(projectRoot);
54
56
  const astIndex = new AstIndexClient(projectRoot, config.astIndex.timeout, {
55
57
  binaryPath: config.astIndex.binaryPath,
@@ -67,6 +69,10 @@ export async function createServer(projectRoot, options) {
67
69
  // entirely; callers that care about durability should not use it.
68
70
  const shutdownFlush = () => {
69
71
  void sessionRegistries.flushAll();
72
+ // Stop the 5-minute ast-index tick so we don't block exit on SIGINT/SIGTERM.
73
+ // .unref() already makes it non-keeping, but clearing is defensive and
74
+ // avoids a stray `update` firing during shutdown.
75
+ astIndex.stopPeriodicUpdate();
70
76
  };
71
77
  process.once("beforeExit", shutdownFlush);
72
78
  process.once("SIGINT", shutdownFlush);
@@ -193,7 +199,6 @@ export async function createServer(projectRoot, options) {
193
199
  let fullFileReadsCount = 0;
194
200
  let totalCallCount = 0;
195
201
  let totalTokensReturned = 0;
196
- const readForEditCalled = new Set();
197
202
  // Detect context-mode companion
198
203
  const cmEnabled = config.contextMode.enabled;
199
204
  const contextModeStatus = await detectContextMode(projectRoot, cmEnabled === "auto" ? undefined : cmEnabled);
@@ -221,13 +226,25 @@ export async function createServer(projectRoot, options) {
221
226
  fileWatcher.onAstUpdate(() => sessionCache.invalidateByAst());
222
227
  }
223
228
  }
224
- // Wire session cache to git watcher
225
- if (sessionCache) {
226
- gitWatcher.onBranchSwitchEvent((changedFiles) => {
229
+ // Wire git-watcher → session cache + AST index.
230
+ // Always registers — even without sessionCache — so branch-switch still
231
+ // triggers the index update. Without this the index went stale on every
232
+ // `git checkout` until the next file-touch (or never, for branches that
233
+ // only moved files the agent hadn't read yet).
234
+ gitWatcher.onBranchSwitchEvent((changedFiles) => {
235
+ if (sessionCache) {
227
236
  sessionCache.invalidateByFiles(changedFiles);
228
237
  sessionCache.invalidateByGit();
229
- });
230
- }
238
+ }
239
+ // Fire-and-forget. incrementalUpdate self-guards against
240
+ // disabled / oversized / uninitialised index states.
241
+ void astIndex.incrementalUpdate();
242
+ });
243
+ // 5-minute safety-net for long sessions where FileWatcher may miss events
244
+ // (Docker bind mounts, NFS, files mutated by sibling processes). Cheap —
245
+ // each tick is a single `ast-index update` call that bails early if the
246
+ // index isn't ready or the previous tick is still running.
247
+ astIndex.startPeriodicUpdate();
231
248
  // Read version from package.json
232
249
  let pkgVersion = "0.1.1";
233
250
  try {
@@ -238,18 +255,20 @@ export async function createServer(projectRoot, options) {
238
255
  catch {
239
256
  /* fallback to hardcoded */
240
257
  }
258
+ // v0.26.3 — tool profiles. TOKEN_PILOT_PROFILE=nav|edit|full|minimal
259
+ // (default: edit since v0.30.0) trims the advertised tools/list payload.
260
+ // Handlers stay live, so a subagent that explicitly names a filtered-out
261
+ // tool still gets a response — we just don't brag about every tool upfront.
262
+ // v0.30.0 — profile also selects matching MCP instructions so the agent
263
+ // doesn't see rules for tools that aren't in its tools/list.
264
+ const activeProfile = parseProfileEnv(process.env.TOKEN_PILOT_PROFILE, (m) => process.stderr.write(m + "\n"));
241
265
  const server = new Server({ name: "token-pilot", version: pkgVersion }, {
242
266
  capabilities: { tools: {} },
243
- instructions: MCP_INSTRUCTIONS,
267
+ instructions: getMcpInstructions(activeProfile),
244
268
  });
245
- // v0.26.3 — tool profiles. TOKEN_PILOT_PROFILE=nav|edit|full (default
246
- // full) trims the advertised tools/list payload. Handlers stay live,
247
- // so a subagent that explicitly names a filtered-out tool still gets
248
- // a response — we just don't brag about every tool upfront.
249
- const activeProfile = parseProfileEnv(process.env.TOKEN_PILOT_PROFILE, (m) => process.stderr.write(m + "\n"));
250
269
  const advertisedTools = filterToolsByProfile(TOOL_DEFINITIONS, activeProfile);
251
- if (activeProfile !== "full") {
252
- process.stderr.write(`[token-pilot] Profile: ${activeProfile} — advertising ${advertisedTools.length}/${TOOL_DEFINITIONS.length} tools. Unset TOKEN_PILOT_PROFILE for the full set.\n`);
270
+ if (activeProfile !== "edit") {
271
+ process.stderr.write(`[token-pilot] Profile: ${activeProfile} — advertising ${advertisedTools.length}/${TOOL_DEFINITIONS.length} tools. Set TOKEN_PILOT_PROFILE=edit for the default set.\n`);
253
272
  }
254
273
  server.setRequestHandler(ListToolsRequestSchema, () => ({
255
274
  tools: advertisedTools,
@@ -289,14 +308,10 @@ export async function createServer(projectRoot, options) {
289
308
  if (isFullReadTool(rest.tool)) {
290
309
  fullFileReadsCount++;
291
310
  }
292
- if (rest.tool === "read_for_edit" && call.path) {
293
- readForEditCalled.add(call.path);
294
- }
295
311
  // Policy check
296
312
  const advisory = checkPolicy(config.policies, rest.tool, {
297
313
  fullFileReadsCount,
298
314
  tokensReturned: rest.tokensReturned,
299
- readForEditCalled,
300
315
  totalCallCount,
301
316
  totalTokensReturned,
302
317
  });
@@ -331,6 +346,14 @@ export async function createServer(projectRoot, options) {
331
346
  switch (name) {
332
347
  case "smart_read": {
333
348
  const validArgs = validateSmartReadArgs(args);
349
+ // v0.30.0 strict mode: cap max_tokens when caller didn't set it.
350
+ let strictReadCapNote;
351
+ if (mode === "strict" && validArgs.max_tokens === undefined) {
352
+ validArgs.max_tokens = STRICT_SMART_READ_MAX_TOKENS;
353
+ strictReadCapNote =
354
+ `\n\n[token-pilot strict] Output capped at ${STRICT_SMART_READ_MAX_TOKENS} tokens ` +
355
+ `(TOKEN_PILOT_MODE=strict). Pass max_tokens explicitly to override.`;
356
+ }
334
357
  const picked = pickRegistry(args);
335
358
  // Try non-code handler for JSON/YAML/MD etc.
336
359
  if (isNonCodeStructured(validArgs.path)) {
@@ -373,8 +396,9 @@ export async function createServer(projectRoot, options) {
373
396
  absPath: resolve(projectRoot, validArgs.path),
374
397
  args: validArgs,
375
398
  });
376
- if (policyAdv)
377
- result.content[0] = { type: "text", text: text + policyAdv };
399
+ const srSuffix = (policyAdv ?? "") + (strictReadCapNote ?? "");
400
+ if (srSuffix)
401
+ result.content[0] = { type: "text", text: text + srSuffix };
378
402
  return result;
379
403
  }
380
404
  case "read_symbol": {
@@ -527,6 +551,15 @@ export async function createServer(projectRoot, options) {
527
551
  }
528
552
  case "find_usages": {
529
553
  const usagesArgs = validateFindUsagesArgs(args);
554
+ // v0.30.0 strict mode: default mode to "list" when caller didn't set it.
555
+ // Injected before cache lookup so the key matches strict-mode cached results.
556
+ let strictFuNote;
557
+ if (mode === "strict" && usagesArgs.mode === undefined) {
558
+ usagesArgs.mode = "list";
559
+ strictFuNote =
560
+ `\n\n[token-pilot strict] find_usages mode defaulted to "list" ` +
561
+ `(TOKEN_PILOT_MODE=strict). Pass mode explicitly to override.`;
562
+ }
530
563
  const cachedUsages = sessionCache?.get("find_usages", usagesArgs);
531
564
  if (cachedUsages) {
532
565
  recordWithTrace({
@@ -558,6 +591,12 @@ export async function createServer(projectRoot, options) {
558
591
  savingsCategory: "compression",
559
592
  args: usagesArgs,
560
593
  });
594
+ if (strictFuNote && usagesResult.content[0]) {
595
+ usagesResult.content[0] = {
596
+ type: "text",
597
+ text: usagesText + strictFuNote,
598
+ };
599
+ }
561
600
  return usagesResult;
562
601
  }
563
602
  case "project_overview": {
@@ -801,6 +840,15 @@ export async function createServer(projectRoot, options) {
801
840
  }
802
841
  case "explore_area": {
803
842
  const eaArgs = validateExploreAreaArgs(args);
843
+ // v0.30.0 strict mode: default include to outline-only when caller didn't set it.
844
+ // Injected before cache lookup so the key matches strict-mode cached results.
845
+ let strictEaCapNote;
846
+ if (mode === "strict" && eaArgs.include === undefined) {
847
+ eaArgs.include = STRICT_EXPLORE_AREA_INCLUDE;
848
+ strictEaCapNote =
849
+ `\n\n[token-pilot strict] include defaulted to ["outline"] ` +
850
+ `(TOKEN_PILOT_MODE=strict). Pass include explicitly to override.`;
851
+ }
804
852
  const cachedEa = sessionCache?.get("explore_area", eaArgs);
805
853
  if (cachedEa) {
806
854
  recordWithTrace({
@@ -833,10 +881,24 @@ export async function createServer(projectRoot, options) {
833
881
  savingsCategory: "compression",
834
882
  args: eaArgs,
835
883
  });
884
+ if (strictEaCapNote && eaResult.content[0]) {
885
+ eaResult.content[0] = {
886
+ type: "text",
887
+ text: eaText + strictEaCapNote,
888
+ };
889
+ }
836
890
  return eaResult;
837
891
  }
838
892
  case "smart_log": {
839
893
  const slArgs = validateSmartLogArgs(args);
894
+ // v0.30.0 strict mode: bound count to 20 when caller didn't set it.
895
+ let strictSlNote;
896
+ if (mode === "strict" && slArgs.count === undefined) {
897
+ slArgs.count = 20;
898
+ strictSlNote =
899
+ `\n\n[token-pilot strict] smart_log count defaulted to 20 ` +
900
+ `(TOKEN_PILOT_MODE=strict). Pass count explicitly to override.`;
901
+ }
840
902
  const slResult = await handleSmartLog(slArgs, projectRoot);
841
903
  const slText = slResult.content[0]?.text ?? "";
842
904
  const slTokens = estimateTokens(slText);
@@ -849,6 +911,12 @@ export async function createServer(projectRoot, options) {
849
911
  savingsCategory: "compression",
850
912
  args: slArgs,
851
913
  });
914
+ if (strictSlNote && slResult.content[0]) {
915
+ slResult.content[0] = {
916
+ type: "text",
917
+ text: slText + strictSlNote,
918
+ };
919
+ }
852
920
  return { content: slResult.content };
853
921
  }
854
922
  case "test_summary": {
package/docs/agents.md ADDED
@@ -0,0 +1,82 @@
1
+ # tp-* Subagents (Claude Code only)
2
+
3
+ `tp-*` subagents are a Claude Code feature. Other clients get the MCP tools + hooks but cannot invoke subagents. Each agent carries an explicit `model:` field in its frontmatter; the budget is enforced post-response — overshoots beyond 10% land in `.token-pilot/over-budget.log`.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npx token-pilot install-agents --scope=user # all projects
9
+ npx token-pilot install-agents --scope=project # this repo only
10
+ npx token-pilot install-agents --scope=user --force # re-apply after an update
11
+ npx token-pilot uninstall-agents --scope=user|project
12
+ ```
13
+
14
+ `init` offers to install these; to add them to another project run `npx token-pilot install-agents`.
15
+
16
+ ## Tier 1 — Workhorses (invoke proactively)
17
+
18
+ | Agent | When to invoke | Budget |
19
+ |-------|---------------|-------:|
20
+ | `tp-run` | General MCP-first workhorse; use when no specialised agent fits | 800 |
21
+ | `tp-onboard` | Orient to an unfamiliar repo (layout, entry points, modules) | 600 |
22
+ | `tp-pr-reviewer` | Review a diff / PR / changeset; verdict-first, Critical/Important tiers | 600 |
23
+ | `tp-impact-analyzer` | Trace blast-radius of a change (callers, transitive deps) | 400 |
24
+ | `tp-refactor-planner` | Plan a refactor with exact edit context per step | 500 |
25
+ | `tp-test-triage` | Investigate test failures → root cause → minimal fix | 500 |
26
+
27
+ ## Tier 2 — Specialists
28
+
29
+ | Agent | When to invoke | Budget |
30
+ |-------|---------------|-------:|
31
+ | `tp-debugger` | Stack trace / error → root-cause line via call-tree traversal | 700 |
32
+ | `tp-migration-scout` | Pre-migration impact map grouped by effort class | 800 |
33
+ | `tp-test-writer` | Write tests for ONE symbol, mirrors project style, runs tests | 900 |
34
+ | `tp-dead-code-finder` | Cross-checked dead-code detection, output-only (never deletes) | 600 |
35
+ | `tp-commit-writer` | Draft Conventional-Commit from staged diff; refuses failing tests | 400 |
36
+ | `tp-history-explorer` | "Why is this like this?" — minimum commit chain explaining current state | 600 |
37
+ | `tp-audit-scanner` | Read-only security / quality audit; Critical / Important / Minor findings | 800 |
38
+ | `tp-session-restorer` | Rehydrate state after /clear or compaction from latest snapshot | 400 |
39
+
40
+ ## Tier 3 — Combo / Workflow
41
+
42
+ | Agent | When to invoke | Budget |
43
+ |-------|---------------|-------:|
44
+ | `tp-review-impact` | Pre-merge blast-radius review (diff × dependents × API surface) | 700 |
45
+ | `tp-test-coverage-gapper` | Find symbols with zero test references, prioritised | 500 |
46
+ | `tp-api-surface-tracker` | Public API diff vs last release → MAJOR / MINOR / PATCH verdict | 600 |
47
+ | `tp-dep-health` | Dep audit: stale × heavily-used × removable | 600 |
48
+ | `tp-incident-timeline` | Correlate an incident window with commits, rank likely culprits | 700 |
49
+
50
+ ## Tier 4 — Methodology
51
+
52
+ | Agent | When to invoke | Budget |
53
+ |-------|---------------|-------:|
54
+ | `tp-context-engineer` | Audit / write CLAUDE.md / AGENTS.md rules files per project | 800 |
55
+ | `tp-spec-writer` | Pre-code spec with gated workflow; surfaces assumptions before code | 900 |
56
+ | `tp-performance-profiler` | Measure → identify → fix → verify → guard; refuses to optimise without data | 800 |
57
+ | `tp-incremental-builder` | Multi-file feature work in thin vertical slices, test between each | 900 |
58
+ | `tp-doc-writer` | ADRs + READMEs + API docs; documents *why* not *what* | 700 |
59
+ | `tp-ship-coordinator` | 5-pillar pre-launch checklist (quality / security / observability / rollback / rollout) | 800 |
60
+
61
+ ## Model Tiers
62
+
63
+ Every agent carries an explicit `model:` field:
64
+
65
+ | Model | Count | Used for |
66
+ |-------|------:|---------|
67
+ | `haiku` | 9 | Structured / format-bound output (commit messages, onboarding maps, ADRs, session briefings) |
68
+ | `sonnet` | 15 | Reasoning tasks (review, debug, test, plan, audit, spec, profile, ship) |
69
+ | `inherit` | 1 | Deep correlation needing the main thread's model (`tp-incident-timeline`) |
70
+
71
+ Under Opus 4.7's +35% tokenizer tax, keeping the majority of agent spawns on haiku/sonnet saves 5–10× model cost vs an all-Opus baseline.
72
+
73
+ ## Third-party Agent Integration (bless-agents)
74
+
75
+ For third-party agents (e.g. `acc-*` plugins) whose tool allowlist excludes token-pilot MCP:
76
+
77
+ ```bash
78
+ npx token-pilot bless-agents # add token-pilot MCP to project-level overrides
79
+ npx token-pilot unbless-agents <name>... | --all
80
+ ```
81
+
82
+ `doctor` warns when the original agent has changed since blessing.