@aeondave/pi-skill-optimizer 3.0.1 → 3.1.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.
package/BENCHMARK.md CHANGED
@@ -62,7 +62,7 @@ The benchmark asks:
62
62
  2. Is the stable base byte-identical across unrelated human requests?
63
63
  3. Does bounded prefetch improve first-turn recall without changing that base?
64
64
  4. Can `skill_search` recover relevant tail skills through bounded pagination?
65
- 5. Can `tool_search` defer schemas without making required tools unavailable?
65
+ 5. Can `optimizer_tool_search` defer schemas without making required tools unavailable?
66
66
  6. Do output and history reductions preserve protected evidence and exact recovery?
67
67
  7. Is every accepted serialized transformation smaller?
68
68
  8. What do providers report for tokens, cache reads, cache writes, and cost?
@@ -174,7 +174,7 @@ Blocking invariants:
174
174
 
175
175
  - core, configured, and already used tools remain active;
176
176
  - inactive schemas are absent from the initial provider request;
177
- - `tool_search` remains active while searchable definitions remain;
177
+ - `optimizer_tool_search` remains active while searchable definitions remain;
178
178
  - activation makes selected tools callable in the same session;
179
179
  - activation is additive and cannot strand an active provider tool loop;
180
180
  - schema indexing work and response pagination remain bounded;
@@ -188,7 +188,7 @@ Reported metrics:
188
188
  tool_recall
189
189
  initial_tool_schema_bytes
190
190
  activated_tool_schema_bytes
191
- tool_search_calls
191
+ optimizer_tool_search_calls
192
192
  activation_failures
193
193
  ```
194
194
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  All notable changes are documented here.
4
4
 
5
+ ## Unreleased
6
+
7
+ ## 3.1.1 - 2026-10-08
8
+
9
+ ### Fixed
10
+
11
+ - Optimize the native Pi transcript before provider conversion, keeping worker notifications, shell
12
+ output and summaries out of human-intent routing and prefetch insertion.
13
+ - Preserve structured system sections and tool declarations; respect provider scoping when the
14
+ request has no selected model.
15
+ - Label prefetch as catalog metadata, not loaded instructions or usage evidence. Emit its actual
16
+ catalog fingerprint and report selected skills only when an overlay was inserted.
17
+ - Recognize native Pi skill-activation calls without double-counting `skill_search` usage.
18
+
19
+ ## 3.1.0 - 2026-10-03
20
+
21
+ ### Breaking
22
+
23
+ - Rename the optimizer's model-callable `tool_search` to `optimizer_tool_search`. Explicit tool calls must use the new name. Pi's built-in `tool_search` now loads alongside the optimizer without a replacement warning.
24
+
25
+ ### Fixed
26
+
27
+ - Preserve an active built-in `tool_search` during optimizer replanning.
28
+ - Retain tools activated later by Pi or another extension, and honor `alwaysTools` for initially inactive `deferred` or `codemode` tools while respecting hidden and inactive direct tools.
29
+ - Capture an explicitly requested benchmark catalog from Pi 1.0's `before_agent_start` skill options.
30
+
31
+ ### Changed
32
+
33
+ - Require Pi 1.0.0 or newer for this release.
34
+ - Run typechecking and host integration tests against Pi 1.0.0 development packages.
35
+ - Add a reproducible Pi CLI black-box smoke test for the packed artifact.
36
+
5
37
  ## 3.0.1 - 2026-09-30
6
38
 
7
39
  ### Fixed
package/README.md CHANGED
@@ -14,7 +14,7 @@ No embedding service or extra model call for ordinary routing. No changes to you
14
14
 
15
15
  ## Install
16
16
 
17
- Requires **Pi 0.87.1+** and **Node.js 26.3+**. Upgrade both before installing v3.
17
+ Requires **Pi 1.0.0+** and **Node.js 26.3+**. Upgrade both before installing v3.1.
18
18
 
19
19
  ```bash
20
20
  pi install npm:@aeondave/pi-skill-optimizer
@@ -26,19 +26,18 @@ Restart Pi, then check that it loaded:
26
26
  /skill-optimizer
27
27
  ```
28
28
 
29
- Pi 0.99.0+ also includes a built-in `tool-search` extension. This optimizer provides
30
- its own `tool_search`, so disable the built-in under **Built-in extensions** in
31
- `pi config` to avoid the startup conflict warning. Use the global setting for a
32
- personal install, or `pi config -l` for a project-only install. The optimizer
33
- continues to find registered `deferred` and `codemode` tools. See the
34
- [tool-search configuration](docs/CONFIGURATION.md#replacing-the-built-in-tool-search).
29
+ On Pi 1.0.0, the optimizer's search tool is `optimizer_tool_search`. Pi's
30
+ built-in `tool_search` can load alongside it without a name conflict. If you
31
+ previously disabled `builtin:tool-search` to avoid the warning, you can remove
32
+ that setting after updating the optimizer. See the
33
+ [tool-search configuration](docs/CONFIGURATION.md#tool-search-and-pi-100).
35
34
 
36
35
  That's enough to start. Optionally run `/skill-optimizer init` to generate routing hints for your library. **Init uses your current Pi model and may incur cost**; normal discovery does not.
37
36
 
38
37
  | Need | Command |
39
38
  | --- | --- |
40
39
  | Install only in this project | `pi install -l npm:@aeondave/pi-skill-optimizer` |
41
- | Pin the current release | `pi install npm:@aeondave/pi-skill-optimizer@3.0.1` |
40
+ | Pin the current release | `pi install npm:@aeondave/pi-skill-optimizer@3.1.1` |
42
41
  | Update an unpinned install | `pi update npm:@aeondave/pi-skill-optimizer` |
43
42
 
44
43
  Restart after updating. Pinned installs stay pinned until you explicitly install another version.
@@ -62,10 +61,14 @@ Say you have hundreds of skills, but you're asking Pi to fix a database migratio
62
61
 
63
62
  AUTO keeps a compact, stable index of **all eligible names**, adds relevant descriptions beside your latest request, and exposes `skill_search` for loading complete instructions. The stable index does not change when your question changes.
64
63
 
64
+ Prefetch is a catalog, not loaded instructions or evidence that a skill was used. Worker updates,
65
+ extension notifications and summaries do not replace your request as the routing signal or receive
66
+ its overlay; there is no need to acknowledge the catalog.
67
+
65
68
  | Area | What AUTO does | What stays available |
66
69
  | --- | --- | --- |
67
70
  | Skills | Shortens catalog descriptions; prefetches relevant ones | Every eligible name and exact full-instruction loading |
68
- | Tools | Defers less relevant schemas behind `tool_search` | Protected, configured, already-used and activated tools |
71
+ | Tools | Defers less relevant schemas behind `optimizer_tool_search` | Protected, configured, already-used and activated tools |
69
72
  | Large results | Applies deterministic reduction; lossless columnar JSON where suitable | Complete originals through `retrieve_output` during archive retention |
70
73
  | Repeated results | Replaces later exact duplicates with recovery references | The first result, errors, protected evidence and mixed media |
71
74
 
@@ -83,7 +86,7 @@ You can leave AUTO running with its defaults.
83
86
 
84
87
  An optional setup hint appears at the first eligible prompt when initialization is missing or needs attention. It never starts init for you or adds instructions to the model's context.
85
88
 
86
- Pi gets three tools: `skill_search`, `tool_search` and `retrieve_output`. Skill search is paginated and exact loads accept only registered names. Output recovery uses opaque identifiers, not exposed archive paths.
89
+ Pi gets three optimizer tools: `skill_search`, `optimizer_tool_search` and `retrieve_output`. Pi may also provide its built-in `tool_search`. Skill search is paginated and exact loads accept only registered names. Output recovery uses opaque identifiers, not exposed archive paths.
87
90
 
88
91
  Skills can be useful even when you never mention them. Init can tag architecture,
89
92
  testing and behavioral guidance by activity, so a matched specialist can bring
@@ -20,8 +20,19 @@ The index contains no filesystem paths. With the same catalog, profile, and conf
20
20
 
21
21
  AUTO extracts intent only from genuine human text in Anthropic, OpenAI Chat and Responses, Gemini, Mistral, and Pi's structured `pi-messages` request shapes. A catalog may live in a system or developer block, but neither is treated as human intent. Assistant text, tool or function results, injected context, image metadata, and empty input cannot create a ranking signal.
22
22
 
23
+ At runtime, catalog optimization and usage extraction run in Pi's `context_with_system` hook,
24
+ before provider conversion erases custom-message provenance. Native `custom`, `bashExecution`,
25
+ `branchSummary`, `compactionSummary` and `toolResult` messages remain intact and cannot become
26
+ the query or overlay target. Structured system sections and tool declarations are preserved;
27
+ request-local clones do not modify persisted conversation entries.
28
+
23
29
  Ranking uses exact matching, BM25F, bounded name and alias typo recovery, reciprocal-rank fusion, diversity, profile evidence, and decayed usage evidence. It scores full source descriptions, then appends a bounded set of verbatim skill names and descriptions (not the full `SKILL.md` instructions) to the latest human block. The overlay is separate from the stable prefix, fingerprinted, idempotent, and omitted when there is no valid target.
24
30
 
31
+ The resolver labels prefetch as discovery metadata, not loaded instructions or usage evidence.
32
+ Diagnostics count only overlays actually inserted. Usage follows explicit human mentions and skill
33
+ activations; the `skill_search` executor records its own loads/resources, so transcript scanning
34
+ does not count those calls twice. Catalog selections and assistant acknowledgments are not usage.
35
+
25
36
  Optional init-generated activation tags distinguish specialists, workflow methods
26
37
  and behavioral discipline. An admitted specialist can bring one activity-matched
27
38
  supporting skill into the ordinary prefetch budget; this does not recursively
@@ -30,9 +41,9 @@ expand dependencies or treat a behavioral tag as universal. See
30
41
 
31
42
  ## Dynamic tools
32
43
 
33
- AUTO keeps protected and predicted tools active and leaves the rest behind `tool_search`. Search is bounded and paginated. A returned result is activated through Pi's runtime API without replacing already active tools. Core, configured, and previously used tools remain callable, so an in-progress tool loop cannot be stranded. Weak or absent routing signal fails open to every permitted tool; safety wins, but that request may retain the full schema cost.
44
+ AUTO keeps protected and predicted tools active and leaves the rest behind `optimizer_tool_search`. Search is bounded and paginated. A returned result is activated through Pi's runtime API without replacing already active tools. Core, configured, and previously used tools remain callable, so an in-progress tool loop cannot be stranded. Pi's built-in `tool_search` remains active when the host enables it. Weak or absent routing signal fails open to every permitted tool; safety wins, but that request may retain the full schema cost.
34
45
 
35
- Selection is recalculated before each agent run, not just the first request. Used tools and explicit search results stay active for the session. The discoverable set starts from Pi's initially active tools plus the optimizer's own resolvers; tools that were initially unavailable are not silently added. This is schema routing, not authorization: another extension's call-time permission checks remain authoritative.
46
+ Selection is recalculated before each agent run, not just the first request. Used tools and explicit search results stay active for the session. The discoverable set starts from Pi's initially active tools plus the optimizer's own resolvers. Newly activated host tools are retained; inactive `deferred` and `codemode` tools can be selected through search or `alwaysTools`. Inactive direct and hidden tools are not silently added. This is schema routing, not authorization: another extension's call-time permission checks remain authoritative.
36
47
 
37
48
  See Pi's [extension and dynamic-tool API](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md).
38
49
 
@@ -7,18 +7,18 @@
7
7
  | Tool | Purpose |
8
8
  | --- | --- |
9
9
  | `skill_search` | Paginated skill search plus exact `load`, contained `resource`, and explicit `location` actions |
10
- | `tool_search` | Search deferred tool definitions and activate bounded matches additively |
10
+ | `optimizer_tool_search` | Search deferred tool definitions and activate bounded matches additively |
11
11
  | `retrieve_output` | Recover archived output or a deduplicated history result by opaque identifier |
12
12
 
13
13
  Exact skill loading accepts only registered names. Resource and location resolution use canonical paths and containment checks to reject traversal and symlink escape. Search cursors bind to the query, catalog and ranked name order; a changed profile or usage prior that reorders results requires a new search, rather than duplicating or skipping a page. This is an exact local resolver guarantee, not a guarantee that the model will select the right skill; see the separate [ranking diagnostics](RANKING.md) and [historical model benchmark](../BENCHMARK.md).
14
14
 
15
15
  `excludeSkills` removes names from discovery and search, not from explicit exact loading. It is a routing preference, not an access-control boundary. The three resolver tools return their content without another output-reduction pass, so full skill instructions and recovered originals remain readable.
16
16
 
17
- ### Replacing the built-in tool search
17
+ ### Tool search and Pi 1.0.0
18
18
 
19
- The extension registers `tool_search` itself, so it replaces Pi's built-in `builtin:tool-search` extension (the built-in is marked replaceable and the tool name decides). While both are in the load set, Pi prints a startup warning that the built-in was not loaded. The optimizer indexes eligible registered tools, including ones it deferred from Pi's active set and Pi-registered `deferred` or `codemode` tools. Matching inactive results activate additively; unrelated fallback results remain inactive. Pi's built-in MCP extension can still activate `tool_search` by name when a deferred server connects.
19
+ The extension registers `optimizer_tool_search`, leaving Pi's built-in `tool_search` available under its own name. The optimizer indexes eligible registered tools, including ones it deferred from Pi's active set and Pi-registered `deferred` or `codemode` tools. Matching inactive results activate additively; unrelated fallback results remain inactive. An already active built-in `tool_search` stays active during optimizer replanning. Pi's built-in MCP extension can activate its own `tool_search` when a deferred server connects.
20
20
 
21
- To silence the warning, disable the built-in where the optimizer runs: toggle `tool-search` off under Built-in extensions in `pi config`, or set `"extensions": ["-builtin:tool-search"]` in the applicable `settings.json`. A personal install needs the global setting in `~/.pi/agent/settings.json`; a project-only install can use `.pi/settings.json`. For development with `pi -e ./src/index.ts`, set the toggle locally or globally. Re-enable the built-in if you remove the optimizer.
21
+ If an older optimizer release led you to add `-builtin:tool-search` in `~/.pi/agent/settings.json` or `.pi/settings.json`, remove that entry after updating if you want Pi's built-in search available again. The built-in is inactive by default unless enabled in Pi's `defaultTools` or activated by Pi for deferred MCP tools; the optimizer's search stays available independently. Existing explicit calls to the optimizer's old `tool_search` name must use `optimizer_tool_search` after this update.
22
22
 
23
23
  ## Commands
24
24
 
@@ -55,7 +55,7 @@ Global configuration lives at `~/.pi/agent/skill-optimizer/config.json`. A proje
55
55
  | `providers` | `[]` | Provider allowlist; empty means all |
56
56
  | `toolPrefetchMax` | `5` | Maximum proactive tool activation |
57
57
  | `toolSearchPageSize` | `8` | Default tool-search page size |
58
- | `alwaysTools` | `[]` | Tools that remain active |
58
+ | `alwaysTools` | `[]` | Keep initially active tools and named `deferred` or `codemode` tools active; hidden tools remain unavailable |
59
59
  | `historyDedupMinBytes` | `4096` | Minimum result size for history deduplication |
60
60
  | `usageHalfLifeDays` | `30` | Usage-evidence decay half-life |
61
61
  | `usageMaxEntries` | `2048` | Persistent usage-evidence bound |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aeondave/pi-skill-optimizer",
3
- "version": "3.0.1",
3
+ "version": "3.1.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Less repeated context for Pi: local skill routing, on-demand tools, and recoverable output reduction.",
@@ -43,12 +43,13 @@
43
43
  ],
44
44
  "scripts": {
45
45
  "typecheck": "tsc --noEmit",
46
- "test": "node --import tsx --test test/request.test.ts test/history.test.ts test/skills.test.ts test/skill-loader.test.ts test/tools.test.ts test/optimize.test.ts test/profile.test.ts test/activation.test.ts test/generate.test.ts test/init.test.ts test/init-readiness.test.ts test/config.test.ts test/fuzz.test.ts test/output.test.ts test/output-recovery.test.ts test/runtime.test.ts test/host-runtime.test.ts test/stats.test.ts test/usage.test.ts test/persistence.test.ts test/corpus.test.ts test/evaluation.test.ts test/luna.test.ts test/release-metadata.test.ts",
46
+ "test": "node --import tsx --test test/request.test.ts test/history.test.ts test/skills.test.ts test/skill-loader.test.ts test/tools.test.ts test/optimize.test.ts test/profile.test.ts test/activation.test.ts test/generate.test.ts test/init.test.ts test/init-readiness.test.ts test/config.test.ts test/fuzz.test.ts test/output.test.ts test/output-recovery.test.ts test/runtime.test.ts test/host-runtime.test.ts test/stats.test.ts test/usage.test.ts test/persistence.test.ts test/corpus.test.ts test/capture-catalog.test.ts test/evaluation.test.ts test/luna.test.ts test/release-metadata.test.ts",
47
47
  "measure": "node --import tsx scripts/measure.ts",
48
48
  "bench": "node --import tsx scripts/bench.ts",
49
49
  "bench:perf": "node --import tsx scripts/perf-bench.ts",
50
50
  "bench:ranking": "node --import tsx scripts/ranking-bench.ts",
51
51
  "bench:output": "node --import tsx scripts/output-bench.ts",
52
+ "smoke:blackbox": "node --import tsx scripts/black-box-smoke.ts",
52
53
  "corpus:build": "node --import tsx scripts/build-real-corpus.ts",
53
54
  "bench:real": "node --import tsx scripts/evaluate-real-corpus.ts"
54
55
  },
@@ -69,8 +70,8 @@
69
70
  }
70
71
  },
71
72
  "devDependencies": {
72
- "@earendil-works/pi-ai": "^0.87.1",
73
- "@earendil-works/pi-coding-agent": "^0.87.1",
73
+ "@earendil-works/pi-ai": "^1.0.0",
74
+ "@earendil-works/pi-coding-agent": "^1.0.0",
74
75
  "@types/node": "^26.6.3",
75
76
  "tsx": "^4.23.13",
76
77
  "typebox": "^1.3.34",
@@ -80,6 +81,6 @@
80
81
  "extensions": [
81
82
  "./src/index.ts"
82
83
  ],
83
- "image": "https://raw.githubusercontent.com/AeonDave/pi-skill-optimizer/v3.0.1/assets/banner.png"
84
+ "image": "https://raw.githubusercontent.com/AeonDave/pi-skill-optimizer/v3.1.1/assets/banner.png"
84
85
  }
85
86
  }
package/src/index.ts CHANGED
@@ -110,7 +110,8 @@ const FLUSH_INTERVAL_MS = 15_000;
110
110
  const LOAD_RETRY_MS = 1_000;
111
111
  const MAX_SEEN_EVIDENCE = 4_096;
112
112
  const MAX_HISTORY_ARTIFACTS = 128;
113
- const PROTECTED_RUNTIME_TOOLS = ["skill_search", "tool_search", "retrieve_output"] as const;
113
+ const PROTECTED_RUNTIME_TOOLS = ["skill_search", "optimizer_tool_search", "retrieve_output"] as const;
114
+ const RESOLVER_RESULT_TOOLS = new Set<string>([...PROTECTED_RUNTIME_TOOLS, "tool_search"]);
114
115
 
115
116
  function isHostSearchableTool(tool: object): boolean {
116
117
  return "exposure" in tool && (tool.exposure === "deferred" || tool.exposure === "codemode");
@@ -574,16 +575,30 @@ export default function skillOptimizer(pi: ExtensionAPI) {
574
575
 
575
576
  const rebuildToolCatalog = (state: RuntimeSnapshot): ToolDiscoveryCatalog => {
576
577
  const allTools = pi.getAllTools();
578
+ const activeNames = new Set(pi.getActiveTools().map((name) => name.toLowerCase()));
579
+ const alwaysToolPrefixes = state.config.alwaysTools
580
+ .map((pattern) => pattern.trim().toLowerCase())
581
+ .filter((pattern) => pattern.length > 0);
577
582
  permittedToolNames ??= new Set([
578
- ...pi.getActiveTools().map((name) => name.toLowerCase()),
583
+ ...activeNames,
579
584
  ...PROTECTED_RUNTIME_TOOLS,
580
585
  ]);
581
586
  for (const tool of allTools) {
582
- if (!isHostHiddenTool(tool)) continue;
583
587
  const name = tool.name.toLowerCase();
584
- permittedToolNames.delete(name);
585
- retainedToolNames.delete(name);
586
- deactivatedToolNames.delete(name);
588
+ if (isHostHiddenTool(tool)) {
589
+ permittedToolNames.delete(name);
590
+ retainedToolNames.delete(name);
591
+ deactivatedToolNames.delete(name);
592
+ continue;
593
+ }
594
+ if (activeNames.has(name)) {
595
+ const newlyActive = !permittedToolNames.has(name);
596
+ const reactivated = deactivatedToolNames.delete(name);
597
+ if (newlyActive || reactivated) retainedToolNames.add(name);
598
+ permittedToolNames.add(name);
599
+ } else if (isHostSearchableTool(tool) && alwaysToolPrefixes.some((pattern) => name.startsWith(pattern))) {
600
+ permittedToolNames.add(name);
601
+ }
587
602
  }
588
603
  const discoverable = allTools.filter((tool) =>
589
604
  !isHostHiddenTool(tool) && permittedToolNames?.has(tool.name.toLowerCase()),
@@ -684,8 +699,8 @@ export default function skillOptimizer(pi: ExtensionAPI) {
684
699
  });
685
700
 
686
701
  pi.registerTool({
687
- name: "tool_search",
688
- label: "Tool Search",
702
+ name: "optimizer_tool_search",
703
+ label: "Optimizer Tool Search",
689
704
  description: "Search local tool definitions and activate matching inactive tools without disabling current tools.",
690
705
  promptSnippet: "Search and activate additional tools on demand",
691
706
  parameters: TOOL_SEARCH_PARAMS,
@@ -1069,13 +1084,14 @@ export default function skillOptimizer(pi: ExtensionAPI) {
1069
1084
  }
1070
1085
  });
1071
1086
 
1072
- pi.on("before_provider_request", (event, ctx) => {
1087
+ pi.on("context_with_system", (event, ctx) => {
1073
1088
  if (isDisabled(ctx.cwd)) return;
1074
1089
  const config = getRuntimeConfig(ctx);
1075
1090
  const providers = config.providers.length > 0 ? config.providers : undefined;
1076
- if (providers && ctx.model && !providers.includes(ctx.model.provider)) return;
1091
+ if (providers && (!ctx.model || !providers.includes(ctx.model.provider))) return;
1077
1092
  const state = ensureSnapshot(ctx);
1078
- const transformed = optimizePayload(event.payload, {
1093
+ const payload = { messages: event.messages };
1094
+ const transformed = optimizePayload(payload, {
1079
1095
  ...state.config,
1080
1096
  profile: state.profile,
1081
1097
  usagePrior: buildUsagePrior(state.usage, {
@@ -1094,7 +1110,7 @@ export default function skillOptimizer(pi: ExtensionAPI) {
1094
1110
  addPending(getStatsFilePath(ctx.cwd), { skills: transformed.removedChars, tools: 0, output: 0 });
1095
1111
  }
1096
1112
  if (registry) {
1097
- const evidence = collectSkillUsageEvidence(event.payload, registry.skills.map((skill) => skill.name)).slice(-MAX_SEEN_EVIDENCE);
1113
+ const evidence = collectSkillUsageEvidence(payload, registry.skills.map((skill) => skill.name)).slice(-MAX_SEEN_EVIDENCE);
1098
1114
  const names: string[] = [];
1099
1115
  for (const item of evidence) {
1100
1116
  if (seenEvidence.has(item.key)) continue;
@@ -1109,7 +1125,9 @@ export default function skillOptimizer(pi: ExtensionAPI) {
1109
1125
  }
1110
1126
  maybeFlush(ctx);
1111
1127
  updateStatus(ctx);
1112
- return transformed.next === event.payload ? undefined : transformed.next;
1128
+ return transformed.next === payload
1129
+ ? undefined
1130
+ : { messages: (transformed.next as typeof payload).messages };
1113
1131
  });
1114
1132
 
1115
1133
  pi.on("context", (event, ctx) => {
@@ -1177,7 +1195,7 @@ export default function skillOptimizer(pi: ExtensionAPI) {
1177
1195
 
1178
1196
  pi.on("tool_result", async (event, ctx) => {
1179
1197
  usedTools.add(event.toolName.toLowerCase());
1180
- if (PROTECTED_RUNTIME_TOOLS.includes(event.toolName as typeof PROTECTED_RUNTIME_TOOLS[number])) return;
1198
+ if (RESOLVER_RESULT_TOOLS.has(event.toolName)) return;
1181
1199
  if (isDisabled(ctx.cwd) || event.isError) return;
1182
1200
  const config = getRuntimeConfig(ctx);
1183
1201
  const providers = config.providers.length > 0 ? config.providers : undefined;
@@ -1373,7 +1391,7 @@ export default function skillOptimizer(pi: ExtensionAPI) {
1373
1391
  ` enabled: ${isDisabled(ctx.cwd) ? "no" : "yes"}`,
1374
1392
  " skills mode: AUTO (stable TSV + skill_search resolver)",
1375
1393
  ` catalog budget:${lastBudget.usedChars}/${lastBudget.requestedChars} chars, ${lastBudget.nameOnlyCount} name-only`,
1376
- ` tools: ${pi.getActiveTools().length}/${pi.getAllTools().length} active; tool_search additive`,
1394
+ ` tools: ${pi.getActiveTools().length}/${pi.getAllTools().length} active; optimizer_tool_search additive`,
1377
1395
  ` output: ${getOutputConfig(state.config).mode}; RTK ${rtkExtensionPresent() ? "detected per result" : "not detected"}`,
1378
1396
  ` scope: ${state.config.providers.join(", ") || "all providers"}`,
1379
1397
  ` init readiness: ${retryAfter !== 0 ? "unavailable (profile could not be read)" : readiness.skillCount === 0 ? "not available (no model-invocable skills)" : initReadinessSummary(readiness)}`,
package/src/optimize.ts CHANGED
@@ -361,25 +361,24 @@ export function optimizePayload<T>(
361
361
  }
362
362
 
363
363
  let addedPrefetchChars = 0;
364
- const selectedSkills =
365
- planSkillPrefetch(catalogSkills, query, {
366
- profile: config.profile,
367
- usagePrior: config.usagePrior,
368
- always: uniqueInOrder([
369
- ...(config.profile?.critical ?? []),
370
- ...(config.alwaysSkills ?? []),
371
- ]),
372
- never: config.excludeSkills,
373
- targetTopK: config.prefetchTarget,
374
- minTopK: config.prefetchMin,
375
- maxTopK: config.prefetchMax,
376
- fullRenderBudgetChars: config.prefetchBudgetChars,
377
- fuzzyCandidateLimit: config.fuzzyCandidateLimit,
378
- }).selected;
364
+ let injectedSkills: string[] = [];
365
+ const prefetchPlan = planSkillPrefetch(catalogSkills, query, {
366
+ profile: config.profile,
367
+ usagePrior: config.usagePrior,
368
+ always: uniqueInOrder([
369
+ ...(config.profile?.critical ?? []),
370
+ ...(config.alwaysSkills ?? []),
371
+ ]),
372
+ never: config.excludeSkills,
373
+ targetTopK: config.prefetchTarget,
374
+ minTopK: config.prefetchMin,
375
+ maxTopK: config.prefetchMax,
376
+ fullRenderBudgetChars: config.prefetchBudgetChars,
377
+ fuzzyCandidateLimit: config.fuzzyCandidateLimit,
378
+ });
379
+ const selectedSkills = prefetchPlan.selected;
379
380
  if (selectedSkills.length > 0) {
380
- const appendix = renderSkillPrefetch({
381
- selected: selectedSkills,
382
- } as Parameters<typeof renderSkillPrefetch>[0]);
381
+ const appendix = renderSkillPrefetch(prefetchPlan);
383
382
  if (appendix) {
384
383
  const withPrefetch = appendToLatestHumanInput(
385
384
  next,
@@ -389,6 +388,7 @@ export function optimizePayload<T>(
389
388
  if (withPrefetch !== next) {
390
389
  addedPrefetchChars = serializedLength(withPrefetch) - serializedLength(next);
391
390
  next = withPrefetch;
391
+ injectedSkills = selectedSkills.map((skill) => skill.name);
392
392
  }
393
393
  }
394
394
  }
@@ -416,7 +416,7 @@ export function optimizePayload<T>(
416
416
  removedChars: originalLength - finalLength,
417
417
  baseRemovedChars,
418
418
  addedPrefetchChars: Math.max(0, addedPrefetchChars),
419
- selected: selectedSkills.map((skill) => skill.name),
419
+ selected: injectedSkills,
420
420
  catalogNames: uniqueInOrder(catalogNames),
421
421
  fingerprints: uniqueInOrder(fingerprints),
422
422
  budget,
package/src/skills.ts CHANGED
@@ -1058,7 +1058,7 @@ export function renderSkillPrefetch(plan: SkillPrefetchPlan): string {
1058
1058
  if (plan.selected.length === 0) return "";
1059
1059
  return [
1060
1060
  `<skill_prefetch catalog_sha256="${plan.fingerprint}">`,
1061
- " <skill_resolver>Use skill_search(name) to load the full skill resource.</skill_resolver>",
1061
+ " <skill_resolver>Catalog metadata only, not loaded instructions or usage evidence. Load a relevant full skill with skill_search(action=load, name); no acknowledgment is needed.</skill_resolver>",
1062
1062
  ...plan.selected.map(renderFullSkill),
1063
1063
  "</skill_prefetch>",
1064
1064
  ].join("\n");
package/src/tools.ts CHANGED
@@ -23,7 +23,7 @@ export const CORE_TOOLS = new Set(
23
23
  "read", "bash", "edit", "write", "multiedit", "str_replace_editor", "notebookedit",
24
24
  "glob", "grep", "ls", "find", "todo", "todowrite", "todoread",
25
25
  "subagent", "fork", "task", "mcp", "skill", "exitplanmode", "enterplanmode",
26
- "webfetch", "websearch", "askuserquestion", "powershell",
26
+ "webfetch", "websearch", "askuserquestion", "powershell", "tool_search",
27
27
  ].map((name) => name.toLowerCase()),
28
28
  );
29
29
 
package/src/usage.ts CHANGED
@@ -91,16 +91,18 @@ export function collectSkillUsageEvidence(payload: unknown, selectedSkills: read
91
91
  const rawMessage = rawMessages[messageIndex];
92
92
  if (!rawMessage || typeof rawMessage !== "object") continue;
93
93
  const message = rawMessage as Record<string, unknown>;
94
+ if (message.role !== "assistant") continue;
94
95
  if (!Array.isArray(message.content)) continue;
95
96
  for (let blockIndex = 0; blockIndex < message.content.length; blockIndex++) {
96
97
  const rawBlock = message.content[blockIndex];
97
98
  if (!rawBlock || typeof rawBlock !== "object") continue;
98
99
  const block = rawBlock as Record<string, unknown>;
99
- if (block.type !== "tool_use" || typeof block.name !== "string") continue;
100
+ if ((block.type !== "tool_use" && block.type !== "toolCall") || typeof block.name !== "string") continue;
100
101
  const toolName = block.name.toLowerCase();
101
102
  if (toolName !== "skill" && toolName !== "activate_skill") continue;
102
- if (!block.input || typeof block.input !== "object") continue;
103
- const input = block.input as Record<string, unknown>;
103
+ const rawInput = block.type === "toolCall" ? block.arguments : block.input;
104
+ if (!rawInput || typeof rawInput !== "object") continue;
105
+ const input = rawInput as Record<string, unknown>;
104
106
  let serialized = "";
105
107
  try { serialized = JSON.stringify(input); } catch { serialized = String(input); }
106
108
  const sourceId = objectId(block);