pi-continuous-learning 0.5.1 → 0.7.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 (89) hide show
  1. package/README.md +78 -0
  2. package/dist/agents-md.d.ts +23 -2
  3. package/dist/agents-md.d.ts.map +1 -1
  4. package/dist/agents-md.js +58 -3
  5. package/dist/agents-md.js.map +1 -1
  6. package/dist/cli/analyze-single-shot.d.ts +62 -0
  7. package/dist/cli/analyze-single-shot.d.ts.map +1 -0
  8. package/dist/cli/analyze-single-shot.js +105 -0
  9. package/dist/cli/analyze-single-shot.js.map +1 -0
  10. package/dist/cli/analyze.js +82 -81
  11. package/dist/cli/analyze.js.map +1 -1
  12. package/dist/command-scaffold.d.ts +25 -0
  13. package/dist/command-scaffold.d.ts.map +1 -0
  14. package/dist/command-scaffold.js +77 -0
  15. package/dist/command-scaffold.js.map +1 -0
  16. package/dist/confidence.d.ts.map +1 -1
  17. package/dist/confidence.js +2 -1
  18. package/dist/confidence.js.map +1 -1
  19. package/dist/config.d.ts +16 -0
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +31 -0
  22. package/dist/config.js.map +1 -1
  23. package/dist/graduation.d.ts +63 -0
  24. package/dist/graduation.d.ts.map +1 -0
  25. package/dist/graduation.js +155 -0
  26. package/dist/graduation.js.map +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +5 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/instinct-cleanup.d.ts +57 -0
  31. package/dist/instinct-cleanup.d.ts.map +1 -0
  32. package/dist/instinct-cleanup.js +150 -0
  33. package/dist/instinct-cleanup.js.map +1 -0
  34. package/dist/instinct-graduate.d.ts +43 -0
  35. package/dist/instinct-graduate.d.ts.map +1 -0
  36. package/dist/instinct-graduate.js +253 -0
  37. package/dist/instinct-graduate.js.map +1 -0
  38. package/dist/instinct-parser.d.ts.map +1 -1
  39. package/dist/instinct-parser.js +12 -0
  40. package/dist/instinct-parser.js.map +1 -1
  41. package/dist/instinct-tools.d.ts.map +1 -1
  42. package/dist/instinct-tools.js +19 -0
  43. package/dist/instinct-tools.js.map +1 -1
  44. package/dist/instinct-validator.d.ts +61 -0
  45. package/dist/instinct-validator.d.ts.map +1 -0
  46. package/dist/instinct-validator.js +235 -0
  47. package/dist/instinct-validator.js.map +1 -0
  48. package/dist/observation-preprocessor.d.ts +26 -0
  49. package/dist/observation-preprocessor.d.ts.map +1 -0
  50. package/dist/observation-preprocessor.js +31 -0
  51. package/dist/observation-preprocessor.js.map +1 -0
  52. package/dist/prompts/analyzer-system-single-shot.d.ts +6 -0
  53. package/dist/prompts/analyzer-system-single-shot.d.ts.map +1 -0
  54. package/dist/prompts/analyzer-system-single-shot.js +164 -0
  55. package/dist/prompts/analyzer-system-single-shot.js.map +1 -0
  56. package/dist/prompts/analyzer-user-single-shot.d.ts +22 -0
  57. package/dist/prompts/analyzer-user-single-shot.d.ts.map +1 -0
  58. package/dist/prompts/analyzer-user-single-shot.js +53 -0
  59. package/dist/prompts/analyzer-user-single-shot.js.map +1 -0
  60. package/dist/prompts/analyzer-user.d.ts +3 -1
  61. package/dist/prompts/analyzer-user.d.ts.map +1 -1
  62. package/dist/prompts/analyzer-user.js +20 -7
  63. package/dist/prompts/analyzer-user.js.map +1 -1
  64. package/dist/skill-scaffold.d.ts +23 -0
  65. package/dist/skill-scaffold.d.ts.map +1 -0
  66. package/dist/skill-scaffold.js +62 -0
  67. package/dist/skill-scaffold.js.map +1 -0
  68. package/dist/types.d.ts +8 -0
  69. package/dist/types.d.ts.map +1 -1
  70. package/package.json +1 -1
  71. package/src/agents-md.ts +73 -3
  72. package/src/cli/analyze-single-shot.ts +175 -0
  73. package/src/cli/analyze.ts +93 -124
  74. package/src/command-scaffold.ts +105 -0
  75. package/src/confidence.ts +2 -1
  76. package/src/config.ts +40 -0
  77. package/src/graduation.ts +243 -0
  78. package/src/index.ts +14 -0
  79. package/src/instinct-cleanup.ts +204 -0
  80. package/src/instinct-graduate.ts +377 -0
  81. package/src/instinct-parser.ts +12 -0
  82. package/src/instinct-tools.ts +26 -0
  83. package/src/instinct-validator.ts +287 -0
  84. package/src/observation-preprocessor.ts +48 -0
  85. package/src/prompts/analyzer-system-single-shot.ts +163 -0
  86. package/src/prompts/analyzer-user-single-shot.ts +94 -0
  87. package/src/prompts/analyzer-user.ts +26 -8
  88. package/src/skill-scaffold.ts +90 -0
  89. package/src/types.ts +10 -0
@@ -0,0 +1,377 @@
1
+ /**
2
+ * /instinct-graduate command for pi-continuous-learning.
3
+ *
4
+ * Scans instincts for graduation candidates, presents proposals to the user,
5
+ * and writes to AGENTS.md / scaffolds skills / scaffolds commands on approval.
6
+ * Also enforces TTL - culling or decaying stale instincts.
7
+ */
8
+
9
+ import { join } from "node:path";
10
+ import { homedir } from "node:os";
11
+ import { unlinkSync } from "node:fs";
12
+ import { writeFileSync, mkdirSync } from "node:fs";
13
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
14
+ import type { Instinct } from "./types.js";
15
+ import { getBaseDir, getProjectInstinctsDir, getGlobalInstinctsDir } from "./storage.js";
16
+ import { loadProjectInstincts, loadGlobalInstincts, saveInstinct } from "./instinct-store.js";
17
+ import { readAgentsMd, appendToAgentsMd } from "./agents-md.js";
18
+ import {
19
+ findAgentsMdCandidates,
20
+ findSkillCandidates,
21
+ findCommandCandidates,
22
+ enforceTtl,
23
+ markGraduated,
24
+ } from "./graduation.js";
25
+ import type { GraduationCandidate, DomainCluster, TtlResult } from "./graduation.js";
26
+ import { generateSkillScaffold } from "./skill-scaffold.js";
27
+ import type { SkillScaffold } from "./skill-scaffold.js";
28
+ import { generateCommandScaffold } from "./command-scaffold.js";
29
+ import type { CommandScaffold } from "./command-scaffold.js";
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Constants
33
+ // ---------------------------------------------------------------------------
34
+
35
+ export const COMMAND_NAME = "instinct-graduate";
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Prompt building
39
+ // ---------------------------------------------------------------------------
40
+
41
+ function formatAgentsMdCandidates(candidates: GraduationCandidate[]): string {
42
+ if (candidates.length === 0) return "";
43
+
44
+ const lines = [
45
+ "## AGENTS.md Graduation Candidates",
46
+ "",
47
+ `Found ${candidates.length} instinct${candidates.length !== 1 ? "s" : ""} ready for AGENTS.md:`,
48
+ "",
49
+ ];
50
+
51
+ for (const candidate of candidates) {
52
+ const inst = candidate.instinct;
53
+ lines.push(
54
+ `- **${inst.id}** - "${inst.title}" (${inst.confidence.toFixed(2)} confidence, ${inst.confirmed_count} confirmations)`,
55
+ ` Trigger: ${inst.trigger}`,
56
+ ` ${candidate.reason}`,
57
+ ""
58
+ );
59
+ }
60
+
61
+ return lines.join("\n");
62
+ }
63
+
64
+ function formatSkillClusters(clusters: DomainCluster[]): string {
65
+ if (clusters.length === 0) return "";
66
+
67
+ const lines = [
68
+ "## Skill Scaffold Candidates",
69
+ "",
70
+ `Found ${clusters.length} domain cluster${clusters.length !== 1 ? "s" : ""} that could become skills:`,
71
+ "",
72
+ ];
73
+
74
+ for (const cluster of clusters) {
75
+ lines.push(
76
+ `- **${cluster.domain}** domain (${cluster.instincts.length} instincts):`,
77
+ ...cluster.instincts.map((i) => ` - ${i.id}: "${i.title}"`),
78
+ ""
79
+ );
80
+ }
81
+
82
+ return lines.join("\n");
83
+ }
84
+
85
+ function formatCommandClusters(clusters: DomainCluster[]): string {
86
+ if (clusters.length === 0) return "";
87
+
88
+ // Filter out clusters already covered by skill candidates (same domain)
89
+ const lines = [
90
+ "## Command Scaffold Candidates",
91
+ "",
92
+ `Found ${clusters.length} domain cluster${clusters.length !== 1 ? "s" : ""} that could become commands:`,
93
+ "",
94
+ ];
95
+
96
+ for (const cluster of clusters) {
97
+ lines.push(
98
+ `- **/${cluster.domain}** command (${cluster.instincts.length} instincts):`,
99
+ ...cluster.instincts.map((i) => ` - ${i.id}: "${i.title}"`),
100
+ ""
101
+ );
102
+ }
103
+
104
+ return lines.join("\n");
105
+ }
106
+
107
+ function formatTtlResults(ttl: TtlResult): string {
108
+ if (ttl.toCull.length === 0 && ttl.toDecay.length === 0) return "";
109
+
110
+ const lines = ["## TTL Enforcement", ""];
111
+
112
+ if (ttl.toCull.length > 0) {
113
+ lines.push(
114
+ `${ttl.toCull.length} instinct${ttl.toCull.length !== 1 ? "s" : ""} exceeded TTL with low confidence (will be deleted):`,
115
+ ""
116
+ );
117
+ for (const inst of ttl.toCull) {
118
+ lines.push(`- ${inst.id}: "${inst.title}" (${inst.confidence.toFixed(2)})`);
119
+ }
120
+ lines.push("");
121
+ }
122
+
123
+ if (ttl.toDecay.length > 0) {
124
+ lines.push(
125
+ `${ttl.toDecay.length} instinct${ttl.toDecay.length !== 1 ? "s" : ""} exceeded TTL but still have moderate confidence (will be aggressively decayed):`,
126
+ ""
127
+ );
128
+ for (const inst of ttl.toDecay) {
129
+ lines.push(`- ${inst.id}: "${inst.title}" (${inst.confidence.toFixed(2)})`);
130
+ }
131
+ lines.push("");
132
+ }
133
+
134
+ return lines.join("\n");
135
+ }
136
+
137
+ /**
138
+ * Builds the full graduation prompt to send to the LLM for user-facing review.
139
+ */
140
+ export function buildGraduationPrompt(
141
+ agentsMdCandidates: GraduationCandidate[],
142
+ skillClusters: DomainCluster[],
143
+ commandClusters: DomainCluster[],
144
+ ttl: TtlResult
145
+ ): string {
146
+ const sections = [
147
+ "I've analyzed your instincts for graduation readiness. Here's what I found:",
148
+ "",
149
+ formatAgentsMdCandidates(agentsMdCandidates),
150
+ formatSkillClusters(skillClusters),
151
+ formatCommandClusters(commandClusters),
152
+ formatTtlResults(ttl),
153
+ "## Next Steps",
154
+ "",
155
+ "For each category above, I can:",
156
+ "1. **Graduate to AGENTS.md** - Write approved instincts as permanent guidelines",
157
+ "2. **Scaffold a skill** - Generate a SKILL.md for a domain cluster",
158
+ "3. **Scaffold a command** - Generate a slash command for a workflow cluster",
159
+ "4. **Enforce TTL** - Delete or decay stale instincts",
160
+ "",
161
+ "Tell me which actions you'd like me to take. I'll use the instinct tools to execute.",
162
+ "You can approve all, pick specific instincts, or skip any category.",
163
+ ].filter((s) => s.length > 0);
164
+
165
+ return sections.join("\n");
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Action helpers (called by LLM via tools, or directly)
170
+ // ---------------------------------------------------------------------------
171
+
172
+ /**
173
+ * Resolves the instinct directory for a given instinct.
174
+ */
175
+ function getInstinctDir(instinct: Instinct, baseDir: string): string {
176
+ if (instinct.scope === "project" && instinct.project_id) {
177
+ return getProjectInstinctsDir(instinct.project_id, "personal", baseDir);
178
+ }
179
+ return getGlobalInstinctsDir("personal", baseDir);
180
+ }
181
+
182
+ /**
183
+ * Graduates instincts to AGENTS.md. Writes entries and marks instincts as graduated.
184
+ */
185
+ export function graduateToAgentsMd(
186
+ instincts: Instinct[],
187
+ agentsMdPath: string,
188
+ baseDir: string
189
+ ): Instinct[] {
190
+ if (instincts.length === 0) return [];
191
+
192
+ appendToAgentsMd(agentsMdPath, instincts);
193
+
194
+ const graduated: Instinct[] = [];
195
+ for (const instinct of instincts) {
196
+ const updated = markGraduated(instinct, "agents-md");
197
+ const dir = getInstinctDir(instinct, baseDir);
198
+ saveInstinct(updated, dir);
199
+ graduated.push(updated);
200
+ }
201
+
202
+ return graduated;
203
+ }
204
+
205
+ /**
206
+ * Graduates instincts to a skill scaffold. Writes SKILL.md and marks instincts.
207
+ */
208
+ export function graduateToSkill(
209
+ cluster: DomainCluster,
210
+ outputDir: string,
211
+ baseDir: string
212
+ ): SkillScaffold {
213
+ const scaffold = generateSkillScaffold(cluster);
214
+
215
+ mkdirSync(outputDir, { recursive: true });
216
+ writeFileSync(join(outputDir, "SKILL.md"), scaffold.content, "utf-8");
217
+
218
+ for (const instinct of cluster.instincts) {
219
+ const updated = markGraduated(instinct, "skill");
220
+ const dir = getInstinctDir(instinct, baseDir);
221
+ saveInstinct(updated, dir);
222
+ }
223
+
224
+ return scaffold;
225
+ }
226
+
227
+ /**
228
+ * Graduates instincts to a command scaffold. Writes command doc and marks instincts.
229
+ */
230
+ export function graduateToCommand(
231
+ cluster: DomainCluster,
232
+ outputDir: string,
233
+ baseDir: string
234
+ ): CommandScaffold {
235
+ const scaffold = generateCommandScaffold(cluster);
236
+
237
+ mkdirSync(outputDir, { recursive: true });
238
+ writeFileSync(join(outputDir, `${scaffold.name}-command.md`), scaffold.content, "utf-8");
239
+
240
+ for (const instinct of cluster.instincts) {
241
+ const updated = markGraduated(instinct, "command");
242
+ const dir = getInstinctDir(instinct, baseDir);
243
+ saveInstinct(updated, dir);
244
+ }
245
+
246
+ return scaffold;
247
+ }
248
+
249
+ /**
250
+ * Deletes TTL-expired instincts from disk.
251
+ */
252
+ export function cullExpiredInstincts(
253
+ instincts: Instinct[],
254
+ baseDir: string
255
+ ): number {
256
+ let deleted = 0;
257
+ for (const instinct of instincts) {
258
+ const dir = getInstinctDir(instinct, baseDir);
259
+ const filePath = join(dir, `${instinct.id}.md`);
260
+ try {
261
+ unlinkSync(filePath);
262
+ deleted++;
263
+ } catch {
264
+ // File may already be gone
265
+ }
266
+ }
267
+ return deleted;
268
+ }
269
+
270
+ /**
271
+ * Aggressively decays TTL-expired instincts by halving their confidence.
272
+ */
273
+ export function decayExpiredInstincts(
274
+ instincts: Instinct[],
275
+ baseDir: string
276
+ ): number {
277
+ let decayed = 0;
278
+ for (const instinct of instincts) {
279
+ const updated: Instinct = {
280
+ ...instinct,
281
+ confidence: Math.max(0.1, instinct.confidence * 0.5),
282
+ updated_at: new Date().toISOString(),
283
+ flagged_for_removal: true,
284
+ };
285
+ const dir = getInstinctDir(instinct, baseDir);
286
+ saveInstinct(updated, dir);
287
+ decayed++;
288
+ }
289
+ return decayed;
290
+ }
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // handleInstinctGraduate
294
+ // ---------------------------------------------------------------------------
295
+
296
+ /**
297
+ * Command handler for /instinct-graduate.
298
+ * Scans for graduation candidates and sends a prompt for user review.
299
+ */
300
+ export async function handleInstinctGraduate(
301
+ _args: string,
302
+ ctx: ExtensionCommandContext,
303
+ pi: ExtensionAPI,
304
+ projectId?: string | null,
305
+ baseDir?: string,
306
+ projectRoot?: string | null
307
+ ): Promise<void> {
308
+ const effectiveBase = baseDir ?? getBaseDir();
309
+
310
+ // Load all instincts
311
+ const projectInstincts = projectId
312
+ ? loadProjectInstincts(projectId, effectiveBase)
313
+ : [];
314
+ const globalInstincts = loadGlobalInstincts(effectiveBase);
315
+ const allInstincts = [...projectInstincts, ...globalInstincts];
316
+
317
+ if (allInstincts.length === 0) {
318
+ ctx.ui.notify(
319
+ "No instincts to analyze. Keep using pi to accumulate instincts first.",
320
+ "info"
321
+ );
322
+ return;
323
+ }
324
+
325
+ // Read AGENTS.md for dedup
326
+ const agentsMdProject =
327
+ projectRoot != null ? readAgentsMd(join(projectRoot, "AGENTS.md")) : null;
328
+ const agentsMdGlobal = readAgentsMd(
329
+ join(homedir(), ".pi", "agent", "AGENTS.md")
330
+ );
331
+ const combinedAgentsMd = [agentsMdProject, agentsMdGlobal]
332
+ .filter(Boolean)
333
+ .join("\n");
334
+
335
+ // Find candidates
336
+ const agentsMdCandidates = findAgentsMdCandidates(
337
+ allInstincts,
338
+ combinedAgentsMd.length > 0 ? combinedAgentsMd : null
339
+ );
340
+
341
+ // Find clusters for skills and commands
342
+ // Only consider non-graduated, non-flagged instincts
343
+ const activeInstincts = allInstincts.filter(
344
+ (i) => i.graduated_to === undefined && !i.flagged_for_removal
345
+ );
346
+ const skillClusters = findSkillCandidates(activeInstincts);
347
+ const commandClusters = findCommandCandidates(activeInstincts);
348
+
349
+ // Enforce TTL
350
+ const ttl = enforceTtl(allInstincts);
351
+
352
+ // Check if there's anything to report
353
+ const hasWork =
354
+ agentsMdCandidates.length > 0 ||
355
+ skillClusters.length > 0 ||
356
+ commandClusters.length > 0 ||
357
+ ttl.toCull.length > 0 ||
358
+ ttl.toDecay.length > 0;
359
+
360
+ if (!hasWork) {
361
+ ctx.ui.notify(
362
+ "No instincts are ready for graduation and no TTL violations found. " +
363
+ "Instincts need >= 7 days age, >= 0.75 confidence, and >= 3 confirmations.",
364
+ "info"
365
+ );
366
+ return;
367
+ }
368
+
369
+ const prompt = buildGraduationPrompt(
370
+ agentsMdCandidates,
371
+ skillClusters,
372
+ commandClusters,
373
+ ttl
374
+ );
375
+
376
+ pi.sendUserMessage(prompt, { deliverAs: "followUp" });
377
+ }
@@ -126,6 +126,12 @@ export function parseInstinct(content: string): Instinct {
126
126
  if (fm["flagged_for_removal"] !== undefined && fm["flagged_for_removal"] !== null) {
127
127
  instinct.flagged_for_removal = Boolean(fm["flagged_for_removal"]);
128
128
  }
129
+ if (fm["graduated_to"] !== undefined && fm["graduated_to"] !== null) {
130
+ (instinct as { graduated_to: string }).graduated_to = String(fm["graduated_to"]);
131
+ }
132
+ if (fm["graduated_at"] !== undefined && fm["graduated_at"] !== null) {
133
+ instinct.graduated_at = String(fm["graduated_at"]);
134
+ }
129
135
 
130
136
  return instinct;
131
137
  }
@@ -165,6 +171,12 @@ export function serializeInstinct(instinct: Instinct): string {
165
171
  if (instinct.flagged_for_removal !== undefined) {
166
172
  frontmatter["flagged_for_removal"] = instinct.flagged_for_removal;
167
173
  }
174
+ if (instinct.graduated_to !== undefined) {
175
+ frontmatter["graduated_to"] = instinct.graduated_to;
176
+ }
177
+ if (instinct.graduated_at !== undefined) {
178
+ frontmatter["graduated_at"] = instinct.graduated_at;
179
+ }
168
180
 
169
181
  const yamlStr = stringifyYaml(frontmatter);
170
182
  return `---\n${yamlStr}---\n\n${instinct.action}\n`;
@@ -14,6 +14,7 @@ import {
14
14
  getProjectInstinctsDir,
15
15
  getGlobalInstinctsDir,
16
16
  } from "./storage.js";
17
+ import { validateInstinct, findSimilarInstinct } from "./instinct-validator.js";
17
18
 
18
19
  function getInstinctsDir(
19
20
  scope: "project" | "global",
@@ -122,11 +123,36 @@ export function createInstinctWriteTool(
122
123
  _onUpdate: unknown,
123
124
  _ctx: unknown
124
125
  ) {
126
+ const validation = validateInstinct({
127
+ action: params.action,
128
+ trigger: params.trigger,
129
+ domain: params.domain,
130
+ });
131
+ if (!validation.valid) {
132
+ throw new Error(`Invalid instinct: ${validation.reason}`);
133
+ }
134
+
125
135
  const dir = getInstinctsDir(params.scope, projectId, baseDir);
126
136
  if (!dir) {
127
137
  throw new Error("Cannot write project-scoped instinct: no project detected");
128
138
  }
129
139
 
140
+ // Dedup check: reject if semantically similar to an existing instinct
141
+ const allInstincts = [
142
+ ...(projectId ? loadProjectInstincts(projectId, baseDir) : []),
143
+ ...loadGlobalInstincts(baseDir),
144
+ ];
145
+ const similar = findSimilarInstinct(
146
+ { trigger: params.trigger, action: params.action },
147
+ allInstincts,
148
+ params.id // skip self on updates
149
+ );
150
+ if (similar) {
151
+ throw new Error(
152
+ `Similar instinct already exists: "${similar.instinct.id}" (similarity: ${(similar.similarity * 100).toFixed(0)}%). Update that instinct instead of creating a duplicate.`
153
+ );
154
+ }
155
+
130
156
  const now = new Date().toISOString();
131
157
  const existing = findInstinctFile(params.id, projectId, baseDir);
132
158