pi-continuous-learning 0.3.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 (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +326 -0
  3. package/dist/active-instincts.d.ts +4 -0
  4. package/dist/active-instincts.d.ts.map +1 -0
  5. package/dist/active-instincts.js +11 -0
  6. package/dist/active-instincts.js.map +1 -0
  7. package/dist/agents-md.d.ts +12 -0
  8. package/dist/agents-md.d.ts.map +1 -0
  9. package/dist/agents-md.js +23 -0
  10. package/dist/agents-md.js.map +1 -0
  11. package/dist/cli/analyze-prompt.d.ts +2 -0
  12. package/dist/cli/analyze-prompt.d.ts.map +1 -0
  13. package/dist/cli/analyze-prompt.js +72 -0
  14. package/dist/cli/analyze-prompt.js.map +1 -0
  15. package/dist/cli/analyze.d.ts +3 -0
  16. package/dist/cli/analyze.d.ts.map +1 -0
  17. package/dist/cli/analyze.js +214 -0
  18. package/dist/cli/analyze.js.map +1 -0
  19. package/dist/confidence.d.ts +25 -0
  20. package/dist/confidence.d.ts.map +1 -0
  21. package/dist/confidence.js +77 -0
  22. package/dist/confidence.js.map +1 -0
  23. package/dist/config.d.ts +19 -0
  24. package/dist/config.d.ts.map +1 -0
  25. package/dist/config.js +89 -0
  26. package/dist/config.js.map +1 -0
  27. package/dist/error-logger.d.ts +34 -0
  28. package/dist/error-logger.d.ts.map +1 -0
  29. package/dist/error-logger.js +102 -0
  30. package/dist/error-logger.js.map +1 -0
  31. package/dist/index.d.ts +10 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +118 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/instinct-decay.d.ts +39 -0
  36. package/dist/instinct-decay.d.ts.map +1 -0
  37. package/dist/instinct-decay.js +93 -0
  38. package/dist/instinct-decay.js.map +1 -0
  39. package/dist/instinct-evolve.d.ts +5 -0
  40. package/dist/instinct-evolve.d.ts.map +1 -0
  41. package/dist/instinct-evolve.js +24 -0
  42. package/dist/instinct-evolve.js.map +1 -0
  43. package/dist/instinct-export.d.ts +40 -0
  44. package/dist/instinct-export.d.ts.map +1 -0
  45. package/dist/instinct-export.js +94 -0
  46. package/dist/instinct-export.js.map +1 -0
  47. package/dist/instinct-import.d.ts +50 -0
  48. package/dist/instinct-import.d.ts.map +1 -0
  49. package/dist/instinct-import.js +168 -0
  50. package/dist/instinct-import.js.map +1 -0
  51. package/dist/instinct-injector.d.ts +39 -0
  52. package/dist/instinct-injector.d.ts.map +1 -0
  53. package/dist/instinct-injector.js +89 -0
  54. package/dist/instinct-injector.js.map +1 -0
  55. package/dist/instinct-loader.d.ts +37 -0
  56. package/dist/instinct-loader.d.ts.map +1 -0
  57. package/dist/instinct-loader.js +96 -0
  58. package/dist/instinct-loader.js.map +1 -0
  59. package/dist/instinct-parser.d.ts +28 -0
  60. package/dist/instinct-parser.d.ts.map +1 -0
  61. package/dist/instinct-parser.js +143 -0
  62. package/dist/instinct-parser.js.map +1 -0
  63. package/dist/instinct-projects.d.ts +32 -0
  64. package/dist/instinct-projects.d.ts.map +1 -0
  65. package/dist/instinct-projects.js +96 -0
  66. package/dist/instinct-projects.js.map +1 -0
  67. package/dist/instinct-promote.d.ts +51 -0
  68. package/dist/instinct-promote.d.ts.map +1 -0
  69. package/dist/instinct-promote.js +169 -0
  70. package/dist/instinct-promote.js.map +1 -0
  71. package/dist/instinct-status.d.ts +39 -0
  72. package/dist/instinct-status.d.ts.map +1 -0
  73. package/dist/instinct-status.js +108 -0
  74. package/dist/instinct-status.js.map +1 -0
  75. package/dist/instinct-store.d.ts +30 -0
  76. package/dist/instinct-store.d.ts.map +1 -0
  77. package/dist/instinct-store.js +118 -0
  78. package/dist/instinct-store.js.map +1 -0
  79. package/dist/instinct-tools.d.ts +161 -0
  80. package/dist/instinct-tools.d.ts.map +1 -0
  81. package/dist/instinct-tools.js +240 -0
  82. package/dist/instinct-tools.js.map +1 -0
  83. package/dist/observations.d.ts +22 -0
  84. package/dist/observations.d.ts.map +1 -0
  85. package/dist/observations.js +62 -0
  86. package/dist/observations.js.map +1 -0
  87. package/dist/observer-guard.d.ts +3 -0
  88. package/dist/observer-guard.d.ts.map +1 -0
  89. package/dist/observer-guard.js +13 -0
  90. package/dist/observer-guard.js.map +1 -0
  91. package/dist/project.d.ts +16 -0
  92. package/dist/project.d.ts.map +1 -0
  93. package/dist/project.js +59 -0
  94. package/dist/project.js.map +1 -0
  95. package/dist/prompt-observer.d.ts +25 -0
  96. package/dist/prompt-observer.d.ts.map +1 -0
  97. package/dist/prompt-observer.js +63 -0
  98. package/dist/prompt-observer.js.map +1 -0
  99. package/dist/prompts/analyzer-user.d.ts +38 -0
  100. package/dist/prompts/analyzer-user.d.ts.map +1 -0
  101. package/dist/prompts/analyzer-user.js +105 -0
  102. package/dist/prompts/analyzer-user.js.map +1 -0
  103. package/dist/prompts/evolve-prompt.d.ts +3 -0
  104. package/dist/prompts/evolve-prompt.d.ts.map +1 -0
  105. package/dist/prompts/evolve-prompt.js +51 -0
  106. package/dist/prompts/evolve-prompt.js.map +1 -0
  107. package/dist/scrubber.d.ts +9 -0
  108. package/dist/scrubber.d.ts.map +1 -0
  109. package/dist/scrubber.js +40 -0
  110. package/dist/scrubber.js.map +1 -0
  111. package/dist/storage.d.ts +22 -0
  112. package/dist/storage.d.ts.map +1 -0
  113. package/dist/storage.js +71 -0
  114. package/dist/storage.js.map +1 -0
  115. package/dist/tool-observer.d.ts +32 -0
  116. package/dist/tool-observer.d.ts.map +1 -0
  117. package/dist/tool-observer.js +77 -0
  118. package/dist/tool-observer.js.map +1 -0
  119. package/dist/types.d.ts +64 -0
  120. package/dist/types.d.ts.map +1 -0
  121. package/dist/types.js +6 -0
  122. package/dist/types.js.map +1 -0
  123. package/package.json +66 -0
  124. package/src/active-instincts.ts +13 -0
  125. package/src/agents-md.ts +23 -0
  126. package/src/cli/analyze-prompt.ts +71 -0
  127. package/src/cli/analyze.ts +286 -0
  128. package/src/confidence.ts +103 -0
  129. package/src/config.ts +111 -0
  130. package/src/error-logger.ts +130 -0
  131. package/src/index.ts +144 -0
  132. package/src/instinct-decay.ts +117 -0
  133. package/src/instinct-evolve.ts +44 -0
  134. package/src/instinct-export.ts +138 -0
  135. package/src/instinct-import.ts +260 -0
  136. package/src/instinct-injector.ts +128 -0
  137. package/src/instinct-loader.ts +146 -0
  138. package/src/instinct-parser.ts +171 -0
  139. package/src/instinct-projects.ts +119 -0
  140. package/src/instinct-promote.ts +231 -0
  141. package/src/instinct-status.ts +135 -0
  142. package/src/instinct-store.ts +149 -0
  143. package/src/instinct-tools.ts +340 -0
  144. package/src/observations.ts +82 -0
  145. package/src/observer-guard.ts +14 -0
  146. package/src/project.ts +70 -0
  147. package/src/prompt-observer.ts +92 -0
  148. package/src/prompts/analyzer-user.ts +156 -0
  149. package/src/prompts/evolve-prompt.ts +71 -0
  150. package/src/scrubber.ts +42 -0
  151. package/src/storage.ts +91 -0
  152. package/src/tool-observer.ts +114 -0
  153. package/src/types.ts +90 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * User prompt and agent end observation handlers for pi-continuous-learning.
3
+ * Captures before_agent_start (user_prompt) and agent_end events as JSONL observations.
4
+ */
5
+
6
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
7
+
8
+ // Local event type definitions (not all pi-coding-agent versions re-export these at top level)
9
+ export interface BeforeAgentStartEvent {
10
+ type: "before_agent_start";
11
+ prompt: string;
12
+ systemPrompt: string;
13
+ }
14
+
15
+ export interface AgentEndEvent {
16
+ type: "agent_end";
17
+ }
18
+
19
+ import { getCurrentActiveInstincts } from "./active-instincts.js";
20
+ import { appendObservation } from "./observations.js";
21
+ import { shouldSkipObservation } from "./observer-guard.js";
22
+ import { scrubSecrets } from "./scrubber.js";
23
+ import { logError } from "./error-logger.js";
24
+ import type { Observation, ProjectEntry } from "./types.js";
25
+
26
+ function getSessionId(ctx: ExtensionContext): string {
27
+ return ctx.sessionManager.getSessionId();
28
+ }
29
+
30
+ function buildActiveInstincts(): Pick<Observation, "active_instincts"> {
31
+ const ids = getCurrentActiveInstincts();
32
+ return ids.length > 0 ? { active_instincts: ids } : {};
33
+ }
34
+
35
+ /**
36
+ * Handles before_agent_start events.
37
+ * Records an observation with event: user_prompt and scrubbed prompt text.
38
+ */
39
+ export function handleBeforeAgentStart(
40
+ event: BeforeAgentStartEvent,
41
+ ctx: ExtensionContext,
42
+ project: ProjectEntry,
43
+ baseDir?: string
44
+ ): void {
45
+ try {
46
+ if (shouldSkipObservation()) return;
47
+
48
+ const input = scrubSecrets(event.prompt);
49
+
50
+ const observation: Observation = {
51
+ timestamp: new Date().toISOString(),
52
+ event: "user_prompt",
53
+ session: getSessionId(ctx),
54
+ project_id: project.id,
55
+ project_name: project.name,
56
+ input,
57
+ ...buildActiveInstincts(),
58
+ };
59
+
60
+ appendObservation(observation, project.id, baseDir);
61
+ } catch (err) {
62
+ logError(project.id, "prompt-observer:handleBeforeAgentStart", err, baseDir);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Handles agent_end events.
68
+ * Records an observation with event: agent_end.
69
+ */
70
+ export function handleAgentEnd(
71
+ _event: AgentEndEvent,
72
+ ctx: ExtensionContext,
73
+ project: ProjectEntry,
74
+ baseDir?: string
75
+ ): void {
76
+ try {
77
+ if (shouldSkipObservation()) return;
78
+
79
+ const observation: Observation = {
80
+ timestamp: new Date().toISOString(),
81
+ event: "agent_end",
82
+ session: getSessionId(ctx),
83
+ project_id: project.id,
84
+ project_name: project.name,
85
+ ...buildActiveInstincts(),
86
+ };
87
+
88
+ appendObservation(observation, project.id, baseDir);
89
+ } catch (err) {
90
+ logError(project.id, "prompt-observer:handleAgentEnd", err, baseDir);
91
+ }
92
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Analyzer user prompt construction.
3
+ * Returns the user prompt string used by the Haiku background analyzer
4
+ * to locate observations and instinct files for the current project.
5
+ */
6
+
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import type { InstalledSkill, ProjectEntry } from "../types.js";
9
+
10
+ /** Maximum number of observation lines to include in analysis. */
11
+ const MAX_TAIL_ENTRIES = 500;
12
+
13
+ /**
14
+ * Reads the last `maxEntries` lines from a JSONL observations file.
15
+ * Returns an empty array if the file does not exist.
16
+ *
17
+ * @param observationsPath - Absolute path to observations.jsonl
18
+ * @param maxEntries - Maximum number of lines to return (default 500)
19
+ */
20
+ export function tailObservations(
21
+ observationsPath: string,
22
+ maxEntries = MAX_TAIL_ENTRIES
23
+ ): string[] {
24
+ if (!existsSync(observationsPath)) {
25
+ return [];
26
+ }
27
+ const content = readFileSync(observationsPath, "utf-8");
28
+ const lines = content
29
+ .split("\n")
30
+ .map((l) => l.trim())
31
+ .filter((l) => l.length > 0);
32
+ return lines.slice(-maxEntries);
33
+ }
34
+
35
+ export interface TailSinceResult {
36
+ lines: string[];
37
+ totalLineCount: number;
38
+ }
39
+
40
+ export function tailObservationsSince(
41
+ observationsPath: string,
42
+ sinceLineCount: number,
43
+ maxEntries = MAX_TAIL_ENTRIES
44
+ ): TailSinceResult {
45
+ if (!existsSync(observationsPath)) {
46
+ return { lines: [], totalLineCount: 0 };
47
+ }
48
+ const content = readFileSync(observationsPath, "utf-8");
49
+ const allLines = content
50
+ .split("\n")
51
+ .map((l) => l.trim())
52
+ .filter((l) => l.length > 0);
53
+
54
+ const totalLineCount = allLines.length;
55
+
56
+ // If file was archived/reset (fewer lines than cursor), treat as fresh
57
+ const effectiveSince = totalLineCount < sinceLineCount ? 0 : sinceLineCount;
58
+ const newLines = allLines.slice(effectiveSince);
59
+
60
+ return {
61
+ lines: newLines.slice(-maxEntries),
62
+ totalLineCount,
63
+ };
64
+ }
65
+
66
+ export interface AnalyzerUserPromptOptions {
67
+ agentsMdProject?: string | null;
68
+ agentsMdGlobal?: string | null;
69
+ installedSkills?: InstalledSkill[];
70
+ observationLines?: string[];
71
+ }
72
+
73
+ /**
74
+ * Builds the user prompt for the background Haiku analyzer.
75
+ * Includes observation and instinct file paths plus project context.
76
+ * Optionally includes AGENTS.md content and installed skills for deduplication.
77
+ * Template construction only - no subprocess I/O.
78
+ *
79
+ * @param observationsPath - Absolute path to the project's observations.jsonl
80
+ * @param instinctsDir - Absolute path to the project's instincts directory
81
+ * @param project - ProjectEntry with id and name
82
+ * @param options - Optional AGENTS.md content and installed skills
83
+ */
84
+ export function buildAnalyzerUserPrompt(
85
+ observationsPath: string,
86
+ instinctsDir: string,
87
+ project: ProjectEntry,
88
+ options: AnalyzerUserPromptOptions = {}
89
+ ): string {
90
+ const { agentsMdProject = null, agentsMdGlobal = null, installedSkills = [], observationLines } = options;
91
+
92
+ const tailedLines = observationLines ?? tailObservations(observationsPath);
93
+ const observationBlock =
94
+ tailedLines.length > 0
95
+ ? tailedLines.join("\n")
96
+ : "(no observations recorded yet)";
97
+
98
+ const entriesLabel = observationLines
99
+ ? `new observations since last analysis (up to ${MAX_TAIL_ENTRIES})`
100
+ : `most recent entries (up to ${MAX_TAIL_ENTRIES})`;
101
+
102
+ const parts: string[] = [
103
+ "## Analysis Task",
104
+ "",
105
+ "Analyze the following session observations and update the instinct files accordingly.",
106
+ "",
107
+ "## Project Context",
108
+ "",
109
+ `project_id: ${project.id}`,
110
+ `project_name: ${project.name}`,
111
+ "",
112
+ "## File Paths",
113
+ "",
114
+ `Observations file: ${observationsPath}`,
115
+ `Instincts directory: ${instinctsDir}`,
116
+ "",
117
+ `The following observations are ${entriesLabel}:`,
118
+ "",
119
+ "```",
120
+ observationBlock,
121
+ "```",
122
+ ];
123
+
124
+ if (agentsMdProject != null || agentsMdGlobal != null) {
125
+ parts.push("", "## Existing Guidelines", "");
126
+ if (agentsMdProject != null) {
127
+ parts.push("### Project AGENTS.md", "", agentsMdProject, "");
128
+ }
129
+ if (agentsMdGlobal != null) {
130
+ parts.push("### Global AGENTS.md", "", agentsMdGlobal, "");
131
+ }
132
+ }
133
+
134
+ if (installedSkills.length > 0) {
135
+ parts.push("", "## Installed Skills", "");
136
+ for (const skill of installedSkills) {
137
+ parts.push(`- **${skill.name}**: ${skill.description}`);
138
+ }
139
+ parts.push("");
140
+ }
141
+
142
+ parts.push(
143
+ "",
144
+ "## Instructions",
145
+ "",
146
+ "1. Read existing instinct files from the instincts directory.",
147
+ "2. Analyze the observations above for patterns following the system prompt rules.",
148
+ "3. Create new instinct files or update existing ones in the instincts directory.",
149
+ "4. Apply feedback analysis using the active_instincts field in each observation.",
150
+ "5. Do not delete any instinct files - only create or update.",
151
+ "",
152
+ "Note: Passive confidence decay has already been applied to existing instincts before this analysis."
153
+ );
154
+
155
+ return parts.join("\n");
156
+ }
@@ -0,0 +1,71 @@
1
+ import type { Instinct, InstalledSkill } from "../types.js";
2
+
3
+ interface InstinctSummary {
4
+ id: string;
5
+ title: string;
6
+ trigger: string;
7
+ action: string;
8
+ confidence: number;
9
+ domain: string;
10
+ scope: string;
11
+ }
12
+
13
+ function summarizeInstinct(i: Instinct): InstinctSummary {
14
+ return {
15
+ id: i.id,
16
+ title: i.title,
17
+ trigger: i.trigger,
18
+ action: i.action,
19
+ confidence: i.confidence,
20
+ domain: i.domain,
21
+ scope: i.scope,
22
+ };
23
+ }
24
+
25
+ export function buildEvolvePrompt(
26
+ instincts: Instinct[],
27
+ agentsMdProject?: string | null,
28
+ agentsMdGlobal?: string | null,
29
+ installedSkills?: InstalledSkill[]
30
+ ): string {
31
+ const parts: string[] = [
32
+ "Analyze the following learned instincts and identify opportunities for improvement.",
33
+ "You have access to instinct tools (instinct_merge, instinct_delete, instinct_write) to act on your findings.",
34
+ "",
35
+ "## Instincts",
36
+ "",
37
+ "```json",
38
+ JSON.stringify(instincts.map(summarizeInstinct), null, 2),
39
+ "```",
40
+ "",
41
+ "## Analysis Tasks",
42
+ "",
43
+ "1. **Merge candidates**: Find instincts with semantically similar triggers or actions (even if worded differently). Offer to merge them using the instinct_merge tool.",
44
+ "2. **Duplicates of AGENTS.md**: Flag instincts already covered by the guidelines below. Offer to delete them.",
45
+ "3. **Promotion candidates**: Project-scoped instincts with confidence >= 0.7 that could become global.",
46
+ "4. **Skill shadows**: Instincts whose purpose is already served by an installed skill (listed below). Offer to delete them.",
47
+ "5. **Low-confidence cleanup**: Instincts with confidence < 0.3 or flagged_for_removal that should be deleted.",
48
+ "",
49
+ "Present your findings conversationally. For each suggestion, explain your reasoning and ask if I'd like you to take action using the instinct tools.",
50
+ "If there are no issues, say so briefly.",
51
+ ];
52
+
53
+ if (agentsMdProject || agentsMdGlobal) {
54
+ parts.push("", "## Current Guidelines (AGENTS.md)", "");
55
+ if (agentsMdProject) {
56
+ parts.push("### Project AGENTS.md", "", agentsMdProject, "");
57
+ }
58
+ if (agentsMdGlobal) {
59
+ parts.push("### Global AGENTS.md", "", agentsMdGlobal, "");
60
+ }
61
+ }
62
+
63
+ if (installedSkills && installedSkills.length > 0) {
64
+ parts.push("", "## Installed Skills", "");
65
+ for (const skill of installedSkills) {
66
+ parts.push(`- **${skill.name}**: ${skill.description}`);
67
+ }
68
+ }
69
+
70
+ return parts.join("\n");
71
+ }
@@ -0,0 +1,42 @@
1
+ /** Secret scrubbing - replaces sensitive values with [REDACTED] before writing to disk */
2
+
3
+ export const REDACTED = "[REDACTED]";
4
+
5
+ /**
6
+ * Ordered list of regex patterns that match common secret formats.
7
+ * Full matches are replaced with REDACTED.
8
+ */
9
+ const SECRET_PATTERNS: readonly RegExp[] = [
10
+ // Authorization header value (bearer, basic, token schemes)
11
+ /authorization\s*:\s*(?:bearer|basic|token)\s+\S+/gi,
12
+ // Standalone bearer token (match full token value, any non-whitespace chars)
13
+ /bearer\s+\S+/gi,
14
+ // HTTP header shorthand: x-api-key: <value>, x-auth-token: <value>
15
+ /x-(?:api-key|auth-token)\s*:\s*\S+/gi,
16
+ // API key assignments: api_key=, apiKey:, api-key =, access_key=, secret_key=
17
+ /(?:api[_-]?key|apikey|access[_-]?key|secret[_-]?key)\s*[:=]\s*\S+/gi,
18
+ // Token assignments: token=, auth_token:, access_token=, refresh_token=
19
+ /(?:auth[_-]?token|access[_-]?token|refresh[_-]?token)\s*[:=]\s*\S+/gi,
20
+ // Password assignments: password=, passwd:, pwd =
21
+ /(?:password|passwd|pwd)\s*[:=]\s*\S+/gi,
22
+ // Secret / credential / private_key assignments
23
+ /(?:secret|credential|private[_-]?key)\s*[:=]\s*\S+/gi,
24
+ // AWS Access Key IDs (AKIA...)
25
+ /AKIA[0-9A-Z]{16}/g,
26
+ // OpenAI / Anthropic SDK keys: sk-..., sk-ant-...
27
+ /sk-(?:ant-api03-)?[a-zA-Z0-9]{32,}/g,
28
+ ];
29
+
30
+ /**
31
+ * Scrub secrets from arbitrary text.
32
+ * Replaces all matched secret patterns with [REDACTED].
33
+ * Non-secret text is returned unchanged.
34
+ */
35
+ export function scrubSecrets(text: string): string {
36
+ let result = text;
37
+ for (const pattern of SECRET_PATTERNS) {
38
+ pattern.lastIndex = 0;
39
+ result = result.replace(pattern, REDACTED);
40
+ }
41
+ return result;
42
+ }
package/src/storage.ts ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Storage layout creation for pi-continuous-learning.
3
+ * Creates the directory structure under ~/.pi/continuous-learning/
4
+ * on first use, and maintains the projects.json registry.
5
+ */
6
+
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import type { ProjectEntry } from "./types.js";
11
+
12
+ export function getBaseDir(): string {
13
+ return join(homedir(), ".pi", "continuous-learning");
14
+ }
15
+
16
+ export function getProjectDir(projectId: string, baseDir = getBaseDir()): string {
17
+ return join(baseDir, "projects", projectId);
18
+ }
19
+
20
+ export function getObservationsPath(projectId: string, baseDir = getBaseDir()): string {
21
+ return join(getProjectDir(projectId, baseDir), "observations.jsonl");
22
+ }
23
+
24
+ export function getArchiveDir(projectId: string, baseDir = getBaseDir()): string {
25
+ return join(getProjectDir(projectId, baseDir), "observations.archive");
26
+ }
27
+
28
+ export function getProjectInstinctsDir(
29
+ projectId: string,
30
+ source: "personal" | "inherited",
31
+ baseDir = getBaseDir()
32
+ ): string {
33
+ return join(getProjectDir(projectId, baseDir), "instincts", source);
34
+ }
35
+
36
+ export function getGlobalInstinctsDir(
37
+ source: "personal" | "inherited",
38
+ baseDir = getBaseDir()
39
+ ): string {
40
+ return join(baseDir, "instincts", source);
41
+ }
42
+
43
+ export function getProjectsRegistryPath(baseDir = getBaseDir()): string {
44
+ return join(baseDir, "projects.json");
45
+ }
46
+
47
+ function ensureDir(dir: string): void {
48
+ mkdirSync(dir, { recursive: true });
49
+ }
50
+
51
+ function readProjectsRegistry(registryPath: string): Record<string, ProjectEntry> {
52
+ if (!existsSync(registryPath)) {
53
+ return {};
54
+ }
55
+ try {
56
+ return JSON.parse(readFileSync(registryPath, "utf-8")) as Record<string, ProjectEntry>;
57
+ } catch {
58
+ return {};
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Ensures the full storage directory layout exists.
64
+ * Idempotent - safe to call multiple times.
65
+ *
66
+ * @param project - Project entry with metadata
67
+ * @param baseDir - Base directory (defaults to ~/.pi/continuous-learning/)
68
+ */
69
+ export function ensureStorageLayout(project: ProjectEntry, baseDir = getBaseDir()): void {
70
+ // Global instinct directories
71
+ ensureDir(join(baseDir, "instincts", "personal"));
72
+ ensureDir(join(baseDir, "instincts", "inherited"));
73
+
74
+ // Project-scoped directories
75
+ const projectDir = getProjectDir(project.id, baseDir);
76
+ ensureDir(join(projectDir, "instincts", "personal"));
77
+ ensureDir(join(projectDir, "instincts", "inherited"));
78
+ ensureDir(join(projectDir, "observations.archive"));
79
+
80
+ // Write project.json only if it does not yet exist
81
+ const projectJsonPath = join(projectDir, "project.json");
82
+ if (!existsSync(projectJsonPath)) {
83
+ writeFileSync(projectJsonPath, JSON.stringify(project, null, 2), "utf-8");
84
+ }
85
+
86
+ // Update projects.json registry (always updates the entry)
87
+ const registryPath = getProjectsRegistryPath(baseDir);
88
+ const registry = readProjectsRegistry(registryPath);
89
+ const updated = { ...registry, [project.id]: project };
90
+ writeFileSync(registryPath, JSON.stringify(updated, null, 2), "utf-8");
91
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Tool call observation handlers for pi-continuous-learning.
3
+ * Captures tool_execution_start and tool_execution_end events as JSONL observations.
4
+ */
5
+
6
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
7
+
8
+ // Local event type definitions (not all pi-coding-agent versions re-export these at top level)
9
+ export interface ToolExecutionStartEvent {
10
+ type: "tool_execution_start";
11
+ toolCallId: string;
12
+ toolName: string;
13
+ args: unknown;
14
+ }
15
+
16
+ export interface ToolExecutionEndEvent {
17
+ type: "tool_execution_end";
18
+ toolCallId: string;
19
+ toolName: string;
20
+ result: unknown;
21
+ isError: boolean;
22
+ }
23
+
24
+ import { getCurrentActiveInstincts } from "./active-instincts.js";
25
+ import { appendObservation } from "./observations.js";
26
+ import { shouldSkipObservation } from "./observer-guard.js";
27
+ import { scrubSecrets } from "./scrubber.js";
28
+ import { logError } from "./error-logger.js";
29
+ import type { Observation, ProjectEntry } from "./types.js";
30
+
31
+ export const MAX_TOOL_INPUT_LENGTH = 5000;
32
+ export const MAX_TOOL_OUTPUT_LENGTH = 5000;
33
+
34
+ function truncate(text: string, maxLen: number): string {
35
+ if (text.length <= maxLen) return text;
36
+ return text.slice(0, maxLen);
37
+ }
38
+
39
+ function getSessionId(ctx: ExtensionContext): string {
40
+ return ctx.sessionManager.getSessionId();
41
+ }
42
+
43
+ function buildActiveInstincts(): Pick<Observation, "active_instincts"> {
44
+ const ids = getCurrentActiveInstincts();
45
+ return ids.length > 0 ? { active_instincts: ids } : {};
46
+ }
47
+
48
+ /**
49
+ * Handles tool_execution_start events.
50
+ * Records an observation with event: tool_start, tool name, and scrubbed/truncated input.
51
+ */
52
+ export function handleToolStart(
53
+ event: ToolExecutionStartEvent,
54
+ ctx: ExtensionContext,
55
+ project: ProjectEntry,
56
+ baseDir?: string
57
+ ): void {
58
+ try {
59
+ if (shouldSkipObservation()) return;
60
+
61
+ const raw = typeof event.args === "string" ? event.args : JSON.stringify(event.args);
62
+ const input = truncate(scrubSecrets(raw), MAX_TOOL_INPUT_LENGTH);
63
+
64
+ const observation: Observation = {
65
+ timestamp: new Date().toISOString(),
66
+ event: "tool_start",
67
+ session: getSessionId(ctx),
68
+ project_id: project.id,
69
+ project_name: project.name,
70
+ tool: event.toolName,
71
+ input,
72
+ ...buildActiveInstincts(),
73
+ };
74
+
75
+ appendObservation(observation, project.id, baseDir);
76
+ } catch (err) {
77
+ logError(project.id, "tool-observer:handleToolStart", err, baseDir);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Handles tool_execution_end events.
83
+ * Records an observation with event: tool_complete, tool name, scrubbed/truncated output, and is_error.
84
+ */
85
+ export function handleToolEnd(
86
+ event: ToolExecutionEndEvent,
87
+ ctx: ExtensionContext,
88
+ project: ProjectEntry,
89
+ baseDir?: string
90
+ ): void {
91
+ try {
92
+ if (shouldSkipObservation()) return;
93
+
94
+ const raw =
95
+ typeof event.result === "string" ? event.result : JSON.stringify(event.result);
96
+ const output = truncate(scrubSecrets(raw), MAX_TOOL_OUTPUT_LENGTH);
97
+
98
+ const observation: Observation = {
99
+ timestamp: new Date().toISOString(),
100
+ event: "tool_complete",
101
+ session: getSessionId(ctx),
102
+ project_id: project.id,
103
+ project_name: project.name,
104
+ tool: event.toolName,
105
+ output,
106
+ is_error: event.isError,
107
+ ...buildActiveInstincts(),
108
+ };
109
+
110
+ appendObservation(observation, project.id, baseDir);
111
+ } catch (err) {
112
+ logError(project.id, "tool-observer:handleToolEnd", err, baseDir);
113
+ }
114
+ }
package/src/types.ts ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Shared TypeScript interfaces for pi-continuous-learning.
3
+ * All modules import from this file for consistent data contracts.
4
+ */
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Observation
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export type ObservationEvent =
11
+ | "tool_start"
12
+ | "tool_complete"
13
+ | "user_prompt"
14
+ | "agent_end";
15
+
16
+ export interface Observation {
17
+ timestamp: string; // ISO 8601 UTC
18
+ event: ObservationEvent;
19
+ session: string;
20
+ project_id: string;
21
+ project_name: string;
22
+ tool?: string;
23
+ input?: string;
24
+ output?: string;
25
+ is_error?: boolean;
26
+ active_instincts?: string[];
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Instinct
31
+ // ---------------------------------------------------------------------------
32
+
33
+ export type InstinctScope = "project" | "global";
34
+ export type InstinctSource = "personal" | "inherited";
35
+
36
+ export interface Instinct {
37
+ id: string; // kebab-case
38
+ title: string;
39
+ trigger: string;
40
+ action: string;
41
+ confidence: number; // 0.1 - 0.9
42
+ domain: string;
43
+ source: InstinctSource;
44
+ scope: InstinctScope;
45
+ project_id?: string;
46
+ project_name?: string;
47
+ created_at: string; // ISO 8601
48
+ updated_at: string; // ISO 8601
49
+ observation_count: number;
50
+ confirmed_count: number;
51
+ contradicted_count: number;
52
+ inactive_count: number;
53
+ evidence?: string[];
54
+ flagged_for_removal?: boolean;
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // ProjectEntry
59
+ // ---------------------------------------------------------------------------
60
+
61
+ export interface ProjectEntry {
62
+ id: string;
63
+ name: string;
64
+ root: string;
65
+ remote: string;
66
+ created_at: string; // ISO 8601
67
+ last_seen: string; // ISO 8601
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Config
72
+ // ---------------------------------------------------------------------------
73
+
74
+ export interface InstalledSkill {
75
+ name: string;
76
+ description: string;
77
+ }
78
+
79
+ export interface Config {
80
+ run_interval_minutes: number;
81
+ min_observations_to_analyze: number;
82
+ min_confidence: number;
83
+ max_instincts: number;
84
+ max_injection_chars: number;
85
+ model: string;
86
+ timeout_seconds: number;
87
+ active_hours_start: number; // 0-23
88
+ active_hours_end: number; // 0-23
89
+ max_idle_seconds: number;
90
+ }