pi-skill-shiori 0.3.1 → 0.4.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 +20 -0
- package/README.md +11 -8
- package/ROADMAP.md +132 -0
- package/package.json +2 -1
- package/src/always-visible.ts +54 -0
- package/src/index.ts +36 -16
- package/src/policy.ts +2 -1
- package/src/prompt.ts +289 -119
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.4.0] - 2026-06-28
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Always Visible Skill policy: Zero-Catalog Mode now keeps only `alwaysVisible` allowlisted skills in the Skill Catalog instead of hiding every entry.
|
|
12
|
+
- Diagnostics for missing or duplicate `alwaysVisible` policy entries in `/shiori:doctor` and one-time runtime warnings for missing skills.
|
|
13
|
+
- Tests for visible, missing, duplicate, and markdown catalog filtering cases.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `alwaysVisible` policy entries are normalized to a deduplicated allowlist at load time.
|
|
18
|
+
|
|
19
|
+
## [0.3.2] - 2026-06-24
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `ROADMAP.md` with maintenance-first phased goals (Month 1–3), template compliance checklist, and maintenance schedule.
|
|
24
|
+
- README links to `ROADMAP.md`.
|
|
25
|
+
- npm package `files` now includes `ROADMAP.md`.
|
|
26
|
+
|
|
7
27
|
## [0.3.1] - 2026-06-19
|
|
8
28
|
|
|
9
29
|
### Added
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Pi Skill Shiori is a [Pi](https://pi.dev/) package that keeps large Agent Skill
|
|
|
4
4
|
|
|
5
5
|
It is built for vaults or projects with many Agent Skills where the default catalog becomes noisy and expensive.
|
|
6
6
|
|
|
7
|
+
For maintenance priorities and the phased plan, see [`ROADMAP.md`](ROADMAP.md).
|
|
8
|
+
|
|
7
9
|
## What it does
|
|
8
10
|
|
|
9
11
|
- **Zero-Catalog Mode**: hides the normal skill catalog when Pi exposes one in the system prompt.
|
|
@@ -15,7 +17,7 @@ It is built for vaults or projects with many Agent Skills where the default cata
|
|
|
15
17
|
|
|
16
18
|
## Status
|
|
17
19
|
|
|
18
|
-
`0.
|
|
20
|
+
`0.4.0` is an early working release. It is useful for local Pi workflows, but the prompt-boundary suppression logic is intentionally conservative: if Shiori cannot safely recognize a catalog boundary, it leaves the prompt untouched and warns instead of deleting too much.
|
|
19
21
|
|
|
20
22
|
## Requirements
|
|
21
23
|
|
|
@@ -37,7 +39,7 @@ Pi core packages are peer dependencies and should be supplied by the Pi runtime:
|
|
|
37
39
|
After the package is published to npm:
|
|
38
40
|
|
|
39
41
|
```bash
|
|
40
|
-
pi install npm:pi-skill-shiori@0.
|
|
42
|
+
pi install npm:pi-skill-shiori@0.4.0
|
|
41
43
|
```
|
|
42
44
|
|
|
43
45
|
Without a version pin:
|
|
@@ -51,7 +53,7 @@ pi install npm:pi-skill-shiori
|
|
|
51
53
|
Use `-l` to write the package to the current project’s `.pi/settings.json`:
|
|
52
54
|
|
|
53
55
|
```bash
|
|
54
|
-
pi install -l npm:pi-skill-shiori@0.
|
|
56
|
+
pi install -l npm:pi-skill-shiori@0.4.0
|
|
55
57
|
```
|
|
56
58
|
|
|
57
59
|
Without a version pin:
|
|
@@ -65,7 +67,7 @@ Equivalent pinned manual `.pi/settings.json` entry:
|
|
|
65
67
|
```json
|
|
66
68
|
{
|
|
67
69
|
"packages": [
|
|
68
|
-
"npm:pi-skill-shiori@0.
|
|
70
|
+
"npm:pi-skill-shiori@0.4.0"
|
|
69
71
|
]
|
|
70
72
|
}
|
|
71
73
|
```
|
|
@@ -83,7 +85,7 @@ Equivalent unpinned manual `.pi/settings.json` entry:
|
|
|
83
85
|
### Global install from GitHub
|
|
84
86
|
|
|
85
87
|
```bash
|
|
86
|
-
pi install git:github.com/eiei114/pi-skill-shiori@v0.
|
|
88
|
+
pi install git:github.com/eiei114/pi-skill-shiori@v0.4.0
|
|
87
89
|
```
|
|
88
90
|
|
|
89
91
|
Without a tag pin:
|
|
@@ -95,7 +97,7 @@ pi install git:github.com/eiei114/pi-skill-shiori
|
|
|
95
97
|
### Project-local install from GitHub
|
|
96
98
|
|
|
97
99
|
```bash
|
|
98
|
-
pi install -l git:github.com/eiei114/pi-skill-shiori@v0.
|
|
100
|
+
pi install -l git:github.com/eiei114/pi-skill-shiori@v0.4.0
|
|
99
101
|
```
|
|
100
102
|
|
|
101
103
|
Without a tag pin:
|
|
@@ -166,7 +168,8 @@ Policy rules:
|
|
|
166
168
|
|
|
167
169
|
- `defaults.activation: explicit` is the safe default. Unlisted skills are not auto-candidates.
|
|
168
170
|
- `activation: triggerable` allows Shiori to recommend the skill for matching requests.
|
|
169
|
-
- `alwaysVisible` lists skills that should remain visible
|
|
171
|
+
- `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.
|
|
172
|
+
- Missing `alwaysVisible` entries are reported by `/shiori:doctor` and warned once per session when Zero-Catalog Mode runs.
|
|
170
173
|
- `candidateInjection.maxCandidates` limits how many suggestions enter the prompt.
|
|
171
174
|
- `candidateInjection.minScore` drops weak matches.
|
|
172
175
|
|
|
@@ -278,7 +281,7 @@ npm login
|
|
|
278
281
|
After publishing, verify the Pi install path:
|
|
279
282
|
|
|
280
283
|
```bash
|
|
281
|
-
pi install npm:pi-skill-shiori@0.
|
|
284
|
+
pi install npm:pi-skill-shiori@0.4.0
|
|
282
285
|
pi install npm:pi-skill-shiori
|
|
283
286
|
pi list
|
|
284
287
|
/shiori:doctor
|
package/ROADMAP.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
Pi Skill Shiori keeps large Agent Skill catalogs out of the model prompt and lets
|
|
4
|
+
the agent load only the skills that match the current task. This roadmap states
|
|
5
|
+
**what we prioritize next** and how the package is maintained.
|
|
6
|
+
|
|
7
|
+
## Direction
|
|
8
|
+
|
|
9
|
+
`0.3.x` is a working release. The near-term priority is **stabilization and
|
|
10
|
+
public quality over new feature surface**. Before widening functionality, we
|
|
11
|
+
harden what already ships:
|
|
12
|
+
|
|
13
|
+
- **Stabilize existing behavior** — make catalog suppression and retrieval reliable.
|
|
14
|
+
- **Speed and token efficiency** — startup cost, index rebuild, and candidate injection footprint.
|
|
15
|
+
- **Explicit design boundaries** — name the contracts (policy file, hook activation, suppression).
|
|
16
|
+
- **Template compliance** — close gaps against the shared Pi extension OSS policy.
|
|
17
|
+
- **Publish quality** — README, CHANGELOG, SECURITY, CI, `npm pack`, and release handoff.
|
|
18
|
+
|
|
19
|
+
Constraints:
|
|
20
|
+
|
|
21
|
+
- Existing commands, tools, and the main UX stay unless a compatibility fix is required.
|
|
22
|
+
- `shiori:*` commands stay argument-less; detail is collected after launch via Pi UI.
|
|
23
|
+
- npm publish stays Trusted Publishing / OIDC. Secrets and production actions remain human-owned.
|
|
24
|
+
|
|
25
|
+
## Skill features vs. template requirements
|
|
26
|
+
|
|
27
|
+
Two kinds of work are tracked separately so feature work is not confused with
|
|
28
|
+
packaging/hygiene work:
|
|
29
|
+
|
|
30
|
+
- **Skill features** — Zero-Catalog suppression, policy-based retrieval, candidate
|
|
31
|
+
injection, on-demand loading, hybrid retrieval. These change what the package does.
|
|
32
|
+
- **Template requirements** — README badges, CI gates, `npm pack` cleanliness, action
|
|
33
|
+
pinning, version-bump checks. These keep the package publishable and aligned with
|
|
34
|
+
`pi-extension-template`.
|
|
35
|
+
|
|
36
|
+
Template gaps are addressed first because they protect every later release.
|
|
37
|
+
|
|
38
|
+
## Maintenance schedule
|
|
39
|
+
|
|
40
|
+
- **Patch release** — as needed for fixes, doc/template compliance, and small behavior changes.
|
|
41
|
+
- **Minor release** — retrieval quality or candidate-flow improvements.
|
|
42
|
+
- **Release cadence** — ship per ready slice; no fixed calendar release. Every mergeable
|
|
43
|
+
change bumps `package.json` and `CHANGELOG.md` so npm can publish.
|
|
44
|
+
- **Dependency review** — monthly; bump `devDependencies`/Pi peers without behavior change.
|
|
45
|
+
- **Release flow** — `npm version <patch|minor>` → push → `auto-release.yml` tags and
|
|
46
|
+
releases → `publish.yml` publishes via Trusted Publishing (provenance verified).
|
|
47
|
+
|
|
48
|
+
`/shiori:stats` reports operational counters (token saved, candidate hits, loads,
|
|
49
|
+
zero-candidate turns, explicit invocations) that feed back into this schedule.
|
|
50
|
+
|
|
51
|
+
## Phased goals (Month 1–3)
|
|
52
|
+
|
|
53
|
+
Each item is a small, independently verifiable slice (roughly 30–90 minutes) with a
|
|
54
|
+
semver classification. Patches default to `patch`; `none` means no package publish.
|
|
55
|
+
|
|
56
|
+
### Month 1 — Public quality and template compliance
|
|
57
|
+
|
|
58
|
+
| # | Slice | Bump | Status |
|
|
59
|
+
|---|---|---|---|
|
|
60
|
+
| 1 | Add README status badges (CI, Publish, npm version, npm downloads, License, Pi package, Trusted Publishing) | patch | planned |
|
|
61
|
+
| 2 | Add `version:check` CI gate (block publishable-path changes without a semver increase) | none | planned |
|
|
62
|
+
| 3 | Pin GitHub Actions to commit SHAs in `ci.yml`, `auto-release.yml`, `publish.yml` | none | planned |
|
|
63
|
+
|
|
64
|
+
### Month 2 — Stabilization and design boundaries
|
|
65
|
+
|
|
66
|
+
| # | Slice | Bump | Status |
|
|
67
|
+
|---|---|---|---|
|
|
68
|
+
| 4 | Harden catalog boundary detection and add boundary tests (reduce `failed-pattern-not-found`) | patch | planned |
|
|
69
|
+
| 5 | Integration test for Dual Hook Activation (input-hook explicit pass-through + `before_agent_start` candidate injection) | none | planned |
|
|
70
|
+
|
|
71
|
+
### Month 3 — Speed and token efficiency
|
|
72
|
+
|
|
73
|
+
| # | Slice | Bump | Status |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| 6 | Add startup/operation timing instrumentation to `/shiori:stats` (Full Index Rebuild + retrieval baseline) | patch | planned |
|
|
76
|
+
| 7 | Startup index strategy (incremental / lazy / cached) — needs a design decision before implementation | none | blocked (design) |
|
|
77
|
+
|
|
78
|
+
## Integration testing for skill hooks
|
|
79
|
+
|
|
80
|
+
Dual Hook Activation is the core discovery path and must be covered end to end:
|
|
81
|
+
|
|
82
|
+
- **Input hook** — explicit invocation pass-through: `/skill:name` and explicit names load
|
|
83
|
+
regardless of activation policy, including under Zero-Catalog Mode.
|
|
84
|
+
- **`before_agent_start` hook** — automatic candidate injection for triggerable skills only,
|
|
85
|
+
bounded by `maxCandidates` and `minScore`, with explicit skills excluded.
|
|
86
|
+
|
|
87
|
+
The Month 2 integration test asserts both paths together (explicit load still works while
|
|
88
|
+
triggerable candidates are injected) so a regression in one hook surfaces immediately.
|
|
89
|
+
|
|
90
|
+
## Template compliance checklist
|
|
91
|
+
|
|
92
|
+
Current status against the shared Pi extension OSS policy. Gaps become Month 1 work.
|
|
93
|
+
|
|
94
|
+
- [x] `README.md`, `LICENSE`, `SECURITY.md`, `CHANGELOG.md` at repo root
|
|
95
|
+
- [x] CI runs `npm run typecheck`, `npm test`, and `npm pack --dry-run`
|
|
96
|
+
- [x] npm Trusted Publishing (`publish.yml` uses `id-token: write`, no `NPM_TOKEN`)
|
|
97
|
+
- [x] `auto-release.yml` → `publish.yml` release handoff
|
|
98
|
+
- [x] `package.json` `files` is explicit (no `node_modules`, tests, or local state)
|
|
99
|
+
- [x] No template `example-skill` / `example` / `example-theme` resources shipped
|
|
100
|
+
- [ ] **README status badges** (CI, Publish, npm version, npm downloads, License, Pi package, Trusted Publishing) — *Month 1 #1*
|
|
101
|
+
- [ ] **`version:check` CI gate** so publishable-path changes cannot merge without a bump — *Month 1 #2*
|
|
102
|
+
- [ ] **Pin GitHub Actions to commit SHAs** (currently floating `@v4`) — *Month 1 #3*
|
|
103
|
+
|
|
104
|
+
## Current backlog integration
|
|
105
|
+
|
|
106
|
+
Feature-track items already shipped (kept here so the roadmap reflects reality, not a
|
|
107
|
+
fresh to-do list):
|
|
108
|
+
|
|
109
|
+
- Recommendation-first `/shiori:recommend` flow with query expansion and planning intent — shipped (`0.2.0`)
|
|
110
|
+
- CI + auto-release + publish handoff — shipped (`0.1.1`+)
|
|
111
|
+
- `shiori:*` commands use Pi UI flows (argument-less) — shipped (`0.3.0`)
|
|
112
|
+
- `SECURITY.md` and `CHANGELOG.md` — shipped (`0.3.1`)
|
|
113
|
+
|
|
114
|
+
Remaining feature-track item, deferred behind the maintenance-first direction:
|
|
115
|
+
|
|
116
|
+
- README/docs alignment with the minimal public-docs policy — open; the Month 1 badge
|
|
117
|
+
and CI slices advance this track without blocking on the broader docs-policy decision.
|
|
118
|
+
|
|
119
|
+
## Later (feature track)
|
|
120
|
+
|
|
121
|
+
- Retrieval quality improvements (embedding candidate added beside SQLite FTS).
|
|
122
|
+
- Cross-vault skill index.
|
|
123
|
+
- ResourceLoader-side catalog replacement (stable-stage Two-stage Catalog Suppression).
|
|
124
|
+
|
|
125
|
+
These are intentionally **after** the Month 1–3 maintenance work.
|
|
126
|
+
|
|
127
|
+
## Non-goals
|
|
128
|
+
|
|
129
|
+
- Rewriting existing commands, tools, or the candidate/recommend UX.
|
|
130
|
+
- Auto-approving npm publish, permission changes, or production actions.
|
|
131
|
+
- Removing the conservative catalog-suppression safety boundary (`not-needed` /
|
|
132
|
+
`failed-pattern-not-found` stay safe-by-default).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-skill-shiori",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"README.md",
|
|
26
|
+
"ROADMAP.md",
|
|
26
27
|
"LICENSE",
|
|
27
28
|
"SECURITY.md",
|
|
28
29
|
"CHANGELOG.md",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { SkillRecord } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export interface AlwaysVisibleDiagnostics {
|
|
4
|
+
missing: string[];
|
|
5
|
+
duplicates: string[];
|
|
6
|
+
resolved: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function normalizeAlwaysVisible(alwaysVisible: string[] | undefined): {
|
|
10
|
+
allowlist: string[];
|
|
11
|
+
duplicates: string[];
|
|
12
|
+
} {
|
|
13
|
+
const duplicates: string[] = [];
|
|
14
|
+
const seen = new Set<string>();
|
|
15
|
+
const allowlist: string[] = [];
|
|
16
|
+
|
|
17
|
+
for (const entry of alwaysVisible ?? []) {
|
|
18
|
+
const name = entry.trim();
|
|
19
|
+
if (!name) continue;
|
|
20
|
+
if (seen.has(name)) {
|
|
21
|
+
if (!duplicates.includes(name)) duplicates.push(name);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
seen.add(name);
|
|
25
|
+
allowlist.push(name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { allowlist, duplicates };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function evaluateAlwaysVisible(
|
|
32
|
+
alwaysVisible: string[] | undefined,
|
|
33
|
+
skills: SkillRecord[],
|
|
34
|
+
): AlwaysVisibleDiagnostics {
|
|
35
|
+
const { allowlist, duplicates } = normalizeAlwaysVisible(alwaysVisible);
|
|
36
|
+
const inventory = new Set(skills.map((skill) => skill.name));
|
|
37
|
+
const missing = allowlist.filter((name) => !inventory.has(name));
|
|
38
|
+
const resolved = allowlist.filter((name) => inventory.has(name));
|
|
39
|
+
return { missing, duplicates, resolved };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatAlwaysVisibleDiagnostics(diagnostics: AlwaysVisibleDiagnostics): string[] {
|
|
43
|
+
const lines: string[] = [];
|
|
44
|
+
if (diagnostics.resolved.length > 0) {
|
|
45
|
+
lines.push(`alwaysVisible resolved: ${diagnostics.resolved.join(", ")}`);
|
|
46
|
+
}
|
|
47
|
+
if (diagnostics.missing.length > 0) {
|
|
48
|
+
lines.push(`alwaysVisible missing: ${diagnostics.missing.join(", ")}`);
|
|
49
|
+
}
|
|
50
|
+
if (diagnostics.duplicates.length > 0) {
|
|
51
|
+
lines.push(`alwaysVisible duplicates: ${diagnostics.duplicates.join(", ")}`);
|
|
52
|
+
}
|
|
53
|
+
return lines;
|
|
54
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { evaluateAlwaysVisible, formatAlwaysVisibleDiagnostics } from "./always-visible.js";
|
|
5
6
|
import { discoverSkills, findDuplicates } from "./discovery.js";
|
|
6
7
|
import { buildSkillIndex, type SkillIndex } from "./indexer.js";
|
|
7
8
|
import { loadRecommendedSkills } from "./load-skills.js";
|
|
@@ -35,6 +36,7 @@ const CANCEL_CHOICE = "Cancel";
|
|
|
35
36
|
|
|
36
37
|
export default function piSkillShiori(pi: ExtensionAPI) {
|
|
37
38
|
let index: SkillIndex | undefined;
|
|
39
|
+
let warnedAlwaysVisibleMissing = new Set<string>();
|
|
38
40
|
const stats = createStats();
|
|
39
41
|
|
|
40
42
|
async function reload(cwd: string): Promise<SkillIndex> {
|
|
@@ -83,7 +85,20 @@ export default function piSkillShiori(pi: ExtensionAPI) {
|
|
|
83
85
|
const current = await ensureIndex(ctx.cwd);
|
|
84
86
|
if (!current.policy.zeroCatalog.enabled) return;
|
|
85
87
|
|
|
86
|
-
const
|
|
88
|
+
const alwaysVisible = evaluateAlwaysVisible(current.policy.alwaysVisible, current.skills);
|
|
89
|
+
for (const name of alwaysVisible.missing) {
|
|
90
|
+
if (warnedAlwaysVisibleMissing.has(name)) continue;
|
|
91
|
+
warnedAlwaysVisibleMissing.add(name);
|
|
92
|
+
ctx.ui.notify(
|
|
93
|
+
`Pi Skill Shiori: always-visible skill not found in inventory: ${name}`,
|
|
94
|
+
"warning",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const suppression = suppressSkillCatalog(event.systemPrompt, {
|
|
99
|
+
alwaysVisible: current.policy.alwaysVisible,
|
|
100
|
+
skills: current.skills,
|
|
101
|
+
});
|
|
87
102
|
stats.suppressionStatus = suppression.status;
|
|
88
103
|
if (suppression.status === "failed-pattern-not-found") {
|
|
89
104
|
ctx.ui.notify("Pi Skill Shiori: Skill Catalog suppression pattern not found; leaving system prompt unchanged.", "warning");
|
|
@@ -186,21 +201,26 @@ export default function piSkillShiori(pi: ExtensionAPI) {
|
|
|
186
201
|
handler: async (_args, ctx) => {
|
|
187
202
|
const current = await ensureIndex(ctx.cwd);
|
|
188
203
|
const duplicates = findDuplicates(current.skills);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
const alwaysVisible = evaluateAlwaysVisible(current.policy.alwaysVisible, current.skills);
|
|
205
|
+
const doctorLines = [
|
|
206
|
+
"Pi Skill Shiori doctor",
|
|
207
|
+
`policy: ${getPolicyPath(ctx.cwd)}`,
|
|
208
|
+
`zeroCatalog: ${current.policy.zeroCatalog.enabled ? "enabled" : "disabled"}`,
|
|
209
|
+
`skills: ${current.skills.length}`,
|
|
210
|
+
`duplicates: ${duplicates.size}`,
|
|
211
|
+
`alwaysVisible: ${current.policy.alwaysVisible.join(", ") || "none"}`,
|
|
212
|
+
...formatAlwaysVisibleDiagnostics(alwaysVisible),
|
|
213
|
+
`retrievalBackend: ${current.retrievalBackend}`,
|
|
214
|
+
`suppression: ${stats.suppressionStatus}`,
|
|
215
|
+
`code: ${SHIORI_CODE_MARKER}`,
|
|
216
|
+
`builtAt: ${current.builtAt}`,
|
|
217
|
+
];
|
|
218
|
+
const hasWarning =
|
|
219
|
+
duplicates.size > 0 ||
|
|
220
|
+
stats.suppressionStatus === "failed-pattern-not-found" ||
|
|
221
|
+
alwaysVisible.missing.length > 0 ||
|
|
222
|
+
alwaysVisible.duplicates.length > 0;
|
|
223
|
+
ctx.ui.notify(doctorLines.join("\n"), hasWarning ? "warning" : "info");
|
|
204
224
|
},
|
|
205
225
|
});
|
|
206
226
|
|
package/src/policy.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { parse } from "yaml";
|
|
4
|
+
import { normalizeAlwaysVisible } from "./always-visible.js";
|
|
4
5
|
import type { ShioriPolicy } from "./types.js";
|
|
5
6
|
|
|
6
7
|
export const DEFAULT_POLICY: ShioriPolicy = {
|
|
@@ -40,7 +41,7 @@ function normalizePolicy(policy: Partial<ShioriPolicy> | undefined): ShioriPolic
|
|
|
40
41
|
maxCandidates: policy?.candidateInjection?.maxCandidates ?? DEFAULT_POLICY.candidateInjection.maxCandidates,
|
|
41
42
|
minScore: policy?.candidateInjection?.minScore ?? DEFAULT_POLICY.candidateInjection.minScore,
|
|
42
43
|
},
|
|
43
|
-
alwaysVisible: policy?.alwaysVisible ?? DEFAULT_POLICY.alwaysVisible,
|
|
44
|
+
alwaysVisible: normalizeAlwaysVisible(policy?.alwaysVisible ?? DEFAULT_POLICY.alwaysVisible).allowlist,
|
|
44
45
|
skills: policy?.skills ?? DEFAULT_POLICY.skills,
|
|
45
46
|
};
|
|
46
47
|
}
|
package/src/prompt.ts
CHANGED
|
@@ -1,119 +1,289 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
lines
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
systemPrompt
|
|
118
|
-
|
|
119
|
-
|
|
1
|
+
import { normalizeAlwaysVisible } from "./always-visible.js";
|
|
2
|
+
import type { SkillCandidate, SkillRecord, SuppressionStatus } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export interface CatalogSuppressionResult {
|
|
5
|
+
systemPrompt: string;
|
|
6
|
+
status: SuppressionStatus;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SuppressCatalogOptions {
|
|
10
|
+
alwaysVisible: string[];
|
|
11
|
+
skills: SkillRecord[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const COMPACT_DESCRIPTION_LIMIT = 120;
|
|
15
|
+
|
|
16
|
+
const CATALOG_BOUNDARY_PATTERNS: Array<{ pattern: RegExp; format: "xml" | "markdown" }> = [
|
|
17
|
+
{
|
|
18
|
+
pattern:
|
|
19
|
+
/\r?\n\r?\nThe following skills provide specialized instructions for specific tasks\.\r?\n[\s\S]*?\r?\n<available_skills>[\s\S]*?\r?\n<\/available_skills>/,
|
|
20
|
+
format: "xml",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
pattern: /(^|\r?\n)<available_skills>[\s\S]*?<\/available_skills>/i,
|
|
24
|
+
format: "xml",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
pattern: /(^|\r?\n)#{2,4}\s*Available skills\b[\s\S]*?(?=\r?\n#{2,4}\s*How to use skills\b)/i,
|
|
28
|
+
format: "markdown",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
pattern: /(^|\r?\n)#{2,4}\s*Available skills\b[\s\S]*?(?=\r?\n#{2,4}\s*Fallback\b)/i,
|
|
32
|
+
format: "markdown",
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export function formatCandidateInjection(candidates: SkillCandidate[]): string {
|
|
37
|
+
if (candidates.length === 0) return "";
|
|
38
|
+
const lines = ["Relevant skills:"];
|
|
39
|
+
for (const candidate of candidates) {
|
|
40
|
+
const desc = compactDescription(candidate.skill.description, COMPACT_DESCRIPTION_LIMIT);
|
|
41
|
+
lines.push(
|
|
42
|
+
`- ${candidate.skill.name}: ${desc} Reason: ${candidate.why}. Load: shiori_load_skill({ skill: "${candidate.skill.name}" })`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return lines.join("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function formatRecommendKickoffMessage(
|
|
49
|
+
query: string,
|
|
50
|
+
loaded: Array<{ candidate: SkillCandidate; content: string }>,
|
|
51
|
+
): string {
|
|
52
|
+
const lines = [query.trim(), ""];
|
|
53
|
+
|
|
54
|
+
if (loaded.length === 0) {
|
|
55
|
+
lines.push(
|
|
56
|
+
"---",
|
|
57
|
+
"Pi Skill Shiori: no matching skills for this task.",
|
|
58
|
+
"Continue without a dedicated skill, or refine the task description and run /shiori:recommend again.",
|
|
59
|
+
);
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const names = loaded.map(({ candidate }) => candidate.skill.name).join(", ");
|
|
64
|
+
lines.push(
|
|
65
|
+
"---",
|
|
66
|
+
`Pi Skill Shiori: pre-loaded ${loaded.length} skill(s): ${names}`,
|
|
67
|
+
"Follow the skill instructions below and continue with the task above.",
|
|
68
|
+
"",
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
for (const { candidate, content } of loaded) {
|
|
72
|
+
lines.push(
|
|
73
|
+
`### Skill: ${candidate.skill.name}`,
|
|
74
|
+
`Score: ${candidate.score.toFixed(2)} | Why: ${candidate.why}`,
|
|
75
|
+
`Path: ${candidate.skill.path}`,
|
|
76
|
+
"",
|
|
77
|
+
content.trim(),
|
|
78
|
+
"",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return lines.join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function formatLoadedSkillsSummary(loaded: Array<{ candidate: SkillCandidate; content: string }>): string {
|
|
86
|
+
if (loaded.length === 0) return "";
|
|
87
|
+
const lines = [`Pre-loaded ${loaded.length} skill(s):`];
|
|
88
|
+
for (const { candidate } of loaded) {
|
|
89
|
+
const desc = compactDescription(candidate.skill.description, COMPACT_DESCRIPTION_LIMIT);
|
|
90
|
+
lines.push(`- ${candidate.skill.name} (${candidate.score.toFixed(2)}): ${desc}`);
|
|
91
|
+
}
|
|
92
|
+
return lines.join("\n");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function formatCandidateDetail(candidate: SkillCandidate): string {
|
|
96
|
+
return [
|
|
97
|
+
candidate.skill.name,
|
|
98
|
+
`Description: ${candidate.skill.description}`,
|
|
99
|
+
`Score: ${candidate.score.toFixed(2)}`,
|
|
100
|
+
`Why: ${candidate.why}`,
|
|
101
|
+
`Path: ${candidate.skill.path}`,
|
|
102
|
+
`Load: shiori_load_skill({ skill: "${candidate.skill.name}" })`,
|
|
103
|
+
].join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function compactDescription(description: string, maxChars = COMPACT_DESCRIPTION_LIMIT): string {
|
|
107
|
+
const singleLine = description.replace(/\s+/g, " ").trim();
|
|
108
|
+
if (singleLine.length <= maxChars) return singleLine;
|
|
109
|
+
return `${singleLine.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function suppressSkillCatalog(
|
|
113
|
+
systemPrompt: string,
|
|
114
|
+
options?: SuppressCatalogOptions,
|
|
115
|
+
): CatalogSuppressionResult {
|
|
116
|
+
for (const { pattern, format } of CATALOG_BOUNDARY_PATTERNS) {
|
|
117
|
+
const match = systemPrompt.match(pattern);
|
|
118
|
+
if (!match) continue;
|
|
119
|
+
const replacement = buildFilteredCatalogReplacement(match[0], format, options);
|
|
120
|
+
return {
|
|
121
|
+
systemPrompt: systemPrompt.replace(pattern, replacement),
|
|
122
|
+
status: "suppressed",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!hasSkillCatalogSignal(systemPrompt)) {
|
|
127
|
+
return { systemPrompt, status: "not-needed" };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { systemPrompt, status: "failed-pattern-not-found" };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function buildFilteredCatalogReplacement(
|
|
134
|
+
catalogSection: string,
|
|
135
|
+
format: "xml" | "markdown",
|
|
136
|
+
options?: SuppressCatalogOptions,
|
|
137
|
+
): string {
|
|
138
|
+
if (!options) {
|
|
139
|
+
return [
|
|
140
|
+
"### Available skills",
|
|
141
|
+
"Normal Skill Catalog hidden by Pi Skill Shiori.",
|
|
142
|
+
"Use injected candidates or shiori_load_skill when relevant.",
|
|
143
|
+
"Explicit /skill:name invocation remains allowed.",
|
|
144
|
+
].join("\n");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const { allowlist } = normalizeAlwaysVisible(options.alwaysVisible);
|
|
148
|
+
const allowSet = new Set(allowlist);
|
|
149
|
+
const skillsByName = new Map(options.skills.map((skill) => [skill.name, skill]));
|
|
150
|
+
|
|
151
|
+
if (format === "xml") {
|
|
152
|
+
return buildFilteredXmlCatalog(catalogSection, allowSet, allowlist, skillsByName);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return buildFilteredMarkdownCatalog(catalogSection, allowSet, allowlist, skillsByName);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function buildFilteredXmlCatalog(
|
|
159
|
+
catalogSection: string,
|
|
160
|
+
allowSet: Set<string>,
|
|
161
|
+
allowlist: string[],
|
|
162
|
+
skillsByName: Map<string, SkillRecord>,
|
|
163
|
+
): string {
|
|
164
|
+
const prefix = extractXmlCatalogPrefix(catalogSection);
|
|
165
|
+
const keptBlocks: string[] = [];
|
|
166
|
+
const keptNames = new Set<string>();
|
|
167
|
+
|
|
168
|
+
for (const block of extractXmlSkillBlocks(catalogSection)) {
|
|
169
|
+
const name = parseXmlSkillName(block);
|
|
170
|
+
if (!name || !allowSet.has(name)) continue;
|
|
171
|
+
keptBlocks.push(block);
|
|
172
|
+
keptNames.add(name);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const name of allowlist) {
|
|
176
|
+
if (keptNames.has(name)) continue;
|
|
177
|
+
const record = skillsByName.get(name);
|
|
178
|
+
if (!record) continue;
|
|
179
|
+
keptBlocks.push(formatXmlSkillBlock(record));
|
|
180
|
+
keptNames.add(name);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (keptBlocks.length === 0) {
|
|
184
|
+
return buildHiddenCatalogStub();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const lines = [...prefix, "<available_skills>"];
|
|
188
|
+
for (const block of keptBlocks) {
|
|
189
|
+
lines.push(block);
|
|
190
|
+
}
|
|
191
|
+
lines.push("</available_skills>");
|
|
192
|
+
return lines.join("\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function buildFilteredMarkdownCatalog(
|
|
196
|
+
catalogSection: string,
|
|
197
|
+
allowSet: Set<string>,
|
|
198
|
+
allowlist: string[],
|
|
199
|
+
skillsByName: Map<string, SkillRecord>,
|
|
200
|
+
): string {
|
|
201
|
+
const headerMatch = catalogSection.match(/(^|\r?\n)(#{2,4}\s*Available skills\b[^\n]*)/i);
|
|
202
|
+
const header = headerMatch?.[2] ?? "### Available skills";
|
|
203
|
+
const keptLines: string[] = [];
|
|
204
|
+
const keptNames = new Set<string>();
|
|
205
|
+
|
|
206
|
+
for (const line of catalogSection.split(/\r?\n/)) {
|
|
207
|
+
const match = line.match(/^\s*[-*]\s+\*\*([^*]+)\*\*:\s*(.*)$/);
|
|
208
|
+
if (!match) continue;
|
|
209
|
+
const name = match[1].trim();
|
|
210
|
+
if (!allowSet.has(name)) continue;
|
|
211
|
+
keptLines.push(`- **${name}**: ${match[2].trim()}`);
|
|
212
|
+
keptNames.add(name);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const name of allowlist) {
|
|
216
|
+
if (keptNames.has(name)) continue;
|
|
217
|
+
const record = skillsByName.get(name);
|
|
218
|
+
if (!record) continue;
|
|
219
|
+
keptLines.push(`- **${record.name}**: ${compactDescription(record.description, 240)}`);
|
|
220
|
+
keptNames.add(name);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (keptLines.length === 0) {
|
|
224
|
+
return buildHiddenCatalogStub();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return [`${header}`, "Pi Skill Shiori: only always-visible skills remain in the Skill Catalog.", ...keptLines].join(
|
|
228
|
+
"\n",
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildHiddenCatalogStub(): string {
|
|
233
|
+
return [
|
|
234
|
+
"### Available skills",
|
|
235
|
+
"Normal Skill Catalog hidden by Pi Skill Shiori.",
|
|
236
|
+
"Use injected candidates or shiori_load_skill when relevant.",
|
|
237
|
+
"Explicit /skill:name invocation remains allowed.",
|
|
238
|
+
].join("\n");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function extractXmlCatalogPrefix(catalogSection: string): string[] {
|
|
242
|
+
const marker = catalogSection.search(/<available_skills>/i);
|
|
243
|
+
if (marker <= 0) return [];
|
|
244
|
+
return catalogSection.slice(0, marker).replace(/\r?\n$/, "").split(/\r?\n/).filter(Boolean);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function extractXmlSkillBlocks(catalogSection: string): string[] {
|
|
248
|
+
return [...catalogSection.matchAll(/<skill>[\s\S]*?<\/skill>/gi)].map((match) => match[0]);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function parseXmlSkillName(block: string): string | undefined {
|
|
252
|
+
const match = block.match(/<name>([\s\S]*?)<\/name>/i);
|
|
253
|
+
if (!match) return undefined;
|
|
254
|
+
return unescapeXml(match[1].trim());
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function formatXmlSkillBlock(skill: SkillRecord): string {
|
|
258
|
+
return [
|
|
259
|
+
" <skill>",
|
|
260
|
+
` <name>${escapeXml(skill.name)}</name>`,
|
|
261
|
+
` <description>${escapeXml(skill.description)}</description>`,
|
|
262
|
+
` <location>${escapeXml(skill.path)}</location>`,
|
|
263
|
+
" </skill>",
|
|
264
|
+
].join("\n");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function escapeXml(value: string): string {
|
|
268
|
+
return value
|
|
269
|
+
.replace(/&/g, "&")
|
|
270
|
+
.replace(/</g, "<")
|
|
271
|
+
.replace(/>/g, ">")
|
|
272
|
+
.replace(/"/g, """)
|
|
273
|
+
.replace(/'/g, "'");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function unescapeXml(value: string): string {
|
|
277
|
+
return value
|
|
278
|
+
.replace(/'/g, "'")
|
|
279
|
+
.replace(/"/g, '"')
|
|
280
|
+
.replace(/>/g, ">")
|
|
281
|
+
.replace(/</g, "<")
|
|
282
|
+
.replace(/&/g, "&");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function hasSkillCatalogSignal(systemPrompt: string): boolean {
|
|
286
|
+
return /<\/?available_skills>|The following skills provide specialized instructions|#{2,4}\s*Available skills\b/i.test(
|
|
287
|
+
systemPrompt,
|
|
288
|
+
);
|
|
289
|
+
}
|