pi-skill-shiori 0.6.2 → 0.6.6

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
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - Add Buy Me a Coffee sponsor button to README and native GitHub funding link via `.github/FUNDING.yml`.
6
+
7
+ ## [0.6.6] - 2026-07-07
8
+
9
+ ### Fixed
10
+
11
+ - Stopped duplicate npm publish attempts caused by `publish.yml` running once on `main` push and again from the tag-dispatch path in auto-release.
12
+ - Kept the auto-release → tag-specific `workflow_dispatch` publish path while removing the redundant `main`/release-triggered publish path.
13
+
14
+ ## [0.6.5] - 2026-07-07
15
+
16
+ ### Fixed
17
+
18
+ - Aligned npm publish and auto-release GitHub Actions with the standard managed OSS workflow so version bumps on `main` create a tag/release and dispatch the publish workflow reliably.
19
+ - Hardened publish-version detection so already-published versions skip cleanly while unexpected npm lookup failures still fail the workflow.
20
+
21
+ ### Changed
22
+
23
+ - Updated release automation to the shared workflow pattern used by other managed OSS repos.
24
+
25
+ ## [0.6.4] - 2026-07-07
26
+
27
+ ### Fixed
28
+
29
+ - Rebuild the Shiori inventory when the effective policy source or policy contents change mid-session, so `/shiori:doctor` and runtime behavior stay aligned after adding or updating a global policy file.
30
+ - Add a regression test covering a global policy file appearing after session start.
31
+
3
32
  All notable changes to this project will be documented in this file.
4
33
 
5
34
  This project follows semantic versioning.
package/Docs/usage.md CHANGED
@@ -50,6 +50,14 @@ Cost/latency trade-offs:
50
50
 
51
51
  Create `.pi/skill-shiori.yml` in the project where Pi runs.
52
52
 
53
+ Shiori also supports a user-global policy at `~/.pi/agent/skill-shiori.yml`. Policy resolution order is:
54
+
55
+ 1. Project policy: `<cwd>/.pi/skill-shiori.yml`
56
+ 2. User-global policy: `~/.pi/agent/skill-shiori.yml`
57
+ 3. Built-in defaults
58
+
59
+ Use the user-global policy for personal defaults that should apply across projects, such as enabling Zero-Catalog Mode everywhere. Use a project policy when a workspace needs different trigger rules, inventory roots, or catalog behavior; project policy always wins over user-global policy.
60
+
53
61
  Minimal config:
54
62
 
55
63
  ```yaml
package/README.md CHANGED
@@ -1,173 +1,174 @@
1
- # Pi Skill Shiori
2
-
3
- [![CI](https://github.com/eiei114/pi-skill-shiori/actions/workflows/ci.yml/badge.svg)](https://github.com/eiei114/pi-skill-shiori/actions/workflows/ci.yml)
4
- [![Publish](https://github.com/eiei114/pi-skill-shiori/actions/workflows/publish.yml/badge.svg)](https://github.com/eiei114/pi-skill-shiori/actions/workflows/publish.yml)
5
- [![npm version](https://img.shields.io/npm/v/pi-skill-shiori)](https://www.npmjs.com/package/pi-skill-shiori)
6
- [![npm downloads](https://img.shields.io/npm/dw/pi-skill-shiori)](https://www.npmjs.com/package/pi-skill-shiori)
7
- [![License: MIT](https://img.shields.io/github/license/eiei114/pi-skill-shiori)](https://github.com/eiei114/pi-skill-shiori/blob/main/LICENSE)
8
- ![Pi Package](https://img.shields.io/badge/Pi-Package-blue)
9
- [![Trusted Publishing](https://img.shields.io/badge/npm-provenance-yellow)](https://docs.npmjs.com/generating-provenance-statements)
10
-
11
- > Pi extension that keeps large Agent Skill catalogs out of the model prompt and loads only the skills that match the current task.
12
-
13
- For maintenance priorities and the phased plan, see [`ROADMAP.md`](ROADMAP.md).
14
-
15
- ## What this is
16
-
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
-
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
-
21
- ## Features
22
-
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.
31
-
32
- ## Install
33
-
34
- ### Global install from npm
35
-
36
- ```bash
37
- pi install npm:pi-skill-shiori@0.6.1
38
- ```
39
-
40
- Without a version pin:
41
-
42
- ```bash
43
- pi install npm:pi-skill-shiori
44
- ```
45
-
46
- ### Project-local install from npm
47
-
48
- ```bash
49
- pi install -l npm:pi-skill-shiori@0.6.1
50
- ```
51
-
52
- Without a version pin:
53
-
54
- ```bash
55
- pi install -l npm:pi-skill-shiori
56
- ```
57
-
58
- ### Install from GitHub
59
-
60
- ```bash
61
- pi install git:github.com/eiei114/pi-skill-shiori@v0.6.1
62
- pi install -l git:github.com/eiei114/pi-skill-shiori
63
- ```
64
-
65
- ### Try without installing
66
-
67
- ```bash
68
- pi -e npm:pi-skill-shiori
69
- pi -e git:github.com/eiei114/pi-skill-shiori
70
- ```
71
-
72
- ### Local development install
73
-
74
- ```bash
75
- pi install -l /absolute/path/to/pi-skill-shiori
76
- ```
77
-
78
- ## Quick start
79
-
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.
85
-
86
- Commands take no inline arguments. Details are collected after launch via Pi UI prompts.
87
-
88
- ## Usage summary
89
-
90
- | Command | Purpose |
91
- |---|---|
92
- | `/shiori:doctor` | Show policy path, indexed roots, inventory count, retrieval backend, suppression status, and code marker. |
93
- | `/shiori:bootstrap` | Generate a review draft policy from discovered skill descriptions. |
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. |
97
-
98
- Shiori registers one tool:
99
-
100
- ```ts
101
- shiori_load_skill({ skill: "reddit-research" })
102
- ```
103
-
104
- The model receives the full selected `SKILL.md` content. The Pi UI shows a compact result like `✓ Loaded reddit-research (6.3KB)`.
105
-
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.
107
-
108
- For inventory roots, policy YAML, suppression statuses, and feedback metrics, see [`Docs/usage.md`](Docs/usage.md).
109
-
110
- ## Package contents
111
-
112
- | Path | Purpose |
113
- |---|---|
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 |
120
-
121
- ## Development
122
-
123
- ```bash
124
- git clone https://github.com/eiei114/pi-skill-shiori.git
125
- cd pi-skill-shiori
126
- npm install
127
- npm run typecheck
128
- npm test
129
- ```
130
-
131
- Run in a Pi project without installing globally:
132
-
133
- ```bash
134
- pi -e /absolute/path/to/pi-skill-shiori
135
- ```
136
-
137
- ## Release
138
-
139
- Releases are automated via Trusted Publishing:
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.
145
-
146
- Maintainer checks before merge:
147
-
148
- ```bash
149
- npm run typecheck
150
- npm test
151
- npm run release:npm:dry
152
- ```
153
-
154
- For version history, see [`CHANGELOG.md`](CHANGELOG.md).
155
-
156
- ## Security
157
-
158
- Pi packages execute local code with the same permissions as Pi. Review third-party packages before installing them.
159
-
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.
161
-
162
- For vulnerability reporting, see [`SECURITY.md`](SECURITY.md).
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
-
171
- ## License
172
-
173
- MIT
1
+ # Pi Skill Shiori
2
+
3
+ [![CI](https://github.com/eiei114/pi-skill-shiori/actions/workflows/ci.yml/badge.svg)](https://github.com/eiei114/pi-skill-shiori/actions/workflows/ci.yml)
4
+ [![Publish](https://github.com/eiei114/pi-skill-shiori/actions/workflows/publish.yml/badge.svg)](https://github.com/eiei114/pi-skill-shiori/actions/workflows/publish.yml)
5
+ [![npm version](https://img.shields.io/npm/v/pi-skill-shiori)](https://www.npmjs.com/package/pi-skill-shiori)
6
+ [![npm downloads](https://img.shields.io/npm/dw/pi-skill-shiori)](https://www.npmjs.com/package/pi-skill-shiori)
7
+ [![License: MIT](https://img.shields.io/github/license/eiei114/pi-skill-shiori)](https://github.com/eiei114/pi-skill-shiori/blob/main/LICENSE)
8
+ ![Pi Package](https://img.shields.io/badge/Pi-Package-blue)
9
+ [![Trusted Publishing](https://img.shields.io/badge/npm-provenance-yellow)](https://docs.npmjs.com/generating-provenance-statements)
10
+ <a href="https://buymeacoffee.com/ekawano114m"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" width="217" height="60"></a>
11
+
12
+ > Pi extension that keeps large Agent Skill catalogs out of the model prompt and loads only the skills that match the current task.
13
+
14
+ For maintenance priorities and the phased plan, see [`ROADMAP.md`](ROADMAP.md).
15
+
16
+ ## What this is
17
+
18
+ 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.
19
+
20
+ `0.6.6` 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.
21
+
22
+ ## Features
23
+
24
+ - **Zero-Catalog Mode** — hides the normal skill catalog when Pi exposes one in the system prompt.
25
+ - **Policy-based retrieval** — treats skills as explicit by default and only candidates skills marked as triggerable.
26
+ - **Compact candidate injection** — injects short skill suggestions instead of full `SKILL.md` files.
27
+ - **On-demand loading** — exposes `shiori_load_skill` so the model can load one selected skill body when needed.
28
+ - **Vault-wide skill inventory** — indexes skills from project and user-global roots, with optional extra roots in policy.
29
+ - **SQLite FTS retrieval** — indexes skill names, descriptions, and policy triggers with `node:sqlite` + FTS5, with token-match fallback.
30
+ - **Recommendation reason badges** — compact `[trigger]`, `[description]`, and `[low match]` labels on surfaced skills.
31
+ - **Session-local feedback metrics** — `/shiori:stats` tracks offers, loads, and follow-through without storing prompts.
32
+
33
+ ## Install
34
+
35
+ ### Global install from npm
36
+
37
+ ```bash
38
+ pi install npm:pi-skill-shiori@0.6.6
39
+ ```
40
+
41
+ Without a version pin:
42
+
43
+ ```bash
44
+ pi install npm:pi-skill-shiori
45
+ ```
46
+
47
+ ### Project-local install from npm
48
+
49
+ ```bash
50
+ pi install -l npm:pi-skill-shiori@0.6.6
51
+ ```
52
+
53
+ Without a version pin:
54
+
55
+ ```bash
56
+ pi install -l npm:pi-skill-shiori
57
+ ```
58
+
59
+ ### Install from GitHub
60
+
61
+ ```bash
62
+ pi install git:github.com/eiei114/pi-skill-shiori@v0.6.6
63
+ pi install -l git:github.com/eiei114/pi-skill-shiori
64
+ ```
65
+
66
+ ### Try without installing
67
+
68
+ ```bash
69
+ pi -e npm:pi-skill-shiori
70
+ pi -e git:github.com/eiei114/pi-skill-shiori
71
+ ```
72
+
73
+ ### Local development install
74
+
75
+ ```bash
76
+ pi install -l /absolute/path/to/pi-skill-shiori
77
+ ```
78
+
79
+ ## Quick start
80
+
81
+ 1. Install the package (see [Install](#install)).
82
+ 2. Create `.pi/skill-shiori.yml` in your Pi project (see [Docs/usage.md](Docs/usage.md#configure)).
83
+ 3. Start Pi and run `/shiori:doctor` to confirm policy path, indexed roots, and suppression status.
84
+ 4. Use `/shiori:bootstrap` to draft a policy from discovered skills, then `/shiori:reload` after edits.
85
+ 5. Ask the agent for help; Shiori injects compact candidates or use `/shiori:recommend` for an interactive flow.
86
+
87
+ Commands take no inline arguments. Details are collected after launch via Pi UI prompts.
88
+
89
+ ## Usage summary
90
+
91
+ | Command | Purpose |
92
+ |---|---|
93
+ | `/shiori:doctor` | Show policy path, indexed roots, inventory count, retrieval backend, suppression status, and code marker. |
94
+ | `/shiori:bootstrap` | Generate a review draft policy from discovered skill descriptions. |
95
+ | `/shiori:reload` | Rebuild Shiori skill inventory; optionally ask Pi to reload runtime resources. |
96
+ | `/shiori:recommend` | Interactive recommendation flow with optional pre-load. |
97
+ | `/shiori:stats` | Show operational counters and recommendation quality summary. |
98
+
99
+ Shiori registers one tool:
100
+
101
+ ```ts
102
+ shiori_load_skill({ skill: "reddit-research" })
103
+ ```
104
+
105
+ The model receives the full selected `SKILL.md` content. The Pi UI shows a compact result like `✓ Loaded reddit-research (6.3KB)`.
106
+
107
+ 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.
108
+
109
+ For inventory roots, policy YAML, suppression statuses, and feedback metrics, see [`Docs/usage.md`](Docs/usage.md).
110
+
111
+ ## Package contents
112
+
113
+ | Path | Purpose |
114
+ |---|---|
115
+ | `src/` | Pi extension entrypoint and Shiori runtime |
116
+ | `Docs/usage.md` | Configuration, inventory, suppression, and metrics details |
117
+ | `Docs/recommendation-surface.md` | Recommendation badges and surface behavior |
118
+ | `ROADMAP.md` | Maintenance direction and phased plan |
119
+ | `CHANGELOG.md` | Version history |
120
+ | `SECURITY.md` | Vulnerability reporting |
121
+
122
+ ## Development
123
+
124
+ ```bash
125
+ git clone https://github.com/eiei114/pi-skill-shiori.git
126
+ cd pi-skill-shiori
127
+ npm install
128
+ npm run typecheck
129
+ npm test
130
+ ```
131
+
132
+ Run in a Pi project without installing globally:
133
+
134
+ ```bash
135
+ pi -e /absolute/path/to/pi-skill-shiori
136
+ ```
137
+
138
+ ## Release
139
+
140
+ Releases are automated via Trusted Publishing:
141
+
142
+ 1. Bump `version` in `package.json` and update `CHANGELOG.md`.
143
+ 2. Merge to `main`.
144
+ 3. **Auto Release** tags `v<version>` and creates a GitHub release.
145
+ 4. The tag triggers **Publish**, which publishes to npm with provenance.
146
+
147
+ Maintainer checks before merge:
148
+
149
+ ```bash
150
+ npm run typecheck
151
+ npm test
152
+ npm run release:npm:dry
153
+ ```
154
+
155
+ For version history, see [`CHANGELOG.md`](CHANGELOG.md).
156
+
157
+ ## Security
158
+
159
+ Pi packages execute local code with the same permissions as Pi. Review third-party packages before installing them.
160
+
161
+ 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.
162
+
163
+ For vulnerability reporting, see [`SECURITY.md`](SECURITY.md).
164
+
165
+ ## Links
166
+
167
+ - **Repository**: <https://github.com/eiei114/pi-skill-shiori>
168
+ - **npm**: <https://www.npmjs.com/package/pi-skill-shiori>
169
+ - **Issues**: <https://github.com/eiei114/pi-skill-shiori/issues>
170
+ - **Pi packages**: <https://pi.dev/packages>
171
+
172
+ ## License
173
+
174
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-skill-shiori",
3
- "version": "0.6.2",
3
+ "version": "0.6.6",
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",
package/src/discovery.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { readdir, readFile, stat } from "node:fs/promises";
1
+ import { readdir, readFile, realpath, stat } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
4
4
  import { parse as parseYaml } from "yaml";
@@ -75,16 +75,34 @@ async function findSkillFiles(root: string): Promise<string[]> {
75
75
  }
76
76
 
77
77
  const result: string[] = [];
78
- await walk(root, result);
78
+ await walk(root, result, new Set<string>());
79
79
  return result;
80
80
  }
81
81
 
82
- async function walk(dir: string, result: string[]): Promise<void> {
82
+ async function walk(dir: string, result: string[], visited: Set<string>): Promise<void> {
83
+ let canonicalDir: string;
84
+ try {
85
+ canonicalDir = await realpath(dir);
86
+ } catch {
87
+ return;
88
+ }
89
+ if (visited.has(canonicalDir)) return;
90
+ visited.add(canonicalDir);
91
+
83
92
  const entries = await readdir(dir, { withFileTypes: true });
84
93
  for (const entry of entries) {
85
94
  const fullPath = join(dir, entry.name);
86
95
  if (entry.isDirectory()) {
87
- await walk(fullPath, result);
96
+ await walk(fullPath, result, visited);
97
+ } else if (entry.isSymbolicLink()) {
98
+ try {
99
+ const target = await stat(fullPath);
100
+ if (target.isDirectory()) {
101
+ await walk(fullPath, result, visited);
102
+ }
103
+ } catch {
104
+ // Ignore broken symlinks.
105
+ }
88
106
  } else if (entry.isFile() && entry.name === SKILL_FILE) {
89
107
  result.push(fullPath);
90
108
  }
package/src/index.ts CHANGED
@@ -9,7 +9,7 @@ import { loadRecommendedSkills } from "./load-skills.js";
9
9
  import { formatPlanningKickoffMessage, isPlanningIntent } from "./planning-kickoff.js";
10
10
  import { createStats } from "./metrics.js";
11
11
  import { createRecommendationFeedback } from "./recommendation-feedback.js";
12
- import { getPolicyPath, loadPolicy } from "./policy.js";
12
+ import { getPolicyPath, loadPolicyWithSource, loadedPolicySignature, type LoadedPolicy } from "./policy.js";
13
13
  import {
14
14
  formatCandidateDetail,
15
15
  formatCandidateInjection,
@@ -40,16 +40,19 @@ export default function piSkillShiori(pi: ExtensionAPI) {
40
40
  let index: SkillIndex | undefined;
41
41
  let inventoryFingerprint: string | undefined;
42
42
  let inventoryRoots: string[] = [];
43
+ let loadedPolicy: LoadedPolicy | undefined;
43
44
  let warnedAlwaysVisibleMissing = new Set<string>();
44
45
  const stats = createStats();
45
46
  const recommendationFeedback = createRecommendationFeedback();
46
47
 
47
- async function reload(cwd: string): Promise<SkillIndex> {
48
- const policy = await loadPolicy(cwd);
49
- const inventory = await refreshSkillInventory(cwd, policy, index);
48
+ async function reload(cwd: string, currentPolicy?: LoadedPolicy): Promise<SkillIndex> {
49
+ currentPolicy ??= await loadPolicyWithSource(cwd);
50
+ const policy = currentPolicy.policy;
51
+ const inventory = await refreshSkillInventory(cwd, policy, index);
50
52
  index = inventory.index;
51
53
  inventoryFingerprint = inventory.fingerprint;
52
54
  inventoryRoots = inventory.roots;
55
+ loadedPolicy = currentPolicy;
53
56
  stats.inventoryCount = index.skills.length;
54
57
  stats.duplicateCount = findDuplicates(index.skills).size;
55
58
  stats.lastReloadAt = index.builtAt;
@@ -59,20 +62,27 @@ export default function piSkillShiori(pi: ExtensionAPI) {
59
62
  return index;
60
63
  }
61
64
 
62
- async function ensureIndex(cwd: string): Promise<SkillIndex> {
63
- if (!index || !inventoryFingerprint) {
64
- return reload(cwd);
65
- }
66
-
67
- const policy = await loadPolicy(cwd);
68
- if (!isAutoRefreshEnabled(policy)) {
69
- return index;
70
- }
71
-
72
- if (await isInventoryStale(cwd, policy, inventoryFingerprint)) {
73
- stats.inventoryAutoRefreshCount += 1;
74
- return reload(cwd);
75
- }
65
+ async function ensureIndex(cwd: string): Promise<SkillIndex> {
66
+ if (!index || !inventoryFingerprint) {
67
+ return reload(cwd);
68
+ }
69
+
70
+ const currentPolicy = await loadPolicyWithSource(cwd);
71
+ if (!loadedPolicy || loadedPolicySignature(currentPolicy) !== loadedPolicySignature(loadedPolicy)) {
72
+ stats.inventoryAutoRefreshCount += 1;
73
+ return reload(cwd, currentPolicy);
74
+ }
75
+
76
+ const policy = currentPolicy.policy;
77
+ loadedPolicy = currentPolicy;
78
+ if (!isAutoRefreshEnabled(policy)) {
79
+ return index;
80
+ }
81
+
82
+ if (await isInventoryStale(cwd, policy, inventoryFingerprint)) {
83
+ stats.inventoryAutoRefreshCount += 1;
84
+ return reload(cwd, currentPolicy);
85
+ }
76
86
 
77
87
  return index;
78
88
  }
@@ -96,6 +106,7 @@ export default function piSkillShiori(pi: ExtensionAPI) {
96
106
  index = undefined;
97
107
  inventoryFingerprint = undefined;
98
108
  inventoryRoots = [];
109
+ loadedPolicy = undefined;
99
110
  });
100
111
 
101
112
  pi.on("input", async (event) => {
@@ -248,7 +259,9 @@ export default function piSkillShiori(pi: ExtensionAPI) {
248
259
  const alwaysVisible = evaluateAlwaysVisible(current.policy.alwaysVisible, current.skills);
249
260
  const doctorLines = [
250
261
  "Pi Skill Shiori doctor",
251
- `policy: ${getPolicyPath(ctx.cwd)}`,
262
+ `policy: ${loadedPolicy?.path ?? "default"}`,
263
+ `policySource: ${loadedPolicy?.source ?? "default"}`,
264
+ `projectPolicy: ${getPolicyPath(ctx.cwd)}`,
252
265
  `zeroCatalog: ${current.policy.zeroCatalog.enabled ? "enabled" : "disabled"}`,
253
266
  `skills: ${current.skills.length}`,
254
267
  `duplicates: ${duplicates.size}`,
package/src/policy.ts CHANGED
@@ -1,9 +1,22 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
2
3
  import { join } from "node:path";
3
4
  import { parse } from "yaml";
4
5
  import { normalizeAlwaysVisible } from "./always-visible.js";
5
6
  import type { ShioriPolicy } from "./types.js";
6
7
 
8
+ export type PolicySourceKind = "project" | "global" | "default";
9
+
10
+ export interface LoadedPolicy {
11
+ policy: ShioriPolicy;
12
+ source: PolicySourceKind;
13
+ path?: string;
14
+ }
15
+
16
+ export interface PolicyLoadOptions {
17
+ globalPolicyPath?: string;
18
+ }
19
+
7
20
  export const DEFAULT_POLICY: ShioriPolicy = {
8
21
  zeroCatalog: { enabled: false },
9
22
  defaults: { activation: "explicit" },
@@ -16,14 +29,46 @@ export function getPolicyPath(cwd: string): string {
16
29
  return join(cwd, ".pi", "skill-shiori.yml");
17
30
  }
18
31
 
19
- export async function loadPolicy(cwd: string): Promise<ShioriPolicy> {
32
+ export function getGlobalPolicyPath(): string {
33
+ return join(homedir(), ".pi", "agent", "skill-shiori.yml");
34
+ }
35
+
36
+ export async function loadPolicy(cwd: string, options: PolicyLoadOptions = {}): Promise<ShioriPolicy> {
37
+ return (await loadPolicyWithSource(cwd, options)).policy;
38
+ }
39
+
40
+ export async function loadPolicyWithSource(cwd: string, options: PolicyLoadOptions = {}): Promise<LoadedPolicy> {
41
+ const projectPath = getPolicyPath(cwd);
42
+ const projectPolicy = await readPolicyIfPresent(projectPath);
43
+ if (projectPolicy) {
44
+ return { policy: projectPolicy, source: "project", path: projectPath };
45
+ }
46
+
47
+ const globalPath = options.globalPolicyPath ?? getGlobalPolicyPath();
48
+ const globalPolicy = await readPolicyIfPresent(globalPath);
49
+ if (globalPolicy) {
50
+ return { policy: globalPolicy, source: "global", path: globalPath };
51
+ }
52
+
53
+ return { policy: DEFAULT_POLICY, source: "default" };
54
+ }
55
+
56
+ export function loadedPolicySignature(loaded: LoadedPolicy): string {
57
+ return JSON.stringify({
58
+ source: loaded.source,
59
+ path: loaded.path ?? null,
60
+ policy: loaded.policy,
61
+ });
62
+ }
63
+
64
+ async function readPolicyIfPresent(path: string): Promise<ShioriPolicy | undefined> {
20
65
  try {
21
- const raw = await readFile(getPolicyPath(cwd), "utf8");
66
+ const raw = await readFile(path, "utf8");
22
67
  const parsed = parse(raw) as Partial<ShioriPolicy> | undefined;
23
68
  return normalizePolicy(parsed);
24
69
  } catch (error) {
25
70
  if ((error as NodeJS.ErrnoException).code === "ENOENT") {
26
- return DEFAULT_POLICY;
71
+ return undefined;
27
72
  }
28
73
  throw error;
29
74
  }