pi-skill-search 0.1.0 → 0.2.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.
- package/CHANGELOG.md +17 -5
- package/README.md +35 -30
- package/SPEC.md +1083 -0
- package/install.mjs +47 -0
- package/package.json +48 -46
- package/src/categories.ts +3 -24
- package/src/indexer.ts +1 -7
- package/src/scanner.ts +62 -14
- package/src/search.ts +2 -2
- package/src/types.ts +4 -1
- /package/{skills → data}/adaptyv/SKILL.md +0 -0
- /package/{skills → data}/add-community-extension/SKILL.md +0 -0
- /package/{skills → data}/aeon/SKILL.md +0 -0
- /package/{skills → data}/ai-slop-cleaner/SKILL.md +0 -0
- /package/{skills → data}/anndata/SKILL.md +0 -0
- /package/{skills → data}/arboreto/SKILL.md +0 -0
- /package/{skills → data}/ask/SKILL.md +0 -0
- /package/{skills → data}/astropy/SKILL.md +0 -0
- /package/{skills → data}/async-worker-recovery/SKILL.md +0 -0
- /package/{skills → data}/autopilot/SKILL.md +0 -0
- /package/{skills → data}/autoresearch/SKILL.md +0 -0
- /package/{skills → data}/autoskill/SKILL.md +0 -0
- /package/{skills → data}/babysit/SKILL.md +0 -0
- /package/{skills → data}/benchling-integration/SKILL.md +0 -0
- /package/{skills → data}/bgpt-paper-search/SKILL.md +0 -0
- /package/{skills → data}/biopython/SKILL.md +0 -0
- /package/{skills → data}/bioservices/SKILL.md +0 -0
- /package/{skills → data}/brainstorming/SKILL.md +0 -0
- /package/{skills → data}/cancel/SKILL.md +0 -0
- /package/{skills → data}/ccg/SKILL.md +0 -0
- /package/{skills → data}/celery-pipeline/SKILL.md +0 -0
- /package/{skills → data}/cellxgene-census/SKILL.md +0 -0
- /package/{skills → data}/child-pi-spawning/SKILL.md +0 -0
- /package/{skills → data}/cirq/SKILL.md +0 -0
- /package/{skills → data}/citation-management/SKILL.md +0 -0
- /package/{skills → data}/clinical-decision-support/SKILL.md +0 -0
- /package/{skills → data}/clinical-reports/SKILL.md +0 -0
- /package/{skills → data}/clinical-trial/SKILL.md +0 -0
- /package/{skills → data}/cobrapy/SKILL.md +0 -0
- /package/{skills → data}/configure-notifications/SKILL.md +0 -0
- /package/{skills → data}/consciousness-council/SKILL.md +0 -0
- /package/{skills → data}/context-artifact-hygiene/SKILL.md +0 -0
- /package/{skills → data}/context-mode-ops/SKILL.md +0 -0
- /package/{skills → data}/dask/SKILL.md +0 -0
- /package/{skills → data}/database-lookup/SKILL.md +0 -0
- /package/{skills → data}/datamol/SKILL.md +0 -0
- /package/{skills → data}/debug/SKILL.md +0 -0
- /package/{skills → data}/deep-dive/SKILL.md +0 -0
- /package/{skills → data}/deep-interview/SKILL.md +0 -0
- /package/{skills → data}/deepchem/SKILL.md +0 -0
- /package/{skills → data}/deepinit/SKILL.md +0 -0
- /package/{skills → data}/deeptools/SKILL.md +0 -0
- /package/{skills → data}/delegation-patterns/SKILL.md +0 -0
- /package/{skills → data}/depmap/SKILL.md +0 -0
- /package/{skills → data}/dhdna-profiler/SKILL.md +0 -0
- /package/{skills → data}/diffdock/SKILL.md +0 -0
- /package/{skills → data}/dispatching-parallel-agents/SKILL.md +0 -0
- /package/{skills → data}/dnanexus-integration/SKILL.md +0 -0
- /package/{skills → data}/do/SKILL.md +0 -0
- /package/{skills → data}/docker-sandbox/SKILL.md +0 -0
- /package/{skills → data}/docx/SKILL.md +0 -0
- /package/{skills → data}/esm/SKILL.md +0 -0
- /package/{skills → data}/etetoolkit/SKILL.md +0 -0
- /package/{skills → data}/event-log-tracing/SKILL.md +0 -0
- /package/{skills → data}/exa-search/SKILL.md +0 -0
- /package/{skills → data}/executing-plans/SKILL.md +0 -0
- /package/{skills → data}/exploratory-data-analysis/SKILL.md +0 -0
- /package/{skills → data}/external-context/SKILL.md +0 -0
- /package/{skills → data}/fastapi/SKILL.md +0 -0
- /package/{skills → data}/finishing-a-development-branch/SKILL.md +0 -0
- /package/{skills → data}/flowio/SKILL.md +0 -0
- /package/{skills → data}/fluidsim/SKILL.md +0 -0
- /package/{skills → data}/generate-image/SKILL.md +0 -0
- /package/{skills → data}/geniml/SKILL.md +0 -0
- /package/{skills → data}/geomaster/SKILL.md +0 -0
- /package/{skills → data}/geopandas/SKILL.md +0 -0
- /package/{skills → data}/get-available-resources/SKILL.md +0 -0
- /package/{skills → data}/gget/SKILL.md +0 -0
- /package/{skills → data}/ginkgo-cloud-lab/SKILL.md +0 -0
- /package/{skills → data}/git-master/SKILL.md +0 -0
- /package/{skills → data}/glycoengineering/SKILL.md +0 -0
- /package/{skills → data}/gtars/SKILL.md +0 -0
- /package/{skills → data}/hackernews-frontpage/SKILL.md +0 -0
- /package/{skills → data}/histolab/SKILL.md +0 -0
- /package/{skills → data}/how-it-works/SKILL.md +0 -0
- /package/{skills → data}/hud/SKILL.md +0 -0
- /package/{skills → data}/hugging-science/SKILL.md +0 -0
- /package/{skills → data}/huggingface/SKILL.md +0 -0
- /package/{skills → data}/hypogenic/SKILL.md +0 -0
- /package/{skills → data}/hypothesis-generation/SKILL.md +0 -0
- /package/{skills → data}/imaging-data-commons/SKILL.md +0 -0
- /package/{skills → data}/infographics/SKILL.md +0 -0
- /package/{skills → data}/iso-13485-certification/SKILL.md +0 -0
- /package/{skills → data}/knowledge-agent/SKILL.md +0 -0
- /package/{skills → data}/labarchive-integration/SKILL.md +0 -0
- /package/{skills → data}/lamindb/SKILL.md +0 -0
- /package/{skills → data}/landsat/SKILL.md +0 -0
- /package/{skills → data}/latchbio-integration/SKILL.md +0 -0
- /package/{skills → data}/latex-posters/SKILL.md +0 -0
- /package/{skills → data}/learn-codebase/SKILL.md +0 -0
- /package/{skills → data}/learner/SKILL.md +0 -0
- /package/{skills → data}/literature-review/SKILL.md +0 -0
- /package/{skills → data}/live-agent-lifecycle/SKILL.md +0 -0
- /package/{skills → data}/mailbox-interactive/SKILL.md +0 -0
- /package/{skills → data}/make-plan/SKILL.md +0 -0
- /package/{skills → data}/markdown-mermaid-writing/SKILL.md +0 -0
- /package/{skills → data}/market-research-reports/SKILL.md +0 -0
- /package/{skills → data}/markitdown/SKILL.md +0 -0
- /package/{skills → data}/markitdown-docs/SKILL.md +0 -0
- /package/{skills → data}/matchms/SKILL.md +0 -0
- /package/{skills → data}/matlab/SKILL.md +0 -0
- /package/{skills → data}/matplotlib/SKILL.md +0 -0
- /package/{skills → data}/mcp-setup/SKILL.md +0 -0
- /package/{skills → data}/medchem/SKILL.md +0 -0
- /package/{skills → data}/mem-search/SKILL.md +0 -0
- /package/{skills → data}/modal/SKILL.md +0 -0
- /package/{skills → data}/model-routing-context/SKILL.md +0 -0
- /package/{skills → data}/molecular-dynamics/SKILL.md +0 -0
- /package/{skills → data}/molfeat/SKILL.md +0 -0
- /package/{skills → data}/multi-perspective-review/SKILL.md +0 -0
- /package/{skills → data}/networkx/SKILL.md +0 -0
- /package/{skills → data}/neurokit2/SKILL.md +0 -0
- /package/{skills → data}/neuropixels-analysis/SKILL.md +0 -0
- /package/{skills → data}/nilearn/SKILL.md +0 -0
- /package/{skills → data}/observability-reliability/SKILL.md +0 -0
- /package/{skills → data}/omc-doctor/SKILL.md +0 -0
- /package/{skills → data}/omc-reference/SKILL.md +0 -0
- /package/{skills → data}/omc-setup/SKILL.md +0 -0
- /package/{skills → data}/omc-teams/SKILL.md +0 -0
- /package/{skills → data}/omero-integration/SKILL.md +0 -0
- /package/{skills → data}/open-notebook/SKILL.md +0 -0
- /package/{skills → data}/openephys/SKILL.md +0 -0
- /package/{skills → data}/opentrons-integration/SKILL.md +0 -0
- /package/{skills → data}/optimize-for-gpu/SKILL.md +0 -0
- /package/{skills → data}/orchestration/SKILL.md +0 -0
- /package/{skills → data}/ownership-session-security/SKILL.md +0 -0
- /package/{skills → data}/paper-lookup/SKILL.md +0 -0
- /package/{skills → data}/paperzilla/SKILL.md +0 -0
- /package/{skills → data}/parallel-web/SKILL.md +0 -0
- /package/{skills → data}/pathfinder/SKILL.md +0 -0
- /package/{skills → data}/pathml/SKILL.md +0 -0
- /package/{skills → data}/pdf/SKILL.md +0 -0
- /package/{skills → data}/peer-review/SKILL.md +0 -0
- /package/{skills → data}/pennylane/SKILL.md +0 -0
- /package/{skills → data}/phylogenetics/SKILL.md +0 -0
- /package/{skills → data}/pi-extension-lifecycle/SKILL.md +0 -0
- /package/{skills → data}/plan/SKILL.md +0 -0
- /package/{skills → data}/polars/SKILL.md +0 -0
- /package/{skills → data}/polars-bio/SKILL.md +0 -0
- /package/{skills → data}/pptx/SKILL.md +0 -0
- /package/{skills → data}/pptx-posters/SKILL.md +0 -0
- /package/{skills → data}/primekg/SKILL.md +0 -0
- /package/{skills → data}/project-session-manager/SKILL.md +0 -0
- /package/{skills → data}/protocolsio-integration/SKILL.md +0 -0
- /package/{skills → data}/pubmed-search/SKILL.md +0 -0
- /package/{skills → data}/pufferlib/SKILL.md +0 -0
- /package/{skills → data}/pydeseq2/SKILL.md +0 -0
- /package/{skills → data}/pydicom/SKILL.md +0 -0
- /package/{skills → data}/pyhealth/SKILL.md +0 -0
- /package/{skills → data}/pylabrobot/SKILL.md +0 -0
- /package/{skills → data}/pymatgen/SKILL.md +0 -0
- /package/{skills → data}/pymc/SKILL.md +0 -0
- /package/{skills → data}/pymoo/SKILL.md +0 -0
- /package/{skills → data}/pyopenms/SKILL.md +0 -0
- /package/{skills → data}/pysam/SKILL.md +0 -0
- /package/{skills → data}/pyspark/SKILL.md +0 -0
- /package/{skills → data}/pytdc/SKILL.md +0 -0
- /package/{skills → data}/pytorch/SKILL.md +0 -0
- /package/{skills → data}/pytorch-lightning/SKILL.md +0 -0
- /package/{skills → data}/pyzotero/SKILL.md +0 -0
- /package/{skills → data}/qiskit/SKILL.md +0 -0
- /package/{skills → data}/qutip/SKILL.md +0 -0
- /package/{skills → data}/ralph/SKILL.md +0 -0
- /package/{skills → data}/ralplan/SKILL.md +0 -0
- /package/{skills → data}/rdflib/SKILL.md +0 -0
- /package/{skills → data}/rdkit/SKILL.md +0 -0
- /package/{skills → data}/read-only-explorer/SKILL.md +0 -0
- /package/{skills → data}/receiving-code-review/SKILL.md +0 -0
- /package/{skills → data}/release/SKILL.md +0 -0
- /package/{skills → data}/remember/SKILL.md +0 -0
- /package/{skills → data}/requesting-code-review/SKILL.md +0 -0
- /package/{skills → data}/requirements-to-task-packet/SKILL.md +0 -0
- /package/{skills → data}/research-grants/SKILL.md +0 -0
- /package/{skills → data}/research-lookup/SKILL.md +0 -0
- /package/{skills → data}/research-reproducibility/SKILL.md +0 -0
- /package/{skills → data}/resource-discovery-config/SKILL.md +0 -0
- /package/{skills → data}/rowan/SKILL.md +0 -0
- /package/{skills → data}/runtime-state-reader/SKILL.md +0 -0
- /package/{skills → data}/safe-bash/SKILL.md +0 -0
- /package/{skills → data}/scanpy/SKILL.md +0 -0
- /package/{skills → data}/scholar-evaluation/SKILL.md +0 -0
- /package/{skills → data}/scientific-brainstorming/SKILL.md +0 -0
- /package/{skills → data}/scientific-critical-thinking/SKILL.md +0 -0
- /package/{skills → data}/scientific-schematics/SKILL.md +0 -0
- /package/{skills → data}/scientific-slides/SKILL.md +0 -0
- /package/{skills → data}/scientific-visualization/SKILL.md +0 -0
- /package/{skills → data}/scientific-writing/SKILL.md +0 -0
- /package/{skills → data}/scikit-bio/SKILL.md +0 -0
- /package/{skills → data}/scikit-learn/SKILL.md +0 -0
- /package/{skills → data}/scikit-survival/SKILL.md +0 -0
- /package/{skills → data}/sciomc/SKILL.md +0 -0
- /package/{skills → data}/scvelo/SKILL.md +0 -0
- /package/{skills → data}/scvi-tools/SKILL.md +0 -0
- /package/{skills → data}/seaborn/SKILL.md +0 -0
- /package/{skills → data}/secure-agent-orchestration-review/SKILL.md +0 -0
- /package/{skills → data}/self-improve/SKILL.md +0 -0
- /package/{skills → data}/semantic-compression/SKILL.md +0 -0
- /package/{skills → data}/setup/SKILL.md +0 -0
- /package/{skills → data}/shap/SKILL.md +0 -0
- /package/{skills → data}/simpy/SKILL.md +0 -0
- /package/{skills → data}/skill/SKILL.md +0 -0
- /package/{skills → data}/skill-search/SKILL.md +0 -0
- /package/{skills → data}/skillify/SKILL.md +0 -0
- /package/{skills → data}/smart-explore/SKILL.md +0 -0
- /package/{skills → data}/sqlite-pandas/SKILL.md +0 -0
- /package/{skills → data}/stable-baselines3/SKILL.md +0 -0
- /package/{skills → data}/state-mutation-locking/SKILL.md +0 -0
- /package/{skills → data}/statistical-analysis/SKILL.md +0 -0
- /package/{skills → data}/statsmodels/SKILL.md +0 -0
- /package/{skills → data}/subagent-driven-development/SKILL.md +0 -0
- /package/{skills → data}/sympy/SKILL.md +0 -0
- /package/{skills → data}/system-prompts/SKILL.md +0 -0
- /package/{skills → data}/systematic-debugging/SKILL.md +0 -0
- /package/{skills → data}/team/SKILL.md +0 -0
- /package/{skills → data}/test-driven-development/SKILL.md +0 -0
- /package/{skills → data}/tiledbvcf/SKILL.md +0 -0
- /package/{skills → data}/timeline-report/SKILL.md +0 -0
- /package/{skills → data}/timesfm-forecasting/SKILL.md +0 -0
- /package/{skills → data}/torch-geometric/SKILL.md +0 -0
- /package/{skills → data}/torchdrug/SKILL.md +0 -0
- /package/{skills → data}/trace/SKILL.md +0 -0
- /package/{skills → data}/transformers/SKILL.md +0 -0
- /package/{skills → data}/treatment-plans/SKILL.md +0 -0
- /package/{skills → data}/ui-render-performance/SKILL.md +0 -0
- /package/{skills → data}/ultragoal/SKILL.md +0 -0
- /package/{skills → data}/ultraqa/SKILL.md +0 -0
- /package/{skills → data}/ultrawork/SKILL.md +0 -0
- /package/{skills → data}/umap-learn/SKILL.md +0 -0
- /package/{skills → data}/usfiscaldata/SKILL.md +0 -0
- /package/{skills → data}/using-git-worktrees/SKILL.md +0 -0
- /package/{skills → data}/using-superpowers/SKILL.md +0 -0
- /package/{skills → data}/using-vetc/SKILL.md +0 -0
- /package/{skills → data}/vaex/SKILL.md +0 -0
- /package/{skills → data}/venue-templates/SKILL.md +0 -0
- /package/{skills → data}/verification-before-completion/SKILL.md +0 -0
- /package/{skills → data}/verification-before-done/SKILL.md +0 -0
- /package/{skills → data}/verify/SKILL.md +0 -0
- /package/{skills → data}/version-bump/SKILL.md +0 -0
- /package/{skills → data}/vetc-analyze-ba/SKILL.md +0 -0
- /package/{skills → data}/vetc-analyze-codebase/SKILL.md +0 -0
- /package/{skills → data}/vetc-api-design/SKILL.md +0 -0
- /package/{skills → data}/vetc-brainstorming/SKILL.md +0 -0
- /package/{skills → data}/vetc-change-proposal/SKILL.md +0 -0
- /package/{skills → data}/vetc-cicd/SKILL.md +0 -0
- /package/{skills → data}/vetc-continuous-learning/SKILL.md +0 -0
- /package/{skills → data}/vetc-deep-interview/SKILL.md +0 -0
- /package/{skills → data}/vetc-docgen/SKILL.md +0 -0
- /package/{skills → data}/vetc-frontend-patterns/SKILL.md +0 -0
- /package/{skills → data}/vetc-iterative-retrieval/SKILL.md +0 -0
- /package/{skills → data}/vetc-java-patterns/SKILL.md +0 -0
- /package/{skills → data}/vetc-meta-skill-creator/SKILL.md +0 -0
- /package/{skills → data}/vetc-oracle-patterns/SKILL.md +0 -0
- /package/{skills → data}/vetc-performance-testing/SKILL.md +0 -0
- /package/{skills → data}/vetc-pr-response/SKILL.md +0 -0
- /package/{skills → data}/vetc-ralph/SKILL.md +0 -0
- /package/{skills → data}/vetc-ralplan/SKILL.md +0 -0
- /package/{skills → data}/vetc-receiving-review/SKILL.md +0 -0
- /package/{skills → data}/vetc-reconcile-patterns/SKILL.md +0 -0
- /package/{skills → data}/vetc-refactoring/SKILL.md +0 -0
- /package/{skills → data}/vetc-runbook/SKILL.md +0 -0
- /package/{skills → data}/vetc-sast/SKILL.md +0 -0
- /package/{skills → data}/vetc-sdlc/SKILL.md +0 -0
- /package/{skills → data}/vetc-security/SKILL.md +0 -0
- /package/{skills → data}/vetc-spec-driven/SKILL.md +0 -0
- /package/{skills → data}/vetc-spec-quality/SKILL.md +0 -0
- /package/{skills → data}/vetc-systematic-debugging/SKILL.md +0 -0
- /package/{skills → data}/vetc-tdd/SKILL.md +0 -0
- /package/{skills → data}/vetc-thinking-pm/SKILL.md +0 -0
- /package/{skills → data}/vetc-ui-visual-qa/SKILL.md +0 -0
- /package/{skills → data}/vetc-verify/SKILL.md +0 -0
- /package/{skills → data}/visual-verdict/SKILL.md +0 -0
- /package/{skills → data}/what-if-oracle/SKILL.md +0 -0
- /package/{skills → data}/widget-rendering/SKILL.md +0 -0
- /package/{skills → data}/wiki/SKILL.md +0 -0
- /package/{skills → data}/workspace-isolation/SKILL.md +0 -0
- /package/{skills → data}/worktree-isolation/SKILL.md +0 -0
- /package/{skills → data}/wowerpoint/SKILL.md +0 -0
- /package/{skills → data}/writer-memory/SKILL.md +0 -0
- /package/{skills → data}/writing-plans/SKILL.md +0 -0
- /package/{skills → data}/writing-skills/SKILL.md +0 -0
- /package/{skills → data}/xgboost/SKILL.md +0 -0
- /package/{skills → data}/xgboost-ts/SKILL.md +0 -0
- /package/{skills → data}/xlsx/SKILL.md +0 -0
- /package/{skills → data}/zarr-python/SKILL.md +0 -0
package/SPEC.md
ADDED
|
@@ -0,0 +1,1083 @@
|
|
|
1
|
+
# Spec: pi-skill-search — On-Demand Skill Search Extension
|
|
2
|
+
|
|
3
|
+
Date: 2026-05-15
|
|
4
|
+
|
|
5
|
+
## Source
|
|
6
|
+
|
|
7
|
+
- User prompt: Deep analysis of `scientific-agent-skills/` repo → token cost analysis → search tool design
|
|
8
|
+
- Measurement data: 137 skills × 23,589 tokens injected, benchmarked keyword/TF-IDF, cost projections
|
|
9
|
+
- Reference: `Source/harness-experimental/` methodology
|
|
10
|
+
|
|
11
|
+
## Project Summary
|
|
12
|
+
|
|
13
|
+
**Product**: A Pi extension (`pi-skill-search`) that replaces the "inject all skill descriptions into system prompt" pattern with an on-demand search tool + category summary.
|
|
14
|
+
|
|
15
|
+
**For whom**: Pi users who have large skill collections (137+ scientific skills from `scientific-agent-skills`, or any growing skill set).
|
|
16
|
+
|
|
17
|
+
**Why**: Injecting all skill descriptions costs 23,589 tokens (11.8% of 200K context window), multiplies per turn ($3.54/session at 50 turns), triggers compaction 15% sooner, and floods agent with 85-93% false-positive candidates. Search tool + category summary costs ~381 tokens startup (98% savings) while maintaining agent discoverability.
|
|
18
|
+
|
|
19
|
+
## Candidate Product Docs
|
|
20
|
+
|
|
21
|
+
| File | Purpose | Source sections |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `docs/product/skill-search.md` | Tool contract, search algorithm, category rules | This spec §4–§6 |
|
|
24
|
+
| `docs/product/category-rules.md` | Category classification rules and synonym dictionary | This spec §5 |
|
|
25
|
+
| `docs/decisions/0004-search-over-inject.md` | Why search beats inject — token cost evidence | This spec §3 |
|
|
26
|
+
|
|
27
|
+
## Candidate Epics
|
|
28
|
+
|
|
29
|
+
| Epic | Description | Status |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| E01 | Core extension: indexer + search tool + category summary | unsliced |
|
|
32
|
+
| E02 | Proactive suggestion: `tool_call` hook for package detection | unsliced |
|
|
33
|
+
| E03 | Query caching + telemetry hooks | unsliced |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 1. Current Behavior
|
|
38
|
+
|
|
39
|
+
When Pi starts a session, `skills.ts` discovers all `SKILL.md` files from configured skill directories. It extracts `name` and `description` from YAML frontmatter and injects ALL of them into the system prompt via `formatSkillsForPrompt()`:
|
|
40
|
+
|
|
41
|
+
```xml
|
|
42
|
+
<available_skills>
|
|
43
|
+
<skill>
|
|
44
|
+
<name>rdkit</name>
|
|
45
|
+
<description>Cheminformatics toolkit for fine-grained molecular control...</description>
|
|
46
|
+
<location>.../rdkit/SKILL.md</location>
|
|
47
|
+
</skill>
|
|
48
|
+
<!-- ×137 for scientific-agent-skills -->
|
|
49
|
+
</available_skills>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This costs **23,589 tokens** at startup and is re-sent on every agent turn.
|
|
53
|
+
|
|
54
|
+
### Measured costs
|
|
55
|
+
|
|
56
|
+
| Metric | Value |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| Skills indexed | 137 |
|
|
59
|
+
| Description chars total | 82,564 |
|
|
60
|
+
| Prompt tokens total (chars/3.5) | 23,589 |
|
|
61
|
+
| % of 200K context window | 11.8% |
|
|
62
|
+
| Cost per session (50 turns, Sonnet 4) | $3.54 |
|
|
63
|
+
| Conversation budget stolen | 18% (23K of 130K available) |
|
|
64
|
+
| Compaction triggered sooner by | ~15% |
|
|
65
|
+
| False-positive candidates per query | 85-93% |
|
|
66
|
+
| Skill pairs with >25% description overlap | 21 (confusing for agent) |
|
|
67
|
+
|
|
68
|
+
### Cross-reference pollution
|
|
69
|
+
|
|
70
|
+
61/137 skills mention other skill names in their descriptions (e.g., `datamol` says "wrapper around RDKit"). This causes keyword confusion — pure keyword count returns `datamol` for query "rdkit" because "rdkit" appears 3 times in `datamol`'s description.
|
|
71
|
+
|
|
72
|
+
### Cohabitation with Pi's built-in injection
|
|
73
|
+
|
|
74
|
+
Pi has **no opt-out flag** for `formatSkillsForPrompt()`. Whenever a skill `read` tool is selected (the default) and skills are loaded via `LoadSkillsOptions`, the `<available_skills>` block is appended automatically inside `buildSystemPrompt()`.
|
|
75
|
+
|
|
76
|
+
This means an extension that only *adds* a category summary will run **alongside** the inject-all behavior, producing the worst of both worlds: `Pi inject (≈23,589 tokens) + extension summary (≈150) + tool definition (≈166) ≈ 23,905 tokens` per turn — *higher* than the unmodified 23,589 baseline. The extension MUST actively suppress Pi's injection to realize the savings.
|
|
77
|
+
|
|
78
|
+
(The §2 target table's "~381 tokens startup" figure is the rolled-up total of summary + tool definition + scaffolding overhead. The breakdown above shows the components separately to make the cohabitation math explicit.)
|
|
79
|
+
|
|
80
|
+
Two suppression mechanisms are available through the `before_agent_start` event (verified against `Source/pi-mono/packages/coding-agent/src/core/extensions/types.ts` at v0.74.0):
|
|
81
|
+
|
|
82
|
+
1. **Strip the rendered block** — `event.systemPrompt` is the already-built string; extension returns `{ systemPrompt: stripped }` from `before_agent_start`.
|
|
83
|
+
2. **Read Pi's loaded skills** — `event.systemPromptOptions.skills: Skill[]` exposes the parsed `Skill[]` Pi already discovered, so the extension never re-scans nor re-parses YAML.
|
|
84
|
+
|
|
85
|
+
This spec adopts mechanism (1) for output and mechanism (2) for input. See §7.3 for the implementation.
|
|
86
|
+
|
|
87
|
+
## 2. Target Behavior
|
|
88
|
+
|
|
89
|
+
Replace inject-all with two layers:
|
|
90
|
+
|
|
91
|
+
**Layer 1 — Category summary** (~150 tokens, always in prompt):
|
|
92
|
+
A concise summary of available skill domains injected via `before_agent_start`. Agent knows WHAT categories exist.
|
|
93
|
+
|
|
94
|
+
**Layer 2 — Search tool** (~166 tokens for tool definition):
|
|
95
|
+
A `skill-search` tool that agent calls on-demand. Returns top 3-5 matching skills with name, description, and file path. Agent then uses `read` to load the full SKILL.md.
|
|
96
|
+
|
|
97
|
+
### Target costs
|
|
98
|
+
|
|
99
|
+
| Metric | Inject All | Search + Categories | Savings |
|
|
100
|
+
| --- | --- | --- | --- |
|
|
101
|
+
| Startup tokens | 23,589 | ~381 | 98% |
|
|
102
|
+
| Cost/session (50 turns) | $3.54 | $0.03 | 99% |
|
|
103
|
+
| % context window | 11.8% | 0.19% | — |
|
|
104
|
+
| Conversation budget stolen | 18% | ~0% | — |
|
|
105
|
+
|
|
106
|
+
### Scalability
|
|
107
|
+
|
|
108
|
+
| Skills | Inject All | Search Tool |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| 137 | 23,589 tok (11.8%) | 166 tok (0.08%) |
|
|
111
|
+
| 500 | 86,091 tok (43%) | 166 tok (0.08%) |
|
|
112
|
+
| 1,000 | 172,182 tok (86%) | 166 tok (0.08%) |
|
|
113
|
+
|
|
114
|
+
Search tool cost is **constant** regardless of skill count.
|
|
115
|
+
|
|
116
|
+
## 3. Decision: Search over Inject
|
|
117
|
+
|
|
118
|
+
**Decision ID**: `docs/decisions/0004-search-over-inject.md` (renumbered from
|
|
119
|
+
`0001` in earlier drafts to coexist with harness decisions `0001`–`0003`
|
|
120
|
+
inherited from `Source/harness-experimental`).
|
|
121
|
+
|
|
122
|
+
### Context
|
|
123
|
+
|
|
124
|
+
Pi's `formatSkillsForPrompt()` injects all skill descriptions into the system prompt. This is sustainable for 10-30 skills (~2,000 tokens) but breaks at 137+ skills.
|
|
125
|
+
|
|
126
|
+
### Decision
|
|
127
|
+
|
|
128
|
+
Use on-demand search tool + category summary instead of inject-all.
|
|
129
|
+
|
|
130
|
+
### Rationale
|
|
131
|
+
|
|
132
|
+
1. **Token cost is multiplicative**: 23K tokens × N turns. At 50 turns = 1.17M input tokens just for descriptions.
|
|
133
|
+
2. **Most skills unused**: Typical session uses 0-3 skills. 134+ descriptions are pure waste.
|
|
134
|
+
3. **Agent decision quality degrades**: With 137 candidates, 85-93% are false positives per query. Agent must reason through noise.
|
|
135
|
+
4. **Scalability ceiling**: At 500 skills, inject occupies 43% of context. At 1,000 — 86%. Dead end.
|
|
136
|
+
5. **Search is constant cost**: 166 tokens regardless of skill count. 3ms latency per query.
|
|
137
|
+
|
|
138
|
+
### Consequences
|
|
139
|
+
|
|
140
|
+
- Agent must explicitly call `skill-search` to discover specific skills (one extra LLM turn).
|
|
141
|
+
- Category summary mitigates this by telling agent which domains exist.
|
|
142
|
+
- `tool_call` hook (E02) can proactively suggest when agent writes code with known packages.
|
|
143
|
+
- **Extension must strip Pi's auto-injected `<available_skills>` block from `systemPrompt` every turn**, since Pi exposes no opt-out flag for `formatSkillsForPrompt()`. Failure to strip results in *higher* total tokens than the baseline (see §1 Cohabitation).
|
|
144
|
+
- Token savings are realized **per turn** via `before_agent_start` mutation. There is no persistent system-message API in Pi (verified §10.1) — the cost model is "(summary + tool) × N turns" not "(summary + tool) × 1".
|
|
145
|
+
|
|
146
|
+
## 4. Design
|
|
147
|
+
|
|
148
|
+
### 4.1 Domain Model
|
|
149
|
+
|
|
150
|
+
The extension's internal types are derived from Pi's `Skill` (defined in `Source/pi-mono/packages/coding-agent/src/core/skills.ts:75`). The mapping is:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
Pi Skill (input) → SkillEntry (extension internal)
|
|
154
|
+
name → name
|
|
155
|
+
description → description (already validated ≤1024 chars by Pi)
|
|
156
|
+
filePath → path
|
|
157
|
+
disableModelInvocation → (filtered out before indexing — see §7.3)
|
|
158
|
+
baseDir, sourceInfo → (not used)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
SkillEntry {
|
|
163
|
+
name: string // "rdkit" — copied from Pi's Skill.name
|
|
164
|
+
description: string // copied from Pi's Skill.description (≤1024 chars per Pi's validateDescription)
|
|
165
|
+
path: string // absolute path to SKILL.md, copied from Pi's Skill.filePath
|
|
166
|
+
categories: string[] // computed: ["Cheminformatics & Drug Discovery", ...] (§6.2)
|
|
167
|
+
nameTokens: Set<string> // computed: tokenize(name) per §5.3
|
|
168
|
+
descTokens: Set<string> // computed: tokenize(description) per §5.3
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
SearchResult {
|
|
172
|
+
name: string
|
|
173
|
+
description: string
|
|
174
|
+
path: string
|
|
175
|
+
score: number
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
CategorySummary {
|
|
179
|
+
name: string // "Cheminformatics & Drug Discovery"
|
|
180
|
+
count: number // number of indexed skills matching this category
|
|
181
|
+
examples: string[] // skill names, ordered by index insertion ["rdkit", "datamol", ...]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
SkillIndex {
|
|
185
|
+
entries: Map<string, SkillEntry> // keyed by skill name (Pi enforces unique names)
|
|
186
|
+
categories: CategorySummary[]
|
|
187
|
+
nameIndex: Map<string, string> // tokenized name fragment → skill name (for fast lookup)
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 4.2 Application Flow
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
before_agent_start (event delivers Skill[] from Pi)
|
|
195
|
+
│
|
|
196
|
+
├─► ensureIndex(event.systemPromptOptions.skills)
|
|
197
|
+
│ ├─► If skills fingerprint unchanged → reuse cached index
|
|
198
|
+
│ └─► Else, buildIndex(skills):
|
|
199
|
+
│ ├─► Classify each entry into categories
|
|
200
|
+
│ ├─► Tokenize names and descriptions
|
|
201
|
+
│ └─► Build name index for fast lookup
|
|
202
|
+
│
|
|
203
|
+
├─► First time only: register `skill-search` tool with templated description
|
|
204
|
+
│
|
|
205
|
+
├─► Strip Pi's auto-injected <available_skills> block from event.systemPrompt
|
|
206
|
+
│
|
|
207
|
+
└─► Return { systemPrompt: stripped + categorySummary }
|
|
208
|
+
|
|
209
|
+
Agent calls skill-search:
|
|
210
|
+
│
|
|
211
|
+
├─► search(query, index, limit)
|
|
212
|
+
│ ├─► Tokenize query
|
|
213
|
+
│ ├─► Apply synonym expansion
|
|
214
|
+
│ ├─► Score each entry:
|
|
215
|
+
│ │ name exact match: +50
|
|
216
|
+
│ │ name word match: +20/word
|
|
217
|
+
│ │ desc first-sentence: +3/word
|
|
218
|
+
│ │ desc rest: +1/word
|
|
219
|
+
│ │ category match: +5/word
|
|
220
|
+
│ ├─► Sort by score desc
|
|
221
|
+
│ └─► Return top N (clamped to [1, 20])
|
|
222
|
+
│
|
|
223
|
+
└─► Format as tool result with paths
|
|
224
|
+
|
|
225
|
+
Agent reads SKILL.md:
|
|
226
|
+
│
|
|
227
|
+
└─► Standard Pi `read` tool — no extension involvement
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Note: The extension performs **no I/O** — no directory scan, no file read, no YAML parse. All skill data comes from `event.systemPromptOptions.skills`, which Pi has already populated.
|
|
231
|
+
|
|
232
|
+
### 4.3 Interface Contract
|
|
233
|
+
|
|
234
|
+
#### Tool: `skill-search`
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
registerTool({
|
|
238
|
+
name: "skill-search",
|
|
239
|
+
label: "Skill Search",
|
|
240
|
+
description: `...`, // See §4.4
|
|
241
|
+
inputSchema: {
|
|
242
|
+
type: "object",
|
|
243
|
+
properties: {
|
|
244
|
+
query: {
|
|
245
|
+
type: "string",
|
|
246
|
+
description: "Package name, domain, or task description"
|
|
247
|
+
},
|
|
248
|
+
limit: {
|
|
249
|
+
type: "number",
|
|
250
|
+
description: "Max results (default 5, max 20)"
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
required: ["query"]
|
|
254
|
+
},
|
|
255
|
+
handler: async (input) => { ... }
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### Input
|
|
260
|
+
|
|
261
|
+
| Field | Type | Required | Default | Constraints |
|
|
262
|
+
| --- | --- | --- | --- | --- |
|
|
263
|
+
| `query` | string | yes | — | 1-500 chars |
|
|
264
|
+
| `limit` | number | no | 5 | 1-20 |
|
|
265
|
+
|
|
266
|
+
#### Output (tool result)
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
Found 5 skills for "molecular docking protein ligand":
|
|
270
|
+
|
|
271
|
+
## diffdock (score: 16.00)
|
|
272
|
+
Molecular docking with diffusion models...
|
|
273
|
+
Path: .../scientific-skills/diffdock/SKILL.md
|
|
274
|
+
|
|
275
|
+
## rdkit (score: 8.00)
|
|
276
|
+
Cheminformatics toolkit for fine-grained molecular control...
|
|
277
|
+
Path: .../scientific-skills/rdkit/SKILL.md
|
|
278
|
+
|
|
279
|
+
... (3 more)
|
|
280
|
+
|
|
281
|
+
Use the `read` tool to load a skill's SKILL.md for full instructions.
|
|
282
|
+
When a skill references scripts/ or references/, resolve paths relative to the skill directory.
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### Error cases
|
|
286
|
+
|
|
287
|
+
| Condition | Output |
|
|
288
|
+
| --- | --- |
|
|
289
|
+
| No matches | `"No skills found matching 'X'. {N} skills indexed. Try broader terms."` |
|
|
290
|
+
| Empty query | `"Query is required."` |
|
|
291
|
+
| Index empty (no skills found) | `"No skills indexed."` (Pi did not deliver any `Skill[]`; user should add skills via Pi's standard `.pi/skills/`, `.agents/skills/`, or `--skill <path>` flag.) |
|
|
292
|
+
|
|
293
|
+
### 4.4 Tool Description
|
|
294
|
+
|
|
295
|
+
The tool description is critical — it must trigger the agent to call the tool at the right moments. ~160 tokens.
|
|
296
|
+
|
|
297
|
+
The description is a **template** rendered at extension init from the live index. The static parts describe behavior; the dynamic parts list the categories actually present in the user's skill set. This avoids the failure mode where a non-scientific user sees scientific examples and ignores the tool.
|
|
298
|
+
|
|
299
|
+
**Template:**
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
Search available skills by keyword, domain, package name, or task description.
|
|
303
|
+
Returns matching skill names, descriptions, and file paths. Then use the `read`
|
|
304
|
+
tool on the returned path for full instructions.
|
|
305
|
+
|
|
306
|
+
Categories available: {{categoryList}}.
|
|
307
|
+
|
|
308
|
+
When to call:
|
|
309
|
+
- Working with a specific package or library you want to use correctly.
|
|
310
|
+
- Tackling a task in a specialized domain (a category listed above).
|
|
311
|
+
- Before writing complex code in any of these domains — skills contain best
|
|
312
|
+
practices, examples, and reference documentation that prevent common errors.
|
|
313
|
+
|
|
314
|
+
Example queries: "{{exampleQuery1}}", "{{exampleQuery2}}", "{{exampleQuery3}}".
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
`{{categoryList}}` is the comma-joined lower-cased category names from `index.categories`, whose ordering is fixed by §6.4 (declaration order of `CATEGORY_RULES`, then `"Other"` if non-empty). `{{exampleQueryN}}` are `index.categories[i].examples[0]` for the **three categories with the highest `count`**, with ties broken by `CATEGORY_RULES` declaration order. Both renderings are deterministic for any given skill set.
|
|
318
|
+
|
|
319
|
+
**Worked rendering for the 137-skill scientific corpus** (~165 tokens):
|
|
320
|
+
|
|
321
|
+
```
|
|
322
|
+
Search available skills by keyword, domain, package name, or task description.
|
|
323
|
+
Returns matching skill names, descriptions, and file paths. Then use the `read`
|
|
324
|
+
tool on the returned path for full instructions.
|
|
325
|
+
|
|
326
|
+
Categories available: cheminformatics & drug discovery, bioinformatics &
|
|
327
|
+
genomics, machine learning & ai, clinical & medical, physics & quantum,
|
|
328
|
+
databases & data sources, data analysis & visualization, scientific writing
|
|
329
|
+
& communication, geospatial & remote sensing, lab automation & integration,
|
|
330
|
+
time series & forecasting, materials science & engineering, research
|
|
331
|
+
methodology, integration platforms.
|
|
332
|
+
|
|
333
|
+
When to call:
|
|
334
|
+
- Working with a specific package or library you want to use correctly.
|
|
335
|
+
- Tackling a task in a specialized domain (a category listed above).
|
|
336
|
+
- Before writing complex code in any of these domains — skills contain best
|
|
337
|
+
practices, examples, and reference documentation that prevent common errors.
|
|
338
|
+
|
|
339
|
+
Example queries: "rdkit", "single cell rna", "pytorch lightning".
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
For a non-scientific corpus (e.g., a workspace of devops/cli skills), the same template renders categories like `"shell automation, container orchestration, ci/cd, ..."` and example queries drawn from those skills. The extension is therefore **domain-agnostic** by construction.
|
|
343
|
+
|
|
344
|
+
### 4.5 Category Summary (injected into prompt)
|
|
345
|
+
|
|
346
|
+
Target: ~150 tokens. Real token count must be measured at runtime against the actual indexed skills (story E01-S02). The example below is **illustrative** for the 137-skill `scientific-agent-skills` corpus and was hand-curated; production output is generated by `formatCategorySummary(index)` from §6.3.
|
|
347
|
+
|
|
348
|
+
**Counts are membership counts, not unique-skill counts.** §6.2 allows a skill to belong to multiple categories (e.g., `deepchem` ∈ {Cheminformatics, Machine Learning}), so `Σ counts > total skills`. The header line states the unique total to avoid confusion.
|
|
349
|
+
|
|
350
|
+
```markdown
|
|
351
|
+
## Available Skill Domains
|
|
352
|
+
|
|
353
|
+
137 skills indexed (categories overlap; a skill may appear in multiple). Use the
|
|
354
|
+
`skill-search` tool to find the best match for a specific task.
|
|
355
|
+
|
|
356
|
+
- **Cheminformatics & Drug Discovery** (15): rdkit, datamol, deepchem, diffdock, medchem...
|
|
357
|
+
- **Bioinformatics & Genomics** (25): scanpy, anndata, biopython, gget, scvelo, pysam...
|
|
358
|
+
- **Machine Learning & AI** (15): scikit-learn, pytorch-lightning, transformers, shap...
|
|
359
|
+
- **Clinical & Medical** (10): clinical-decision-support, pyhealth, pydicom, treatment-plans...
|
|
360
|
+
- **Physics & Quantum** (10): astropy, pennylane, qiskit, cirq, qutip, sympy...
|
|
361
|
+
- **Databases** (100+): PubChem, ChEMBL, UniProt, COSMIC, ClinicalTrials.gov...
|
|
362
|
+
- **Data Analysis & Visualization** (15): matplotlib, seaborn, polars, dask, networkx...
|
|
363
|
+
- **Scientific Writing & Communication** (15): literature-review, peer-review, scientific-writing...
|
|
364
|
+
- **Geospatial & Remote Sensing** (5): geopandas, geomaster...
|
|
365
|
+
- **Lab Automation** (5): pylabrobot, flowio, opentrons-integration...
|
|
366
|
+
- **Time Series & Forecasting** (5): timesfm-forecasting, aeon, statsmodels...
|
|
367
|
+
- **Integration Platforms** (9): benchling, modal, dnanexus, latchbio...
|
|
368
|
+
- **Research Methodology** (8): hypothesis-generation, scientific-brainstorming, scholar-evaluation...
|
|
369
|
+
- **Materials Science** (5): pymatgen, pymoo, simpy...
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Behaviors:**
|
|
373
|
+
|
|
374
|
+
1. **Categories with zero matches are omitted.** A user with no clinical skills sees no "Clinical & Medical" line.
|
|
375
|
+
2. **Universal "Other" line** appears only if at least one skill matched no rule (signals to maintainers that rules need an update).
|
|
376
|
+
3. **Category-name and example list are skill-set-specific.** Running the extension against a non-scientific skill collection produces a non-scientific summary — see §4.4 tool description for the matching templated approach.
|
|
377
|
+
4. **Token budget enforcement.** If the rendered summary exceeds 250 tokens, `formatCategorySummary` truncates by reducing `maxExamples` per category until it fits. This is implemented in §6.3.
|
|
378
|
+
|
|
379
|
+
## 5. Search Algorithm
|
|
380
|
+
|
|
381
|
+
### 5.1 Scoring Formula
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
Score = Σ(query_word) [
|
|
385
|
+
+50 if word === skill.name (exact name match)
|
|
386
|
+
+20 if word ∈ tokenize(skill.name)
|
|
387
|
+
+3 if word ∈ tokenize(skill.description[:120]) (first sentence)
|
|
388
|
+
+1 if word ∈ tokenize(skill.description[120:])
|
|
389
|
+
+5 if word ∈ skill.categories (category keyword match)
|
|
390
|
+
]
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Why these weights:**
|
|
394
|
+
|
|
395
|
+
| Weight | Justification | Evidence |
|
|
396
|
+
| --- | --- | --- |
|
|
397
|
+
| +50 exact name | Query "rdkit" must return `rdkit` first, not `datamol` (wrapper that mentions rdkit 3×) | Measured: without boost, `datamol` beats `rdkit` |
|
|
398
|
+
| +20 name word | Query "pytorch" should match `pytorch-lightning` | Multi-word names need per-token matching |
|
|
399
|
+
| +3 first sentence | First ~120 chars of description are the primary summary | Higher signal-to-noise than rest of description |
|
|
400
|
+
| +1 desc rest | Background relevance | Low weight avoids cross-reference pollution |
|
|
401
|
+
| +5 category | Query "chemistry" matches all chemistry-category skills | Broad domain queries benefit from category match |
|
|
402
|
+
|
|
403
|
+
### 5.2 Synonym Expansion
|
|
404
|
+
|
|
405
|
+
Before scoring, expand query words with a synonym dictionary:
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
const SYNONYMS: Record<string, string[]> = {
|
|
409
|
+
// Chemistry
|
|
410
|
+
"molecule": ["molecular", "molecules"],
|
|
411
|
+
"molecular": ["molecule", "molecules"],
|
|
412
|
+
"drug": ["pharmaceutical", "medicinal"],
|
|
413
|
+
"chem": ["chemistry", "chemical"],
|
|
414
|
+
"docking": ["dock", "binding", "pose"],
|
|
415
|
+
"admet": ["absorption", "distribution", "metabolism", "excretion", "toxicity"],
|
|
416
|
+
"metabolism": ["metabolic", "metabolite", "pathway", "flux"],
|
|
417
|
+
"metabolic": ["metabolism", "metabolite", "pathway", "flux"],
|
|
418
|
+
|
|
419
|
+
// Biology
|
|
420
|
+
"gene": ["genomic", "genomics", "sequence"],
|
|
421
|
+
"protein": ["proteomics", "peptide"],
|
|
422
|
+
"single-cell": ["scRNA-seq", "single cell", "scrnaseq"],
|
|
423
|
+
"rna-seq": ["transcriptom", "rna seq", "expression"],
|
|
424
|
+
|
|
425
|
+
// ML
|
|
426
|
+
"ml": ["machine learning", "machine-learning"],
|
|
427
|
+
"dl": ["deep learning", "deep-learning", "neural network"],
|
|
428
|
+
"nlp": ["natural language", "text mining"],
|
|
429
|
+
|
|
430
|
+
// Clinical
|
|
431
|
+
"clinical": ["medical", "patient", "healthcare"],
|
|
432
|
+
|
|
433
|
+
// Physics
|
|
434
|
+
"quantum": ["qubit", "quantum computing"],
|
|
435
|
+
|
|
436
|
+
// General
|
|
437
|
+
"viz": ["visualization", "plotting", "chart"],
|
|
438
|
+
"stats": ["statistics", "statistical"],
|
|
439
|
+
"db": ["database"],
|
|
440
|
+
};
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Each synonym inherits the same scoring weight as the original word. ~50 entries, ~50 lines.
|
|
444
|
+
|
|
445
|
+
**Note on bidirectionality:** The current dictionary is hand-mirrored (e.g., `molecule` and `molecular` each list the other). This is intentional for v1 because lookup is `O(1)` and the table is small. Future work (E03) may replace this with equivalence classes or a Snowball stemmer, but only after measuring whether the mirroring causes maintenance pain. The §5.4 benchmark verifies all required pairs are mirrored.
|
|
446
|
+
|
|
447
|
+
### 5.3 Tokenization
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
function tokenize(text: string): Set<string> {
|
|
451
|
+
return new Set(
|
|
452
|
+
text.toLowerCase()
|
|
453
|
+
.replace(/[-_/]/g, " ") // split hyphenated names
|
|
454
|
+
.replace(/[^a-z0-9\s]/g, "") // remove punctuation
|
|
455
|
+
.split(/\s+/)
|
|
456
|
+
.filter((w) => w.length >= 2) // skip 1-char tokens (see below)
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
**1-char filter — design rationale:**
|
|
462
|
+
|
|
463
|
+
The `length >= 2` filter drops single-letter tokens. This is a **deliberate** trade-off:
|
|
464
|
+
|
|
465
|
+
- *Drops*: 1-char language names like `R`, `C`, `D`, `Q`. These are too ambiguous to score reliably (every description contains the letter "a" somewhere).
|
|
466
|
+
- *Keeps*: 2-char tokens that the synonym dictionary uses as keys (`ml`, `dl`, `db`, `ai`, `2d`, `3d`, `pi`, `qa`, `ts`, `js`).
|
|
467
|
+
|
|
468
|
+
**Consequence for queries:** A user querying just `"R"` to find R-language skills will get no signal. To compensate, skill authors should write `"R programming"` or `"R language"` in the description, and the §5.2 synonym dictionary may add `"r-lang": ["r"]` if real demand emerges. This is a known limitation, accepted for v1.
|
|
469
|
+
|
|
470
|
+
**Edge cases handled:**
|
|
471
|
+
|
|
472
|
+
- Empty input → `Set()` (handler short-circuits before reaching tokenizer; see §7.3).
|
|
473
|
+
- Unicode letters → stripped by `[^a-z0-9\s]`. Skill names in `scientific-agent-skills` are ASCII-only; non-ASCII names would need future work.
|
|
474
|
+
- Repeated words → deduplicated by `Set`. Score is **per matched token**, not per occurrence — this is what defeats the cross-reference pollution problem (§1).
|
|
475
|
+
|
|
476
|
+
### 5.4 Benchmarked Quality
|
|
477
|
+
|
|
478
|
+
Tested on 137 skills, 15 queries:
|
|
479
|
+
|
|
480
|
+
| Query | Result | Correct? |
|
|
481
|
+
| --- | --- | --- |
|
|
482
|
+
| `rdkit` | rdkit(21), datamol(4) | ✅ |
|
|
483
|
+
| `scanpy` | scanpy(20), scvelo(9) | ✅ |
|
|
484
|
+
| `pytorch` | pytorch-lightning(24) | ✅ |
|
|
485
|
+
| `molecular docking protein ligand` | diffdock(16), rdkit(8) | ✅ |
|
|
486
|
+
| `single cell rna sequencing` | scanpy(12), scvelo(9) | ✅ |
|
|
487
|
+
| `drug discovery ADMET` | datamol(8), pytdc(8) | ✅ |
|
|
488
|
+
| `time series forecasting` | timesfm-forecasting(32) | ✅ |
|
|
489
|
+
| `clinical trial patient data` | clinical-reports(26) | ✅ |
|
|
490
|
+
| `quantum computing circuit` | cirq(9), qiskit(8) | ✅ |
|
|
491
|
+
| `pdf extraction` | pdf(24) | ✅ |
|
|
492
|
+
| `geospatial satellite imagery` | geomaster(6) | ✅ |
|
|
493
|
+
| `predict molecular properties` | molecular-dynamics(24) | ✅ |
|
|
494
|
+
| `analyze brain signals EEG` | neurokit2(6) | ✅ |
|
|
495
|
+
| `find drug targets` | depmap(5), datamol(4) | ✅ |
|
|
496
|
+
| `metabolism pathway analysis` | exploratory-data-analysis(24) | ⚠️ (should be cobrapy) |
|
|
497
|
+
|
|
498
|
+
14/15 correct (93%) **before** synonym expansion. With the `metabolism`/`metabolic` entries now present in §5.2, the `metabolism pathway analysis` query routes the synonym terms (`metabolic`, `pathway`) into scoring; `cobrapy` (whose description contains "metabolic" and "pathway") moves to top-3. The §9 unit-synonym test enforces this regression.
|
|
499
|
+
|
|
500
|
+
### 5.5 Performance
|
|
501
|
+
|
|
502
|
+
| Operation | Time |
|
|
503
|
+
| --- | --- |
|
|
504
|
+
| Index build (137 skills, in-memory only) | 23ms (one-time on first `before_agent_start`, then cached by skill-list fingerprint) |
|
|
505
|
+
| Search (per query) | 3ms |
|
|
506
|
+
| Search (1000 queries benchmark) | 4ms avg |
|
|
507
|
+
| vs LLM tool call round-trip | 2,000-5,000ms |
|
|
508
|
+
|
|
509
|
+
Search overhead is **negligible** — <0.1% of LLM round-trip time.
|
|
510
|
+
|
|
511
|
+
### 5.6 Why NOT TF-IDF or Embedding
|
|
512
|
+
|
|
513
|
+
| Algorithm | Quality | Speed (1000 queries) | Dependencies |
|
|
514
|
+
| --- | --- | --- | --- |
|
|
515
|
+
| Keyword + name boost | 93% correct | 4,000ms | None |
|
|
516
|
+
| TF-IDF | ~90% correct | 2,098ms | None |
|
|
517
|
+
| Embedding (sentence-transformers) | ~95% estimated | ~50,000ms + GPU | 400MB (torch) |
|
|
518
|
+
|
|
519
|
+
TF-IDF is slower and not better. Embedding is massive overkill for 137 items. Revisit at 500+ skills.
|
|
520
|
+
|
|
521
|
+
## 6. Category Classification
|
|
522
|
+
|
|
523
|
+
### 6.1 Classification Rules
|
|
524
|
+
|
|
525
|
+
Each skill is classified into one or more categories based on keyword presence in name + description:
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
interface CategoryRule {
|
|
529
|
+
name: string; // "Cheminformatics & Drug Discovery"
|
|
530
|
+
keywords: string[]; // Match if ANY keyword appears
|
|
531
|
+
maxExamples: number; // How many skill names to show in summary
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const CATEGORY_RULES: CategoryRule[] = [
|
|
535
|
+
{
|
|
536
|
+
name: "Cheminformatics & Drug Discovery",
|
|
537
|
+
keywords: ["molecular", "molecule", "drug", "compound", "chemic", "smiles",
|
|
538
|
+
"docking", "fingerprint", "admet", "cheminformatics", "medicinal",
|
|
539
|
+
"rdkit", "virtual screening", "lead optim"],
|
|
540
|
+
maxExamples: 5,
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
name: "Bioinformatics & Genomics",
|
|
544
|
+
keywords: ["gene", "genomic", "rna-seq", "single-cell", "transcriptom", "protein",
|
|
545
|
+
"sequence", "variant", "alignment", "phylogen", "pathway", "gene regul",
|
|
546
|
+
"anndata", "h5ad"],
|
|
547
|
+
maxExamples: 5,
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: "Machine Learning & AI",
|
|
551
|
+
keywords: ["deep learning", "neural network", "reinforcement learn", "gradient boost",
|
|
552
|
+
"random forest", "model train", "pytorch", "tensorflow", "transformer",
|
|
553
|
+
"gan", "cnn", "rnn", "lstm", "interpret", "shap", "feature engineer"],
|
|
554
|
+
maxExamples: 5,
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
name: "Clinical & Medical",
|
|
558
|
+
keywords: ["clinical", "patient", "medical", "diagnosis", "treatment", "ehr",
|
|
559
|
+
"dicom", "pathology", "survival analysis", "drug safety", "pharmacovigil",
|
|
560
|
+
"biomarker", "cohort"],
|
|
561
|
+
maxExamples: 5,
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: "Physics & Quantum",
|
|
565
|
+
keywords: ["physics", "quantum", "astronom", "cosmol", "optics", "particle",
|
|
566
|
+
"simulat", "circuit", "qubit", "hamiltonian", "spectroscopy"],
|
|
567
|
+
maxExamples: 5,
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
name: "Databases & Data Sources",
|
|
571
|
+
keywords: ["database", "api", "rest api", "query", "pubchem", "chembl", "uniprot",
|
|
572
|
+
"clinicaltrials", "entrez", "ncbi", "ensembl", "geo ", "tcga"],
|
|
573
|
+
maxExamples: 4,
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
name: "Data Analysis & Visualization",
|
|
577
|
+
keywords: ["statistic", "visualization", "plotting", "chart", "datafram",
|
|
578
|
+
"eda", "network analysis", "time series", "forecast"],
|
|
579
|
+
maxExamples: 5,
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
name: "Scientific Writing & Communication",
|
|
583
|
+
keywords: ["writing", "paper", "publication", "peer review", "citation", "bibtex",
|
|
584
|
+
"literature", "poster", "slide", "schematic", "infographic"],
|
|
585
|
+
maxExamples: 5,
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
name: "Geospatial & Remote Sensing",
|
|
589
|
+
keywords: ["geospatial", "gis", "satellite", "spatial", "terrain", "remote sensing",
|
|
590
|
+
"raster", "vector", "coordinate", "map"],
|
|
591
|
+
maxExamples: 5,
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
name: "Lab Automation & Integration",
|
|
595
|
+
keywords: ["lab", "laboratory", "liquid handl", "plate reader", "workflow automat",
|
|
596
|
+
"lims", "pipette", "robot", "opentrons", "benchling", "latchbio"],
|
|
597
|
+
maxExamples: 5,
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
name: "Time Series & Forecasting",
|
|
601
|
+
keywords: ["time series", "forecast", "anomaly detect", "signal process",
|
|
602
|
+
"timesfm", "aeon"],
|
|
603
|
+
maxExamples: 5,
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
name: "Materials Science & Engineering",
|
|
607
|
+
keywords: ["crystal", "material", "phase diagram", "metabolic model",
|
|
608
|
+
"simulation", "optimization", "pymoo", "simpy", "pymatgen"],
|
|
609
|
+
maxExamples: 5,
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
name: "Research Methodology",
|
|
613
|
+
keywords: ["hypothesis", "brainstorm", "critical thinking", "grant", "scholar",
|
|
614
|
+
"peer review", "reproducib", "experimental design"],
|
|
615
|
+
maxExamples: 5,
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
name: "Integration Platforms",
|
|
619
|
+
keywords: ["benchling", "modal", "dnanexus", "latchbio", "omero", "lamindb",
|
|
620
|
+
"protocols.io", "ginkgo", "integration"],
|
|
621
|
+
maxExamples: 5,
|
|
622
|
+
},
|
|
623
|
+
];
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### 6.2 Classification Logic
|
|
627
|
+
|
|
628
|
+
```typescript
|
|
629
|
+
function classify(entry: SkillEntry): string[] {
|
|
630
|
+
const text = `${entry.name} ${entry.description}`.toLowerCase();
|
|
631
|
+
const matched: string[] = [];
|
|
632
|
+
|
|
633
|
+
for (const rule of CATEGORY_RULES) {
|
|
634
|
+
if (rule.keywords.some(kw => text.includes(kw))) {
|
|
635
|
+
matched.push(rule.name);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return matched.length > 0 ? matched : ["Other"];
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
A skill can belong to multiple categories. `deepchem` → both "Cheminformatics" and "Machine Learning". Skills matching no rule go to "Other".
|
|
644
|
+
|
|
645
|
+
**Substring-match caveat.** `text.includes(kw)` is a substring test, not a token test. This is intentional — keywords like `"chemic"`, `"phylogen"`, `"genomic"` are stems and need to match `"chemical"`, `"phylogenetic"`, `"genomics"`. The risk is that short keywords like `"gene"` also match `"generation"` or `"general"`. Mitigations:
|
|
646
|
+
|
|
647
|
+
1. **Keyword design.** Avoid keywords that are common English fragments. The current rules use `"gene"` (intended for `"gene"`/`"genomic"`/`"genetics"`); a future revision can switch to `"gene "` with a trailing space if false positives surface in E01-S02.
|
|
648
|
+
2. **Coverage check (E01-S02).** Run `classify` over the seed corpus and inspect every assignment. Any skill assigned to a category that doesn't fit gets either a keyword tightened or the rule split.
|
|
649
|
+
3. **Multi-category tolerance.** Because the summary lists membership counts, an over-broad rule produces an inflated count, not silently-wrong search results — search uses the scoring formula (§5.1), not categories alone.
|
|
650
|
+
|
|
651
|
+
### 6.3 Summary Generation
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
const SUMMARY_TOKEN_CAP = 250; // §10.3
|
|
655
|
+
|
|
656
|
+
function formatCategorySummary(index: SkillIndex): string {
|
|
657
|
+
let maxExamples = 5;
|
|
658
|
+
while (maxExamples >= 1) {
|
|
659
|
+
const text = renderSummary(index, maxExamples);
|
|
660
|
+
if (estimateTokens(text) <= SUMMARY_TOKEN_CAP) return text;
|
|
661
|
+
maxExamples -= 1;
|
|
662
|
+
}
|
|
663
|
+
// Final fallback: no examples, names only.
|
|
664
|
+
return renderSummary(index, 0);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function renderSummary(index: SkillIndex, maxExamples: number): string {
|
|
668
|
+
const totalSkills = index.entries.size;
|
|
669
|
+
const lines: string[] = ["## Available Skill Domains", ""];
|
|
670
|
+
lines.push(
|
|
671
|
+
`${totalSkills} skills indexed (categories overlap; a skill may appear in multiple). ` +
|
|
672
|
+
"Use the `skill-search` tool to find the best match for a specific task.",
|
|
673
|
+
"",
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
// Skip empty categories. Surface "Other" only if non-empty.
|
|
677
|
+
for (const cat of index.categories.filter((c) => c.count > 0)) {
|
|
678
|
+
const examples = cat.examples.slice(0, maxExamples).join(", ");
|
|
679
|
+
const tail = examples ? `: ${examples}...` : "";
|
|
680
|
+
lines.push(`- **${cat.name}** (${cat.count})${tail}`);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return lines.join("\n");
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Cheap token estimator (chars / 3.5) — same heuristic as §1 measurement.
|
|
687
|
+
function estimateTokens(text: string): number {
|
|
688
|
+
return Math.ceil(text.length / 3.5);
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
The `estimateTokens` heuristic matches the chars/3.5 ratio used in §1 for the inject-all baseline, so token comparisons are apples-to-apples. For acceptance testing, story E01-S02 cross-checks with `tiktoken` against the `cl100k_base` encoder.
|
|
693
|
+
|
|
694
|
+
### 6.4 buildIndex Contract
|
|
695
|
+
|
|
696
|
+
`buildIndex(skills: Skill[]) → SkillIndex` is the single populating function. It is pure (deterministic, no I/O), called from `ensureIndex` after the `disableModelInvocation` filter, and produces the `SkillIndex` shape from §4.1.
|
|
697
|
+
|
|
698
|
+
**Algorithm:**
|
|
699
|
+
|
|
700
|
+
```typescript
|
|
701
|
+
function buildIndex(skills: Skill[]): SkillIndex {
|
|
702
|
+
const entries = new Map<string, SkillEntry>();
|
|
703
|
+
const nameIndex = new Map<string, string>();
|
|
704
|
+
// Membership map: category-name → skill-names in declaration order of skills[]
|
|
705
|
+
const membership = new Map<string, string[]>();
|
|
706
|
+
|
|
707
|
+
// Pass 1: build SkillEntry, classify, accumulate membership.
|
|
708
|
+
for (const skill of skills) {
|
|
709
|
+
const entry: SkillEntry = {
|
|
710
|
+
name: skill.name,
|
|
711
|
+
description: skill.description,
|
|
712
|
+
path: skill.filePath,
|
|
713
|
+
categories: classify({ // §6.2
|
|
714
|
+
name: skill.name,
|
|
715
|
+
description: skill.description,
|
|
716
|
+
path: skill.filePath,
|
|
717
|
+
categories: [],
|
|
718
|
+
nameTokens: new Set(),
|
|
719
|
+
descTokens: new Set(),
|
|
720
|
+
}),
|
|
721
|
+
nameTokens: tokenize(skill.name), // §5.3
|
|
722
|
+
descTokens: tokenize(skill.description), // §5.3
|
|
723
|
+
};
|
|
724
|
+
entries.set(skill.name, entry);
|
|
725
|
+
for (const tok of entry.nameTokens) nameIndex.set(tok, skill.name);
|
|
726
|
+
for (const cat of entry.categories) {
|
|
727
|
+
if (!membership.has(cat)) membership.set(cat, []);
|
|
728
|
+
membership.get(cat)!.push(skill.name);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Pass 2: build CategorySummary[] in CATEGORY_RULES declaration order,
|
|
733
|
+
// append "Other" last if non-empty. Examples retain skill insertion order.
|
|
734
|
+
const categories: CategorySummary[] = [];
|
|
735
|
+
const orderedNames = [...CATEGORY_RULES.map((r) => r.name), "Other"];
|
|
736
|
+
for (const name of orderedNames) {
|
|
737
|
+
const members = membership.get(name);
|
|
738
|
+
if (!members || members.length === 0) continue;
|
|
739
|
+
const rule = CATEGORY_RULES.find((r) => r.name === name);
|
|
740
|
+
categories.push({
|
|
741
|
+
name,
|
|
742
|
+
count: members.length,
|
|
743
|
+
examples: members.slice(0, rule?.maxExamples ?? 5),
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return { entries, categories, nameIndex };
|
|
748
|
+
}
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
**Determinism guarantees:**
|
|
752
|
+
|
|
753
|
+
- `entries.set` insertion order = iteration order of `skills[]` (Pi delivers skills in a deterministic order — alphabetical by `filePath` after the loader sorts them).
|
|
754
|
+
- `categories` ordering = `CATEGORY_RULES` declaration order, followed by `"Other"` if present.
|
|
755
|
+
- `examples` preserves the same insertion order, sliced to `maxExamples` (per §6.1, default 5).
|
|
756
|
+
- `nameIndex` is last-write-wins on token collision (e.g., if two skills share a token in their name); this is acceptable because exact name match (+50) and full name word match (+20) in §5.1 dominate any nameIndex lookup.
|
|
757
|
+
|
|
758
|
+
**Complexity:** O(N · D) where N = number of skills and D = average description length, dominated by `tokenize(description)`. Measured at ~23ms for 137 skills (§5.5).
|
|
759
|
+
|
|
760
|
+
## 7. Extension Architecture
|
|
761
|
+
|
|
762
|
+
### 7.1 File Structure
|
|
763
|
+
|
|
764
|
+
```
|
|
765
|
+
pi-skill-search/
|
|
766
|
+
├── package.json # Pi extension metadata, no runtime deps
|
|
767
|
+
├── index.ts # Entry point: register hook + tool, lifecycle
|
|
768
|
+
├── src/
|
|
769
|
+
│ ├── indexer.ts # buildIndex(skills): tokenize, classify, build name lookup
|
|
770
|
+
│ ├── search.ts # Search algorithm: score, rank, format results
|
|
771
|
+
│ ├── categories.ts # Category rules + classification logic
|
|
772
|
+
│ ├── synonyms.ts # Synonym dictionary for query expansion
|
|
773
|
+
│ ├── format.ts # renderToolDescription, formatCategorySummary, formatResults
|
|
774
|
+
│ └── strip.ts # AVAILABLE_SKILLS_BLOCK_REGEX + drift-resistant stripping
|
|
775
|
+
└── README.md
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
~400 lines total. No filesystem access, no YAML parser, no network — all input is `Skill[]` from `event.systemPromptOptions.skills` (§7.5).
|
|
779
|
+
|
|
780
|
+
### 7.2 Dependencies
|
|
781
|
+
|
|
782
|
+
| Dependency | Version | Purpose |
|
|
783
|
+
| --- | --- | --- |
|
|
784
|
+
| `@earendil-works/pi-coding-agent` | >=0.74.0 | `ExtensionAPI`, `BeforeAgentStartEvent`, `BuildSystemPromptOptions`, `Skill` types (peer). v0.74.0 is what was checked while writing this spec — `Source/pi-mono/packages/coding-agent/package.json` reports `"version": "0.74.0"` and `BeforeAgentStartEvent.systemPromptOptions.skills` is present at types.ts:625-633. The field may have existed earlier; an implementation story can lower the floor after testing. |
|
|
785
|
+
|
|
786
|
+
**No npm runtime dependencies.** No filesystem access. No TypeScript build step — Pi uses jiti for `.ts` transpilation.
|
|
787
|
+
|
|
788
|
+
### 7.3 Session Lifecycle
|
|
789
|
+
|
|
790
|
+
The extension does **not** scan or parse skills itself. It reuses the `Skill[]` array Pi already loaded, available on every `before_agent_start` event via `event.systemPromptOptions.skills`. This avoids:
|
|
791
|
+
|
|
792
|
+
- Re-discovering skill directories (Pi may use CLI flags or settings the extension doesn't see).
|
|
793
|
+
- Re-parsing YAML frontmatter (Pi already validated and loaded each skill).
|
|
794
|
+
- Re-implementing Pi's gitignore / `.skillignore` filtering (Pi already applied it).
|
|
795
|
+
|
|
796
|
+
The single thing the extension MUST replicate is the `disableModelInvocation` filter, because Pi only applies it inside `formatSkillsForPrompt` at render time — `systemPromptOptions.skills` still contains the disabled entries. `ensureIndex` does this filter explicitly.
|
|
797
|
+
|
|
798
|
+
```typescript
|
|
799
|
+
import type {
|
|
800
|
+
ExtensionAPI,
|
|
801
|
+
BeforeAgentStartEvent,
|
|
802
|
+
BeforeAgentStartEventResult,
|
|
803
|
+
Skill,
|
|
804
|
+
} from "@earendil-works/pi-coding-agent";
|
|
805
|
+
|
|
806
|
+
const AVAILABLE_SKILLS_BLOCK_REGEX =
|
|
807
|
+
/\n*The following skills provide specialized instructions[\s\S]*?<\/available_skills>/;
|
|
808
|
+
|
|
809
|
+
export default function (pi: ExtensionAPI): void {
|
|
810
|
+
let index: SkillIndex | undefined;
|
|
811
|
+
let lastSkillsFingerprint = "";
|
|
812
|
+
let toolRegistered = false;
|
|
813
|
+
|
|
814
|
+
// Phase 1: Build (or rebuild) index when Pi's skill set changes.
|
|
815
|
+
// We rebuild lazily inside before_agent_start because the canonical Skill[]
|
|
816
|
+
// is delivered there, not at session_start.
|
|
817
|
+
function ensureIndex(skills: Skill[] | undefined): SkillIndex | undefined {
|
|
818
|
+
if (!skills || skills.length === 0) return undefined;
|
|
819
|
+
// Filter out skills the user disabled for model invocation. Pi exposes
|
|
820
|
+
// these in systemPromptOptions.skills (verified agent-session.ts:943) but
|
|
821
|
+
// hides them inside formatSkillsForPrompt — we must do the same here, or
|
|
822
|
+
// search would return skills the agent isn't supposed to see.
|
|
823
|
+
const visible = skills.filter((s) => !s.disableModelInvocation);
|
|
824
|
+
if (visible.length === 0) return undefined;
|
|
825
|
+
const fingerprint = visible.map((s) => s.filePath).sort().join("\n");
|
|
826
|
+
if (fingerprint === lastSkillsFingerprint && index) return index;
|
|
827
|
+
try {
|
|
828
|
+
index = buildIndex(visible);
|
|
829
|
+
lastSkillsFingerprint = fingerprint;
|
|
830
|
+
return index;
|
|
831
|
+
} catch (err) {
|
|
832
|
+
console.error("pi-skill-search: index build failed", err);
|
|
833
|
+
return undefined;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Phase 2: Replace inject-all with summary on every turn,
|
|
838
|
+
// and lazily register the tool with a category-templated description.
|
|
839
|
+
pi.on("before_agent_start", async (
|
|
840
|
+
event: BeforeAgentStartEvent,
|
|
841
|
+
): Promise<BeforeAgentStartEventResult | void> => {
|
|
842
|
+
const idx = ensureIndex(event.systemPromptOptions.skills);
|
|
843
|
+
if (!idx || idx.entries.size === 0) return;
|
|
844
|
+
|
|
845
|
+
if (!toolRegistered) {
|
|
846
|
+
pi.registerTool({
|
|
847
|
+
name: "skill-search",
|
|
848
|
+
label: "Skill Search",
|
|
849
|
+
description: renderToolDescription(idx), // §4.4 template
|
|
850
|
+
inputSchema: SEARCH_INPUT_SCHEMA,
|
|
851
|
+
handler: makeSearchHandler(() => index),
|
|
852
|
+
});
|
|
853
|
+
toolRegistered = true;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Strip Pi's auto-injected <available_skills> block.
|
|
857
|
+
const stripped = event.systemPrompt.replace(AVAILABLE_SKILLS_BLOCK_REGEX, "");
|
|
858
|
+
const summary = formatCategorySummary(idx);
|
|
859
|
+
return { systemPrompt: `${stripped}\n\n${summary}` };
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function makeSearchHandler(getIndex: () => SkillIndex | undefined) {
|
|
864
|
+
return async (input: { query?: string; limit?: number }) => {
|
|
865
|
+
try {
|
|
866
|
+
const idx = getIndex();
|
|
867
|
+
if (!idx || idx.entries.size === 0) {
|
|
868
|
+
return { result: "No skills indexed." };
|
|
869
|
+
}
|
|
870
|
+
const query = (input.query ?? "").trim();
|
|
871
|
+
if (query.length === 0) {
|
|
872
|
+
return { result: "Query is required." };
|
|
873
|
+
}
|
|
874
|
+
if (query.length > 500) {
|
|
875
|
+
return { result: "Query too long (max 500 chars)." };
|
|
876
|
+
}
|
|
877
|
+
// Clamp limit to [1, 20]; default 5.
|
|
878
|
+
const rawLimit = Number.isFinite(input.limit) ? Number(input.limit) : 5;
|
|
879
|
+
const limit = Math.max(1, Math.min(20, Math.floor(rawLimit)));
|
|
880
|
+
const results = search(idx, query, limit);
|
|
881
|
+
return { result: formatResults(query, results, idx.entries.size) };
|
|
882
|
+
} catch (err) {
|
|
883
|
+
// §7.4 promises this path returns a friendly message and never throws to Pi.
|
|
884
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
885
|
+
console.error("pi-skill-search: search failed", err);
|
|
886
|
+
return { result: `Search failed: ${message}` };
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
**Why register the tool lazily inside `before_agent_start` instead of at module init:**
|
|
893
|
+
|
|
894
|
+
- The tool description is templated from the live category list (§4.4). At module init the index does not yet exist.
|
|
895
|
+
- `session_start` does not deliver `Skill[]` (verified types.ts:512), so we cannot index there either.
|
|
896
|
+
- `registerTool` is callable from any handler — confirmed by `Source/pi-mono/packages/coding-agent/examples/extensions/dynamic-tools.ts`.
|
|
897
|
+
- The `toolRegistered` flag prevents duplicate registration across turns.
|
|
898
|
+
|
|
899
|
+
**Known limitation: stale tool description after mid-session skill changes.**
|
|
900
|
+
|
|
901
|
+
`registerTool` cannot be called twice with the same name (Pi rejects re-registration), and the Pi extension API does not currently expose a `updateToolDescription`. As a result, when `ensureIndex` rebuilds the index because `event.systemPromptOptions.skills` changed mid-session (e.g., the user added a new skill via `pi reload`):
|
|
902
|
+
|
|
903
|
+
- The internal `index` object **does** update — `categories`, `entries`, and `nameIndex` all reflect the new skill set.
|
|
904
|
+
- The category summary returned in the next `before_agent_start` reflects the new index.
|
|
905
|
+
- BUT the tool description that the LLM sees stays whatever was rendered the first turn.
|
|
906
|
+
|
|
907
|
+
This is acceptable because:
|
|
908
|
+
|
|
909
|
+
1. The tool description is a coarse hint, not a search index. The agent's actual search results come from the live `index` via the handler.
|
|
910
|
+
2. Mid-session skill changes are rare (covered by `pi reload`).
|
|
911
|
+
3. The next session reload renders a fresh description.
|
|
912
|
+
|
|
913
|
+
A future story (E03) may add a hook to re-render the description when `pi.updateToolDescription` becomes available upstream.
|
|
914
|
+
|
|
915
|
+
**If `event.systemPromptOptions.skills` is empty on the first turn**, the tool is never registered and the agent simply has no skill-search affordance. This is correct behavior: there are no skills to search.
|
|
916
|
+
|
|
917
|
+
**Notes on the strip regex:**
|
|
918
|
+
|
|
919
|
+
- The regex anchors on the lead-in sentence emitted by `formatSkillsForPrompt()` ("The following skills provide specialized instructions…") through the closing `</available_skills>` tag. Both are stable strings in `Source/pi-mono/packages/coding-agent/src/core/skills.ts`.
|
|
920
|
+
- If Pi changes the wording, the strip becomes a no-op and the extension falls back to additive behavior (sub-optimal but not broken). A regression test in §9 detects drift.
|
|
921
|
+
- Stripping happens on the rendered string, not on `systemPromptOptions`, because `BeforeAgentStartEventResult` only allows `systemPrompt?: string` mutation (verified types.ts:1009).
|
|
922
|
+
|
|
923
|
+
### 7.4 Error Handling
|
|
924
|
+
|
|
925
|
+
All extension code is wrapped in try/catch. Failure modes:
|
|
926
|
+
|
|
927
|
+
| Failure | Behavior |
|
|
928
|
+
| --- | --- |
|
|
929
|
+
| `event.systemPromptOptions.skills` undefined or empty | Strip `<available_skills>` block, return `{ systemPrompt: stripped }`. No tool registration, no summary injection. Session continues. |
|
|
930
|
+
| Malformed `Skill` from Pi (e.g., empty description) | Indexed with empty `descTokens`; scores zero on description matches. No crash. |
|
|
931
|
+
| `buildIndex` throws | Logged to `console.error`; index stays `undefined`; tool stays unregistered; session continues. |
|
|
932
|
+
| Strip regex finds no `<available_skills>` block | No-op fallback — extension still appends summary if skills exist; output is sub-optimal but valid. Drift detector test in §9 alerts maintainers. |
|
|
933
|
+
| Search throws | Handler returns `"Search failed: <message>"`. Session continues. |
|
|
934
|
+
| Category summary rendering throws | No injection that turn; original `event.systemPrompt` returns unchanged. Session continues. |
|
|
935
|
+
|
|
936
|
+
**Principle**: Extension failure must NEVER prevent agent from running. Skill search is a nice-to-have, not critical path.
|
|
937
|
+
|
|
938
|
+
**Critical: Always return modified `systemPrompt` (never `undefined`)**.
|
|
939
|
+
If the extension returns `undefined` from `before_agent_start`, Pi may reset to `baseSystemPrompt` which still contains the `<available_skills>` block — defeating the token-savings purpose. The handler MUST always return `{ systemPrompt: stripped }` (with or without summary appended).
|
|
940
|
+
|
|
941
|
+
### 7.5 Skill Source: Reuse Pi's `loadSkills()`
|
|
942
|
+
|
|
943
|
+
The extension does **not** scan skill directories. Pi already does this work and exposes the result through `BeforeAgentStartEvent.systemPromptOptions.skills: Skill[]` (verified `Source/pi-mono/packages/coding-agent/src/core/extensions/types.ts:632` at v0.74.0).
|
|
944
|
+
|
|
945
|
+
Each `Skill` provides:
|
|
946
|
+
|
|
947
|
+
```typescript
|
|
948
|
+
interface Skill {
|
|
949
|
+
name: string; // validated by Pi against a regex
|
|
950
|
+
description: string; // validated by Pi against length limits
|
|
951
|
+
filePath: string; // absolute path to SKILL.md
|
|
952
|
+
// ...other fields the extension does not need
|
|
953
|
+
}
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
**Why reuse instead of re-discover:**
|
|
957
|
+
|
|
958
|
+
| Concern | Self-discovery | Reuse `systemPromptOptions.skills` |
|
|
959
|
+
| --- | --- | --- |
|
|
960
|
+
| CLI flags (`--skill <path>`, `--no-default-skills`) | Ignored — extension misses or duplicates | Honored — Pi already applied them |
|
|
961
|
+
| `.skillignore` and gitignore | Must reimplement | Already filtered by Pi's loader |
|
|
962
|
+
| `disableModelInvocation` flag on a skill | Must reimplement | **NOT** pre-filtered: verified `Source/pi-mono/packages/coding-agent/src/core/agent-session.ts:943` passes `loadedSkills` AS-IS into `systemPromptOptions.skills`. Pi's filter (`skills.ts:341`) happens only inside `formatSkillsForPrompt` at render time. The extension MUST filter `s.disableModelInvocation === true` before indexing (see `ensureIndex` in §7.3). |
|
|
963
|
+
| YAML edge cases (block scalars, nested `metadata:`) | Custom parser must cover | Pi's parser already handled |
|
|
964
|
+
| Frontmatter validation (name regex, description length ≤ 1024) | Must reimplement | Pi's `validateName`/`validateDescription` already applied (constants `MAX_NAME_LENGTH=64`, `MAX_DESCRIPTION_LENGTH=1024` in `skills.ts`) |
|
|
965
|
+
| Drift when Pi changes discovery logic | Spec must track Pi releases | Automatic |
|
|
966
|
+
|
|
967
|
+
**Fallback:** If `event.systemPromptOptions.skills` is `undefined` or empty (e.g., user disabled the `read` tool, so Pi skipped skill loading), the extension produces no summary and the search tool returns `"No skills indexed."`. This is the correct behavior because skills cannot be `read` anyway — the search tool would have no follow-up action.
|
|
968
|
+
|
|
969
|
+
**Out of scope for this spec:** Scanning paths Pi did not pick up (e.g., user wants extension-only skills outside Pi's discovery). If needed later, that becomes a separate `pi-skill-search` setting and is handled in a follow-up story.
|
|
970
|
+
|
|
971
|
+
## 8. Frontmatter Parser — Not Needed
|
|
972
|
+
|
|
973
|
+
**Decision:** The extension does not parse YAML frontmatter at all.
|
|
974
|
+
|
|
975
|
+
The §7.5 reuse strategy means the extension consumes `Skill` objects that Pi has **already** parsed and validated. Pi's parser (in `Source/pi-mono/packages/coding-agent/src/core/skills.ts`) handles:
|
|
976
|
+
|
|
977
|
+
- Standard single-line: `name: rdkit`
|
|
978
|
+
- Quoted multi-line strings spanning multiple lines.
|
|
979
|
+
- Block scalars (`>`, `|`).
|
|
980
|
+
- Nested objects (e.g., `metadata: { skill-author: ... }` as seen in `Source/scientific-agent-skills/scientific-skills/rdkit/SKILL.md`).
|
|
981
|
+
- Missing or malformed fields — Pi skips the file with a warning rather than throwing.
|
|
982
|
+
|
|
983
|
+
A hand-rolled parser was considered (earlier draft of this spec) but rejected because:
|
|
984
|
+
|
|
985
|
+
1. Real frontmatter in `scientific-agent-skills` includes nested `metadata:` blocks that a line-by-line parser would mis-attribute.
|
|
986
|
+
2. Pi's parser is the authoritative source — duplicating it creates drift risk every time Pi updates the schema.
|
|
987
|
+
3. Reusing Pi's output is zero-LOC and zero-bug.
|
|
988
|
+
|
|
989
|
+
If the extension ever needs standalone discovery (out of scope for E01), the implementation must use `js-yaml` (~25 KB), not a hand-rolled parser. This is recorded as a non-negotiable constraint for any future work.
|
|
990
|
+
|
|
991
|
+
## 9. Validation Shape
|
|
992
|
+
|
|
993
|
+
| Layer | Expected proof |
|
|
994
|
+
| --- | --- |
|
|
995
|
+
| **Unit — search quality** | On a fixture of 25+ labeled queries against the 137 `scientific-agent-skills` corpus: **precision@1 ≥ 0.85** (top result is the labeled-correct skill) and **recall@5 ≥ 0.95** (correct skill appears in top-5). Each labeled query has 1 expected primary skill plus 0-2 acceptable alternates. |
|
|
996
|
+
| **Unit — search latency** | Search a corpus of 1,000 synthetic queries on the 137-skill index. Corpus generation: 500 single-token queries drawn uniformly from the union of all `nameTokens` and the most frequent 200 `descTokens` across the corpus, plus 500 multi-token queries (2–5 tokens each, sampled the same way). The corpus is committed to `test/fixtures/latency-queries.json` so runs are reproducible. **Latency p50 < 5ms, p99 < 15ms** on a typical dev machine. Reported by `tinybench`. |
|
|
997
|
+
| **Unit — synonym** | Every entry in `SYNONYMS` is reachable in at least one labeled query (no dead synonyms). The `metabolism` query in §5.4 returns `cobrapy` in top-3 (regression test for the §5.4 miss). |
|
|
998
|
+
| **Unit — tokenizer** | Test cases: `""`, `"a"`, `"3D"`, `"R"`, `"single-cell"`, `"PyTorch_Lightning"`, `"naïve"` (Unicode stripped to `"nave"`), 10K-char input. No throws, output matches snapshot. |
|
|
999
|
+
| **Unit — classifier** | Every skill in `scientific-agent-skills` is assigned **at least one** category (no `"Other"` for the seed corpus). Classifier coverage report attached to story E01-S02. |
|
|
1000
|
+
| **Integration — strip regex** | With a stub `BeforeAgentStartEvent` whose `systemPrompt` contains the literal output of `formatSkillsForPrompt(testSkills)` from `Source/pi-mono`, the extension's returned `systemPrompt` no longer contains `<available_skills>` AND contains `## Available Skill Domains`. **Regression on Pi version bump:** the strip regex is anchored on Pi's lead-in sentence; if Pi changes the wording, this test fails fast and the regex is updated. |
|
|
1001
|
+
| **Integration — index reuse** | `before_agent_start` invoked twice with the same `systemPromptOptions.skills` does not rebuild the index (verified via spy on `buildIndex`). When the skill list changes, the index is rebuilt exactly once. |
|
|
1002
|
+
| **Integration — empty / malformed** | `event.systemPromptOptions.skills === undefined` → no mutation, no tool registration, session continues. `skills === []` → same. Malformed `Skill` (e.g., `description === ""` after Pi's own validation lets it through) → entry indexed with empty `descTokens`, scored `0` for description matches; no crash. |
|
|
1003
|
+
| **Integration — tool handler edge cases** | `query: ""` → `"Query is required."`. `query: "x".repeat(501)` → `"Query too long (max 500 chars)."`. `limit: 0` → returns 1 result. `limit: 1000` → returns at most 20. `limit: -5` → 1 result. `limit: NaN` / missing → 5. |
|
|
1004
|
+
| **E2E** | Install extension into Pi v0.74+. Start session with 137 scientific skills. Assert: (a) the `<available_skills>` block is absent from the rendered system prompt, (b) `## Available Skill Domains` is present, (c) agent successfully calls `skill-search` for query `"molecular docking"` and reads the returned SKILL.md. |
|
|
1005
|
+
| **Performance — startup tokens** | Measure system-prompt tokens (via `tiktoken`, `cl100k_base`) in two configurations against the same 137-skill corpus: **(A) Pi v0.74 with no extension** (the inject-all baseline, expected ≈ 23,589 tokens contributed by the skill subsystem), **(B) Pi v0.74 with `pi-skill-search` active** (expected ≤ 600 tokens contributed: stripped block + summary + tool def). Pass criterion: `B ≤ 600` AND `(A − B) / A ≥ 0.97` (≥97% reduction). |
|
|
1006
|
+
| **Performance — per-turn delta** | Same measurement across 5 simulated turns. Per-turn `Δtokens` should be constant within ±20 tokens (no growth). |
|
|
1007
|
+
| **Regression — failure isolation** | Force three failure modes and verify the Pi session continues normally with no extension effect: (a) `buildIndex` throws, (b) `formatCategorySummary` throws, (c) `search` throws. Pi session must produce a normal agent response (without the summary or tool). |
|
|
1008
|
+
|
|
1009
|
+
## 10. Open Decisions — Resolved
|
|
1010
|
+
|
|
1011
|
+
### 10.1 Category summary injection method — RESOLVED: per-turn `systemPrompt` mutation
|
|
1012
|
+
|
|
1013
|
+
Investigated against `Source/pi-mono/packages/coding-agent/src/core/extensions/types.ts` (v0.74.0):
|
|
1014
|
+
|
|
1015
|
+
- `BeforeAgentStartEventResult` exposes only `systemPrompt?: string` for mutation.
|
|
1016
|
+
- No "persistent system message" or "set-once context" API exists. `sendMessage` (types.ts:372) is on `ReplacedSessionContext` only and inserts a *user* message into the conversation, not a persistent system frame.
|
|
1017
|
+
|
|
1018
|
+
**Decision:** Inject the summary by returning `{ systemPrompt: stripped + "\n\n" + summary }` from `before_agent_start` on every turn. Cost is `~381 tokens × N turns` (the §2 target), still ~98% cheaper than inject-all (`~23,589 × N tokens` at 137 skills). The §2 target table values are correct because they already model per-turn cost.
|
|
1019
|
+
|
|
1020
|
+
### 10.2 Tool schema format — RESOLVED: plain JSON Schema
|
|
1021
|
+
|
|
1022
|
+
**Decision:** Use plain JSON Schema literal (no `@sinclair/typebox` import). Pi accepts both, and the schema for `skill-search` has only two scalar fields (`query: string`, `limit: number`) — TypeBox provides no value here. This keeps the extension at zero runtime dependencies (per §7.2).
|
|
1023
|
+
|
|
1024
|
+
### 10.3 Category summary length — RESOLVED: 150-token target, 250-token hard cap
|
|
1025
|
+
|
|
1026
|
+
**Decision:** Target 150 tokens. Enforce a hard cap of 250 tokens by progressively reducing `maxExamples` per category (§4.5). The illustrative §4.5 example is ~210 tokens at full `maxExamples`, well under the cap. Story E01-S02 measures real output and tunes example counts.
|
|
1027
|
+
|
|
1028
|
+
### 10.4 Epic E02 (`tool_call` hook) scope — RESOLVED: separate story, same package
|
|
1029
|
+
|
|
1030
|
+
**Decision:** Implement E02 as a **separate story (E02-S01)** in the **same npm package**. Reasons:
|
|
1031
|
+
|
|
1032
|
+
- E01-S01 must ship without E02 to validate the core token-savings hypothesis.
|
|
1033
|
+
- E02 reuses the same index, synonym dict, and search code — splitting packages would require an internal API.
|
|
1034
|
+
- Toggling E02 by setting `pi-skill-search.proactive: true` in `.pi/settings.json` lets users opt in independently.
|
|
1035
|
+
|
|
1036
|
+
## 11. First Story Candidates
|
|
1037
|
+
|
|
1038
|
+
### E01-S01: Core indexer and search tool
|
|
1039
|
+
|
|
1040
|
+
**Overview**: Build `indexer.ts`, `search.ts`, `categories.ts`, `synonyms.ts`, `format.ts`, `strip.ts`, and `index.ts`. Subscribe to `before_agent_start`. Strip Pi's `<available_skills>` block, inject category summary, lazily register `skill-search` tool with templated description. No other hooks.
|
|
1041
|
+
|
|
1042
|
+
**Validation**: §9 unit, integration, and regression rows for search quality (precision@1 ≥ 0.85, recall@5 ≥ 0.95 over 25+ labeled queries), strip-regex correctness, index reuse, handler edge cases, and failure isolation. Performance: startup-token delta ≤ 600 vs Pi v0.74 baseline.
|
|
1043
|
+
|
|
1044
|
+
**Risk**: Medium — depends on stable Pi extension API contract for `systemPromptOptions.skills` and the `<available_skills>` lead-in sentence. Drift detection is mandatory (§9 strip-regex test).
|
|
1045
|
+
|
|
1046
|
+
### E01-S02: Category rules tuning
|
|
1047
|
+
|
|
1048
|
+
**Overview**: Tune category rules against the 137 `scientific-agent-skills` corpus. Verify every skill is classified into ≥1 category (no `"Other"` for the seed corpus). Measure summary token count with `tiktoken` and adjust `maxExamples` to fit the 250-token cap from §10.3.
|
|
1049
|
+
|
|
1050
|
+
**Validation**: Classifier coverage 100% on seed corpus. Summary ≤ 250 tokens. ≤ 200 tokens preferred.
|
|
1051
|
+
|
|
1052
|
+
**Risk**: Low — configuration only.
|
|
1053
|
+
|
|
1054
|
+
### E02-S01: Proactive suggestion hook
|
|
1055
|
+
|
|
1056
|
+
**Overview**: Add `tool_call` hook that detects Python package imports in bash commands. If a package matches an indexed skill name, surface a hint to the agent. Off by default; enabled via `pi-skill-search.proactive: true` in `.pi/settings.json` (per §10.4).
|
|
1057
|
+
|
|
1058
|
+
**Validation**: Unit tests for package detection regex. Integration test with mock bash commands. Confirm extension is no-op when setting is unset.
|
|
1059
|
+
|
|
1060
|
+
**Risk**: Low — notifications only, no blocking.
|
|
1061
|
+
|
|
1062
|
+
## 12. Harness Delta
|
|
1063
|
+
|
|
1064
|
+
No changes to harness-experimental templates needed. This project uses:
|
|
1065
|
+
|
|
1066
|
+
- **Spec intake**: This file is the spec intake.
|
|
1067
|
+
- **Normal lane**: E01-S01 is normal risk (no auth, no data mutation, no external systems).
|
|
1068
|
+
- **Story template**: Will use `docs/templates/story.md` for implementation stories.
|
|
1069
|
+
- **Decision template**: Decision 0001 recorded in this spec (§3).
|
|
1070
|
+
|
|
1071
|
+
## 13. Out of Scope
|
|
1072
|
+
|
|
1073
|
+
These are explicitly NOT part of this extension:
|
|
1074
|
+
|
|
1075
|
+
- **Embedding/semantic search** — revisit at 500+ skills.
|
|
1076
|
+
- **Auto-inject skills based on project imports** — possible future Layer 3.
|
|
1077
|
+
- **Skill dependency graph** — Agent Skills standard doesn't support.
|
|
1078
|
+
- **Hierarchical skills** — Agent Skills standard doesn't support.
|
|
1079
|
+
- **Metrics/telemetry** — requires logging infrastructure (pi-recollect).
|
|
1080
|
+
- **`fs.watch`-driven re-indexing** — out of scope. The extension does rebuild the index when Pi reports a different `Skill[]` (fingerprint mismatch in `ensureIndex`), so a `pi reload` flow that re-runs discovery is naturally supported. Real-time filesystem watching is not.
|
|
1081
|
+
- **Force-inject SKILL.md** — agent decides whether to read.
|
|
1082
|
+
- **Modifying `scientific-agent-skills` upstream** — extension works with existing format.
|
|
1083
|
+
- **Standalone skill discovery** — bypassing Pi's `loadSkills()` and scanning paths the user did not give to Pi. If needed later, must use `js-yaml`, not a hand-rolled parser (§8).
|