pi-skill-shiori 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/Docs/recommendation-surface.md +62 -0
- package/Docs/usage.md +164 -0
- package/README.md +62 -229
- package/ROADMAP.md +1 -1
- package/package.json +2 -1
- package/src/index.ts +51 -9
- package/src/prompt.ts +6 -6
- package/src/query.ts +13 -4
- package/src/recommendation-feedback.ts +177 -0
- package/src/recommendation-reason.ts +43 -0
- package/src/retrieval.ts +19 -10
- package/src/types.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
This project follows semantic versioning.
|
|
6
6
|
|
|
7
|
+
## [0.6.1] - 2026-07-01
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- README aligned with Pi OSS minimal-docs policy: badges, required entrypoint sections, Quick start, Package contents, Links.
|
|
12
|
+
- Detailed configuration and operational notes moved to `Docs/usage.md`.
|
|
13
|
+
- README install examples and status text updated to match package version `0.6.1`.
|
|
14
|
+
|
|
15
|
+
## [0.6.0] - 2026-06-29
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Bounded recommendation reason badges (`[trigger]`, `[description]`, `[low match]`) on recommended skills in auto-inject, `/shiori:recommend`, and `shiori_recommend` surfaces.
|
|
20
|
+
- `Docs/recommendation-surface.md` describing badge vocabulary, a multi-skill example, and how badges relate to Always Visible Skill policy and recommendation metrics.
|
|
21
|
+
- Tests for reason present, absent, low-confidence, and badge formatting.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Candidate summaries use compact badges instead of freeform `Why:` lines in the default Shiori surfaces.
|
|
26
|
+
|
|
7
27
|
## [0.5.0] - 2026-06-28
|
|
8
28
|
|
|
9
29
|
### Added
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Recommendation surface
|
|
2
|
+
|
|
3
|
+
This document describes how Pi Skill Shiori surfaces recommended skills and the bounded reason badges shown with each match.
|
|
4
|
+
|
|
5
|
+
## Reason badges
|
|
6
|
+
|
|
7
|
+
Shiori attaches a compact reason badge to each recommended skill. The vocabulary is intentionally small:
|
|
8
|
+
|
|
9
|
+
| Badge | Meaning |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `[trigger]` | A policy `triggers.include` phrase matched the user task. |
|
|
12
|
+
| `[description]` | Skill name/description tokens matched strongly enough to pass `candidateInjection.minScore`. |
|
|
13
|
+
| `[low match]` | The skill passed `minScore` but sits near the threshold; treat as a weaker suggestion. |
|
|
14
|
+
|
|
15
|
+
When Shiori cannot classify a match, it omits the badge instead of inventing a freeform explanation.
|
|
16
|
+
|
|
17
|
+
## Where badges appear
|
|
18
|
+
|
|
19
|
+
- **Auto-inject candidates** in the system prompt (`## Pi Skill Shiori Candidates`)
|
|
20
|
+
- **`/shiori:recommend` review mode** notifications and follow-up picker labels
|
|
21
|
+
- **`shiori_recommend` tool** collapsed result line (badges only, no long reports)
|
|
22
|
+
- **Pre-load summaries** when skills are queued for the agent
|
|
23
|
+
|
|
24
|
+
Badges are labels, not confidence scores. Numeric scores remain available in expanded detail views when needed.
|
|
25
|
+
|
|
26
|
+
## Example: multiple surfaced skills
|
|
27
|
+
|
|
28
|
+
Task: `browser scraping screenshot`
|
|
29
|
+
|
|
30
|
+
Possible matches:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
Matched 2 skill(s): playwright-cli [trigger], gstack-browse [description]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Auto-inject candidate block:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
## Pi Skill Shiori Candidates
|
|
40
|
+
|
|
41
|
+
Relevant skills:
|
|
42
|
+
- playwright-cli [trigger]: Browser automation, web scraping, screenshots. Load: shiori_load_skill({ skill: "playwright-cli" })
|
|
43
|
+
- gstack-browse [description]: Headless browser QA for scripted flows. Load: shiori_load_skill({ skill: "gstack-browse" })
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A near-threshold match degrades gracefully:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
- notes-helper [low match]: Search markdown notes in the vault. Load: shiori_load_skill({ skill: "notes-helper" })
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Relation to Always Visible Skill
|
|
53
|
+
|
|
54
|
+
`alwaysVisible` controls which skills stay in the Skill Catalog during Zero-Catalog Mode. Reason badges apply only to **recommended** (`triggerable`) skills surfaced by retrieval — not to always-visible catalog entries.
|
|
55
|
+
|
|
56
|
+
Always-visible skills may still appear as recommendations when they match a task; their badge explains the retrieval reason, not catalog visibility.
|
|
57
|
+
|
|
58
|
+
## Relation to recommendation metrics
|
|
59
|
+
|
|
60
|
+
`/shiori:stats` recommendation feedback records offers, loads, and follow-through by source (`auto-inject`, `command`, `tool`). Metrics do **not** store reason badges or per-reason tallies.
|
|
61
|
+
|
|
62
|
+
Use badges to understand individual suggestions during a session. Use metrics to see whether recommendations are being loaded after they are offered.
|
package/Docs/usage.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Usage
|
|
2
|
+
|
|
3
|
+
Detailed configuration and operational notes for Pi Skill Shiori. For install and quick start, see [README](../README.md).
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Pi Coding Agent with package support
|
|
8
|
+
- Node.js `>=22.5`
|
|
9
|
+
- `npm`
|
|
10
|
+
|
|
11
|
+
Pi core packages are peer dependencies and should be supplied by the Pi runtime:
|
|
12
|
+
|
|
13
|
+
- `@earendil-works/pi-ai`
|
|
14
|
+
- `@earendil-works/pi-coding-agent`
|
|
15
|
+
- `@earendil-works/pi-tui`
|
|
16
|
+
- `typebox`
|
|
17
|
+
|
|
18
|
+
## Vault-wide skill inventory
|
|
19
|
+
|
|
20
|
+
Shiori builds a searchable inventory from these roots (in discovery order; first match wins on duplicate skill names):
|
|
21
|
+
|
|
22
|
+
| Root | Scope |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `<cwd>/.pi/skills` | Project-local Pi skills |
|
|
25
|
+
| `<cwd>/.agents/skills` | Project-local Agent skills |
|
|
26
|
+
| `~/.pi/agent/skills` | User-global Pi agent skills |
|
|
27
|
+
| `inventory.roots` entries | Optional extra roots (relative to `<cwd>` or absolute) |
|
|
28
|
+
|
|
29
|
+
Refresh behavior:
|
|
30
|
+
|
|
31
|
+
- **Session start** builds the initial inventory and SQLite FTS index.
|
|
32
|
+
- **Auto-refresh** (default) rescans roots before retrieval when any `SKILL.md` path changes. This is a directory walk only; it rebuilds the FTS index when the fingerprint changes.
|
|
33
|
+
- **Manual refresh** via `/shiori:reload` always rebuilds the inventory and FTS index immediately.
|
|
34
|
+
- Disable auto-refresh when you prefer a fixed snapshot:
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
inventory:
|
|
38
|
+
autoRefreshOnChange: false
|
|
39
|
+
roots:
|
|
40
|
+
- custom-skills
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Cost/latency trade-offs:
|
|
44
|
+
|
|
45
|
+
- Auto-refresh adds a shallow filesystem walk on each agent turn while enabled. It avoids stale search results after local skill edits without restarting Pi.
|
|
46
|
+
- A full refresh rebuilds the SQLite FTS database and scales with total indexed skills and description length. Large vaults with many roots may prefer `autoRefreshOnChange: false` plus explicit `/shiori:reload` after bulk imports.
|
|
47
|
+
- Search stays description-first and policy-aware: only `triggerable` skills become candidates, with FTS ranking over names, descriptions, and policy triggers.
|
|
48
|
+
|
|
49
|
+
## Configure
|
|
50
|
+
|
|
51
|
+
Create `.pi/skill-shiori.yml` in the project where Pi runs.
|
|
52
|
+
|
|
53
|
+
Minimal config:
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
zeroCatalog:
|
|
57
|
+
enabled: true
|
|
58
|
+
|
|
59
|
+
defaults:
|
|
60
|
+
activation: explicit
|
|
61
|
+
|
|
62
|
+
candidateInjection:
|
|
63
|
+
maxCandidates: 3
|
|
64
|
+
minScore: 0.62
|
|
65
|
+
|
|
66
|
+
alwaysVisible:
|
|
67
|
+
- pi-skill-shiori
|
|
68
|
+
|
|
69
|
+
skills:
|
|
70
|
+
reddit-research:
|
|
71
|
+
activation: triggerable
|
|
72
|
+
triggers:
|
|
73
|
+
include:
|
|
74
|
+
- Reddit
|
|
75
|
+
- Redditで調べて
|
|
76
|
+
- reputation on Reddit
|
|
77
|
+
exclude: []
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Policy rules:
|
|
81
|
+
|
|
82
|
+
- `defaults.activation: explicit` is the safe default. Unlisted skills are not auto-candidates.
|
|
83
|
+
- `activation: triggerable` allows Shiori to recommend the skill for matching requests.
|
|
84
|
+
- `alwaysVisible` lists skills that should remain visible in the Skill Catalog during Zero-Catalog Mode. Use this for a tiny safety-critical allowlist instead of broad trigger rules.
|
|
85
|
+
- Missing `alwaysVisible` entries are reported by `/shiori:doctor` and warned once per session when Zero-Catalog Mode runs.
|
|
86
|
+
- `candidateInjection.maxCandidates` limits how many suggestions enter the prompt.
|
|
87
|
+
- `candidateInjection.minScore` drops weak matches.
|
|
88
|
+
|
|
89
|
+
Generate a starter policy from discovered skills:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
/shiori:bootstrap
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
This writes a generated review file next to `.pi/skill-shiori.yml`. Review it before using it as your real policy.
|
|
96
|
+
|
|
97
|
+
## Suppression statuses
|
|
98
|
+
|
|
99
|
+
`/shiori:doctor` reports the last suppression result:
|
|
100
|
+
|
|
101
|
+
| Status | Meaning |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `disabled` | `zeroCatalog.enabled` is false. |
|
|
104
|
+
| `not-needed` | No normal Skill Catalog marker was present in that turn's prompt, so nothing needed deletion. |
|
|
105
|
+
| `suppressed` | Shiori recognized and removed the normal Skill Catalog for that turn. |
|
|
106
|
+
| `failed-pattern-not-found` | A catalog-like marker existed, but Shiori could not safely identify its boundary. Prompt left unchanged. |
|
|
107
|
+
|
|
108
|
+
`not-needed` is not an error. It often means Pi or another configuration already avoided injecting the normal catalog.
|
|
109
|
+
|
|
110
|
+
## Retrieval backend
|
|
111
|
+
|
|
112
|
+
Shiori prefers `sqlite-fts` using Node's built-in `node:sqlite` and FTS5. If that is unavailable, it falls back to `token-match`.
|
|
113
|
+
|
|
114
|
+
Check backend:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
/shiori:doctor
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
retrievalBackend: sqlite-fts
|
|
124
|
+
code: prompt-boundary-v3
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Recommendation feedback metrics
|
|
128
|
+
|
|
129
|
+
`/shiori:stats` includes session-local recommendation feedback so operators can tune trigger rules from real outcomes instead of guesswork.
|
|
130
|
+
|
|
131
|
+
Recorded (bounded, in-memory for the current Pi session):
|
|
132
|
+
|
|
133
|
+
- recommendation offers and candidate slot counts per source (`auto-inject`, `command`, `tool`)
|
|
134
|
+
- load follow-through when recommended skills are pre-loaded or later loaded via `shiori_load_skill`
|
|
135
|
+
- abandoned recommendation flows (candidates shown but not loaded, e.g. review-only mode)
|
|
136
|
+
- zero-candidate queries (obvious misses)
|
|
137
|
+
- per-skill offered/loaded tallies (up to 50 skill names, oldest entries dropped first)
|
|
138
|
+
|
|
139
|
+
Not recorded:
|
|
140
|
+
|
|
141
|
+
- user prompts, task descriptions, or transcript bodies
|
|
142
|
+
- skill `SKILL.md` contents
|
|
143
|
+
- filesystem paths beyond skill names already visible in policy/inventory
|
|
144
|
+
|
|
145
|
+
Retention boundary:
|
|
146
|
+
|
|
147
|
+
- counters reset when the Pi session ends
|
|
148
|
+
- no disk persistence or cross-session analytics in this slice
|
|
149
|
+
- inspect the compact summary at the top of `/shiori:stats`, with full JSON below
|
|
150
|
+
|
|
151
|
+
Example summary line:
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
Recommendation feedback (session-local, no prompts stored)
|
|
155
|
+
offers: 4 | candidate slots: 7 | loaded: 3
|
|
156
|
+
abandoned: 1 | zero-candidate queries: 2 | follow-through: 42.9%
|
|
157
|
+
auto-inject: offers=2, loaded=1, zeroHits=1
|
|
158
|
+
command: offers=1, loaded=2, zeroHits=1
|
|
159
|
+
top skills: auth-helper(1/2), deploy-kit(1/1)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Related docs
|
|
163
|
+
|
|
164
|
+
- [Recommendation surface](recommendation-surface.md) — reason badges, Always Visible policy, and metrics interplay
|
package/README.md
CHANGED
|
@@ -1,46 +1,40 @@
|
|
|
1
1
|
# Pi Skill Shiori
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/eiei114/pi-skill-shiori/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/eiei114/pi-skill-shiori/actions/workflows/publish.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/pi-skill-shiori)
|
|
6
|
+
[](https://www.npmjs.com/package/pi-skill-shiori)
|
|
7
|
+
[](https://github.com/eiei114/pi-skill-shiori/blob/main/LICENSE)
|
|
8
|
+

|
|
9
|
+
[](https://docs.npmjs.com/generating-provenance-statements)
|
|
4
10
|
|
|
5
|
-
|
|
11
|
+
> Pi extension that keeps large Agent Skill catalogs out of the model prompt and loads only the skills that match the current task.
|
|
6
12
|
|
|
7
13
|
For maintenance priorities and the phased plan, see [`ROADMAP.md`](ROADMAP.md).
|
|
8
14
|
|
|
9
|
-
## What
|
|
15
|
+
## What this is
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
- **Policy-based retrieval**: treats skills as explicit by default and only candidates skills marked as triggerable.
|
|
13
|
-
- **Compact candidate injection**: injects short skill suggestions instead of full `SKILL.md` files.
|
|
14
|
-
- **On-demand loading**: exposes `shiori_load_skill` so the model can load one selected skill body when needed.
|
|
15
|
-
- **Vault-wide skill inventory**: indexes skills from project and user-global roots, with optional extra roots in policy.
|
|
16
|
-
- **SQLite FTS retrieval**: indexes skill names, descriptions, and policy triggers with `node:sqlite` + FTS5, with token-match fallback.
|
|
17
|
-
- **Compact UI output**: keeps skill-load logs short while still passing full skill text to the model.
|
|
17
|
+
Pi Skill Shiori is a [Pi](https://pi.dev/) package for vaults or projects with many Agent Skills where the default catalog becomes noisy and expensive. It hides the normal skill catalog when safe, recommends triggerable skills with compact summaries, and loads full `SKILL.md` bodies on demand.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
`0.6.1` is an early working release. Prompt-boundary suppression is intentionally conservative: if Shiori cannot safely recognize a catalog boundary, it leaves the prompt untouched and warns instead of deleting too much.
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
## Features
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- `@earendil-works/pi-ai`
|
|
32
|
-
- `@earendil-works/pi-coding-agent`
|
|
33
|
-
- `@earendil-works/pi-tui`
|
|
34
|
-
- `typebox`
|
|
23
|
+
- **Zero-Catalog Mode** — hides the normal skill catalog when Pi exposes one in the system prompt.
|
|
24
|
+
- **Policy-based retrieval** — treats skills as explicit by default and only candidates skills marked as triggerable.
|
|
25
|
+
- **Compact candidate injection** — injects short skill suggestions instead of full `SKILL.md` files.
|
|
26
|
+
- **On-demand loading** — exposes `shiori_load_skill` so the model can load one selected skill body when needed.
|
|
27
|
+
- **Vault-wide skill inventory** — indexes skills from project and user-global roots, with optional extra roots in policy.
|
|
28
|
+
- **SQLite FTS retrieval** — indexes skill names, descriptions, and policy triggers with `node:sqlite` + FTS5, with token-match fallback.
|
|
29
|
+
- **Recommendation reason badges** — compact `[trigger]`, `[description]`, and `[low match]` labels on surfaced skills.
|
|
30
|
+
- **Session-local feedback metrics** — `/shiori:stats` tracks offers, loads, and follow-through without storing prompts.
|
|
35
31
|
|
|
36
32
|
## Install
|
|
37
33
|
|
|
38
34
|
### Global install from npm
|
|
39
35
|
|
|
40
|
-
After the package is published to npm:
|
|
41
|
-
|
|
42
36
|
```bash
|
|
43
|
-
pi install npm:pi-skill-shiori@0.
|
|
37
|
+
pi install npm:pi-skill-shiori@0.6.1
|
|
44
38
|
```
|
|
45
39
|
|
|
46
40
|
Without a version pin:
|
|
@@ -51,10 +45,8 @@ pi install npm:pi-skill-shiori
|
|
|
51
45
|
|
|
52
46
|
### Project-local install from npm
|
|
53
47
|
|
|
54
|
-
Use `-l` to write the package to the current project’s `.pi/settings.json`:
|
|
55
|
-
|
|
56
48
|
```bash
|
|
57
|
-
pi install -l npm:pi-skill-shiori@0.
|
|
49
|
+
pi install -l npm:pi-skill-shiori@0.6.1
|
|
58
50
|
```
|
|
59
51
|
|
|
60
52
|
Without a version pin:
|
|
@@ -63,47 +55,10 @@ Without a version pin:
|
|
|
63
55
|
pi install -l npm:pi-skill-shiori
|
|
64
56
|
```
|
|
65
57
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
```json
|
|
69
|
-
{
|
|
70
|
-
"packages": [
|
|
71
|
-
"npm:pi-skill-shiori@0.5.0"
|
|
72
|
-
]
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Equivalent unpinned manual `.pi/settings.json` entry:
|
|
77
|
-
|
|
78
|
-
```json
|
|
79
|
-
{
|
|
80
|
-
"packages": [
|
|
81
|
-
"npm:pi-skill-shiori"
|
|
82
|
-
]
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### Global install from GitHub
|
|
87
|
-
|
|
88
|
-
```bash
|
|
89
|
-
pi install git:github.com/eiei114/pi-skill-shiori@v0.5.0
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Without a tag pin:
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
pi install git:github.com/eiei114/pi-skill-shiori
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### Project-local install from GitHub
|
|
99
|
-
|
|
100
|
-
```bash
|
|
101
|
-
pi install -l git:github.com/eiei114/pi-skill-shiori@v0.5.0
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
Without a tag pin:
|
|
58
|
+
### Install from GitHub
|
|
105
59
|
|
|
106
60
|
```bash
|
|
61
|
+
pi install git:github.com/eiei114/pi-skill-shiori@v0.6.1
|
|
107
62
|
pi install -l git:github.com/eiei114/pi-skill-shiori
|
|
108
63
|
```
|
|
109
64
|
|
|
@@ -116,114 +71,29 @@ pi -e git:github.com/eiei114/pi-skill-shiori
|
|
|
116
71
|
|
|
117
72
|
### Local development install
|
|
118
73
|
|
|
119
|
-
From a project that should use your checkout:
|
|
120
|
-
|
|
121
74
|
```bash
|
|
122
75
|
pi install -l /absolute/path/to/pi-skill-shiori
|
|
123
76
|
```
|
|
124
77
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
```json
|
|
128
|
-
{
|
|
129
|
-
"packages": [
|
|
130
|
-
"../../OSS/pi-skill-shiori"
|
|
131
|
-
]
|
|
132
|
-
}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
> Note: policy/index changes can be refreshed with `/shiori:reload`, and Shiori auto-refreshes the vault-wide inventory when skill files change under indexed roots. Extension code changes still require restarting the Pi process because Node keeps imported extension modules cached.
|
|
136
|
-
|
|
137
|
-
## Vault-wide skill inventory
|
|
138
|
-
|
|
139
|
-
Shiori builds a searchable inventory from these roots (in discovery order; first match wins on duplicate skill names):
|
|
140
|
-
|
|
141
|
-
| Root | Scope |
|
|
142
|
-
|---|---|
|
|
143
|
-
| `<cwd>/.pi/skills` | Project-local Pi skills |
|
|
144
|
-
| `<cwd>/.agents/skills` | Project-local Agent skills |
|
|
145
|
-
| `~/.pi/agent/skills` | User-global Pi agent skills |
|
|
146
|
-
| `inventory.roots` entries | Optional extra roots (relative to `<cwd>` or absolute) |
|
|
147
|
-
|
|
148
|
-
Refresh behavior:
|
|
149
|
-
|
|
150
|
-
- **Session start** builds the initial inventory and SQLite FTS index.
|
|
151
|
-
- **Auto-refresh** (default) rescans roots before retrieval when any `SKILL.md` path changes. This is a directory walk only; it rebuilds the FTS index when the fingerprint changes.
|
|
152
|
-
- **Manual refresh** via `/shiori:reload` always rebuilds the inventory and FTS index immediately.
|
|
153
|
-
- Disable auto-refresh when you prefer a fixed snapshot:
|
|
154
|
-
|
|
155
|
-
```yaml
|
|
156
|
-
inventory:
|
|
157
|
-
autoRefreshOnChange: false
|
|
158
|
-
roots:
|
|
159
|
-
- custom-skills
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
Cost/latency trade-offs:
|
|
163
|
-
|
|
164
|
-
- Auto-refresh adds a shallow filesystem walk on each agent turn while enabled. It avoids stale search results after local skill edits without restarting Pi.
|
|
165
|
-
- A full refresh rebuilds the SQLite FTS database and scales with total indexed skills and description length. Large vaults with many roots may prefer `autoRefreshOnChange: false` plus explicit `/shiori:reload` after bulk imports.
|
|
166
|
-
- Search stays description-first and policy-aware: only `triggerable` skills become candidates, with FTS ranking over names, descriptions, and policy triggers.
|
|
167
|
-
|
|
168
|
-
## Configure
|
|
78
|
+
## Quick start
|
|
169
79
|
|
|
170
|
-
|
|
80
|
+
1. Install the package (see [Install](#install)).
|
|
81
|
+
2. Create `.pi/skill-shiori.yml` in your Pi project (see [Docs/usage.md](Docs/usage.md#configure)).
|
|
82
|
+
3. Start Pi and run `/shiori:doctor` to confirm policy path, indexed roots, and suppression status.
|
|
83
|
+
4. Use `/shiori:bootstrap` to draft a policy from discovered skills, then `/shiori:reload` after edits.
|
|
84
|
+
5. Ask the agent for help; Shiori injects compact candidates or use `/shiori:recommend` for an interactive flow.
|
|
171
85
|
|
|
172
|
-
|
|
86
|
+
Commands take no inline arguments. Details are collected after launch via Pi UI prompts.
|
|
173
87
|
|
|
174
|
-
|
|
175
|
-
zeroCatalog:
|
|
176
|
-
enabled: true
|
|
177
|
-
|
|
178
|
-
defaults:
|
|
179
|
-
activation: explicit
|
|
180
|
-
|
|
181
|
-
candidateInjection:
|
|
182
|
-
maxCandidates: 3
|
|
183
|
-
minScore: 0.62
|
|
184
|
-
|
|
185
|
-
alwaysVisible:
|
|
186
|
-
- pi-skill-shiori
|
|
187
|
-
|
|
188
|
-
skills:
|
|
189
|
-
reddit-research:
|
|
190
|
-
activation: triggerable
|
|
191
|
-
triggers:
|
|
192
|
-
include:
|
|
193
|
-
- Reddit
|
|
194
|
-
- Redditで調べて
|
|
195
|
-
- reputation on Reddit
|
|
196
|
-
exclude: []
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
Policy rules:
|
|
200
|
-
|
|
201
|
-
- `defaults.activation: explicit` is the safe default. Unlisted skills are not auto-candidates.
|
|
202
|
-
- `activation: triggerable` allows Shiori to recommend the skill for matching requests.
|
|
203
|
-
- `alwaysVisible` lists skills that should remain visible in the Skill Catalog during Zero-Catalog Mode. Use this for a tiny safety-critical allowlist instead of broad trigger rules.
|
|
204
|
-
- Missing `alwaysVisible` entries are reported by `/shiori:doctor` and warned once per session when Zero-Catalog Mode runs.
|
|
205
|
-
- `candidateInjection.maxCandidates` limits how many suggestions enter the prompt.
|
|
206
|
-
- `candidateInjection.minScore` drops weak matches.
|
|
207
|
-
|
|
208
|
-
Generate a starter policy from discovered skills:
|
|
209
|
-
|
|
210
|
-
```text
|
|
211
|
-
/shiori:bootstrap
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
This writes a generated review file next to `.pi/skill-shiori.yml`. Review it before using it as your real policy.
|
|
215
|
-
|
|
216
|
-
## Commands
|
|
88
|
+
## Usage summary
|
|
217
89
|
|
|
218
90
|
| Command | Purpose |
|
|
219
91
|
|---|---|
|
|
220
92
|
| `/shiori:doctor` | Show policy path, indexed roots, inventory count, retrieval backend, suppression status, and code marker. |
|
|
221
93
|
| `/shiori:bootstrap` | Generate a review draft policy from discovered skill descriptions. |
|
|
222
|
-
| `/shiori:reload` |
|
|
223
|
-
| `/shiori:recommend` |
|
|
224
|
-
| `/shiori:stats` | Show operational counters. |
|
|
225
|
-
|
|
226
|
-
## Tool
|
|
94
|
+
| `/shiori:reload` | Rebuild Shiori skill inventory; optionally ask Pi to reload runtime resources. |
|
|
95
|
+
| `/shiori:recommend` | Interactive recommendation flow with optional pre-load. |
|
|
96
|
+
| `/shiori:stats` | Show operational counters and recommendation quality summary. |
|
|
227
97
|
|
|
228
98
|
Shiori registers one tool:
|
|
229
99
|
|
|
@@ -231,41 +101,22 @@ Shiori registers one tool:
|
|
|
231
101
|
shiori_load_skill({ skill: "reddit-research" })
|
|
232
102
|
```
|
|
233
103
|
|
|
234
|
-
The model receives the full selected `SKILL.md` content. The Pi UI shows a compact result like
|
|
104
|
+
The model receives the full selected `SKILL.md` content. The Pi UI shows a compact result like `✓ Loaded reddit-research (6.3KB)`.
|
|
235
105
|
|
|
236
|
-
|
|
237
|
-
✓ Loaded reddit-research (6.3KB)
|
|
238
|
-
```
|
|
106
|
+
Recommended skills show compact reason badges (`[trigger]`, `[description]`, `[low match]`). See [`Docs/recommendation-surface.md`](Docs/recommendation-surface.md) for badge vocabulary and metrics interplay.
|
|
239
107
|
|
|
240
|
-
|
|
108
|
+
For inventory roots, policy YAML, suppression statuses, and feedback metrics, see [`Docs/usage.md`](Docs/usage.md).
|
|
241
109
|
|
|
242
|
-
|
|
110
|
+
## Package contents
|
|
243
111
|
|
|
244
|
-
|
|
|
112
|
+
| Path | Purpose |
|
|
245
113
|
|---|---|
|
|
246
|
-
| `
|
|
247
|
-
| `
|
|
248
|
-
| `
|
|
249
|
-
| `
|
|
250
|
-
|
|
251
|
-
`
|
|
252
|
-
|
|
253
|
-
## Retrieval backend
|
|
254
|
-
|
|
255
|
-
Shiori prefers `sqlite-fts` using Node’s built-in `node:sqlite` and FTS5. If that is unavailable, it falls back to `token-match`.
|
|
256
|
-
|
|
257
|
-
Check backend:
|
|
258
|
-
|
|
259
|
-
```text
|
|
260
|
-
/shiori:doctor
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
Example:
|
|
264
|
-
|
|
265
|
-
```text
|
|
266
|
-
retrievalBackend: sqlite-fts
|
|
267
|
-
code: prompt-boundary-v3
|
|
268
|
-
```
|
|
114
|
+
| `src/` | Pi extension entrypoint and Shiori runtime |
|
|
115
|
+
| `Docs/usage.md` | Configuration, inventory, suppression, and metrics details |
|
|
116
|
+
| `Docs/recommendation-surface.md` | Recommendation badges and surface behavior |
|
|
117
|
+
| `ROADMAP.md` | Maintenance direction and phased plan |
|
|
118
|
+
| `CHANGELOG.md` | Version history |
|
|
119
|
+
| `SECURITY.md` | Vulnerability reporting |
|
|
269
120
|
|
|
270
121
|
## Development
|
|
271
122
|
|
|
@@ -274,6 +125,7 @@ git clone https://github.com/eiei114/pi-skill-shiori.git
|
|
|
274
125
|
cd pi-skill-shiori
|
|
275
126
|
npm install
|
|
276
127
|
npm run typecheck
|
|
128
|
+
npm test
|
|
277
129
|
```
|
|
278
130
|
|
|
279
131
|
Run in a Pi project without installing globally:
|
|
@@ -282,59 +134,40 @@ Run in a Pi project without installing globally:
|
|
|
282
134
|
pi -e /absolute/path/to/pi-skill-shiori
|
|
283
135
|
```
|
|
284
136
|
|
|
285
|
-
|
|
137
|
+
## Release
|
|
286
138
|
|
|
287
|
-
|
|
288
|
-
npm run typecheck
|
|
289
|
-
npm test
|
|
290
|
-
```
|
|
139
|
+
Releases are automated via Trusted Publishing:
|
|
291
140
|
|
|
141
|
+
1. Bump `version` in `package.json` and update `CHANGELOG.md`.
|
|
142
|
+
2. Merge to `main`.
|
|
143
|
+
3. **Auto Release** tags `v<version>` and creates a GitHub release.
|
|
144
|
+
4. The tag triggers **Publish**, which publishes to npm with provenance.
|
|
292
145
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
Release checklist for npm:
|
|
146
|
+
Maintainer checks before merge:
|
|
296
147
|
|
|
297
148
|
```bash
|
|
298
149
|
npm run typecheck
|
|
299
150
|
npm test
|
|
300
151
|
npm run release:npm:dry
|
|
301
|
-
npm whoami
|
|
302
|
-
npm run release:npm
|
|
303
152
|
```
|
|
304
153
|
|
|
305
154
|
For version history, see [`CHANGELOG.md`](CHANGELOG.md).
|
|
306
155
|
|
|
307
|
-
For a first-time publish, login first:
|
|
308
|
-
|
|
309
|
-
```bash
|
|
310
|
-
npm login
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
After publishing, verify the Pi install path:
|
|
314
|
-
|
|
315
|
-
```bash
|
|
316
|
-
pi install npm:pi-skill-shiori@0.5.0
|
|
317
|
-
pi install npm:pi-skill-shiori
|
|
318
|
-
pi list
|
|
319
|
-
/shiori:doctor
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
For future versions:
|
|
323
|
-
|
|
324
|
-
```bash
|
|
325
|
-
npm version patch
|
|
326
|
-
git push --follow-tags
|
|
327
|
-
npm run release:npm
|
|
328
|
-
```
|
|
329
|
-
|
|
330
156
|
## Security
|
|
331
157
|
|
|
332
158
|
Pi packages execute local code with the same permissions as Pi. Review third-party packages before installing them.
|
|
333
159
|
|
|
334
|
-
Shiori
|
|
160
|
+
Shiori does not sandbox skills. It only changes how skill candidates are discovered and loaded. A loaded skill can still instruct the model to run tools, edit files, or execute commands according to your Pi/tool permissions.
|
|
335
161
|
|
|
336
162
|
For vulnerability reporting, see [`SECURITY.md`](SECURITY.md).
|
|
337
163
|
|
|
164
|
+
## Links
|
|
165
|
+
|
|
166
|
+
- **Repository**: <https://github.com/eiei114/pi-skill-shiori>
|
|
167
|
+
- **npm**: <https://www.npmjs.com/package/pi-skill-shiori>
|
|
168
|
+
- **Issues**: <https://github.com/eiei114/pi-skill-shiori/issues>
|
|
169
|
+
- **Pi packages**: <https://pi.dev/packages>
|
|
170
|
+
|
|
338
171
|
## License
|
|
339
172
|
|
|
340
173
|
MIT
|
package/ROADMAP.md
CHANGED
|
@@ -57,7 +57,7 @@ semver classification. Patches default to `patch`; `none` means no package publi
|
|
|
57
57
|
|
|
58
58
|
| # | Slice | Bump | Status |
|
|
59
59
|
|---|---|---|---|
|
|
60
|
-
| 1 | Add README status badges (CI, Publish, npm version, npm downloads, License, Pi package, Trusted Publishing) | patch |
|
|
60
|
+
| 1 | Add README status badges (CI, Publish, npm version, npm downloads, License, Pi package, Trusted Publishing) | patch | done |
|
|
61
61
|
| 2 | Add `version:check` CI gate (block publishable-path changes without a semver increase) | none | planned |
|
|
62
62
|
| 3 | Pin GitHub Actions to commit SHAs in `ci.yml`, `auto-release.yml`, `publish.yml` | none | planned |
|
|
63
63
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-skill-shiori",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Pi extension that reduces Agent Skill catalog tokens with policy-based skill retrieval and on-demand skill loading.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"LICENSE",
|
|
28
28
|
"SECURITY.md",
|
|
29
29
|
"CHANGELOG.md",
|
|
30
|
+
"Docs",
|
|
30
31
|
"src"
|
|
31
32
|
],
|
|
32
33
|
"scripts": {
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { SkillIndex } from "./indexer.js";
|
|
|
8
8
|
import { loadRecommendedSkills } from "./load-skills.js";
|
|
9
9
|
import { formatPlanningKickoffMessage, isPlanningIntent } from "./planning-kickoff.js";
|
|
10
10
|
import { createStats } from "./metrics.js";
|
|
11
|
+
import { createRecommendationFeedback } from "./recommendation-feedback.js";
|
|
11
12
|
import { getPolicyPath, loadPolicy } from "./policy.js";
|
|
12
13
|
import {
|
|
13
14
|
formatCandidateDetail,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
formatRecommendKickoffMessage,
|
|
17
18
|
suppressSkillCatalog,
|
|
18
19
|
} from "./prompt.js";
|
|
20
|
+
import { formatReasonBadgeSuffix } from "./recommendation-reason.js";
|
|
19
21
|
import { retrieveCandidatesExpanded } from "./retrieval-expanded.js";
|
|
20
22
|
import type { SkillRecord } from "./types.js";
|
|
21
23
|
|
|
@@ -40,6 +42,7 @@ export default function piSkillShiori(pi: ExtensionAPI) {
|
|
|
40
42
|
let inventoryRoots: string[] = [];
|
|
41
43
|
let warnedAlwaysVisibleMissing = new Set<string>();
|
|
42
44
|
const stats = createStats();
|
|
45
|
+
const recommendationFeedback = createRecommendationFeedback();
|
|
43
46
|
|
|
44
47
|
async function reload(cwd: string): Promise<SkillIndex> {
|
|
45
48
|
const policy = await loadPolicy(cwd);
|
|
@@ -126,8 +129,16 @@ export default function piSkillShiori(pi: ExtensionAPI) {
|
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
const candidates = retrieveCandidatesExpanded(event.prompt, current.skills, current.policy, current);
|
|
129
|
-
if (candidates.length === 0)
|
|
130
|
-
|
|
132
|
+
if (candidates.length === 0) {
|
|
133
|
+
stats.zeroCandidateCount += 1;
|
|
134
|
+
recommendationFeedback.recordZeroCandidates("auto-inject");
|
|
135
|
+
} else {
|
|
136
|
+
stats.candidateHitCount += 1;
|
|
137
|
+
recommendationFeedback.recordRecommendation(
|
|
138
|
+
"auto-inject",
|
|
139
|
+
candidates.map((candidate) => candidate.skill.name),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
131
142
|
|
|
132
143
|
const injection = formatCandidateInjection(candidates);
|
|
133
144
|
const candidateSection = injection ? `\n\n## Pi Skill Shiori Candidates\n\n${injection}\n` : "";
|
|
@@ -153,7 +164,13 @@ export default function piSkillShiori(pi: ExtensionAPI) {
|
|
|
153
164
|
const current = await ensureIndex(ctx.cwd);
|
|
154
165
|
const policy = withRecommendLimits(current);
|
|
155
166
|
const candidates = retrieveCandidatesExpanded(params.task, current.skills, policy, current);
|
|
167
|
+
const candidateNames = candidates.map((candidate) => candidate.skill.name);
|
|
156
168
|
const loaded = await loadRecommendedSkills(candidates);
|
|
169
|
+
recommendationFeedback.recordRecommendation(
|
|
170
|
+
"tool",
|
|
171
|
+
candidateNames,
|
|
172
|
+
loaded.map((entry) => entry.candidate.skill.name),
|
|
173
|
+
);
|
|
157
174
|
const summary = formatLoadedSkillsSummary(loaded);
|
|
158
175
|
const blocks = loaded.map(({ candidate, content }) => ({
|
|
159
176
|
type: "text" as const,
|
|
@@ -163,11 +180,16 @@ export default function piSkillShiori(pi: ExtensionAPI) {
|
|
|
163
180
|
content: summary
|
|
164
181
|
? [{ type: "text", text: summary }, ...blocks]
|
|
165
182
|
: [{ type: "text", text: "No matching skills for that task." }],
|
|
166
|
-
details: {
|
|
183
|
+
details: {
|
|
184
|
+
query: params.task,
|
|
185
|
+
matches: candidates.map((c) => `${c.skill.name}${formatReasonBadgeSuffix(c.reason)}`),
|
|
186
|
+
},
|
|
167
187
|
};
|
|
168
188
|
},
|
|
169
189
|
renderResult(result, { expanded }, theme) {
|
|
170
|
-
const details = result.details as
|
|
190
|
+
const details = result.details as
|
|
191
|
+
| { query?: string; matches?: string[] }
|
|
192
|
+
| undefined;
|
|
171
193
|
const names = details?.matches?.join(", ") ?? "none";
|
|
172
194
|
const collapsed = `${theme.fg("success", "✓ Shiori")} ${theme.bold(names)}`;
|
|
173
195
|
if (!expanded) return new Text(collapsed, 0, 0);
|
|
@@ -193,6 +215,7 @@ export default function piSkillShiori(pi: ExtensionAPI) {
|
|
|
193
215
|
if (!record) throw new Error(`Skill not found: ${params.skill}`);
|
|
194
216
|
const content = await readFile(record.path, "utf8");
|
|
195
217
|
stats.loadedSkillCount += 1;
|
|
218
|
+
recommendationFeedback.recordSkillLoaded(record.name);
|
|
196
219
|
const details: ShioriLoadDetails = {
|
|
197
220
|
skill: record.name,
|
|
198
221
|
path: record.path,
|
|
@@ -305,12 +328,18 @@ export default function piSkillShiori(pi: ExtensionAPI) {
|
|
|
305
328
|
|
|
306
329
|
const policy = withRecommendLimits(current);
|
|
307
330
|
const candidates = retrieveCandidatesExpanded(query, current.skills, policy, current);
|
|
331
|
+
const candidateNames = candidates.map((candidate) => candidate.skill.name);
|
|
308
332
|
const planning = isPlanningIntent(query);
|
|
309
333
|
const reviewOnly = action === RECOMMEND_REVIEW_ONLY;
|
|
310
334
|
const loaded = planning || reviewOnly ? [] : await loadRecommendedSkills(candidates);
|
|
335
|
+
recommendationFeedback.recordRecommendation(
|
|
336
|
+
"command",
|
|
337
|
+
candidateNames,
|
|
338
|
+
loaded.map((entry) => entry.candidate.skill.name),
|
|
339
|
+
);
|
|
311
340
|
const summary = planning || reviewOnly
|
|
312
341
|
? candidates.length > 0
|
|
313
|
-
? `Matched ${candidates.length} skill(s): ${candidates.map((c) => c.skill.name).join(", ")}`
|
|
342
|
+
? `Matched ${candidates.length} skill(s): ${candidates.map((c) => `${c.skill.name}${formatReasonBadgeSuffix(c.reason)}`).join(", ")}`
|
|
314
343
|
: ""
|
|
315
344
|
: formatLoadedSkillsSummary(loaded);
|
|
316
345
|
|
|
@@ -341,14 +370,16 @@ export default function piSkillShiori(pi: ExtensionAPI) {
|
|
|
341
370
|
}
|
|
342
371
|
|
|
343
372
|
const choice = await ctx.ui.select("Follow up on a recommendation:", [
|
|
344
|
-
...candidates.map((candidate) => candidate.skill.name),
|
|
373
|
+
...candidates.map((candidate) => `${candidate.skill.name}${formatReasonBadgeSuffix(candidate.reason)}`),
|
|
345
374
|
"Done",
|
|
346
375
|
]);
|
|
347
376
|
if (!choice || choice === "Done") {
|
|
348
377
|
return;
|
|
349
378
|
}
|
|
350
379
|
|
|
351
|
-
const selected = candidates.find(
|
|
380
|
+
const selected = candidates.find(
|
|
381
|
+
(candidate) => choice === `${candidate.skill.name}${formatReasonBadgeSuffix(candidate.reason)}`,
|
|
382
|
+
);
|
|
352
383
|
if (!selected) {
|
|
353
384
|
return;
|
|
354
385
|
}
|
|
@@ -369,9 +400,20 @@ export default function piSkillShiori(pi: ExtensionAPI) {
|
|
|
369
400
|
});
|
|
370
401
|
|
|
371
402
|
pi.registerCommand("shiori:stats", {
|
|
372
|
-
description: "Show Pi Skill Shiori operational counters",
|
|
403
|
+
description: "Show Pi Skill Shiori operational counters and recommendation feedback",
|
|
373
404
|
handler: async (_args, ctx) => {
|
|
374
|
-
|
|
405
|
+
const payload = {
|
|
406
|
+
...stats,
|
|
407
|
+
recommendationFeedback: recommendationFeedback.summarize(),
|
|
408
|
+
};
|
|
409
|
+
ctx.ui.notify(
|
|
410
|
+
[
|
|
411
|
+
recommendationFeedback.formatSummary(),
|
|
412
|
+
"",
|
|
413
|
+
JSON.stringify(payload, null, 2),
|
|
414
|
+
].join("\n"),
|
|
415
|
+
"info",
|
|
416
|
+
);
|
|
375
417
|
},
|
|
376
418
|
});
|
|
377
419
|
|
package/src/prompt.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeAlwaysVisible } from "./always-visible.js";
|
|
2
|
+
import { formatReasonBadgeSuffix } from "./recommendation-reason.js";
|
|
2
3
|
import type { SkillCandidate, SkillRecord, SuppressionStatus } from "./types.js";
|
|
3
4
|
|
|
4
5
|
export interface CatalogSuppressionResult {
|
|
@@ -39,7 +40,7 @@ export function formatCandidateInjection(candidates: SkillCandidate[]): string {
|
|
|
39
40
|
for (const candidate of candidates) {
|
|
40
41
|
const desc = compactDescription(candidate.skill.description, COMPACT_DESCRIPTION_LIMIT);
|
|
41
42
|
lines.push(
|
|
42
|
-
`- ${candidate.skill.name}
|
|
43
|
+
`- ${candidate.skill.name}${formatReasonBadgeSuffix(candidate.reason)}: ${desc} Load: shiori_load_skill({ skill: "${candidate.skill.name}" })`,
|
|
43
44
|
);
|
|
44
45
|
}
|
|
45
46
|
return lines.join("\n");
|
|
@@ -70,8 +71,8 @@ export function formatRecommendKickoffMessage(
|
|
|
70
71
|
|
|
71
72
|
for (const { candidate, content } of loaded) {
|
|
72
73
|
lines.push(
|
|
73
|
-
`### Skill: ${candidate.skill.name}`,
|
|
74
|
-
`Score: ${candidate.score.toFixed(2)}
|
|
74
|
+
`### Skill: ${candidate.skill.name}${formatReasonBadgeSuffix(candidate.reason)}`,
|
|
75
|
+
`Score: ${candidate.score.toFixed(2)}`,
|
|
75
76
|
`Path: ${candidate.skill.path}`,
|
|
76
77
|
"",
|
|
77
78
|
content.trim(),
|
|
@@ -87,17 +88,16 @@ export function formatLoadedSkillsSummary(loaded: Array<{ candidate: SkillCandid
|
|
|
87
88
|
const lines = [`Pre-loaded ${loaded.length} skill(s):`];
|
|
88
89
|
for (const { candidate } of loaded) {
|
|
89
90
|
const desc = compactDescription(candidate.skill.description, COMPACT_DESCRIPTION_LIMIT);
|
|
90
|
-
lines.push(`- ${candidate.skill.name} (${candidate.score.toFixed(2)}): ${desc}`);
|
|
91
|
+
lines.push(`- ${candidate.skill.name}${formatReasonBadgeSuffix(candidate.reason)} (${candidate.score.toFixed(2)}): ${desc}`);
|
|
91
92
|
}
|
|
92
93
|
return lines.join("\n");
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
export function formatCandidateDetail(candidate: SkillCandidate): string {
|
|
96
97
|
return [
|
|
97
|
-
candidate.skill.name
|
|
98
|
+
`${candidate.skill.name}${formatReasonBadgeSuffix(candidate.reason)}`,
|
|
98
99
|
`Description: ${candidate.skill.description}`,
|
|
99
100
|
`Score: ${candidate.score.toFixed(2)}`,
|
|
100
|
-
`Why: ${candidate.why}`,
|
|
101
101
|
`Path: ${candidate.skill.path}`,
|
|
102
102
|
`Load: shiori_load_skill({ skill: "${candidate.skill.name}" })`,
|
|
103
103
|
].join("\n");
|
package/src/query.ts
CHANGED
|
@@ -28,10 +28,19 @@ const TERM_ALIASES: Record<string, string[]> = {
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
/** Whole-prompt intents → English queries that match planning skills in descriptions. */
|
|
31
|
-
const INTENT_QUERY_PACKS: Array<{ test: RegExp; queries: string[] }> = [
|
|
32
|
-
{
|
|
33
|
-
test:
|
|
34
|
-
|
|
31
|
+
const INTENT_QUERY_PACKS: Array<{ test: RegExp; queries: string[] }> = [
|
|
32
|
+
{
|
|
33
|
+
test:
|
|
34
|
+
/\bx\s+(?:api|data|export|follower|monitor|post|search|social|timeline|tweet)\b|\b(?:twitter|tweet|timeline|follower)\b|social(?:\s+media)?/i,
|
|
35
|
+
queries: [
|
|
36
|
+
"tweet search social media data",
|
|
37
|
+
"twitter timeline follower export",
|
|
38
|
+
"x api tweet monitor",
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
test: /計画|プラン|立てたい|planning/i,
|
|
43
|
+
queries: [
|
|
35
44
|
"to-prd PRD plan",
|
|
36
45
|
"to-issues breakdown plan",
|
|
37
46
|
"grill me plan design",
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
export type RecommendationSource = "auto-inject" | "command" | "tool";
|
|
2
|
+
|
|
3
|
+
export interface SourceFeedbackCounts {
|
|
4
|
+
offers: number;
|
|
5
|
+
candidateSlots: number;
|
|
6
|
+
loaded: number;
|
|
7
|
+
zeroHits: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SkillFeedbackCounts {
|
|
11
|
+
offered: number;
|
|
12
|
+
loaded: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RecommendationFeedbackSummary {
|
|
16
|
+
totalOffers: number;
|
|
17
|
+
totalCandidateSlots: number;
|
|
18
|
+
totalLoaded: number;
|
|
19
|
+
totalAbandoned: number;
|
|
20
|
+
zeroCandidateQueries: number;
|
|
21
|
+
followThroughRate: number | null;
|
|
22
|
+
bySource: Record<RecommendationSource, SourceFeedbackCounts>;
|
|
23
|
+
topSkills: Array<{ name: string; offered: number; loaded: number }>;
|
|
24
|
+
retention: {
|
|
25
|
+
maxSkillEntries: number;
|
|
26
|
+
skillEntries: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const SOURCES: RecommendationSource[] = ["auto-inject", "command", "tool"];
|
|
31
|
+
const MAX_SKILL_ENTRIES = 50;
|
|
32
|
+
|
|
33
|
+
function emptySourceCounts(): SourceFeedbackCounts {
|
|
34
|
+
return { offers: 0, candidateSlots: 0, loaded: 0, zeroHits: 0 };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function emptyBySource(): Record<RecommendationSource, SourceFeedbackCounts> {
|
|
38
|
+
return {
|
|
39
|
+
"auto-inject": emptySourceCounts(),
|
|
40
|
+
command: emptySourceCounts(),
|
|
41
|
+
tool: emptySourceCounts(),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RecommendationFeedback {
|
|
46
|
+
recordRecommendation(source: RecommendationSource, candidates: string[], loaded?: string[]): void;
|
|
47
|
+
recordZeroCandidates(source: RecommendationSource): void;
|
|
48
|
+
recordSkillLoaded(skillName: string): void;
|
|
49
|
+
summarize(): RecommendationFeedbackSummary;
|
|
50
|
+
formatSummary(): string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createRecommendationFeedback(): RecommendationFeedback {
|
|
54
|
+
const bySource = emptyBySource();
|
|
55
|
+
const bySkill = new Map<string, SkillFeedbackCounts>();
|
|
56
|
+
const recentAutoInjectCandidates = new Set<string>();
|
|
57
|
+
|
|
58
|
+
let totalOffers = 0;
|
|
59
|
+
let totalCandidateSlots = 0;
|
|
60
|
+
let totalLoaded = 0;
|
|
61
|
+
let totalAbandoned = 0;
|
|
62
|
+
let zeroCandidateQueries = 0;
|
|
63
|
+
|
|
64
|
+
function bumpSkill(name: string, field: "offered" | "loaded"): void {
|
|
65
|
+
const current = bySkill.get(name) ?? { offered: 0, loaded: 0 };
|
|
66
|
+
current[field] += 1;
|
|
67
|
+
bySkill.set(name, current);
|
|
68
|
+
while (bySkill.size > MAX_SKILL_ENTRIES) {
|
|
69
|
+
const oldest = bySkill.keys().next().value;
|
|
70
|
+
if (oldest === undefined) break;
|
|
71
|
+
bySkill.delete(oldest);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function recordLoadedSkills(source: RecommendationSource, loaded: string[]): void {
|
|
76
|
+
for (const name of loaded) {
|
|
77
|
+
bumpSkill(name, "loaded");
|
|
78
|
+
bySource[source].loaded += 1;
|
|
79
|
+
totalLoaded += 1;
|
|
80
|
+
recentAutoInjectCandidates.delete(name);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
recordRecommendation(source, candidates, loaded = []) {
|
|
86
|
+
if (candidates.length === 0) {
|
|
87
|
+
this.recordZeroCandidates(source);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
totalOffers += 1;
|
|
92
|
+
totalCandidateSlots += candidates.length;
|
|
93
|
+
const sourceCounts = bySource[source];
|
|
94
|
+
sourceCounts.offers += 1;
|
|
95
|
+
sourceCounts.candidateSlots += candidates.length;
|
|
96
|
+
|
|
97
|
+
for (const name of candidates) {
|
|
98
|
+
bumpSkill(name, "offered");
|
|
99
|
+
if (source === "auto-inject") {
|
|
100
|
+
recentAutoInjectCandidates.add(name);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (loaded.length > 0) {
|
|
105
|
+
recordLoadedSkills(source, loaded);
|
|
106
|
+
} else if (source !== "auto-inject") {
|
|
107
|
+
totalAbandoned += 1;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
recordZeroCandidates(source) {
|
|
112
|
+
zeroCandidateQueries += 1;
|
|
113
|
+
bySource[source].zeroHits += 1;
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
recordSkillLoaded(skillName) {
|
|
117
|
+
if (!recentAutoInjectCandidates.has(skillName)) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
recordLoadedSkills("auto-inject", [skillName]);
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
summarize() {
|
|
124
|
+
const followThroughRate =
|
|
125
|
+
totalCandidateSlots > 0 ? Number((totalLoaded / totalCandidateSlots).toFixed(3)) : null;
|
|
126
|
+
|
|
127
|
+
const topSkills = [...bySkill.entries()]
|
|
128
|
+
.map(([name, counts]) => ({ name, ...counts }))
|
|
129
|
+
.sort((a, b) => b.offered - a.offered || a.name.localeCompare(b.name))
|
|
130
|
+
.slice(0, 10);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
totalOffers,
|
|
134
|
+
totalCandidateSlots,
|
|
135
|
+
totalLoaded,
|
|
136
|
+
totalAbandoned,
|
|
137
|
+
zeroCandidateQueries,
|
|
138
|
+
followThroughRate,
|
|
139
|
+
bySource,
|
|
140
|
+
topSkills,
|
|
141
|
+
retention: {
|
|
142
|
+
maxSkillEntries: MAX_SKILL_ENTRIES,
|
|
143
|
+
skillEntries: bySkill.size,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
formatSummary() {
|
|
149
|
+
const summary = this.summarize();
|
|
150
|
+
const rate =
|
|
151
|
+
summary.followThroughRate === null ? "n/a" : `${(summary.followThroughRate * 100).toFixed(1)}%`;
|
|
152
|
+
const lines = [
|
|
153
|
+
"Recommendation feedback (session-local, no prompts stored)",
|
|
154
|
+
`offers: ${summary.totalOffers} | candidate slots: ${summary.totalCandidateSlots} | loaded: ${summary.totalLoaded}`,
|
|
155
|
+
`abandoned: ${summary.totalAbandoned} | zero-candidate queries: ${summary.zeroCandidateQueries} | follow-through: ${rate}`,
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
for (const source of SOURCES) {
|
|
159
|
+
const counts = summary.bySource[source];
|
|
160
|
+
if (counts.offers === 0 && counts.zeroHits === 0) continue;
|
|
161
|
+
lines.push(
|
|
162
|
+
`${source}: offers=${counts.offers}, loaded=${counts.loaded}, zeroHits=${counts.zeroHits}`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (summary.topSkills.length > 0) {
|
|
167
|
+
const skillLine = summary.topSkills
|
|
168
|
+
.slice(0, 5)
|
|
169
|
+
.map((skill) => `${skill.name}(${skill.loaded}/${skill.offered})`)
|
|
170
|
+
.join(", ");
|
|
171
|
+
lines.push(`top skills: ${skillLine}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return lines.join("\n");
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { RecommendationReasonKind, SkillCandidate } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** Score within this margin above minScore is treated as low-confidence. */
|
|
4
|
+
export const LOW_CONFIDENCE_MARGIN = 0.08;
|
|
5
|
+
|
|
6
|
+
export const RECOMMENDATION_REASON_LABELS: Record<RecommendationReasonKind, string> = {
|
|
7
|
+
trigger: "trigger",
|
|
8
|
+
description: "description",
|
|
9
|
+
"low-confidence": "low match",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type RecommendationMatchSource = "trigger" | "description";
|
|
13
|
+
|
|
14
|
+
export function classifyRecommendationReason(
|
|
15
|
+
source: RecommendationMatchSource,
|
|
16
|
+
score: number,
|
|
17
|
+
minScore: number,
|
|
18
|
+
): RecommendationReasonKind {
|
|
19
|
+
if (source === "trigger") return "trigger";
|
|
20
|
+
return score < minScore + LOW_CONFIDENCE_MARGIN ? "low-confidence" : "description";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatReasonBadge(reason: RecommendationReasonKind | null | undefined): string {
|
|
24
|
+
if (!reason) return "";
|
|
25
|
+
const label = RECOMMENDATION_REASON_LABELS[reason];
|
|
26
|
+
return label ? `[${label}]` : "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatReasonBadgeSuffix(reason: RecommendationReasonKind | null | undefined): string {
|
|
30
|
+
const badge = formatReasonBadge(reason);
|
|
31
|
+
return badge ? ` ${badge}` : "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function attachRecommendationReason(
|
|
35
|
+
candidate: Omit<SkillCandidate, "reason">,
|
|
36
|
+
source: RecommendationMatchSource,
|
|
37
|
+
minScore: number,
|
|
38
|
+
): SkillCandidate {
|
|
39
|
+
return {
|
|
40
|
+
...candidate,
|
|
41
|
+
reason: classifyRecommendationReason(source, candidate.score, minScore),
|
|
42
|
+
};
|
|
43
|
+
}
|
package/src/retrieval.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { attachRecommendationReason } from "./recommendation-reason.js";
|
|
1
2
|
import type { ShioriPolicy, SkillCandidate, SkillRecord } from "./types.js";
|
|
2
3
|
|
|
3
4
|
interface RetrievalIndex {
|
|
@@ -51,22 +52,30 @@ function evaluateSkill(
|
|
|
51
52
|
const includes = skillPolicy?.triggers?.include ?? [];
|
|
52
53
|
const matchedInclude = includes.find((trigger) => includesNormalized(normalizedQuery, trigger));
|
|
53
54
|
if (matchedInclude) {
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
return attachRecommendationReason(
|
|
56
|
+
{
|
|
57
|
+
skill,
|
|
58
|
+
score: 0.95,
|
|
59
|
+
why: `matched trigger "${matchedInclude}"`,
|
|
60
|
+
},
|
|
61
|
+
"trigger",
|
|
62
|
+
policy.candidateInjection.minScore,
|
|
63
|
+
);
|
|
59
64
|
}
|
|
60
65
|
if (exactTriggerOnly) return undefined;
|
|
61
66
|
|
|
62
67
|
const text = normalize(`${skill.name} ${skill.description} ${includes.join(" ")}`);
|
|
63
68
|
const tokenScore = scoreTokens(normalizedQuery, text);
|
|
64
69
|
if (tokenScore < policy.candidateInjection.minScore) return undefined;
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
return attachRecommendationReason(
|
|
71
|
+
{
|
|
72
|
+
skill,
|
|
73
|
+
score: tokenScore,
|
|
74
|
+
why: "matched skill description",
|
|
75
|
+
},
|
|
76
|
+
"description",
|
|
77
|
+
policy.candidateInjection.minScore,
|
|
78
|
+
);
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
function normalize(input: string): string {
|
package/src/types.ts
CHANGED
|
@@ -39,10 +39,13 @@ export interface SkillRecord {
|
|
|
39
39
|
source: string;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export type RecommendationReasonKind = "trigger" | "description" | "low-confidence";
|
|
43
|
+
|
|
42
44
|
export interface SkillCandidate {
|
|
43
45
|
skill: SkillRecord;
|
|
44
46
|
score: number;
|
|
45
47
|
why: string;
|
|
48
|
+
reason: RecommendationReasonKind | null;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
export interface ShioriStats {
|