@aeondave/pi-skill-optimizer 3.1.0 → 3.1.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  All notable changes are documented here.
4
4
 
5
+ ## Unreleased
6
+
7
+ ## 3.1.2 - 2026-10-13
8
+
9
+ ### Fixed
10
+
11
+ - Handle Pi 1.2 custom-triggered runs as no-intent starts: fail open for permitted tools, ignore custom message text even if a stray prompt field is present, and preserve the previous human request for output extraction.
12
+
13
+ ## 3.1.1 - 2026-10-08
14
+
15
+ ### Fixed
16
+
17
+ - Optimize the native Pi transcript before provider conversion, keeping worker notifications, shell
18
+ output and summaries out of human-intent routing and prefetch insertion.
19
+ - Preserve structured system sections and tool declarations; respect provider scoping when the
20
+ request has no selected model.
21
+ - Label prefetch as catalog metadata, not loaded instructions or usage evidence. Emit its actual
22
+ catalog fingerprint and report selected skills only when an overlay was inserted.
23
+ - Recognize native Pi skill-activation calls without double-counting `skill_search` usage.
24
+
5
25
  ## 3.1.0 - 2026-10-03
6
26
 
7
27
  ### Breaking
package/README.md CHANGED
@@ -37,7 +37,7 @@ That's enough to start. Optionally run `/skill-optimizer init` to generate routi
37
37
  | Need | Command |
38
38
  | --- | --- |
39
39
  | Install only in this project | `pi install -l npm:@aeondave/pi-skill-optimizer` |
40
- | Pin the current release | `pi install npm:@aeondave/pi-skill-optimizer@3.1.0` |
40
+ | Pin the current release | `pi install npm:@aeondave/pi-skill-optimizer@3.1.2` |
41
41
  | Update an unpinned install | `pi update npm:@aeondave/pi-skill-optimizer` |
42
42
 
43
43
  Restart after updating. Pinned installs stay pinned until you explicitly install another version.
@@ -61,6 +61,10 @@ Say you have hundreds of skills, but you're asking Pi to fix a database migratio
61
61
 
62
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.
63
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
+
64
68
  | Area | What AUTO does | What stays available |
65
69
  | --- | --- | --- |
66
70
  | Skills | Shortens catalog descriptions; prefetches relevant ones | Every eligible name and exact full-instruction loading |
@@ -20,8 +20,27 @@ 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
+
29
+ Pi 1.2's `before_agent_start` event distinguishes user starts (`messageType: "user"` with `prompt`)
30
+ from custom-triggered starts (`messageType: "custom"` with `message`). Only a user prompt updates the
31
+ last human request and drives tool activation. A custom start has no routing signal, so optimizer
32
+ activation fails open across permitted named tools while retaining protected, used, and searched
33
+ tools; custom message text is ignored even if the event also carries a `prompt` property. The
34
+ previous human request remains available to output extraction. On Pi 1.0, the supported event has no
35
+ `messageType` discriminator, so its string `prompt` continues to identify user intent.
36
+
23
37
  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
38
 
39
+ The resolver labels prefetch as discovery metadata, not loaded instructions or usage evidence.
40
+ Diagnostics count only overlays actually inserted. Usage follows explicit human mentions and skill
41
+ activations; the `skill_search` executor records its own loads/resources, so transcript scanning
42
+ does not count those calls twice. Catalog selections and assistant acknowledgments are not usage.
43
+
25
44
  Optional init-generated activation tags distinguish specialists, workflow methods
26
45
  and behavioral discipline. An admitted specialist can bring one activity-matched
27
46
  supporting skill into the ordinary prefetch budget; this does not recursively
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aeondave/pi-skill-optimizer",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
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.",
@@ -81,6 +81,6 @@
81
81
  "extensions": [
82
82
  "./src/index.ts"
83
83
  ],
84
- "image": "https://raw.githubusercontent.com/AeonDave/pi-skill-optimizer/v3.1.0/assets/banner.png"
84
+ "image": "https://raw.githubusercontent.com/AeonDave/pi-skill-optimizer/v3.1.2/assets/banner.png"
85
85
  }
86
86
  }
package/src/index.ts CHANGED
@@ -1059,7 +1059,13 @@ export default function skillOptimizer(pi: ExtensionAPI) {
1059
1059
  });
1060
1060
 
1061
1061
  pi.on("before_agent_start", (event, ctx) => {
1062
- latestPrompt = event.prompt;
1062
+ // Pi 1.2 also starts runs from custom messages, which have no human prompt.
1063
+ // Keep their content out of routing and preserve the last real request for extraction.
1064
+ const customStart = "messageType" in event && event.messageType === "custom";
1065
+ const prompt = !customStart && "prompt" in event && typeof event.prompt === "string"
1066
+ ? event.prompt
1067
+ : undefined;
1068
+ if (prompt !== undefined) latestPrompt = prompt;
1063
1069
  if (isDisabled(ctx.cwd)) {
1064
1070
  restorePermittedTools();
1065
1071
  return;
@@ -1074,7 +1080,7 @@ export default function skillOptimizer(pi: ExtensionAPI) {
1074
1080
  try {
1075
1081
  const skills = skillInputs(event.systemPromptOptions.skills ?? []);
1076
1082
  registry = makeSkillRegistry(skills);
1077
- initializeToolSelection(state, event.prompt);
1083
+ initializeToolSelection(state, prompt ?? "");
1078
1084
  maybeShowInitNotice(ctx, state, skills);
1079
1085
  updateStatus(ctx);
1080
1086
  return;
@@ -1084,13 +1090,14 @@ export default function skillOptimizer(pi: ExtensionAPI) {
1084
1090
  }
1085
1091
  });
1086
1092
 
1087
- pi.on("before_provider_request", (event, ctx) => {
1093
+ pi.on("context_with_system", (event, ctx) => {
1088
1094
  if (isDisabled(ctx.cwd)) return;
1089
1095
  const config = getRuntimeConfig(ctx);
1090
1096
  const providers = config.providers.length > 0 ? config.providers : undefined;
1091
- if (providers && ctx.model && !providers.includes(ctx.model.provider)) return;
1097
+ if (providers && (!ctx.model || !providers.includes(ctx.model.provider))) return;
1092
1098
  const state = ensureSnapshot(ctx);
1093
- const transformed = optimizePayload(event.payload, {
1099
+ const payload = { messages: event.messages };
1100
+ const transformed = optimizePayload(payload, {
1094
1101
  ...state.config,
1095
1102
  profile: state.profile,
1096
1103
  usagePrior: buildUsagePrior(state.usage, {
@@ -1109,7 +1116,7 @@ export default function skillOptimizer(pi: ExtensionAPI) {
1109
1116
  addPending(getStatsFilePath(ctx.cwd), { skills: transformed.removedChars, tools: 0, output: 0 });
1110
1117
  }
1111
1118
  if (registry) {
1112
- const evidence = collectSkillUsageEvidence(event.payload, registry.skills.map((skill) => skill.name)).slice(-MAX_SEEN_EVIDENCE);
1119
+ const evidence = collectSkillUsageEvidence(payload, registry.skills.map((skill) => skill.name)).slice(-MAX_SEEN_EVIDENCE);
1113
1120
  const names: string[] = [];
1114
1121
  for (const item of evidence) {
1115
1122
  if (seenEvidence.has(item.key)) continue;
@@ -1124,7 +1131,9 @@ export default function skillOptimizer(pi: ExtensionAPI) {
1124
1131
  }
1125
1132
  maybeFlush(ctx);
1126
1133
  updateStatus(ctx);
1127
- return transformed.next === event.payload ? undefined : transformed.next;
1134
+ return transformed.next === payload
1135
+ ? undefined
1136
+ : { messages: (transformed.next as typeof payload).messages };
1128
1137
  });
1129
1138
 
1130
1139
  pi.on("context", (event, ctx) => {
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/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);