pi-skill-shiori 0.6.9 → 0.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## [0.6.11] - 2026-09-21
6
+
7
+ ### Fixed
8
+
9
+ - Rank curated trigger matches ahead of heuristic description matches when enforcing candidate limits. Derived single-token query variants can score a perfect `1.00` from substring matches while trigger matches are fixed at `0.95`, so a genuine trigger match could be dropped by the candidate cap. The same ordering now applies at both cap sites, because the per-variant cap runs before expanded retrieval merges variants and a match dropped there can never be restored.
10
+ - Apply the original query's `exclude` triggers across merged variants in expanded retrieval. A derived single-token variant could drop the context that made an exclude match (`do not use auth` becomes `auth`), which reintroduced an excluded skill.
11
+
12
+ ## [0.6.10] - 2026-09-21
13
+
14
+ ### Fixed
15
+
16
+ - Prune stale `.pi/cache/skill-shiori/index-*.sqlite` databases. Every index rebuild wrote a new `index-<pid>-<ts>.sqlite` file and never removed the older ones, so a long-lived vault accumulated hundreds of files (336 files / 33 MB in the local vault). Rebuilds now delete databases older than a one hour grace window; the live database, recent files, and locked files are left in place.
17
+
5
18
  ## [0.6.9] - 2026-08-22
6
19
 
7
20
  ### Changed
package/Docs/usage.md CHANGED
@@ -100,7 +100,7 @@ Generate a starter policy from discovered skills:
100
100
  /shiori:bootstrap
101
101
  ```
102
102
 
103
- This writes a generated review file next to `.pi/skill-shiori.yml`. Review it before using it as your real policy.
103
+ This writes `.pi/skill-shiori.generated.yml` next to your project policy. Review the draft, copy the sections you want into `.pi/skill-shiori.yml`, then run `/shiori:reload`.
104
104
 
105
105
  ## Suppression statuses
106
106
 
@@ -167,6 +167,24 @@ command: offers=1, loaded=2, zeroHits=1
167
167
  top skills: auth-helper(1/2), deploy-kit(1/1)
168
168
  ```
169
169
 
170
+ ## Agent tools
171
+
172
+ Shiori registers two tools the agent can call after candidates appear:
173
+
174
+ ```ts
175
+ // Discover matching skills for a natural-language task (may pre-load multiple bodies)
176
+ shiori_recommend({ task: "browser scraping screenshot" })
177
+
178
+ // Load one selected skill body after a candidate matches
179
+ shiori_load_skill({ skill: "reddit-research" })
180
+ ```
181
+
182
+ `shiori_recommend` returns compact summaries and may pre-load matching `SKILL.md` bodies. The Pi UI shows a collapsed result like `✓ Shiori playwright-cli [trigger], gstack-browse [description]`.
183
+
184
+ `shiori_load_skill` loads one skill by name. The Pi UI shows a compact result like `✓ Loaded reddit-research (6.3KB)`.
185
+
186
+ See [Recommendation surface](recommendation-surface.md) for badge vocabulary and multi-skill examples.
187
+
170
188
  ## Related docs
171
189
 
172
190
  - [Recommendation surface](recommendation-surface.md) — reason badges, Always Visible policy, and metrics interplay
package/README.md CHANGED
@@ -19,7 +19,7 @@ For maintenance priorities and the phased plan, see [`ROADMAP.md`](ROADMAP.md).
19
19
 
20
20
  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.
21
21
 
22
- `0.6.9` is the current 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.
22
+ `0.6.11` is the current 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.
23
23
 
24
24
  ## Features
25
25
 
@@ -37,7 +37,7 @@ Pi Skill Shiori is a [Pi](https://pi.dev/) package for vaults or projects with m
37
37
  ### Global install from npm
38
38
 
39
39
  ```bash
40
- pi install npm:pi-skill-shiori@0.6.9
40
+ pi install npm:pi-skill-shiori@0.6.11
41
41
  ```
42
42
 
43
43
  Without a version pin:
@@ -49,7 +49,7 @@ pi install npm:pi-skill-shiori
49
49
  ### Project-local install from npm
50
50
 
51
51
  ```bash
52
- pi install -l npm:pi-skill-shiori@0.6.9
52
+ pi install -l npm:pi-skill-shiori@0.6.11
53
53
  ```
54
54
 
55
55
  Without a version pin:
@@ -61,7 +61,7 @@ pi install -l npm:pi-skill-shiori
61
61
  ### Install from GitHub
62
62
 
63
63
  ```bash
64
- pi install git:github.com/eiei114/pi-skill-shiori@v0.6.9
64
+ pi install git:github.com/eiei114/pi-skill-shiori@v0.6.11
65
65
  pi install -l git:github.com/eiei114/pi-skill-shiori
66
66
  ```
67
67
 
@@ -83,7 +83,7 @@ pi install -l /absolute/path/to/pi-skill-shiori
83
83
  1. Install the package (see [Install](#install)).
84
84
  2. Create `.pi/skill-shiori.yml` in your Pi project (see [Docs/usage.md](Docs/usage.md#configure)).
85
85
  3. Start Pi and run `/shiori:doctor` to confirm policy path, indexed roots, and suppression status.
86
- 4. Use `/shiori:bootstrap` to draft a policy from discovered skills, then `/shiori:reload` after edits.
86
+ 4. Use `/shiori:bootstrap` to write `.pi/skill-shiori.generated.yml`, review it, copy needed sections into `.pi/skill-shiori.yml`, then run `/shiori:reload`.
87
87
  5. Ask the agent for help; Shiori injects compact candidates or use `/shiori:recommend` for an interactive flow.
88
88
 
89
89
  Commands take no inline arguments. Details are collected after launch via Pi UI prompts.
@@ -98,13 +98,19 @@ Commands take no inline arguments. Details are collected after launch via Pi UI
98
98
  | `/shiori:recommend` | Interactive recommendation flow with optional pre-load. |
99
99
  | `/shiori:stats` | Show operational counters and recommendation quality summary. |
100
100
 
101
- Shiori registers one tool:
101
+ Shiori registers two tools:
102
102
 
103
103
  ```ts
104
+ // Discover matching skills for a natural-language task (may pre-load multiple bodies)
105
+ shiori_recommend({ task: "browser scraping screenshot" })
106
+
107
+ // Load one selected skill body after a candidate matches
104
108
  shiori_load_skill({ skill: "reddit-research" })
105
109
  ```
106
110
 
107
- The model receives the full selected `SKILL.md` content. The Pi UI shows a compact result like `✓ Loaded reddit-research (6.3KB)`.
111
+ `shiori_recommend` returns compact summaries and may pre-load matching `SKILL.md` bodies. The Pi UI shows a collapsed result like `✓ Shiori playwright-cli [trigger], gstack-browse [description]`.
112
+
113
+ `shiori_load_skill` loads one skill by name. The Pi UI shows a compact result like `✓ Loaded reddit-research (6.3KB)`.
108
114
 
109
115
  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.
110
116
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-skill-shiori",
3
- "version": "0.6.9",
3
+ "version": "0.6.11",
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",
@@ -54,9 +54,9 @@
54
54
  "typebox": "*"
55
55
  },
56
56
  "devDependencies": {
57
- "@earendil-works/pi-ai": "^0.84.2",
57
+ "@earendil-works/pi-ai": "^0.85.0",
58
58
  "@earendil-works/pi-coding-agent": "^0.84.1",
59
- "@earendil-works/pi-tui": "^0.84.2",
59
+ "@earendil-works/pi-tui": "^0.85.1",
60
60
  "@types/node": "^26.0.0",
61
61
  "tsx": "^4.22.4",
62
62
  "typebox": "latest",
package/src/indexer.ts CHANGED
@@ -1,8 +1,62 @@
1
- import { mkdir } from "node:fs/promises";
1
+ import { mkdir, readdir, stat, unlink } from "node:fs/promises";
2
2
  import { createRequire } from "node:module";
3
- import { join } from "node:path";
3
+ import { basename, join } from "node:path";
4
4
  import type { RetrievalBackend, ShioriPolicy, SkillRecord } from "./types.js";
5
5
 
6
+ const INDEX_FILE_PATTERN = /^index-\d+-\d+\.sqlite(?:-(?:wal|shm|journal))?$/;
7
+
8
+ /**
9
+ * Index databases are written with a per-process name, so every rebuild adds a
10
+ * file. Prune older ones after a grace window: long enough that a concurrent
11
+ * process keeps its database, short enough that a busy vault does not grow
12
+ * without bound.
13
+ */
14
+ export const DEFAULT_INDEX_GRACE_MS = 60 * 60 * 1000;
15
+
16
+ export interface PruneIndexOptions {
17
+ /** File name to keep, usually the database this process just opened. */
18
+ keep?: string;
19
+ /** Files newer than this window are left alone. */
20
+ graceMs?: number;
21
+ /** Clock override for tests. */
22
+ now?: number;
23
+ }
24
+
25
+ /**
26
+ * Best-effort cleanup of stale index databases. Locked files (a live process on
27
+ * Windows) and unreadable entries are skipped instead of failing the build.
28
+ */
29
+ export async function pruneStaleIndexFiles(cacheDir: string, options: PruneIndexOptions = {}): Promise<string[]> {
30
+ const graceMs = options.graceMs ?? DEFAULT_INDEX_GRACE_MS;
31
+ const now = options.now ?? Date.now();
32
+ const removed: string[] = [];
33
+
34
+ let entries: string[];
35
+ try {
36
+ entries = await readdir(cacheDir);
37
+ } catch {
38
+ return removed;
39
+ }
40
+
41
+ for (const entry of entries) {
42
+ if (!INDEX_FILE_PATTERN.test(entry)) continue;
43
+ if (options.keep && entry === options.keep) continue;
44
+
45
+ const target = join(cacheDir, entry);
46
+ try {
47
+ const info = await stat(target);
48
+ if (!info.isFile()) continue;
49
+ if (now - info.mtimeMs < graceMs) continue;
50
+ await unlink(target);
51
+ removed.push(entry);
52
+ } catch {
53
+ // Locked (a live process owns it) or already gone: leave it for next time.
54
+ }
55
+ }
56
+
57
+ return removed;
58
+ }
59
+
6
60
  export interface SkillIndex {
7
61
  skills: SkillRecord[];
8
62
  policy: ShioriPolicy;
@@ -24,6 +78,7 @@ export async function buildSkillIndex(cwd: string, skills: SkillRecord[], policy
24
78
  await mkdir(cacheDir, { recursive: true });
25
79
 
26
80
  const db = new sqlite.DatabaseSync(dbPath);
81
+ await pruneStaleIndexFiles(cacheDir, { keep: basename(dbPath) });
27
82
  db.exec(
28
83
  "CREATE VIRTUAL TABLE skill_fts USING fts5(name, description, triggers, path, tokenize='unicode61 remove_diacritics 2')",
29
84
  );
@@ -1,5 +1,5 @@
1
1
  import { buildRetrievalQueries } from "./query.js";
2
- import { retrieveCandidates } from "./retrieval.js";
2
+ import { compareCandidates, excludedSkillNames, retrieveCandidates } from "./retrieval.js";
3
3
  import type { ShioriPolicy, SkillCandidate, SkillRecord } from "./types.js";
4
4
 
5
5
  interface RetrievalIndex {
@@ -14,17 +14,21 @@ export function retrieveCandidatesExpanded(
14
14
  ): SkillCandidate[] {
15
15
  const byName = new Map<string, SkillCandidate>();
16
16
  const max = policy.candidateInjection.maxCandidates;
17
+ // Apply the original query's excludes across the merged set. A derived
18
+ // single-token variant can drop the context that made an exclude match
19
+ // ("do not use auth" becomes "auth"), which would otherwise let an excluded
20
+ // skill back in once variants are merged.
21
+ const excluded = excludedSkillNames(query, skills, policy);
17
22
 
18
23
  for (const variant of buildRetrievalQueries(query)) {
19
24
  for (const candidate of retrieveCandidates(variant, skills, policy, index)) {
25
+ if (excluded.has(candidate.skill.name)) continue;
20
26
  const previous = byName.get(candidate.skill.name);
21
27
  byName.set(candidate.skill.name, preferExpandedCandidate(previous, candidate));
22
28
  }
23
29
  }
24
30
 
25
- return [...byName.values()]
26
- .sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name))
27
- .slice(0, max);
31
+ return [...byName.values()].sort(compareCandidates).slice(0, max);
28
32
  }
29
33
 
30
34
  /** Keep trigger badges when expanded query variants also score the description higher. */
package/src/retrieval.ts CHANGED
@@ -34,10 +34,58 @@ export function retrieveCandidates(
34
34
  }
35
35
 
36
36
  return [...candidatesByName.values()]
37
- .sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name))
37
+ .sort(compareCandidates)
38
38
  .slice(0, policy.candidateInjection.maxCandidates);
39
39
  }
40
40
 
41
+ /**
42
+ * Order candidates so curated trigger matches rank ahead of heuristic
43
+ * description matches.
44
+ *
45
+ * Trigger matches carry a fixed score (0.95, see `evaluateSkill`), while
46
+ * description matches can reach a higher score from short query tokens that
47
+ * happen to appear as substrings — "is" in "history", "fix" in "prefix".
48
+ * Sorting by score alone therefore lets that noise push a genuine trigger match
49
+ * past a candidate cap, where it is dropped without a trace. A trigger is an
50
+ * explicit statement that the skill is relevant, so it outranks a heuristic
51
+ * substring match even when the heuristic scored higher.
52
+ *
53
+ * Every cap site must use this comparator: the per-variant cap inside
54
+ * `retrieveCandidates` runs before expanded retrieval merges variants, so a
55
+ * trigger match dropped there can never be restored downstream.
56
+ */
57
+ export function compareCandidates(a: SkillCandidate, b: SkillCandidate): number {
58
+ const triggerDelta = Number(b.reason === "trigger") - Number(a.reason === "trigger");
59
+ if (triggerDelta !== 0) return triggerDelta;
60
+ return b.score - a.score || a.skill.name.localeCompare(b.skill.name);
61
+ }
62
+
63
+ /**
64
+ * Skill names the original query excludes via policy exclude triggers.
65
+ *
66
+ * `retrieveCandidates` evaluates excludes against whichever query string it
67
+ * receives. Expanded retrieval passes derived variants, and a derived variant can
68
+ * drop the surrounding context that made an exclude match ("do not use auth"
69
+ * becomes "auth"), so the exclusion no longer applies and the skill leaks back
70
+ * in. Callers that merge variants should apply the original query's excludes
71
+ * across the merged set.
72
+ */
73
+ export function excludedSkillNames(
74
+ query: string,
75
+ skills: SkillRecord[],
76
+ policy: ShioriPolicy,
77
+ ): Set<string> {
78
+ const normalizedQuery = normalize(query);
79
+ const excluded = new Set<string>();
80
+ for (const skill of skills) {
81
+ const excludes = policy.skills[skill.name]?.triggers?.exclude ?? [];
82
+ if (excludes.some((trigger) => includesNormalized(normalizedQuery, trigger))) {
83
+ excluded.add(skill.name);
84
+ }
85
+ }
86
+ return excluded;
87
+ }
88
+
41
89
  function evaluateSkill(
42
90
  skill: SkillRecord,
43
91
  normalizedQuery: string,