guardian-framework 0.1.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.
Files changed (216) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +463 -0
  3. package/dist/cli.js +12958 -0
  4. package/dist/exports.js +12765 -0
  5. package/package.json +61 -0
  6. package/templates/languages/go-patterns.md +411 -0
  7. package/templates/languages/java-patterns.md +545 -0
  8. package/templates/languages/python-patterns.md +312 -0
  9. package/templates/languages/rust-patterns.md +232 -0
  10. package/templates/languages/typescript-patterns.md +313 -0
  11. package/templates/pi/INDEX.md +437 -0
  12. package/templates/pi/agent/AGENTS.md +166 -0
  13. package/templates/pi/agents/architecture-coordinator.md +40 -0
  14. package/templates/pi/agents/architecture-validator.md +36 -0
  15. package/templates/pi/agents/bootstrap-implementer.md +37 -0
  16. package/templates/pi/agents/issue-factory.md +31 -0
  17. package/templates/pi/agents/operations-validator.md +32 -0
  18. package/templates/pi/agents/security-validator.md +33 -0
  19. package/templates/pi/architecture/CHANGELOG.md +78 -0
  20. package/templates/pi/architecture/decisions/ADR-template.md +76 -0
  21. package/templates/pi/architecture/diagrams/system-overview.md +159 -0
  22. package/templates/pi/architecture/modules/module-template.md +152 -0
  23. package/templates/pi/context/checklists.md +91 -0
  24. package/templates/pi/context/domain-workflow.md +180 -0
  25. package/templates/pi/context/output-formats.md +135 -0
  26. package/templates/pi/context/patterns-base.md +47 -0
  27. package/templates/pi/context/patterns.md +38 -0
  28. package/templates/pi/context/project.md +88 -0
  29. package/templates/pi/domain/exploration.md +93 -0
  30. package/templates/pi/domain/ubiquitous-language.md +24 -0
  31. package/templates/pi/extensions/architect-lib/generators.ts +528 -0
  32. package/templates/pi/extensions/architect-lib/helpers.ts +344 -0
  33. package/templates/pi/extensions/architect-lib/types.ts +117 -0
  34. package/templates/pi/extensions/architect.ts +1425 -0
  35. package/templates/pi/extensions/ask-user-question.ts +579 -0
  36. package/templates/pi/extensions/bash-guard.ts +556 -0
  37. package/templates/pi/extensions/config-reload.ts +115 -0
  38. package/templates/pi/extensions/coordinator.ts +304 -0
  39. package/templates/pi/extensions/curator.ts +543 -0
  40. package/templates/pi/extensions/domain-explorer.ts +990 -0
  41. package/templates/pi/extensions/filechanges.ts +656 -0
  42. package/templates/pi/extensions/goal-loop.ts +717 -0
  43. package/templates/pi/extensions/hooks.ts +452 -0
  44. package/templates/pi/extensions/kanban.ts +547 -0
  45. package/templates/pi/extensions/pipeline.ts +1246 -0
  46. package/templates/pi/extensions/plan-mode.ts +209 -0
  47. package/templates/pi/extensions/project-scaffolder.ts +284 -0
  48. package/templates/pi/extensions/read-only-mode.ts +192 -0
  49. package/templates/pi/extensions/redaction.ts +128 -0
  50. package/templates/pi/extensions/session-persistence.ts +174 -0
  51. package/templates/pi/extensions/slash-commands.ts +229 -0
  52. package/templates/pi/extensions/snippets.ts +201 -0
  53. package/templates/pi/extensions/validation-runner.ts +100 -0
  54. package/templates/pi/github/agents/architecture-coordinator.agent.md +27 -0
  55. package/templates/pi/github/agents/architecture-validator.agent.md +20 -0
  56. package/templates/pi/github/agents/bootstrap-implementer.agent.md +20 -0
  57. package/templates/pi/github/agents/epic-planner.agent.md +70 -0
  58. package/templates/pi/github/agents/issue-factory.agent.md +20 -0
  59. package/templates/pi/github/agents/operations-validator.agent.md +20 -0
  60. package/templates/pi/github/agents/security-validator.agent.md +20 -0
  61. package/templates/pi/github/copilot/settings.json +16 -0
  62. package/templates/pi/github/copilot-instructions.md +155 -0
  63. package/templates/pi/github/instructions/architecture.instructions.md +71 -0
  64. package/templates/pi/github/instructions/validation.instructions.md +69 -0
  65. package/templates/pi/github/workflows/01-planning-workflow.md +75 -0
  66. package/templates/pi/github/workflows/02-issue-generation-workflow.md +69 -0
  67. package/templates/pi/github/workflows/03-implementation-workflow.md +80 -0
  68. package/templates/pi/github/workflows/04-validation-workflow.md +68 -0
  69. package/templates/pi/preflight_report.json +99 -0
  70. package/templates/pi/prompts/blueprint-update.md +262 -0
  71. package/templates/pi/prompts/blueprint-validate.md +218 -0
  72. package/templates/pi/prompts/bug-fix.md +50 -0
  73. package/templates/pi/prompts/ci-blueprint.md +131 -0
  74. package/templates/pi/prompts/context-refresh.md +258 -0
  75. package/templates/pi/prompts/epic-plan.md +356 -0
  76. package/templates/pi/prompts/epic-template.md +186 -0
  77. package/templates/pi/prompts/feature-development.md +131 -0
  78. package/templates/pi/prompts/git-issues.md +243 -0
  79. package/templates/pi/prompts/hotfix.md +71 -0
  80. package/templates/pi/prompts/issue-closeout.md +342 -0
  81. package/templates/pi/prompts/issue-draft.md +218 -0
  82. package/templates/pi/prompts/issue-implementation-series.md +173 -0
  83. package/templates/pi/prompts/issue-merge.md +372 -0
  84. package/templates/pi/prompts/issue-template-set.md +392 -0
  85. package/templates/pi/prompts/issue-template.md +207 -0
  86. package/templates/pi/prompts/pattern-extract.md +269 -0
  87. package/templates/pi/prompts/plan-to-issues.md +430 -0
  88. package/templates/pi/prompts/refactoring.md +82 -0
  89. package/templates/pi/prompts/scope-analyzer.md +254 -0
  90. package/templates/pi/prompts/sync-check.md +230 -0
  91. package/templates/pi/scripts/categorize-issues.sh +171 -0
  92. package/templates/pi/scripts/ci/check_architecture_conformance.sh +748 -0
  93. package/templates/pi/scripts/ci/check_planning_packet.py +228 -0
  94. package/templates/pi/scripts/ci/run_hardening_stages.sh +264 -0
  95. package/templates/pi/scripts/ci/run_preflight.sh +649 -0
  96. package/templates/pi/scripts/ci/run_stage.sh +193 -0
  97. package/templates/pi/scripts/ci/stage_docs_policy.sh +82 -0
  98. package/templates/pi/scripts/ci/stage_integration.sh +3 -0
  99. package/templates/pi/scripts/ci/stage_lint.sh +130 -0
  100. package/templates/pi/scripts/ci/stage_migration_verify.sh +3 -0
  101. package/templates/pi/scripts/ci/stage_package_build.sh +3 -0
  102. package/templates/pi/scripts/ci/stage_release_readiness.sh +3 -0
  103. package/templates/pi/scripts/ci/stage_remaining.sh +242 -0
  104. package/templates/pi/scripts/ci/stage_security.sh +3 -0
  105. package/templates/pi/scripts/ci/stage_static_analysis.sh +114 -0
  106. package/templates/pi/scripts/ci/stage_unit.sh +119 -0
  107. package/templates/pi/scripts/ci/validate_agent_output.py +181 -0
  108. package/templates/pi/scripts/ci/validate_agent_output.sh +243 -0
  109. package/templates/pi/scripts/create-feature-branch.sh +136 -0
  110. package/templates/pi/scripts/create-mr.sh +117 -0
  111. package/templates/pi/scripts/fetch-issues.sh +131 -0
  112. package/templates/pi/scripts/generate-architecture.sh +161 -0
  113. package/templates/pi/scripts/git/close-epic.sh +69 -0
  114. package/templates/pi/scripts/git/close-issue.sh +35 -0
  115. package/templates/pi/scripts/git/create-tracking-issue.sh +174 -0
  116. package/templates/pi/scripts/git/link-issue-to-epic.sh +51 -0
  117. package/templates/pi/scripts/git/update-tracking-issue.sh +66 -0
  118. package/templates/pi/scripts/languages/go/validate-architecture.sh +139 -0
  119. package/templates/pi/scripts/languages/go/validate-canonical.sh +129 -0
  120. package/templates/pi/scripts/languages/go/validate-ci.sh +132 -0
  121. package/templates/pi/scripts/languages/go/validate-integration.sh +156 -0
  122. package/templates/pi/scripts/languages/go/validate-operations.sh +146 -0
  123. package/templates/pi/scripts/languages/go/validate-security.sh +135 -0
  124. package/templates/pi/scripts/languages/go/validate-tests.sh +125 -0
  125. package/templates/pi/scripts/languages/java/validate-annotations.sh +185 -0
  126. package/templates/pi/scripts/languages/java/validate-architecture.sh +159 -0
  127. package/templates/pi/scripts/languages/java/validate-canonical.sh +114 -0
  128. package/templates/pi/scripts/languages/java/validate-ci.sh +269 -0
  129. package/templates/pi/scripts/languages/java/validate-integration.sh +149 -0
  130. package/templates/pi/scripts/languages/java/validate-operations.sh +142 -0
  131. package/templates/pi/scripts/languages/java/validate-security.sh +174 -0
  132. package/templates/pi/scripts/languages/java/validate-spring-architecture.sh +227 -0
  133. package/templates/pi/scripts/languages/java/validate-tests.sh +159 -0
  134. package/templates/pi/scripts/languages/python/validate-architecture.sh +99 -0
  135. package/templates/pi/scripts/languages/python/validate-canonical.sh +124 -0
  136. package/templates/pi/scripts/languages/python/validate-ci.sh +130 -0
  137. package/templates/pi/scripts/languages/python/validate-integration.sh +119 -0
  138. package/templates/pi/scripts/languages/python/validate-operations.sh +107 -0
  139. package/templates/pi/scripts/languages/python/validate-security.sh +68 -0
  140. package/templates/pi/scripts/languages/python/validate-tests.sh +102 -0
  141. package/templates/pi/scripts/languages/rust/validate-architecture.sh +188 -0
  142. package/templates/pi/scripts/languages/rust/validate-canonical.sh +146 -0
  143. package/templates/pi/scripts/languages/rust/validate-ci.sh +147 -0
  144. package/templates/pi/scripts/languages/rust/validate-integration.sh +149 -0
  145. package/templates/pi/scripts/languages/rust/validate-operations.sh +154 -0
  146. package/templates/pi/scripts/languages/rust/validate-security.sh +153 -0
  147. package/templates/pi/scripts/languages/rust/validate-tests.sh +150 -0
  148. package/templates/pi/scripts/languages/typescript/validate-architecture.sh +157 -0
  149. package/templates/pi/scripts/languages/typescript/validate-canonical.sh +135 -0
  150. package/templates/pi/scripts/languages/typescript/validate-ci.sh +244 -0
  151. package/templates/pi/scripts/languages/typescript/validate-integration.sh +166 -0
  152. package/templates/pi/scripts/languages/typescript/validate-operations.sh +162 -0
  153. package/templates/pi/scripts/languages/typescript/validate-security.sh +169 -0
  154. package/templates/pi/scripts/languages/typescript/validate-tests.sh +158 -0
  155. package/templates/pi/scripts/merge-mr.sh +95 -0
  156. package/templates/pi/scripts/mr-validation.sh +228 -0
  157. package/templates/pi/scripts/validate-architecture-readiness.sh +153 -0
  158. package/templates/pi/scripts/validate-architecture.sh +22 -0
  159. package/templates/pi/scripts/validate-canonical.sh +22 -0
  160. package/templates/pi/scripts/validate-ci.sh +37 -0
  161. package/templates/pi/scripts/validate-integration.sh +22 -0
  162. package/templates/pi/scripts/validate-operations.sh +22 -0
  163. package/templates/pi/scripts/validate-security.sh +22 -0
  164. package/templates/pi/scripts/validate-tests.sh +22 -0
  165. package/templates/pi/scripts/validate-ubiquitous-language.sh +291 -0
  166. package/templates/pi/scripts/validation-cache.sh +139 -0
  167. package/templates/pi/skills/agents/architecture-coordinator.md +178 -0
  168. package/templates/pi/skills/agents/architecture-generator.md +79 -0
  169. package/templates/pi/skills/agents/architecture-validator.md +74 -0
  170. package/templates/pi/skills/agents/ci-mr-validator.md +46 -0
  171. package/templates/pi/skills/agents/code-developer.md +76 -0
  172. package/templates/pi/skills/agents/commit.md +46 -0
  173. package/templates/pi/skills/agents/curator.md +71 -0
  174. package/templates/pi/skills/agents/debug.md +59 -0
  175. package/templates/pi/skills/agents/documentation-maintainer.md +36 -0
  176. package/templates/pi/skills/agents/goal-loop.md +125 -0
  177. package/templates/pi/skills/agents/hooks.md +122 -0
  178. package/templates/pi/skills/agents/integration-validator.md +35 -0
  179. package/templates/pi/skills/agents/issue-creator.md +75 -0
  180. package/templates/pi/skills/agents/issue-factory.md +143 -0
  181. package/templates/pi/skills/agents/kanban.md +85 -0
  182. package/templates/pi/skills/agents/land.md +59 -0
  183. package/templates/pi/skills/agents/operations-validator.md +43 -0
  184. package/templates/pi/skills/agents/pipeline.md +113 -0
  185. package/templates/pi/skills/agents/plan-mode.md +77 -0
  186. package/templates/pi/skills/agents/pull.md +40 -0
  187. package/templates/pi/skills/agents/push.md +41 -0
  188. package/templates/pi/skills/agents/security-validator.md +85 -0
  189. package/templates/pi/skills/agents/session-persistence.md +119 -0
  190. package/templates/pi/skills/agents/slash-commands.md +117 -0
  191. package/templates/pi/skills/agents/snippets.md +99 -0
  192. package/templates/pi/skills/agents/subagent-registry.md +78 -0
  193. package/templates/pi/skills/agents/test-validator.md +42 -0
  194. package/templates/pi/skills/validators/architecture-validator.md +43 -0
  195. package/templates/pi/skills/validators/ci-validator.md +27 -0
  196. package/templates/pi/skills/validators/context-compaction.md +73 -0
  197. package/templates/pi/skills/validators/integration-validator.md +38 -0
  198. package/templates/pi/skills/validators/model-registry.md +96 -0
  199. package/templates/pi/skills/validators/operations-validator.md +34 -0
  200. package/templates/pi/skills/validators/security-guards.md +72 -0
  201. package/templates/pi/skills/validators/security-validator.md +41 -0
  202. package/templates/pi/skills/validators/system-prompt-tiers.md +69 -0
  203. package/templates/pi/skills/validators/test-validator.md +33 -0
  204. package/templates/pi/types.ts +62 -0
  205. package/templates/pi/validators/README.md +93 -0
  206. package/templates/pi/validators/default.toml +209 -0
  207. package/templates/pi/validators/spring.toml +142 -0
  208. package/templates/pi/workpad.md +53 -0
  209. package/templates/project/java/Dockerfile +14 -0
  210. package/templates/project/java/README.md +40 -0
  211. package/templates/project/java/pom.xml +132 -0
  212. package/templates/project/typescript/Dockerfile +15 -0
  213. package/templates/project/typescript/README.md +29 -0
  214. package/templates/project/typescript/package.json +23 -0
  215. package/templates/project/typescript/tsconfig.json +21 -0
  216. package/templates/workflow.md +123 -0
@@ -0,0 +1,543 @@
1
+ /**
2
+ * Skill Curator Extension for pi
3
+ *
4
+ * Background maintenance for agent-created skills.
5
+ * Tracks usage (view, use, patch counts), detects stale/unused skills,
6
+ * and recommends consolidation or archival.
7
+ *
8
+ * Inspired by Hermes-Agent Curator — adapted for Guardian's skill system
9
+ * where skills live in .pi/skills/ and are loaded via snippet expansion.
10
+ */
11
+
12
+ import {
13
+ existsSync,
14
+ mkdirSync,
15
+ readFileSync,
16
+ readdirSync,
17
+ renameSync,
18
+ statSync,
19
+ writeFileSync,
20
+ } from "node:fs";
21
+ import { dirname, join } from "node:path";
22
+
23
+ // ── Types ──
24
+
25
+ type ExtensionContext = {
26
+ cwd: string;
27
+ ui: {
28
+ notify(message: string, level?: string): void;
29
+ setStatus(key: string, message: string | null): void;
30
+ confirm(title: string, message: string): Promise<boolean>;
31
+ select(title: string, options: string[]): Promise<string | null>;
32
+ };
33
+ shell: {
34
+ execute(
35
+ command: string,
36
+ options?: { signal?: AbortSignal },
37
+ ): Promise<{
38
+ exitCode: number;
39
+ stdout: string;
40
+ }>;
41
+ };
42
+ tools: { execute(name: string, params: Record<string, unknown>): Promise<unknown> };
43
+ };
44
+
45
+ type ExtensionAPI = {
46
+ on(event: string, handler: (event: unknown, ctx: ExtensionContext) => void | Promise<void>): void;
47
+ registerTool(options: {
48
+ name: string;
49
+ label: string;
50
+ description: string;
51
+ parameters: unknown;
52
+ execute(
53
+ toolCallId: string,
54
+ params: Record<string, unknown>,
55
+ signal: AbortSignal,
56
+ onUpdate: (update: { type: string; message: string }) => void,
57
+ ctx: ExtensionContext,
58
+ ): unknown | Promise<unknown>;
59
+ }): void;
60
+ registerCommand(
61
+ name: string,
62
+ options: {
63
+ description: string;
64
+ handler(args: string, ctx: ExtensionContext): unknown | Promise<unknown>;
65
+ },
66
+ ): void;
67
+ };
68
+
69
+ type SkillUsage = {
70
+ name: string;
71
+ useCount: number;
72
+ viewCount: number;
73
+ patchCount: number;
74
+ lastUsedAt: string | null;
75
+ lastViewedAt: string | null;
76
+ lastPatchedAt: string | null;
77
+ createdAt: string;
78
+ state: "active" | "stale" | "archived";
79
+ pinned: boolean;
80
+ };
81
+
82
+ type CuratorState = {
83
+ skills: Record<string, SkillUsage>;
84
+ lastRunAt: string | null;
85
+ runCount: number;
86
+ };
87
+
88
+ // ── Constants ──
89
+
90
+ const USAGE_FILE = ".pi/.guardian-skill-usage.json";
91
+ const ARCHIVE_DIR = ".pi/skills/.archive";
92
+ const BUNDLED_SKILLS = new Set([
93
+ "architecture-coordinator",
94
+ "architecture-validator",
95
+ "ci-mr-validator",
96
+ "code-developer",
97
+ "commit",
98
+ "debug",
99
+ "documentation-maintainer",
100
+ "integration-validator",
101
+ "issue-creator",
102
+ "land",
103
+ "operations-validator",
104
+ "plan-mode",
105
+ "pull",
106
+ "push",
107
+ "security-validator",
108
+ "session-persistence",
109
+ "slash-commands",
110
+ "snippets",
111
+ "subagent-registry",
112
+ "test-validator",
113
+ // validators
114
+ "architecture-validator",
115
+ "ci-validator",
116
+ "context-compaction",
117
+ "integration-validator",
118
+ "model-registry",
119
+ "operations-validator",
120
+ "security-guards",
121
+ "security-validator",
122
+ "system-prompt-tiers",
123
+ "test-validator",
124
+ ]);
125
+
126
+ const STALE_AFTER_DAYS = 30;
127
+ const ARCHIVE_AFTER_DAYS = 90;
128
+
129
+ // ── Persistence ──
130
+
131
+ function loadUsage(cwd: string): CuratorState {
132
+ const p = join(cwd, USAGE_FILE);
133
+ if (!existsSync(p)) {
134
+ return { skills: {}, lastRunAt: null, runCount: 0 };
135
+ }
136
+ return JSON.parse(readFileSync(p, "utf-8")) as CuratorState;
137
+ }
138
+
139
+ function saveUsage(cwd: string, state: CuratorState): void {
140
+ const p = join(cwd, USAGE_FILE);
141
+ const dir = dirname(p);
142
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
143
+ writeFileSync(p, JSON.stringify(state, null, 2));
144
+ }
145
+
146
+ function ensureSkillTracked(cwd: string, name: string): SkillUsage {
147
+ const state = loadUsage(cwd);
148
+ if (state.skills[name]) return state.skills[name];
149
+
150
+ const skillPath = join(cwd, `.pi/skills/agents/${name}.md`);
151
+ const validatorPath = join(cwd, `.pi/skills/validators/${name}.md`);
152
+ const path = existsSync(skillPath) ? skillPath : validatorPath;
153
+ let createdAt = new Date().toISOString();
154
+ if (existsSync(path)) {
155
+ try {
156
+ createdAt = statSync(path).birthtime.toISOString();
157
+ } catch {
158
+ // birthtime not available, use mtime
159
+ createdAt = statSync(path).mtime.toISOString();
160
+ }
161
+ }
162
+
163
+ state.skills[name] = {
164
+ name,
165
+ useCount: 0,
166
+ viewCount: 0,
167
+ patchCount: 0,
168
+ lastUsedAt: null,
169
+ lastViewedAt: null,
170
+ lastPatchedAt: null,
171
+ createdAt,
172
+ state: "active",
173
+ pinned: false,
174
+ };
175
+ saveUsage(cwd, state);
176
+ return state.skills[name];
177
+ }
178
+
179
+ // ── Curator ──
180
+
181
+ class Curator {
182
+ private state: CuratorState;
183
+
184
+ constructor(private cwd: string) {
185
+ this.state = loadUsage(cwd);
186
+ }
187
+
188
+ recordUse(name: string): void {
189
+ const skill = ensureSkillTracked(this.cwd, name);
190
+ skill.useCount++;
191
+ skill.lastUsedAt = new Date().toISOString();
192
+ if (skill.state === "stale") skill.state = "active";
193
+ saveUsage(this.cwd, this.state);
194
+ }
195
+
196
+ recordView(name: string): void {
197
+ const skill = ensureSkillTracked(this.cwd, name);
198
+ skill.viewCount++;
199
+ skill.lastViewedAt = new Date().toISOString();
200
+ saveUsage(this.cwd, this.state);
201
+ }
202
+
203
+ recordPatch(name: string): void {
204
+ const skill = ensureSkillTracked(this.cwd, name);
205
+ skill.patchCount++;
206
+ skill.lastPatchedAt = new Date().toISOString();
207
+ saveUsage(this.cwd, this.state);
208
+ }
209
+
210
+ /**
211
+ * Run the curator review pass.
212
+ * Returns a report of actions taken.
213
+ */
214
+ review(dryRun = false): { actions: string[]; summary: string } {
215
+ const actions: string[] = [];
216
+ const now = Date.now();
217
+ const staleThreshold = STALE_AFTER_DAYS * 24 * 60 * 60 * 1000;
218
+ const archiveThreshold = ARCHIVE_AFTER_DAYS * 24 * 60 * 60 * 1000;
219
+
220
+ // Discover all skills on disk
221
+ const skillDirs = ["agents", "validators"];
222
+ const diskSkills = new Set<string>();
223
+ for (const dir of skillDirs) {
224
+ const skillPath = join(this.cwd, `.pi/skills/${dir}`);
225
+ if (existsSync(skillPath)) {
226
+ try {
227
+ for (const f of readdirSync(skillPath)) {
228
+ if (f.endsWith(".md")) {
229
+ diskSkills.add(f.replace(".md", ""));
230
+ }
231
+ }
232
+ } catch {
233
+ // ignore
234
+ }
235
+ }
236
+ }
237
+
238
+ // Transition skills based on usage age
239
+ for (const [name, skill] of Object.entries(this.state.skills)) {
240
+ // Skip bundled skills
241
+ if (BUNDLED_SKILLS.has(name)) continue;
242
+ // Skip pinned skills
243
+ if (skill.pinned) continue;
244
+ // Skip already archived
245
+ if (skill.state === "archived") continue;
246
+
247
+ const lastActivity = skill.lastUsedAt
248
+ ? new Date(skill.lastUsedAt).getTime()
249
+ : new Date(skill.createdAt).getTime();
250
+ const age = now - lastActivity;
251
+
252
+ if (age > archiveThreshold && skill.state !== "archived") {
253
+ if (!dryRun) {
254
+ this.archiveSkill(name);
255
+ }
256
+ actions.push(
257
+ `📦 archived ${name} (unused for ${Math.round(age / (24 * 60 * 60 * 1000))} days)`,
258
+ );
259
+ skill.state = "archived";
260
+ } else if (age > staleThreshold && skill.state === "active") {
261
+ skill.state = "stale";
262
+ actions.push(
263
+ `⚠️ marked stale: ${name} (unused for ${Math.round(age / (24 * 60 * 60 * 1000))} days)`,
264
+ );
265
+ }
266
+ }
267
+
268
+ // Detect skills on disk but not tracked
269
+ for (const name of diskSkills) {
270
+ if (!BUNDLED_SKILLS.has(name) && !this.state.skills[name]) {
271
+ ensureSkillTracked(this.cwd, name);
272
+ }
273
+ }
274
+
275
+ // Update metadata
276
+ this.state.lastRunAt = new Date().toISOString();
277
+ this.state.runCount++;
278
+ if (!dryRun) saveUsage(this.cwd, this.state);
279
+
280
+ // Build summary
281
+ const totalSkills = Object.keys(this.state.skills).length;
282
+ const activeCount = Object.values(this.state.skills).filter((s) => s.state === "active").length;
283
+ const staleCount = Object.values(this.state.skills).filter((s) => s.state === "stale").length;
284
+ const archivedCount = Object.values(this.state.skills).filter(
285
+ (s) => s.state === "archived",
286
+ ).length;
287
+ const pinnedCount = Object.values(this.state.skills).filter((s) => s.pinned).length;
288
+
289
+ const summary = `Curator Review #${this.state.runCount}\nTotal: ${totalSkills} | Active: ${activeCount} | Stale: ${staleCount} | Archived: ${archivedCount} | Pinned: ${pinnedCount}\n${actions.length > 0 ? `\nActions:\n${actions.join("\n")}` : "\nNo actions needed."}`;
290
+
291
+ return { actions, summary };
292
+ }
293
+
294
+ private archiveSkill(name: string): void {
295
+ const dirs = ["agents", "validators"];
296
+ for (const dir of dirs) {
297
+ const src = join(this.cwd, `.pi/skills/${dir}/${name}.md`);
298
+ if (existsSync(src)) {
299
+ const archivePath = join(this.cwd, ARCHIVE_DIR);
300
+ if (!existsSync(archivePath)) mkdirSync(archivePath, { recursive: true });
301
+ const dest = join(archivePath, `${name}.md`);
302
+ renameSync(src, dest);
303
+ break;
304
+ }
305
+ }
306
+ }
307
+
308
+ pin(name: string): boolean {
309
+ const skill = this.state.skills[name];
310
+ if (!skill) return false;
311
+ skill.pinned = true;
312
+ saveUsage(this.cwd, this.state);
313
+ return true;
314
+ }
315
+
316
+ unpin(name: string): boolean {
317
+ const skill = this.state.skills[name];
318
+ if (!skill) return false;
319
+ skill.pinned = false;
320
+ saveUsage(this.cwd, this.state);
321
+ return true;
322
+ }
323
+
324
+ restore(name: string): boolean {
325
+ const skill = this.state.skills[name];
326
+ if (!skill) return false;
327
+ skill.state = "active";
328
+ saveUsage(this.cwd, this.state);
329
+
330
+ // Move from archive back
331
+ const archivePath = join(this.cwd, ARCHIVE_DIR, `${name}.md`);
332
+ if (existsSync(archivePath)) {
333
+ const dest = join(this.cwd, `.pi/skills/agents/${name}.md`);
334
+ renameSync(archivePath, dest);
335
+ }
336
+ return true;
337
+ }
338
+
339
+ statusReport(): string {
340
+ const skills = Object.values(this.state.skills);
341
+ const active = skills
342
+ .filter((s) => s.state === "active")
343
+ .sort((a, b) => b.useCount - a.useCount);
344
+ const stale = skills.filter((s) => s.state === "stale");
345
+ const archived = skills.filter((s) => s.state === "archived");
346
+ const pinned = skills.filter((s) => s.pinned);
347
+
348
+ const lines = [
349
+ "## Guardian Skill Curator Status",
350
+ "",
351
+ `**Last review:** ${this.state.lastRunAt || "never"}`,
352
+ `**Reviews run:** ${this.state.runCount}`,
353
+ "",
354
+ `**Active:** ${active.length} | **Stale:** ${stale.length} | **Archived:** ${archived.length} | **Pinned:** ${pinned.length}`,
355
+ ];
356
+
357
+ if (active.length > 0) {
358
+ lines.push("\n### Most Used Skills");
359
+ for (const s of active.slice(0, 10)) {
360
+ lines.push(
361
+ ` - ${s.name}: ${s.useCount} uses, ${s.viewCount} views, ${s.patchCount} patches`,
362
+ );
363
+ }
364
+ }
365
+
366
+ if (stale.length > 0) {
367
+ lines.push("\n### Stale Skills (will archive if unused)");
368
+ for (const s of stale) {
369
+ lines.push(` - ${s.name} (last used: ${s.lastUsedAt || "never"})`);
370
+ }
371
+ }
372
+
373
+ if (pinned.length > 0) {
374
+ lines.push("\n### Pinned Skills (protected from archival)");
375
+ for (const s of pinned) {
376
+ lines.push(` - ${s.name}`);
377
+ }
378
+ }
379
+
380
+ if (archived.length > 0) {
381
+ lines.push("\n### Archived Skills");
382
+ for (const s of archived) {
383
+ lines.push(` - ${s.name} (restore with /curator restore ${s.name})`);
384
+ }
385
+ }
386
+
387
+ return lines.join("\n");
388
+ }
389
+ }
390
+
391
+ // ── Helpers ──
392
+
393
+ function toolResult(text: string) {
394
+ return { content: [{ type: "text" as const, text }] };
395
+ }
396
+
397
+ function toolError(text: string) {
398
+ return { content: [{ type: "text" as const, text }], isError: true };
399
+ }
400
+
401
+ // ── Extension ──
402
+
403
+ export default function (pi: ExtensionAPI) {
404
+ let curator: Curator | null = null;
405
+
406
+ pi.on("session_start", async (_event, ctx) => {
407
+ curator = new Curator(ctx.cwd);
408
+ ctx.ui.notify("Guardian skill curator initialized", "info");
409
+ });
410
+
411
+ // ── curator_review tool ──
412
+ pi.registerTool({
413
+ name: "curator_review",
414
+ label: "Skill Curator Review",
415
+ description:
416
+ "Run the skill curator review pass. Detects stale/unused skills and recommends archival.",
417
+ parameters: {
418
+ type: "object",
419
+ properties: {
420
+ dryRun: { type: "boolean", description: "Preview only, no mutations" },
421
+ },
422
+ },
423
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
424
+ if (!curator) curator = new Curator(ctx.cwd);
425
+ const dryRun = (params.dryRun as boolean) || false;
426
+ const result = curator.review(dryRun);
427
+ return toolResult(result.summary);
428
+ },
429
+ });
430
+
431
+ // ── curator_pin tool ──
432
+ pi.registerTool({
433
+ name: "curator_pin",
434
+ label: "Pin Skill",
435
+ description: "Pin a skill to protect it from curator archival.",
436
+ parameters: {
437
+ type: "object",
438
+ properties: {
439
+ name: { type: "string", description: "Skill name" },
440
+ },
441
+ required: ["name"],
442
+ },
443
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
444
+ if (!curator) curator = new Curator(ctx.cwd);
445
+ const name = (params.name as string)?.trim();
446
+ if (!name) return toolError("name is required");
447
+ if (curator.pin(name)) {
448
+ return toolResult(`📌 Pinned: ${name}`);
449
+ }
450
+ return toolError(`Skill ${name} not found`);
451
+ },
452
+ });
453
+
454
+ // ── curator_unpin tool ──
455
+ pi.registerTool({
456
+ name: "curator_unpin",
457
+ label: "Unpin Skill",
458
+ description: "Unpin a skill to allow curator archival.",
459
+ parameters: {
460
+ type: "object",
461
+ properties: {
462
+ name: { type: "string", description: "Skill name" },
463
+ },
464
+ required: ["name"],
465
+ },
466
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
467
+ if (!curator) curator = new Curator(ctx.cwd);
468
+ const name = (params.name as string)?.trim();
469
+ if (!name) return toolError("name is required");
470
+ if (curator.unpin(name)) {
471
+ return toolResult(`Unpinned: ${name}`);
472
+ }
473
+ return toolError(`Skill ${name} not found`);
474
+ },
475
+ });
476
+
477
+ // ── /curator command ──
478
+ pi.registerCommand("curator", {
479
+ description: "Manage Guardian skill curator",
480
+ handler: async (args, ctx) => {
481
+ if (!curator) curator = new Curator(ctx.cwd);
482
+ const raw = typeof args === "string" ? args : "";
483
+ const tokens = raw.split(/\s+/).filter(Boolean);
484
+ const action = tokens[0];
485
+
486
+ if (!action || action === "status") {
487
+ ctx.ui.notify(curator.statusReport(), "info");
488
+ return;
489
+ }
490
+
491
+ if (action === "review") {
492
+ const dryRun = tokens.includes("--dry-run");
493
+ const result = curator.review(dryRun);
494
+ ctx.ui.notify(result.summary, dryRun ? "info" : "success");
495
+ return;
496
+ }
497
+
498
+ if (action === "pin") {
499
+ const name = tokens[1];
500
+ if (!name) {
501
+ ctx.ui.notify("Usage: /curator pin <skill-name>", "error");
502
+ return;
503
+ }
504
+ if (curator.pin(name)) {
505
+ ctx.ui.notify(`📌 Pinned: ${name}`, "success");
506
+ } else {
507
+ ctx.ui.notify(`Skill ${name} not found`, "error");
508
+ }
509
+ return;
510
+ }
511
+
512
+ if (action === "unpin") {
513
+ const name = tokens[1];
514
+ if (!name) {
515
+ ctx.ui.notify("Usage: /curator unpin <skill-name>", "error");
516
+ return;
517
+ }
518
+ if (curator.unpin(name)) {
519
+ ctx.ui.notify(`Unpinned: ${name}`, "info");
520
+ } else {
521
+ ctx.ui.notify(`Skill ${name} not found`, "error");
522
+ }
523
+ return;
524
+ }
525
+
526
+ if (action === "restore") {
527
+ const name = tokens[1];
528
+ if (!name) {
529
+ ctx.ui.notify("Usage: /curator restore <skill-name>", "error");
530
+ return;
531
+ }
532
+ if (curator.restore(name)) {
533
+ ctx.ui.notify(`Restored: ${name}`, "success");
534
+ } else {
535
+ ctx.ui.notify(`Skill ${name} not found`, "error");
536
+ }
537
+ return;
538
+ }
539
+
540
+ ctx.ui.notify("Usage: /curator [status|review [--dry-run]|pin|unpin|restore] <name>", "info");
541
+ },
542
+ });
543
+ }