pi-continuous-learning 0.6.0 → 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 (77) 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 +8 -2
  7. package/dist/cli/analyze-single-shot.d.ts.map +1 -1
  8. package/dist/cli/analyze-single-shot.js +25 -3
  9. package/dist/cli/analyze-single-shot.js.map +1 -1
  10. package/dist/cli/analyze.js +20 -8
  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/prompts/analyzer-system-single-shot.d.ts.map +1 -1
  49. package/dist/prompts/analyzer-system-single-shot.js +41 -1
  50. package/dist/prompts/analyzer-system-single-shot.js.map +1 -1
  51. package/dist/prompts/analyzer-user-single-shot.d.ts.map +1 -1
  52. package/dist/prompts/analyzer-user-single-shot.js +1 -1
  53. package/dist/prompts/analyzer-user-single-shot.js.map +1 -1
  54. package/dist/skill-scaffold.d.ts +23 -0
  55. package/dist/skill-scaffold.d.ts.map +1 -0
  56. package/dist/skill-scaffold.js +62 -0
  57. package/dist/skill-scaffold.js.map +1 -0
  58. package/dist/types.d.ts +8 -0
  59. package/dist/types.d.ts.map +1 -1
  60. package/package.json +1 -1
  61. package/src/agents-md.ts +73 -3
  62. package/src/cli/analyze-single-shot.ts +33 -3
  63. package/src/cli/analyze.ts +19 -8
  64. package/src/command-scaffold.ts +105 -0
  65. package/src/confidence.ts +2 -1
  66. package/src/config.ts +40 -0
  67. package/src/graduation.ts +243 -0
  68. package/src/index.ts +14 -0
  69. package/src/instinct-cleanup.ts +204 -0
  70. package/src/instinct-graduate.ts +377 -0
  71. package/src/instinct-parser.ts +12 -0
  72. package/src/instinct-tools.ts +26 -0
  73. package/src/instinct-validator.ts +287 -0
  74. package/src/prompts/analyzer-system-single-shot.ts +41 -1
  75. package/src/prompts/analyzer-user-single-shot.ts +6 -0
  76. package/src/skill-scaffold.ts +90 -0
  77. package/src/types.ts +10 -0
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Instinct validation and semantic deduplication.
3
+ * Rejects instincts with empty, undefined, nonsense, or low-quality fields,
4
+ * and detects near-duplicate instincts via Jaccard similarity.
5
+ */
6
+
7
+ import type { Instinct } from "./types.js";
8
+
9
+ const MIN_FIELD_LENGTH = 10;
10
+ const INVALID_LITERALS = new Set(["undefined", "null", "none", ""]);
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Known domains (with "other" as escape hatch)
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export const KNOWN_DOMAINS = new Set([
17
+ "git",
18
+ "testing",
19
+ "debugging",
20
+ "workflow",
21
+ "typescript",
22
+ "javascript",
23
+ "python",
24
+ "go",
25
+ "css",
26
+ "design",
27
+ "security",
28
+ "performance",
29
+ "documentation",
30
+ "react",
31
+ "node",
32
+ "database",
33
+ "api",
34
+ "devops",
35
+ "architecture",
36
+ "other",
37
+ ]);
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Verb heuristic for action field
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /** Common imperative verbs expected at the start of an instinct action. */
44
+ export const KNOWN_VERBS = new Set([
45
+ "add",
46
+ "always",
47
+ "analyze",
48
+ "apply",
49
+ "ask",
50
+ "avoid",
51
+ "build",
52
+ "call",
53
+ "catch",
54
+ "check",
55
+ "clean",
56
+ "confirm",
57
+ "consider",
58
+ "create",
59
+ "define",
60
+ "delete",
61
+ "document",
62
+ "emit",
63
+ "ensure",
64
+ "exclude",
65
+ "export",
66
+ "extract",
67
+ "fetch",
68
+ "find",
69
+ "fix",
70
+ "follow",
71
+ "format",
72
+ "generate",
73
+ "get",
74
+ "handle",
75
+ "import",
76
+ "include",
77
+ "inspect",
78
+ "load",
79
+ "log",
80
+ "look",
81
+ "merge",
82
+ "monitor",
83
+ "move",
84
+ "never",
85
+ "parse",
86
+ "pass",
87
+ "prefer",
88
+ "print",
89
+ "read",
90
+ "record",
91
+ "refactor",
92
+ "reject",
93
+ "rename",
94
+ "require",
95
+ "resolve",
96
+ "return",
97
+ "run",
98
+ "save",
99
+ "scan",
100
+ "search",
101
+ "send",
102
+ "set",
103
+ "show",
104
+ "skip",
105
+ "start",
106
+ "stop",
107
+ "test",
108
+ "track",
109
+ "update",
110
+ "use",
111
+ "validate",
112
+ "verify",
113
+ "watch",
114
+ "wrap",
115
+ "write",
116
+ ]);
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Validation types
120
+ // ---------------------------------------------------------------------------
121
+
122
+ export interface ValidationResult {
123
+ valid: boolean;
124
+ reason?: string;
125
+ /** Non-fatal warnings that indicate lower-quality instincts. */
126
+ warnings?: string[];
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Internal helpers
131
+ // ---------------------------------------------------------------------------
132
+
133
+ function isInvalidField(value: unknown, fieldName: string): string | null {
134
+ if (value === undefined || value === null) {
135
+ return `${fieldName} is ${String(value)}`;
136
+ }
137
+ if (typeof value !== "string") {
138
+ return `${fieldName} is not a string (got ${typeof value})`;
139
+ }
140
+ const trimmed = value.trim();
141
+ if (INVALID_LITERALS.has(trimmed.toLowerCase())) {
142
+ return `${fieldName} is the literal string "${trimmed}"`;
143
+ }
144
+ if (trimmed.length < MIN_FIELD_LENGTH) {
145
+ return `${fieldName} is too short (${trimmed.length} chars, minimum ${MIN_FIELD_LENGTH})`;
146
+ }
147
+ return null;
148
+ }
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // validateInstinct
152
+ // ---------------------------------------------------------------------------
153
+
154
+ /**
155
+ * Validates that an instinct's fields meet quality requirements.
156
+ *
157
+ * Rules:
158
+ * - action and trigger must be non-empty, non-null, non-"undefined", and >= 10 chars
159
+ * - action first word should be a known imperative verb (warning if not)
160
+ * - domain, if provided, must be in KNOWN_DOMAINS (with "other" as escape hatch)
161
+ *
162
+ * Returns { valid: true } or { valid: false, reason: "..." }.
163
+ * Non-fatal issues are reported in the optional `warnings` array.
164
+ */
165
+ export function validateInstinct(fields: {
166
+ action: unknown;
167
+ trigger: unknown;
168
+ domain?: unknown;
169
+ }): ValidationResult {
170
+ const actionError = isInvalidField(fields.action, "action");
171
+ if (actionError) {
172
+ return { valid: false, reason: actionError };
173
+ }
174
+
175
+ const triggerError = isInvalidField(fields.trigger, "trigger");
176
+ if (triggerError) {
177
+ return { valid: false, reason: triggerError };
178
+ }
179
+
180
+ const warnings: string[] = [];
181
+
182
+ // Domain validation
183
+ if (fields.domain !== undefined) {
184
+ if (typeof fields.domain !== "string" || !KNOWN_DOMAINS.has(fields.domain.toLowerCase().trim())) {
185
+ return {
186
+ valid: false,
187
+ reason: `domain "${String(fields.domain)}" is not in the known set. Use one of: ${[...KNOWN_DOMAINS].join(", ")}`,
188
+ };
189
+ }
190
+ }
191
+
192
+ // Verb heuristic (warning only - does not reject)
193
+ const action = (fields.action as string).trim();
194
+ const firstWord = action.split(/\s+/)[0]?.toLowerCase() ?? "";
195
+ if (firstWord && !KNOWN_VERBS.has(firstWord)) {
196
+ warnings.push(
197
+ `action should start with an imperative verb (got "${firstWord}"). Consider rewriting as a clear instruction.`
198
+ );
199
+ }
200
+
201
+ return warnings.length > 0 ? { valid: true, warnings } : { valid: true };
202
+ }
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // Jaccard similarity deduplication
206
+ // ---------------------------------------------------------------------------
207
+
208
+ const STOP_WORDS = new Set([
209
+ "a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
210
+ "have", "has", "had", "do", "does", "did", "will", "would", "should",
211
+ "could", "may", "might", "shall", "can", "of", "in", "on", "at", "to",
212
+ "for", "with", "by", "from", "up", "about", "into", "it", "its", "this",
213
+ "that", "these", "those", "and", "or", "but", "if", "as", "when", "where",
214
+ "how", "what", "which", "who", "not", "no", "so",
215
+ ]);
216
+
217
+ /**
218
+ * Tokenizes text into a set of meaningful lowercase words.
219
+ * Splits on non-alphanumeric characters and filters stop words and short tokens.
220
+ */
221
+ export function tokenize(text: string): Set<string> {
222
+ return new Set(
223
+ text
224
+ .toLowerCase()
225
+ .split(/[^a-z0-9]+/)
226
+ .filter((t) => t.length > 2 && !STOP_WORDS.has(t))
227
+ );
228
+ }
229
+
230
+ /**
231
+ * Computes Jaccard similarity between two token sets.
232
+ * Returns 1.0 for two empty sets, 0.0 if one is empty.
233
+ */
234
+ export function jaccardSimilarity(a: Set<string>, b: Set<string>): number {
235
+ if (a.size === 0 && b.size === 0) return 1.0;
236
+ if (a.size === 0 || b.size === 0) return 0.0;
237
+
238
+ let intersection = 0;
239
+ for (const token of a) {
240
+ if (b.has(token)) intersection++;
241
+ }
242
+
243
+ const union = a.size + b.size - intersection;
244
+ return intersection / union;
245
+ }
246
+
247
+ export interface SimilarityMatch {
248
+ instinct: Instinct;
249
+ similarity: number;
250
+ }
251
+
252
+ /**
253
+ * Checks whether a candidate instinct is semantically similar to any existing instinct.
254
+ *
255
+ * Tokenizes trigger+action for both candidate and each existing instinct,
256
+ * computes Jaccard similarity, and returns the closest match above the threshold.
257
+ *
258
+ * @param candidate - The new instinct being considered (trigger + action)
259
+ * @param existing - All current instincts to check against
260
+ * @param skipId - ID to skip (the candidate's own ID on updates)
261
+ * @param threshold - Similarity threshold; default 0.6
262
+ */
263
+ export function findSimilarInstinct(
264
+ candidate: { trigger: string; action: string },
265
+ existing: Instinct[],
266
+ skipId?: string,
267
+ threshold = 0.6
268
+ ): SimilarityMatch | null {
269
+ const candidateTokens = tokenize(`${candidate.trigger} ${candidate.action}`);
270
+
271
+ let bestMatch: SimilarityMatch | null = null;
272
+
273
+ for (const instinct of existing) {
274
+ if (instinct.id === skipId) continue;
275
+
276
+ const existingTokens = tokenize(`${instinct.trigger} ${instinct.action}`);
277
+ const similarity = jaccardSimilarity(candidateTokens, existingTokens);
278
+
279
+ if (similarity >= threshold) {
280
+ if (bestMatch === null || similarity > bestMatch.similarity) {
281
+ bestMatch = { instinct, similarity };
282
+ }
283
+ }
284
+ }
285
+
286
+ return bestMatch;
287
+ }
@@ -119,5 +119,45 @@ When in doubt, prefer project scope.
119
119
  4. New instincts from observation data alone are capped at 0.85 confidence.
120
120
  5. Check existing instincts (provided in the user message) for duplicates before creating. Update instead.
121
121
  6. Write actions as clear instructions starting with a verb.
122
- 7. Be skeptical of outliers - patterns seen only in unusual circumstances should not become instincts.`;
122
+ 7. Be skeptical of outliers - patterns seen only in unusual circumstances should not become instincts.
123
+
124
+ ## Quality Tiers
125
+
126
+ Not all patterns are worth recording as instincts. Use this classification before creating:
127
+
128
+ ### Tier 1 - Project Conventions (RECORD as instinct)
129
+ Patterns specific to this project's tech stack, codebase conventions, or team practices.
130
+ Examples:
131
+ - "Use the Result<T, E> type for error handling in this project"
132
+ - "Place tests next to source files as *.test.ts"
133
+ - "Prefer functional style - avoid class-based patterns in this codebase"
134
+
135
+ ### Tier 2 - Workflow Patterns (RECORD as global instinct)
136
+ Recurring multi-step workflows that apply across many projects.
137
+ Examples:
138
+ - "Run linter and type-checker after every code change"
139
+ - "Write tests before implementation in TDD projects"
140
+
141
+ ### Tier 3 - Generic Agent Behavior (DO NOT RECORD - already known)
142
+ Fundamental behaviors all coding agents should follow. These are not project-specific patterns.
143
+
144
+ **DO NOT create instincts for:**
145
+ - Reading files before editing them ("read before edit")
146
+ - Grepping for context before modifying code
147
+ - Clarifying ambiguous requirements before starting
148
+ - Using conventional commit message formats
149
+ - Checking for errors after tool calls
150
+ - Any behavior that is basic good practice for any coding agent
151
+
152
+ These patterns belong in AGENTS.md from the start, not in learned instincts.
153
+
154
+ ## AGENTS.md Deduplication
155
+
156
+ The user message includes the current AGENTS.md content for this project and globally.
157
+ Before creating any instinct, check: **is this pattern already covered by AGENTS.md?**
158
+
159
+ If yes: do NOT create an instinct. The pattern is already enforced.
160
+ If a pattern appears in AGENTS.md in the same form, skip it entirely.
161
+
162
+ Only create instincts for patterns that are genuinely absent from AGENTS.md.`;
123
163
  }
@@ -80,6 +80,12 @@ export function buildSingleShotUserPrompt(
80
80
  "3. Return a JSON change-set: create new instincts, update existing ones, or delete obsolete ones.",
81
81
  "4. Apply feedback analysis using the active_instincts field in each observation.",
82
82
  "5. Passive confidence decay has already been applied before this analysis.",
83
+ "6. Before creating any instinct, check the Existing Guidelines section above.",
84
+ " If the pattern is already covered by AGENTS.md, do NOT create an instinct for it.",
85
+ "7. Apply the Quality Tier rules from the system prompt:",
86
+ " - Generic agent behaviors (read-before-edit, clarify-before-implement) -> skip entirely",
87
+ " - Project-specific patterns -> project-scoped instinct",
88
+ " - Universal workflow patterns -> global-scoped instinct",
83
89
  "",
84
90
  "Return ONLY the JSON object. No prose, no markdown fences."
85
91
  );
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Skill scaffolding from instinct clusters.
3
+ *
4
+ * When 3+ related instincts in the same domain form a cohesive topic,
5
+ * generates a SKILL.md file that can be installed as a Pi skill.
6
+ */
7
+
8
+ import type { Instinct } from "./types.js";
9
+ import type { DomainCluster } from "./graduation.js";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Types
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export interface SkillScaffold {
16
+ name: string;
17
+ description: string;
18
+ domain: string;
19
+ content: string;
20
+ sourceInstinctIds: string[];
21
+ }
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function toSkillName(domain: string): string {
28
+ return domain.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
29
+ }
30
+
31
+ function formatInstinctAsSection(instinct: Instinct, index: number): string {
32
+ return [
33
+ `### ${index + 1}. ${instinct.title}`,
34
+ "",
35
+ `**When:** ${instinct.trigger}`,
36
+ "",
37
+ instinct.action,
38
+ "",
39
+ ].join("\n");
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Public API
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /**
47
+ * Generates a SKILL.md scaffold from a domain cluster of instincts.
48
+ */
49
+ export function generateSkillScaffold(cluster: DomainCluster): SkillScaffold {
50
+ const name = toSkillName(cluster.domain);
51
+ const sortedInstincts = [...cluster.instincts].sort(
52
+ (a, b) => b.confidence - a.confidence
53
+ );
54
+
55
+ const description = `Learned ${cluster.domain} patterns from coding sessions. ` +
56
+ `Covers ${sortedInstincts.length} practices distilled from instinct observations.`;
57
+
58
+ const sections = sortedInstincts.map((inst, i) => formatInstinctAsSection(inst, i));
59
+
60
+ const content = [
61
+ `# ${cluster.domain} Skill`,
62
+ "",
63
+ `> Auto-generated from ${sortedInstincts.length} graduated instincts in the "${cluster.domain}" domain.`,
64
+ "",
65
+ `## Description`,
66
+ "",
67
+ description,
68
+ "",
69
+ `## Practices`,
70
+ "",
71
+ ...sections,
72
+ ].join("\n");
73
+
74
+ return {
75
+ name,
76
+ description,
77
+ domain: cluster.domain,
78
+ content,
79
+ sourceInstinctIds: sortedInstincts.map((i) => i.id),
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Generates skill scaffolds for all qualifying clusters.
85
+ */
86
+ export function generateAllSkillScaffolds(
87
+ clusters: DomainCluster[]
88
+ ): SkillScaffold[] {
89
+ return clusters.map(generateSkillScaffold);
90
+ }
package/src/types.ts CHANGED
@@ -67,8 +67,12 @@ export interface Instinct {
67
67
  inactive_count: number;
68
68
  evidence?: string[];
69
69
  flagged_for_removal?: boolean;
70
+ graduated_to?: GraduationTarget;
71
+ graduated_at?: string; // ISO 8601
70
72
  }
71
73
 
74
+ export type GraduationTarget = "agents-md" | "skill" | "command";
75
+
72
76
  // ---------------------------------------------------------------------------
73
77
  // ProjectEntry
74
78
  // ---------------------------------------------------------------------------
@@ -103,4 +107,10 @@ export interface Config {
103
107
  active_hours_end: number; // 0-23
104
108
  max_idle_seconds: number;
105
109
  log_path?: string; // Override analyzer log location (default: ~/.pi/continuous-learning/analyzer.log)
110
+ // Volume control
111
+ max_total_instincts_per_project: number; // hard cap per project (enforced by auto-deletion)
112
+ max_total_instincts_global: number; // hard cap for global instincts (enforced by auto-deletion)
113
+ max_new_instincts_per_run: number; // creation rate limit per analyzer run
114
+ flagged_cleanup_days: number; // auto-delete flagged instincts after N days
115
+ instinct_ttl_days: number; // auto-delete zero-confirmation instincts after N days
106
116
  }