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,243 @@
1
+ /**
2
+ * Graduation pipeline - pure functions for instinct lifecycle management.
3
+ *
4
+ * Determines which instincts are mature enough to graduate into permanent
5
+ * knowledge (AGENTS.md, skills, or commands), and which have exceeded
6
+ * their TTL and should be culled.
7
+ */
8
+
9
+ import type { Instinct, GraduationTarget } from "./types.js";
10
+ import {
11
+ GRADUATION_MIN_AGE_DAYS,
12
+ GRADUATION_MIN_CONFIDENCE,
13
+ GRADUATION_MIN_CONFIRMED,
14
+ GRADUATION_MAX_CONTRADICTED,
15
+ GRADUATION_SKILL_CLUSTER_SIZE,
16
+ GRADUATION_COMMAND_CLUSTER_SIZE,
17
+ GRADUATION_TTL_MAX_DAYS,
18
+ GRADUATION_TTL_CULL_CONFIDENCE,
19
+ } from "./config.js";
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Constants
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Types
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export interface MaturityCheck {
32
+ eligible: boolean;
33
+ reasons: string[];
34
+ }
35
+
36
+ export interface GraduationCandidate {
37
+ instinct: Instinct;
38
+ target: GraduationTarget;
39
+ reason: string;
40
+ }
41
+
42
+ export interface DomainCluster {
43
+ domain: string;
44
+ instincts: Instinct[];
45
+ }
46
+
47
+ export interface TtlResult {
48
+ toCull: Instinct[];
49
+ toDecay: Instinct[];
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Age helpers
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /**
57
+ * Returns the age of an instinct in days based on created_at.
58
+ * Uses a reference date for testability.
59
+ */
60
+ export function getAgeDays(instinct: Instinct, now = Date.now()): number {
61
+ const createdAt = new Date(instinct.created_at).getTime();
62
+ return Math.max(0, (now - createdAt) / MS_PER_DAY);
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Maturity check
67
+ // ---------------------------------------------------------------------------
68
+
69
+ /**
70
+ * Checks whether an instinct meets all graduation maturity criteria.
71
+ * Returns structured result with reasons for any failures.
72
+ */
73
+ export function checkMaturity(
74
+ instinct: Instinct,
75
+ agentsMdContent: string | null,
76
+ now = Date.now()
77
+ ): MaturityCheck {
78
+ const reasons: string[] = [];
79
+
80
+ if (instinct.graduated_to !== undefined) {
81
+ return { eligible: false, reasons: [`Already graduated to ${instinct.graduated_to}`] };
82
+ }
83
+
84
+ if (instinct.flagged_for_removal) {
85
+ return { eligible: false, reasons: ["Flagged for removal"] };
86
+ }
87
+
88
+ const ageDays = getAgeDays(instinct, now);
89
+ if (ageDays < GRADUATION_MIN_AGE_DAYS) {
90
+ reasons.push(
91
+ `Age ${ageDays.toFixed(1)}d < ${GRADUATION_MIN_AGE_DAYS}d minimum`
92
+ );
93
+ }
94
+
95
+ if (instinct.confidence < GRADUATION_MIN_CONFIDENCE) {
96
+ reasons.push(
97
+ `Confidence ${instinct.confidence.toFixed(2)} < ${GRADUATION_MIN_CONFIDENCE} minimum`
98
+ );
99
+ }
100
+
101
+ if (instinct.confirmed_count < GRADUATION_MIN_CONFIRMED) {
102
+ reasons.push(
103
+ `Confirmed ${instinct.confirmed_count} < ${GRADUATION_MIN_CONFIRMED} minimum`
104
+ );
105
+ }
106
+
107
+ if (instinct.contradicted_count > GRADUATION_MAX_CONTRADICTED) {
108
+ reasons.push(
109
+ `Contradicted ${instinct.contradicted_count} > ${GRADUATION_MAX_CONTRADICTED} maximum`
110
+ );
111
+ }
112
+
113
+ // Check for duplicates in AGENTS.md (simple substring match on title/trigger)
114
+ if (agentsMdContent !== null) {
115
+ const lowerContent = agentsMdContent.toLowerCase();
116
+ const titleMatch = lowerContent.includes(instinct.title.toLowerCase());
117
+ const triggerMatch = lowerContent.includes(instinct.trigger.toLowerCase());
118
+ if (titleMatch && triggerMatch) {
119
+ reasons.push("Appears to duplicate existing AGENTS.md content");
120
+ }
121
+ }
122
+
123
+ return { eligible: reasons.length === 0, reasons };
124
+ }
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // Candidate scanning
128
+ // ---------------------------------------------------------------------------
129
+
130
+ /**
131
+ * Finds all instincts that qualify for graduation to AGENTS.md.
132
+ */
133
+ export function findAgentsMdCandidates(
134
+ instincts: Instinct[],
135
+ agentsMdContent: string | null,
136
+ now = Date.now()
137
+ ): GraduationCandidate[] {
138
+ const candidates: GraduationCandidate[] = [];
139
+
140
+ for (const instinct of instincts) {
141
+ const check = checkMaturity(instinct, agentsMdContent, now);
142
+ if (check.eligible) {
143
+ candidates.push({
144
+ instinct,
145
+ target: "agents-md",
146
+ reason: `Mature instinct (${instinct.confidence.toFixed(2)} confidence, ${instinct.confirmed_count} confirmations)`,
147
+ });
148
+ }
149
+ }
150
+
151
+ return candidates;
152
+ }
153
+
154
+ /**
155
+ * Groups instincts by domain, returning only clusters meeting the size threshold.
156
+ */
157
+ export function findDomainClusters(
158
+ instincts: Instinct[],
159
+ minSize: number
160
+ ): DomainCluster[] {
161
+ const byDomain = new Map<string, Instinct[]>();
162
+
163
+ for (const instinct of instincts) {
164
+ if (instinct.graduated_to !== undefined) continue;
165
+ if (instinct.flagged_for_removal) continue;
166
+
167
+ const existing = byDomain.get(instinct.domain) ?? [];
168
+ byDomain.set(instinct.domain, [...existing, instinct]);
169
+ }
170
+
171
+ const clusters: DomainCluster[] = [];
172
+ for (const [domain, domainInstincts] of byDomain) {
173
+ if (domainInstincts.length >= minSize) {
174
+ clusters.push({ domain, instincts: domainInstincts });
175
+ }
176
+ }
177
+
178
+ return clusters.sort((a, b) => b.instincts.length - a.instincts.length);
179
+ }
180
+
181
+ /**
182
+ * Finds instinct clusters that qualify for skill scaffolding.
183
+ */
184
+ export function findSkillCandidates(instincts: Instinct[]): DomainCluster[] {
185
+ return findDomainClusters(instincts, GRADUATION_SKILL_CLUSTER_SIZE);
186
+ }
187
+
188
+ /**
189
+ * Finds instinct clusters that qualify for command scaffolding.
190
+ */
191
+ export function findCommandCandidates(instincts: Instinct[]): DomainCluster[] {
192
+ return findDomainClusters(instincts, GRADUATION_COMMAND_CLUSTER_SIZE);
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // TTL enforcement
197
+ // ---------------------------------------------------------------------------
198
+
199
+ /**
200
+ * Identifies instincts that have exceeded the TTL without graduating.
201
+ * - Instincts with confidence < cull threshold are marked for outright deletion
202
+ * - Others are marked for aggressive decay
203
+ */
204
+ export function enforceTtl(
205
+ instincts: Instinct[],
206
+ now = Date.now()
207
+ ): TtlResult {
208
+ const toCull: Instinct[] = [];
209
+ const toDecay: Instinct[] = [];
210
+
211
+ for (const instinct of instincts) {
212
+ // Skip already-graduated instincts
213
+ if (instinct.graduated_to !== undefined) continue;
214
+
215
+ const ageDays = getAgeDays(instinct, now);
216
+ if (ageDays < GRADUATION_TTL_MAX_DAYS) continue;
217
+
218
+ if (instinct.confidence < GRADUATION_TTL_CULL_CONFIDENCE) {
219
+ toCull.push(instinct);
220
+ } else {
221
+ toDecay.push(instinct);
222
+ }
223
+ }
224
+
225
+ return { toCull, toDecay };
226
+ }
227
+
228
+ /**
229
+ * Marks an instinct as graduated. Returns a new instinct with graduated_to
230
+ * and graduated_at set. Does not mutate the original.
231
+ */
232
+ export function markGraduated(
233
+ instinct: Instinct,
234
+ target: GraduationTarget,
235
+ now = new Date()
236
+ ): Instinct {
237
+ return {
238
+ ...instinct,
239
+ graduated_to: target,
240
+ graduated_at: now.toISOString(),
241
+ updated_at: now.toISOString(),
242
+ };
243
+ }
package/src/index.ts CHANGED
@@ -32,6 +32,7 @@ import { handleInstinctImport, COMMAND_NAME as IMPORT_CMD } from "./instinct-imp
32
32
  import { handleInstinctPromote, COMMAND_NAME as PROMOTE_CMD } from "./instinct-promote.js";
33
33
  import { handleInstinctEvolve, COMMAND_NAME as EVOLVE_CMD } from "./instinct-evolve.js";
34
34
  import { handleInstinctProjects, COMMAND_NAME as PROJECTS_CMD } from "./instinct-projects.js";
35
+ import { handleInstinctGraduate, COMMAND_NAME as GRADUATE_CMD } from "./instinct-graduate.js";
35
36
  import { registerAllTools } from "./instinct-tools.js";
36
37
  import { logError } from "./error-logger.js";
37
38
  import type { Config, InstalledSkill, ProjectEntry } from "./types.js";
@@ -193,4 +194,17 @@ export default function (pi: ExtensionAPI): void {
193
194
  handler: (args: string, ctx: ExtensionCommandContext) =>
194
195
  handleInstinctProjects(args, ctx),
195
196
  });
197
+
198
+ pi.registerCommand(GRADUATE_CMD, {
199
+ description: "Graduate mature instincts to AGENTS.md, skills, or commands",
200
+ handler: (args: string, ctx: ExtensionCommandContext) =>
201
+ handleInstinctGraduate(
202
+ args,
203
+ ctx,
204
+ pi,
205
+ project?.id,
206
+ undefined,
207
+ project?.root ?? null
208
+ ),
209
+ });
196
210
  }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Auto-cleanup rules for instinct volume control.
3
+ *
4
+ * Cleanup is run at the start of each analysis pass, before decay.
5
+ * Rules (all thresholds are config-driven):
6
+ * 1. Delete flagged_for_removal instincts older than `flagged_cleanup_days`.
7
+ * 2. Delete zero-confirmation instincts older than `instinct_ttl_days`.
8
+ * 3. Enforce per-dir hard caps by deleting lowest-confidence instincts.
9
+ */
10
+
11
+ import { unlinkSync, existsSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import type { Instinct, Config } from "./types.js";
14
+ import { listInstincts, invalidateCache } from "./instinct-store.js";
15
+ import {
16
+ getBaseDir,
17
+ getProjectInstinctsDir,
18
+ getGlobalInstinctsDir,
19
+ } from "./storage.js";
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Helpers
23
+ // ---------------------------------------------------------------------------
24
+
25
+ function ageInDays(isoDate: string): number {
26
+ return (Date.now() - new Date(isoDate).getTime()) / (24 * 60 * 60 * 1000);
27
+ }
28
+
29
+ function deleteInstinctFile(instinct: Instinct, dir: string): boolean {
30
+ const filePath = join(dir, `${instinct.id}.md`);
31
+ if (!existsSync(filePath)) return false;
32
+ try {
33
+ unlinkSync(filePath);
34
+ return true;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Cleanup rules
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /**
45
+ * Deletes instincts marked `flagged_for_removal` whose `updated_at` is older
46
+ * than `flaggedCleanupDays`. `updated_at` is set when the flag is applied,
47
+ * so it serves as a proxy for when the instinct was flagged.
48
+ *
49
+ * @returns Number of instincts deleted.
50
+ */
51
+ export function cleanupFlaggedInstincts(
52
+ dir: string,
53
+ flaggedCleanupDays: number
54
+ ): number {
55
+ const instincts = listInstincts(dir);
56
+ let deleted = 0;
57
+ for (const instinct of instincts) {
58
+ if (
59
+ instinct.flagged_for_removal &&
60
+ ageInDays(instinct.updated_at) >= flaggedCleanupDays
61
+ ) {
62
+ if (deleteInstinctFile(instinct, dir)) {
63
+ deleted++;
64
+ }
65
+ }
66
+ }
67
+ if (deleted > 0) invalidateCache(dir);
68
+ return deleted;
69
+ }
70
+
71
+ /**
72
+ * Deletes instincts with `confirmed_count === 0` whose `created_at` is older
73
+ * than `ttlDays`. These instincts were never validated by the agent and have
74
+ * aged out of relevance.
75
+ *
76
+ * @returns Number of instincts deleted.
77
+ */
78
+ export function cleanupZeroConfirmedInstincts(
79
+ dir: string,
80
+ ttlDays: number
81
+ ): number {
82
+ const instincts = listInstincts(dir);
83
+ let deleted = 0;
84
+ for (const instinct of instincts) {
85
+ if (
86
+ instinct.confirmed_count === 0 &&
87
+ ageInDays(instinct.created_at) >= ttlDays
88
+ ) {
89
+ if (deleteInstinctFile(instinct, dir)) {
90
+ deleted++;
91
+ }
92
+ }
93
+ }
94
+ if (deleted > 0) invalidateCache(dir);
95
+ return deleted;
96
+ }
97
+
98
+ /**
99
+ * Enforces a hard cap on the number of instincts in a directory.
100
+ * When the count exceeds `maxCount`, deletes the lowest-confidence instincts
101
+ * until the count is at or below the cap.
102
+ *
103
+ * @returns Number of instincts deleted.
104
+ */
105
+ export function enforceInstinctCap(dir: string, maxCount: number): number {
106
+ const instincts = listInstincts(dir);
107
+ if (instincts.length <= maxCount) return 0;
108
+
109
+ // Sort ascending by confidence - lowest confidence deleted first
110
+ const sorted = [...instincts].sort((a, b) => a.confidence - b.confidence);
111
+ const toDelete = sorted.slice(0, instincts.length - maxCount);
112
+
113
+ let deleted = 0;
114
+ for (const instinct of toDelete) {
115
+ if (deleteInstinctFile(instinct, dir)) {
116
+ deleted++;
117
+ }
118
+ }
119
+ if (deleted > 0) invalidateCache(dir);
120
+ return deleted;
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Result type
125
+ // ---------------------------------------------------------------------------
126
+
127
+ export interface CleanupResult {
128
+ flaggedDeleted: number;
129
+ zeroConfirmedDeleted: number;
130
+ capDeleted: number;
131
+ total: number;
132
+ }
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Orchestrator
136
+ // ---------------------------------------------------------------------------
137
+
138
+ /**
139
+ * Runs all cleanup rules against a single directory.
140
+ * Order: flagged → zero-confirmed → cap enforcement (cap runs last so it
141
+ * accounts for deletions made by the earlier rules).
142
+ */
143
+ export function cleanupDir(
144
+ dir: string,
145
+ config: Config,
146
+ maxCount: number
147
+ ): CleanupResult {
148
+ const flaggedDeleted = cleanupFlaggedInstincts(dir, config.flagged_cleanup_days);
149
+ const zeroConfirmedDeleted = cleanupZeroConfirmedInstincts(
150
+ dir,
151
+ config.instinct_ttl_days
152
+ );
153
+ const capDeleted = enforceInstinctCap(dir, maxCount);
154
+ const total = flaggedDeleted + zeroConfirmedDeleted + capDeleted;
155
+ return { flaggedDeleted, zeroConfirmedDeleted, capDeleted, total };
156
+ }
157
+
158
+ /**
159
+ * Runs a full cleanup pass over project and global instinct directories.
160
+ * Called at the start of each analysis run, before decay.
161
+ *
162
+ * @param projectId - Project ID to clean up (skipped when null/undefined)
163
+ * @param config - Runtime config (provides all thresholds)
164
+ * @param baseDir - Base storage directory (defaults to ~/.pi/continuous-learning/)
165
+ * @returns Aggregated cleanup result across both scopes
166
+ */
167
+ export function runCleanupPass(
168
+ projectId: string | null | undefined,
169
+ config: Config,
170
+ baseDir = getBaseDir()
171
+ ): CleanupResult {
172
+ const result: CleanupResult = {
173
+ flaggedDeleted: 0,
174
+ zeroConfirmedDeleted: 0,
175
+ capDeleted: 0,
176
+ total: 0,
177
+ };
178
+
179
+ if (projectId) {
180
+ const projectDir = getProjectInstinctsDir(projectId, "personal", baseDir);
181
+ const projectResult = cleanupDir(
182
+ projectDir,
183
+ config,
184
+ config.max_total_instincts_per_project
185
+ );
186
+ result.flaggedDeleted += projectResult.flaggedDeleted;
187
+ result.zeroConfirmedDeleted += projectResult.zeroConfirmedDeleted;
188
+ result.capDeleted += projectResult.capDeleted;
189
+ result.total += projectResult.total;
190
+ }
191
+
192
+ const globalDir = getGlobalInstinctsDir("personal", baseDir);
193
+ const globalResult = cleanupDir(
194
+ globalDir,
195
+ config,
196
+ config.max_total_instincts_global
197
+ );
198
+ result.flaggedDeleted += globalResult.flaggedDeleted;
199
+ result.zeroConfirmedDeleted += globalResult.zeroConfirmedDeleted;
200
+ result.capDeleted += globalResult.capDeleted;
201
+ result.total += globalResult.total;
202
+
203
+ return result;
204
+ }