@aeondave/pi-skill-optimizer 3.0.0

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.
@@ -0,0 +1,66 @@
1
+ # How AUTO works
2
+
3
+ [Back to the README](../README.md)
4
+
5
+ ### Stable skill base
6
+
7
+ AUTO renders a query-independent index in the stable prompt prefix:
8
+
9
+ ```xml
10
+ <!--skill-optimizer:auto:v2-->
11
+ <skill_index format="tsv" columns="name,intent">
12
+ skill-name\tShort routing intent
13
+ </skill_index>
14
+ <skill_resolver>Use skill_search to search, load, inspect resources, or request a location.</skill_resolver>
15
+ ```
16
+
17
+ The index contains no filesystem paths. With the same catalog, profile, and configuration, its bytes remain unchanged across requests. `catalogBudgetChars` is a request-global soft budget shared by every catalog found in one request. Intents yield first, but the combined all-name floor may exceed the budget.
18
+
19
+ ## Request-specific prefetch
20
+
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
+
23
+ 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
+
25
+ Optional init-generated activation tags distinguish specialists, workflow methods
26
+ and behavioral discipline. An admitted specialist can bring one activity-matched
27
+ supporting skill into the ordinary prefetch budget; this does not recursively
28
+ expand dependencies or treat a behavioral tag as universal. See
29
+ [contextual routing](RANKING.md#contextual-and-behavioral-skills).
30
+
31
+ ## Dynamic tools
32
+
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.
34
+
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.
36
+
37
+ See Pi's [extension and dynamic-tool API](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md).
38
+
39
+ ## Recoverable output
40
+
41
+ The default `smart` reducer is deterministic and UTF-8 byte-aware. It preserves protected evidence and accepts a reduction only when the complete returned representation clears both configured saving thresholds. Size and line settings trigger reduction; they are not hard output caps. Protected evidence may make the returned result exceed them.
42
+
43
+ Homogeneous JSON object arrays may use a lossless columnar form after an exact internal round trip. With `outputMode: "extract"` and an explicit `outputModel`, sufficiently large text may first use model-assisted extraction. Extracted content must be an ordered verbatim subsequence, retain protected evidence, and provide material savings. Rejection falls back to smart reduction, then to the original.
44
+
45
+ Before AUTO returns any reduced result, it stores the complete original in a local, immutable, content-addressed archive. Provider-visible recovery text contains only an opaque identifier. `retrieve_output` verifies and resolves that identifier with bounded line access.
46
+
47
+ Savings include the recovery label and are checked before an archive write. The optimizer's own resolver results are exempt from reduction, avoiding a second handle in place of the original you just requested. Custom-tool failures throw through Pi's error path instead of returning a success-shaped error object.
48
+
49
+ Columnar conversion conservatively leaves unsafe integers, non-finite numbers,
50
+ negative zero and noncanonical numeric spellings untouched. Parsing twice is not
51
+ a losslessness oracle when both parses can round the same value. Invalid Unicode
52
+ strings also fail open. Exact recovery retains the source's original line endings;
53
+ it does not normalize Windows output to LF.
54
+
55
+ History deduplication keeps the first large successful result intact and replaces only later exact copies. Errors, images, mixed media, small results, and protected evidence are never deduplicated.
56
+
57
+ ## Scale and limits
58
+
59
+ AUTO reduces metadata in the tested catalogs, but does not guarantee model discovery.
60
+
61
+ - The current contract keeps every eligible skill name in the stable index. Its minimum size therefore grows linearly with the number and length of names. This all-name information floor cannot be compressed away while preserving immediate name-level discovery.
62
+ - A catalog with 100,000 skills is outside the scale demonstrated by the current 332-skill benchmark. It may still produce an impractically large name-only base.
63
+ - Extreme catalogs require an additional resolver-only, namespace-sharded, or server-side paginated discovery layer. That would deliberately change the current all-name visibility guarantee.
64
+ - Arbitrarily large system prompts are not automatically reduced. AUTO transforms recognized skill catalogs, manages tool definitions, and processes eligible tool results; it does not summarize unrelated policy, instructions, or conversation content.
65
+ - Provider tokenization, caching, and pricing remain provider-controlled. Stable bytes improve cacheability but do not prove a cache hit or guarantee a specific bill.
66
+ - Very small catalogs or outputs may remain unchanged when framing overhead would eliminate the saving.
@@ -0,0 +1,138 @@
1
+ # Configuration and runtime reference
2
+
3
+ [Back to the README](../README.md)
4
+
5
+ ## Runtime tools
6
+
7
+ | Tool | Purpose |
8
+ | --- | --- |
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 |
11
+ | `retrieve_output` | Recover archived output or a deduplicated history result by opaque identifier |
12
+
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
+
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
+
17
+ ## Commands
18
+
19
+ | Command | Purpose |
20
+ | --- | --- |
21
+ | `/skill-optimizer` | Show setup readiness, AUTO state, catalog budget, critical skills, usage prior, tool activation, provider telemetry, and savings |
22
+ | `/skill-optimizer init` | Generate or incrementally update routing hints and contextual activation tags |
23
+ | `/skill-optimizer audit` | Report description issues without editing source skills |
24
+
25
+ The optional setup hint is informational, not an error: ordinary routing works
26
+ without init. It appears on the first eligible interactive prompt, at most once
27
+ per profile scope, readiness state and init version in the current session.
28
+ Session metadata preserves that suppression across reload/resume. It is not sent
29
+ to the model and does not run init or incur model cost itself. Headless sessions,
30
+ disabled/out-of-scope routing and empty catalogs do not emit it. Status remains
31
+ available even after the hint has been shown. Nonempty hand-authored profiles
32
+ without generation metadata are treated as manual, not as a failed setup.
33
+
34
+ ## Configuration
35
+
36
+ Global configuration lives at `~/.pi/agent/skill-optimizer/config.json`. A project can override it with `.pi/skill-optimizer/config.json`.
37
+
38
+ | Key | Default | Purpose |
39
+ | --- | ---: | --- |
40
+ | `catalogBudgetChars` | `8000` | Request-global soft budget for stable-index intents |
41
+ | `intentMaxChars` | `96` | Maximum rendered intent length |
42
+ | `prefetchTarget` | `5` | Ordinary prefetch target |
43
+ | `prefetchMin` | `3` | Minimum adaptive prefetch count |
44
+ | `prefetchMax` | `8` | Maximum adaptive prefetch count |
45
+ | `prefetchBudgetChars` | `6000` | Full-definition overlay budget |
46
+ | `fuzzyCandidateLimit` | `8` | Bound for name and alias typo recovery |
47
+ | `alwaysSkills` | `[]` | Skills favored for routing and prefetch |
48
+ | `excludeSkills` | `[]` | Skills intentionally removed from discovery |
49
+ | `providers` | `[]` | Provider allowlist; empty means all |
50
+ | `toolPrefetchMax` | `5` | Maximum proactive tool activation |
51
+ | `toolSearchPageSize` | `8` | Default tool-search page size |
52
+ | `alwaysTools` | `[]` | Tools that remain active |
53
+ | `historyDedupMinBytes` | `4096` | Minimum result size for history deduplication |
54
+ | `usageHalfLifeDays` | `30` | Usage-evidence decay half-life |
55
+ | `usageMaxEntries` | `2048` | Persistent usage-evidence bound |
56
+ | `usageStaleDays` | `180` | Age for pruning stale one-off evidence |
57
+ | `outputMode` | `"smart"` | `"off"`, `"smart"`, or guarded `"extract"` |
58
+ | `outputMaxLines` | `400` | Line-count activation threshold |
59
+ | `outputMaxBytes` | `16000` | UTF-8 byte activation threshold |
60
+ | `outputMinSavingsBytes` | `512` | Minimum absolute saving |
61
+ | `outputMinSavingsRatio` | `0.1` | Minimum relative saving |
62
+ | `outputTools` | `["*"]` | Eligible tool-name patterns |
63
+ | `outputModel` | `""` | Explicit model for guarded extraction |
64
+ | `outputExtractMinBytes` | `32000` | Minimum input size for extraction |
65
+ | `outputExtractExclude` | common listing commands | Tools excluded from extraction |
66
+ | `outputArchiveTtlHours` | `168` | Archive retention period |
67
+
68
+ Configuration is also available through `PI_SKILL_OPTIMIZER_*` environment variables. Lists are comma-separated. The bypass exists only as `PI_SKILL_OPTIMIZER_DISABLE` and uses boolean semantics; there is no `disable` configuration key. Output exceptions retain the exact names `PI_SKILL_OPTIMIZER_OUTPUT_MIN_SAVINGS_RATIO` and `PI_SKILL_OPTIMIZER_OUTPUT_EXCLUDE`.
69
+
70
+ Configuration refreshes at session start, before each agent run, and when you use a `/skill-optimizer` command. Provider and tool-result hooks reuse that snapshot instead of rereading files on every event. The environment bypass is checked immediately. Profile and usage state are retained separately; refreshing settings does not reset pending usage or telemetry.
71
+
72
+ ## RTK coexistence
73
+
74
+ Ownership is decided per result:
75
+
76
+ | Result | Owner |
77
+ | --- | --- |
78
+ | Shell output already handled by RTK | RTK only |
79
+ | Shell output not handled by RTK | AUTO eligible |
80
+ | Read, web, MCP, and other eligible output | AUTO eligible |
81
+ | Already reduced output | No second reduction |
82
+
83
+ RTK never changes skill discovery or dynamic tool activation.
84
+
85
+ ## Telemetry and persistence
86
+
87
+ Provider telemetry records only authoritative usage fields:
88
+
89
+ ```text
90
+ requests, input, output, cacheRead, cacheWrite, totalCost
91
+ ```
92
+
93
+ AUTO does not infer cache hits from stable bytes. Characters, UTF-8 bytes, provider tokens, and cost remain separate metrics.
94
+
95
+ Profile updates use atomic replacement with revision checks; usage and statistics merge lock-serialized additive deltas. Archive content is immutable and SHA-256 verified. A separate expiry record belongs to each archive: saving identical content may extend its retention, but a reader or project with a shorter TTL cannot shorten it. Legacy archives without expiry records get a fixed seven-day lifetime from their last modification; corrupt expiry records fail closed without deleting content. Usage evidence is bounded and decays over time.
96
+
97
+ An old lock is reclaimed only when its recorded process is no longer alive;
98
+ elapsed time alone must not interrupt a slow or suspended writer. Unknown lock
99
+ ownership fails closed rather than risking a concurrent overwrite.
100
+
101
+ Recovery preserves original LF, CRLF, CR and mixed line endings, including the selected lines' terminators. Adjacent pages concatenate to the original text; asking past the end returns an empty page. Invalid Unicode strings containing unpaired UTF-16 surrogates are left unreduced rather than archived with replacement characters.
102
+
103
+ ## Privacy
104
+
105
+ - Normal hooks never collect a benchmark corpus.
106
+ - Corpus building and real-provider evaluation are explicit commands.
107
+ - Remote evaluation receives only validated, sanitized cases with keyed irreversible identifiers.
108
+ - Raw private sessions, catalogs, paths, credentials, hostnames, and account identifiers are not published.
109
+ - Generated profile queries are routing hints, not benchmark labels.
110
+ - Full output archives stay local.
111
+ - `.pi/skill-optimizer/benchmark/` must not be committed.
112
+
113
+ ## Routing profile initialization
114
+
115
+ `init` is explicit model work. It notifies at start and completion while per-batch progress stays in the transient status line. It batches full descriptions by skill count and UTF-8 input size, validates complete coverage and a successful provider stop reason, retries only retryable failures, and checkpoints each successful batch so interrupted work is not lost. A changed skill whose regeneration fails stays unhashed and inactive until retry, rather than serving stale routing. Init also repairs global/project profile ownership without another model call. Ordinary discovery does not call a model.
116
+
117
+
118
+ The profile is optional. Without one, AUTO still uses the catalog's names and descriptions. Run `init` when you want generated routing hints, and rerun it after changing your skills. It uses the current Pi model and can incur provider cost.
119
+
120
+ Profile init version 5 also generates optional `activation` tags. Each installed
121
+ skill can be a `specialist`, `workflow`, or `behavioral` skill and declare applicable
122
+ activities. For example, the profile may contain:
123
+
124
+ ```json
125
+ {
126
+ "activation": {
127
+ "python-patterns": { "kind": "specialist", "tasks": ["software-change"] },
128
+ "software-architecture": { "kind": "workflow", "tasks": ["software-design", "software-change"] },
129
+ "test-driven-development": { "kind": "behavioral", "tasks": ["software-change"] }
130
+ }
131
+ }
132
+ ```
133
+
134
+ These are example entries inside `profile.json`, not `config.json`, and only make
135
+ sense for installed skills. They allow bounded contextual support without turning
136
+ every behavioral skill into a permanent default. The next explicit init upgrades
137
+ older generated profiles; no regeneration or model call happens on ordinary
138
+ startup. See [contextual routing and its limits](RANKING.md#contextual-and-behavioral-skills).
@@ -0,0 +1,155 @@
1
+ # Ranking decisions
2
+
3
+ [Back to the README](../README.md)
4
+
5
+ ## Why this is still a local lexical router
6
+
7
+ The job is to find a few useful entries in a known skill catalog, before the model
8
+ starts work. Ordinary routing must be deterministic, cheap to run, and usable
9
+ without another provider or a generated profile. AUTO's hybrid is a reasonable
10
+ fit for those constraints; it is not a claim that lexical retrieval is universally
11
+ better than embeddings or that these weights are optimal.
12
+
13
+ The current pipeline is:
14
+
15
+ 1. Normalize the latest genuine human text and expand catalog-supported aliases.
16
+ 2. Score names, short intents, full descriptions and optional profile queries with
17
+ field-weighted BM25F. Compact display text is not the only searchable text.
18
+ 3. Add token-bounded name/example matches and bounded name/alias typo candidates.
19
+ 4. Combine the rankings with weighted reciprocal-rank fusion (RRF). Decayed usage
20
+ can reinforce an already relevant candidate, not invent relevance.
21
+ 5. Apply negative hints and a small cluster-diversity adjustment, then select
22
+ within adaptive count and character budgets. Explicitly protected skills have
23
+ their own guarantee; they can exceed ordinary prefetch budgets.
24
+
25
+ The stable all-name index is separate from this ranking. A miss in prefetch does
26
+ not remove a skill from the library or its exact resolver.
27
+
28
+ ## Contextual and behavioral skills
29
+
30
+ Literal similarity is not the whole job. A task that selects a language specialist
31
+ can also need architecture or test-first discipline without naming either.
32
+ An optional, name-owned `activation` entry in the profile records two tags:
33
+
34
+ - `kind`: `specialist`, `workflow`, or `behavioral`.
35
+ - `tasks`: a small subset of shared activities such as `software-change`,
36
+ `software-review`, `docs-write`, or `research`.
37
+
38
+ Explicit `/skill-optimizer init` assigns these from each skill's description.
39
+ The fixed activity vocabulary works across init batches: no full-catalog model
40
+ request or runtime inference call is needed. Old profiles still work; rerun init
41
+ to regenerate them with the new tags. That regeneration uses your selected model
42
+ and can incur cost. This update does not run it automatically.
43
+
44
+ The router first selects a positively matched, tagged specialist. It can then
45
+ reserve **one ordinary prefetch slot** for a workflow or behavioral skill sharing
46
+ an activity tag, even with no lexical match to the user's words. This is one-hop
47
+ support, not recursive expansion. It retains the primary specialist, honors
48
+ exclusions and negative hints, and uses the same count and character budget.
49
+ Already selected skills are not duplicated; a one-slot budget leaves only the
50
+ primary. An explicit exact-name selection takes precedence.
51
+
52
+ Contextual search entries carry the reason `contextual`, without pretending that
53
+ they had a lexical score. Direct search ordering is retained. `clusters` represent
54
+ confusable substitutes only; complementary phases are not penalized as substitutes.
55
+
56
+ The `behavioral` tag does **not** mean always-on. Truly universal disciplines still
57
+ belong in `critical`; task-specific design, debugging and testing do not. An empty
58
+ or unrelated request cannot trigger support just from its tags or usage history.
59
+
60
+ These are routing hints, not a dependency solver or proof that the LLM classified
61
+ every skill correctly. Activity tags are deliberately coarse and the router does
62
+ not infer task phases from tool output or inspect repository files. The contextual
63
+ tests use reviewed fixtures; they do not establish generated-profile quality.
64
+
65
+ The typo fallback is deliberately conservative: it runs only when fewer than
66
+ 60% of query terms are covered by the catalog or its aliases. A typo surrounded
67
+ by enough known words can therefore be missed. This is a known quality tradeoff,
68
+ not a promise to repair every misspelling.
69
+
70
+ ## What the algorithm comparison can tell us
71
+
72
+ Run the diagnostic fixture locally:
73
+
74
+ ```bash
75
+ npm run bench:ranking
76
+ ```
77
+
78
+ It compares BM25F-only ordering, AUTO without fuzzy candidates, and full AUTO on
79
+ the same 14 synthetic skills, 18 labeled positive queries and four no-signal
80
+ queries. It prints Recall@1, Recall@5, mean reciprocal rank and no-signal
81
+ promotions. It also rejects the regression where `git` was treated as an exact
82
+ name inside `digital`.
83
+
84
+ A separate two-case support diagnostic checks architecture and behavioral guidance
85
+ without literal query overlap, and verifies no promotion on unrelated input. It
86
+ uses reviewed activation tags, not init-generated labels, and does not enter the
87
+ 18-query lexical comparison below.
88
+
89
+ These are hand-authored diagnostics, **not a held-out or production dataset**.
90
+ They include typos and nearby tasks, but no generated profile, learned usage or
91
+ cluster-diversity labels. The BM25F baseline reuses AUTO's lexical component; it is
92
+ not an independent implementation. Results can reveal a tradeoff or regression,
93
+ not establish the best router for all users. Keep the denominators and report
94
+ weaker results as well as improvements.
95
+
96
+ Local diagnostic run, 2026-09-28 (working tree, Node 26.3.0, Windows x64):
97
+
98
+ | Variant | Recall@1 | Recall@5 | MRR | No-signal promotions |
99
+ | --- | --- | --- | --- | --- |
100
+ | BM25F only | 17/18 (94.4%) | 17/18 (94.4%) | 0.944 | 0/4 |
101
+ | AUTO without fuzzy | 15/18 (83.3%) | 17/18 (94.4%) | 0.889 | 0/4 |
102
+ | AUTO | 16/18 (88.9%) | 18/18 (100%) | 0.944 | 0/4 |
103
+
104
+ The hybrid recovers the final relevant item within five results, but does not win
105
+ top-one accuracy. This is why no new weight set or replacement is justified by
106
+ this fixture alone. Re-run the command for current-tree results.
107
+
108
+ | Candidate | Why keep or consider it | What is still missing |
109
+ | --- | --- | --- |
110
+ | BM25F + exact/fuzzy + RRF | Local, reproducible, no training labels or score calibration required | Representative relevance labels for weight/confidence tuning |
111
+ | BM25F alone | Simpler comparator; can lead on straightforward lexical queries | Name-typo recovery and broader intent coverage |
112
+ | Calibrated score fusion | Retains score magnitude that rank fusion discards | A separate tuning set and held-out evaluation |
113
+ | Embeddings / reranking | Could help paraphrases with little shared vocabulary | Measured quality gain, latency and privacy/cost tradeoffs; not implemented or benchmarked here |
114
+
115
+ Do not tune defaults merely to win this tiny fixture. Before replacing the router,
116
+ use separately reviewed relevance labels, keep tuning and evaluation cases
117
+ separate, compare identical catalogs and budgets, and measure both false
118
+ promotions and missed skills. The [private evaluation protocol](../BENCHMARK.md)
119
+ defines the broader gates; running it is an explicit operation.
120
+
121
+ ## Performance versus quality
122
+
123
+ ```bash
124
+ npm run bench:perf
125
+ ```
126
+
127
+ This measures `optimizePayload` with 201 messages and 284 / 2,000 synthetic skills.
128
+ Each catalog gets one cold call and 30 warm calls; mean, p50 and p95 are reported
129
+ along with runtime/platform. Warm calls reuse the same source request and query,
130
+ not an already transformed payload. The size check must remain deterministic.
131
+
132
+ This is request-preparation latency, **not Pi startup, terminal redraw or model
133
+ latency**. There is no universal millisecond gate: compare repeated runs on the
134
+ same host. Index caching should remove repeated analysis without changing scores,
135
+ and caches must remain bounded and invalidate when catalog/profile content changes.
136
+
137
+ ## Research behind the choices
138
+
139
+ BM25F combines weighted document fields before term saturation, rather than
140
+ summing independently saturated field scores. The original formulation is
141
+ [Robertson, Zaragoza and Taylor, 2004](https://www.researchgate.net/publication/221613382_Simple_BM25_extension_to_multiple_weighted_fields)
142
+ (author-uploaded paper).
143
+
144
+ RRF combines positions rather than requiring compatible score scales. Its common
145
+ constant of 60 comes from experiments in
146
+ [Cormack, Clarke and Buettcher, 2009](https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf),
147
+ not a universal optimum for skill catalogs.
148
+
149
+ Later work found tuned convex score fusion stronger than RRF in its evaluated
150
+ lexical/semantic settings, and found RRF sensitive to its parameters. See
151
+ [Bruch, Gai and Ingber, 2023](https://arxiv.org/abs/2210.11934).
152
+ Those experiments do not directly validate or invalidate this skill router.
153
+ The design judgment here is to retain the local hybrid until project-specific
154
+ evaluation justifies a replacement, while fixing demonstrated correctness and
155
+ implementation-cost problems now.
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@aeondave/pi-skill-optimizer",
3
+ "version": "3.0.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Less repeated context for Pi: local skill routing, on-demand tools, and recoverable output reduction.",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "keywords": [
11
+ "pi-package",
12
+ "pi-extension",
13
+ "optimization",
14
+ "tokens",
15
+ "skills",
16
+ "system-prompt",
17
+ "tool-output",
18
+ "mcp"
19
+ ],
20
+ "license": "MIT",
21
+ "author": "AeonDave",
22
+ "homepage": "https://github.com/AeonDave/pi-skill-optimizer#readme",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/AeonDave/pi-skill-optimizer.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/AeonDave/pi-skill-optimizer/issues"
29
+ },
30
+ "engines": {
31
+ "node": ">=26.3.0"
32
+ },
33
+ "files": [
34
+ "src",
35
+ "docs/ARCHITECTURE.md",
36
+ "docs/CONFIGURATION.md",
37
+ "docs/RANKING.md",
38
+ "assets/banner.png",
39
+ "README.md",
40
+ "BENCHMARK.md",
41
+ "LICENSE",
42
+ "CHANGELOG.md"
43
+ ],
44
+ "scripts": {
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",
47
+ "measure": "node --import tsx scripts/measure.ts",
48
+ "bench": "node --import tsx scripts/bench.ts",
49
+ "bench:perf": "node --import tsx scripts/perf-bench.ts",
50
+ "bench:ranking": "node --import tsx scripts/ranking-bench.ts",
51
+ "bench:output": "node --import tsx scripts/output-bench.ts",
52
+ "corpus:build": "node --import tsx scripts/build-real-corpus.ts",
53
+ "bench:real": "node --import tsx scripts/evaluate-real-corpus.ts"
54
+ },
55
+ "peerDependencies": {
56
+ "@earendil-works/pi-ai": "*",
57
+ "@earendil-works/pi-coding-agent": "*",
58
+ "typebox": "*"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "@earendil-works/pi-ai": {
62
+ "optional": true
63
+ },
64
+ "@earendil-works/pi-coding-agent": {
65
+ "optional": true
66
+ },
67
+ "typebox": {
68
+ "optional": true
69
+ }
70
+ },
71
+ "devDependencies": {
72
+ "@earendil-works/pi-ai": "^0.87.1",
73
+ "@earendil-works/pi-coding-agent": "^0.87.1",
74
+ "@types/node": "^26.6.3",
75
+ "tsx": "^4.23.13",
76
+ "typebox": "^1.3.34",
77
+ "typescript": "^5.9.3"
78
+ },
79
+ "pi": {
80
+ "extensions": [
81
+ "./src/index.ts"
82
+ ],
83
+ "image": "https://raw.githubusercontent.com/AeonDave/pi-skill-optimizer/v3.0.0/assets/banner.png"
84
+ }
85
+ }
@@ -0,0 +1,88 @@
1
+ export const ACTIVATION_TASKS = [
2
+ "software-design",
3
+ "software-change",
4
+ "software-debug",
5
+ "software-test",
6
+ "software-review",
7
+ "software-read",
8
+ "docs-write",
9
+ "docs-review",
10
+ "data-analysis",
11
+ "research",
12
+ "operations",
13
+ ] as const;
14
+
15
+ export type ActivationTask = (typeof ACTIVATION_TASKS)[number];
16
+ export type SkillActivationKind = "specialist" | "workflow" | "behavioral";
17
+
18
+ export interface SkillActivation {
19
+ kind: SkillActivationKind;
20
+ tasks: ActivationTask[];
21
+ }
22
+
23
+ const ACTIVATION_KINDS: ReadonlySet<string> = new Set(["specialist", "workflow", "behavioral"]);
24
+ const ACTIVATION_TASK_SET: ReadonlySet<string> = new Set(ACTIVATION_TASKS);
25
+
26
+ function isRecord(value: unknown): value is Record<string, unknown> {
27
+ return typeof value === "object" && value !== null && !Array.isArray(value);
28
+ }
29
+
30
+ function isActivationKind(value: unknown): value is SkillActivationKind {
31
+ return typeof value === "string" && ACTIVATION_KINDS.has(value);
32
+ }
33
+
34
+ /** Keep only exact names and validated activation tags from untrusted generated JSON. */
35
+ export function normalizeActivation(value: unknown): Record<string, SkillActivation> {
36
+ const normalized: Record<string, SkillActivation> = {};
37
+ if (!isRecord(value)) return normalized;
38
+
39
+ for (const [rawOwner, rawActivation] of Object.entries(value)) {
40
+ const owner = rawOwner.trim();
41
+ if (!owner || Object.prototype.hasOwnProperty.call(normalized, owner) || !isRecord(rawActivation)) continue;
42
+ if (!isActivationKind(rawActivation.kind) || !Array.isArray(rawActivation.tasks)) continue;
43
+
44
+ const tasks: ActivationTask[] = [];
45
+ const seen = new Set<ActivationTask>();
46
+ for (const task of rawActivation.tasks) {
47
+ if (typeof task !== "string" || !ACTIVATION_TASK_SET.has(task)) continue;
48
+ const activationTask = task as ActivationTask;
49
+ if (seen.has(activationTask)) continue;
50
+ seen.add(activationTask);
51
+ tasks.push(activationTask);
52
+ }
53
+ if (tasks.length === 0) continue;
54
+
55
+ Object.defineProperty(normalized, owner, {
56
+ value: { kind: rawActivation.kind, tasks },
57
+ enumerable: true,
58
+ configurable: true,
59
+ writable: true,
60
+ });
61
+ }
62
+
63
+ return normalized;
64
+ }
65
+
66
+ /** Return one-hop workflow/behavioral companions with shared task tags. */
67
+ export function contextualSkillNames(
68
+ activation: Readonly<Record<string, SkillActivation>> | undefined,
69
+ primaryName: string | undefined,
70
+ ): string[] {
71
+ if (!activation || !primaryName || !Object.prototype.hasOwnProperty.call(activation, primaryName)) return [];
72
+ const primary = activation[primaryName];
73
+ if (!primary || !Array.isArray(primary.tasks) || primary.tasks.length === 0) return [];
74
+
75
+ const primaryTasks = new Set(primary.tasks);
76
+ const candidates: Array<{ name: string; overlap: number }> = [];
77
+ for (const [name, candidate] of Object.entries(activation)) {
78
+ if (name === primaryName || (candidate.kind !== "workflow" && candidate.kind !== "behavioral")) continue;
79
+ const candidateTasks = new Set(candidate.tasks);
80
+ let overlap = 0;
81
+ for (const task of primaryTasks) if (candidateTasks.has(task)) overlap += 1;
82
+ if (overlap > 0) candidates.push({ name, overlap });
83
+ }
84
+ candidates.sort((left, right) =>
85
+ right.overlap - left.overlap || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0),
86
+ );
87
+ return candidates.map(({ name }) => name);
88
+ }
package/src/aliases.ts ADDED
@@ -0,0 +1,111 @@
1
+ export type QueryAliasMap = ReadonlyMap<string, readonly string[]>;
2
+ export type AliasRecord = Record<string, string[]>;
3
+
4
+ // Static, reviewed vocabulary only. Catalog filtering below prevents an alias
5
+ // from adding terms that do not exist in the active catalog.
6
+ export const CANDIDATE_QUERY_ALIASES: Readonly<Record<string, readonly string[]>> = Object.freeze({
7
+ ad: ["active", "directory", "kerberos", "ldap"],
8
+ adb: ["android", "mobile"],
9
+ apk: ["android", "mobile", "application"],
10
+ authn: ["authentication", "identity"],
11
+ authz: ["authorization", "permission"],
12
+ aws: ["cloud", "amazon"],
13
+ azure: ["cloud", "microsoft"],
14
+ binary: ["reverse", "reversing", "exploitation"],
15
+ bug: ["debug", "debugging", "defect"],
16
+ ci: ["cicd", "pipeline"],
17
+ cli: ["command", "shell"],
18
+ container: ["docker", "kubernetes"],
19
+ crack: ["cracking", "password", "hash"],
20
+ ctf: ["challenge", "exploit"],
21
+ db: ["database", "sql"],
22
+ debug: ["debugging", "gdb", "windbg"],
23
+ deploy: ["deployment", "release"],
24
+ docker: ["container"],
25
+ forensic: ["forensics", "artifact"],
26
+ fuzz: ["fuzzer", "fuzzing"],
27
+ git: ["github", "repository"],
28
+ incident: ["forensics", "response"],
29
+ js: ["javascript", "typescript", "node"],
30
+ k8s: ["kubernetes", "container"],
31
+ malware: ["reverse", "analysis"],
32
+ mobile: ["android", "ios", "application"],
33
+ network: ["packet", "traffic", "protocol"],
34
+ osint: ["intelligence", "reconnaissance"],
35
+ pentest: ["security", "assessment"],
36
+ plugin: ["extension"],
37
+ pr: ["pull", "request", "review"],
38
+ pwn: ["binary", "exploitation"],
39
+ py: ["python"],
40
+ recon: ["reconnaissance", "discovery", "enumeration"],
41
+ recover: ["recovery"],
42
+ refactor: ["architecture", "design"],
43
+ reverse: ["reversing", "analysis"],
44
+ rtk: ["output", "compression"],
45
+ sqli: ["sql", "injection"],
46
+ test: ["testing", "tests"],
47
+ token: ["context", "prompt"],
48
+ tunnel: ["pivot", "proxy"],
49
+ vuln: ["vulnerability", "security"],
50
+ web: ["http", "browser"],
51
+ });
52
+
53
+ const OVERBROAD_TARGETS = new Set(["app", "code", "file", "tool", "use", "workflow"]);
54
+
55
+ function normalizeToken(value: string): string {
56
+ return value
57
+ .normalize("NFKD")
58
+ .replace(/[\u0300-\u036f]/g, "")
59
+ .trim()
60
+ .toLocaleLowerCase("en-US")
61
+ .replace(/[^\p{L}\p{N}#+.-]+/gu, "-")
62
+ .replace(/^-+|-+$/g, "");
63
+ }
64
+
65
+ export function normalizeAliasRecord(value: unknown): AliasRecord {
66
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
67
+ const out: AliasRecord = {};
68
+ for (const [rawSource, rawTargets] of Object.entries(value)) {
69
+ const source = normalizeToken(rawSource);
70
+ if (!source || !Array.isArray(rawTargets)) continue;
71
+ const targets = [...new Set(rawTargets
72
+ .filter((target): target is string => typeof target === "string")
73
+ .map(normalizeToken)
74
+ .filter((target) => target && target !== source && !OVERBROAD_TARGETS.has(target)))];
75
+ if (targets.length > 0) out[source] = targets;
76
+ }
77
+ return out;
78
+ }
79
+
80
+ /** Build an immutable alias view scoped to one catalog. */
81
+ export function buildCatalogAliases(
82
+ hasTerm: (term: string) => boolean,
83
+ extraCandidates: AliasRecord = {},
84
+ ): QueryAliasMap {
85
+ const candidates = { ...CANDIDATE_QUERY_ALIASES, ...normalizeAliasRecord(extraCandidates) };
86
+ const aliases = new Map<string, readonly string[]>();
87
+ for (const [source, rawTargets] of Object.entries(candidates)) {
88
+ const targets = [...new Set(rawTargets
89
+ .map(normalizeToken)
90
+ .filter((target) => target && !OVERBROAD_TARGETS.has(target) && hasTerm(target)))];
91
+ if (targets.length > 0) aliases.set(normalizeToken(source), Object.freeze(targets));
92
+ }
93
+ return aliases;
94
+ }
95
+
96
+ export function expandQueryTokens(tokens: readonly string[], aliases: QueryAliasMap): string[] {
97
+ const expanded: string[] = [];
98
+ const seen = new Set<string>();
99
+ for (const rawToken of tokens) {
100
+ const token = normalizeToken(rawToken);
101
+ if (!token || seen.has(token)) continue;
102
+ seen.add(token);
103
+ expanded.push(token);
104
+ for (const target of aliases.get(token) ?? []) {
105
+ if (seen.has(target)) continue;
106
+ seen.add(target);
107
+ expanded.push(target);
108
+ }
109
+ }
110
+ return expanded;
111
+ }