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,103 @@
1
+ /**
2
+ * Pure functions for instinct confidence scoring.
3
+ * No I/O - all functions take plain values and return plain values.
4
+ */
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Constants
8
+ // ---------------------------------------------------------------------------
9
+
10
+ const CLAMP_MIN = 0.1;
11
+ const CLAMP_MAX = 0.9;
12
+
13
+ // initialConfidence brackets
14
+ const INITIAL_LOW = 0.3;
15
+ const INITIAL_MED = 0.5;
16
+ const INITIAL_HIGH = 0.7;
17
+ const INITIAL_VERY_HIGH = 0.85;
18
+
19
+ const OBS_BRACKET_LOW_MAX = 2;
20
+ const OBS_BRACKET_MED_MAX = 5;
21
+ const OBS_BRACKET_HIGH_MAX = 10;
22
+
23
+ // adjustConfidence deltas
24
+ const DELTA_CONFIRMED = 0.05;
25
+ const DELTA_CONTRADICTED = -0.15;
26
+ const DELTA_INACTIVE = 0;
27
+
28
+ // applyPassiveDecay
29
+ const DECAY_PER_WEEK = 0.02;
30
+ const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000;
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Types
34
+ // ---------------------------------------------------------------------------
35
+
36
+ export type FeedbackOutcome = "confirmed" | "contradicted" | "inactive";
37
+
38
+ export interface ConfidenceResult {
39
+ confidence: number;
40
+ flaggedForRemoval: boolean;
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Helpers
45
+ // ---------------------------------------------------------------------------
46
+
47
+ function clamp(value: number): number {
48
+ return Math.max(CLAMP_MIN, Math.min(CLAMP_MAX, value));
49
+ }
50
+
51
+ function toResult(raw: number): ConfidenceResult {
52
+ const flaggedForRemoval = raw < CLAMP_MIN;
53
+ return { confidence: clamp(raw), flaggedForRemoval };
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Public API
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /**
61
+ * Returns the initial confidence score for a newly discovered instinct
62
+ * based on how many observations support it.
63
+ */
64
+ export function initialConfidence(observationCount: number): number {
65
+ if (observationCount <= OBS_BRACKET_LOW_MAX) return INITIAL_LOW;
66
+ if (observationCount <= OBS_BRACKET_MED_MAX) return INITIAL_MED;
67
+ if (observationCount <= OBS_BRACKET_HIGH_MAX) return INITIAL_HIGH;
68
+ return INITIAL_VERY_HIGH;
69
+ }
70
+
71
+ /**
72
+ * Adjusts confidence based on a feedback outcome from the observer loop.
73
+ * Returns the clamped confidence and a flag indicating if removal is warranted.
74
+ */
75
+ export function adjustConfidence(
76
+ current: number,
77
+ outcome: FeedbackOutcome,
78
+ ): ConfidenceResult {
79
+ const deltas: Record<FeedbackOutcome, number> = {
80
+ confirmed: DELTA_CONFIRMED,
81
+ contradicted: DELTA_CONTRADICTED,
82
+ inactive: DELTA_INACTIVE,
83
+ };
84
+ const raw = current + deltas[outcome];
85
+ return toResult(raw);
86
+ }
87
+
88
+ /**
89
+ * Applies passive time-based decay of -0.02 per week since lastUpdated.
90
+ * Future lastUpdated values produce zero decay.
91
+ */
92
+ export function applyPassiveDecay(
93
+ confidence: number,
94
+ lastUpdated: string,
95
+ ): ConfidenceResult {
96
+ const now = Date.now();
97
+ const updatedAt = new Date(lastUpdated).getTime();
98
+ const elapsedMs = Math.max(0, now - updatedAt);
99
+ const weeksElapsed = elapsedMs / MS_PER_WEEK;
100
+ const decay = weeksElapsed * DECAY_PER_WEEK;
101
+ const raw = confidence - decay;
102
+ return toResult(raw);
103
+ }
package/src/config.ts ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Configuration module for pi-continuous-learning.
3
+ * Loads user settings from ~/.pi/continuous-learning/config.json with defaults.
4
+ */
5
+
6
+ import * as fs from "node:fs";
7
+ import * as os from "node:os";
8
+ import * as path from "node:path";
9
+ import { Type, type Static } from "@sinclair/typebox";
10
+ import { Value } from "@sinclair/typebox/value";
11
+ import type { Config } from "./types.js";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Constants
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /**
18
+ * Maps instinct domain names to human-readable purposes.
19
+ * Used by findSkillShadows() to detect when an instinct is covered by an installed Pi skill.
20
+ */
21
+ export const SKILL_DOMAINS: Record<string, string> = {
22
+ git: "version control and git workflows",
23
+ testing: "test writing and test frameworks",
24
+ debugging: "error analysis and debugging",
25
+ workflow: "development workflow and automation",
26
+ typescript: "TypeScript language and type system",
27
+ css: "CSS and styling",
28
+ design: "UI design and component patterns",
29
+ security: "security practices and vulnerability prevention",
30
+ performance: "performance optimization",
31
+ documentation: "documentation writing and standards",
32
+ };
33
+
34
+ export const CONFIG_PATH = path.join(
35
+ os.homedir(),
36
+ ".pi",
37
+ "continuous-learning",
38
+ "config.json"
39
+ );
40
+
41
+ export const DEFAULT_CONFIG: Config = {
42
+ run_interval_minutes: 5,
43
+ min_observations_to_analyze: 20,
44
+ min_confidence: 0.5,
45
+ max_instincts: 20,
46
+ max_injection_chars: 4000,
47
+ model: "claude-haiku-4-5",
48
+ timeout_seconds: 120,
49
+ active_hours_start: 8,
50
+ active_hours_end: 23,
51
+ max_idle_seconds: 1800,
52
+ };
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // TypeBox schema for partial config overrides (runtime validation)
56
+ // ---------------------------------------------------------------------------
57
+
58
+ const PartialConfigSchema = Type.Partial(
59
+ Type.Object({
60
+ run_interval_minutes: Type.Number(),
61
+ min_observations_to_analyze: Type.Number(),
62
+ min_confidence: Type.Number(),
63
+ max_instincts: Type.Number(),
64
+ max_injection_chars: Type.Number(),
65
+ model: Type.String(),
66
+ timeout_seconds: Type.Number(),
67
+ active_hours_start: Type.Number(),
68
+ active_hours_end: Type.Number(),
69
+ max_idle_seconds: Type.Number(),
70
+ })
71
+ );
72
+
73
+ type PartialConfig = Static<typeof PartialConfigSchema>;
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // loadConfig
77
+ // ---------------------------------------------------------------------------
78
+
79
+ /**
80
+ * Loads config from ~/.pi/continuous-learning/config.json.
81
+ * Returns defaults when file is absent or contains invalid JSON.
82
+ * Merges partial overrides with defaults (overrides win).
83
+ */
84
+ export function loadConfig(): Config {
85
+ if (!fs.existsSync(CONFIG_PATH)) {
86
+ return { ...DEFAULT_CONFIG };
87
+ }
88
+
89
+ let raw: string;
90
+ try {
91
+ raw = fs.readFileSync(CONFIG_PATH, "utf-8") as string;
92
+ } catch (err) {
93
+ console.warn(`[pi-continuous-learning] Failed to read config.json: ${String(err)}`);
94
+ return { ...DEFAULT_CONFIG };
95
+ }
96
+
97
+ let parsed: unknown;
98
+ try {
99
+ parsed = JSON.parse(raw);
100
+ } catch (err) {
101
+ console.warn(
102
+ `[pi-continuous-learning] Invalid JSON in config.json: ${String(err)}. Using defaults.`
103
+ );
104
+ return { ...DEFAULT_CONFIG };
105
+ }
106
+
107
+ // Validate and extract only known config fields (runtime boundary check)
108
+ const cleaned = Value.Clean(PartialConfigSchema, parsed) as PartialConfig;
109
+
110
+ return { ...DEFAULT_CONFIG, ...cleaned };
111
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Error logging utility for pi-continuous-learning.
3
+ * Writes structured error entries to projects/<id>/analyzer.log.
4
+ * All write failures are silently swallowed - the logger must never throw.
5
+ */
6
+
7
+ import { appendFileSync, mkdirSync } from "node:fs";
8
+ import { join, dirname } from "node:path";
9
+ import { getProjectDir, getBaseDir } from "./storage.js";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Constants
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const LOG_FILENAME = "analyzer.log";
16
+ const PREFIX = "[pi-continuous-learning]";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Helpers
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Returns the absolute path to the analyzer log for the given project.
24
+ * Exported for testing.
25
+ */
26
+ export function getLogPath(projectId: string, baseDir?: string): string {
27
+ return join(getProjectDir(projectId, baseDir ?? getBaseDir()), LOG_FILENAME);
28
+ }
29
+
30
+ function formatError(context: string, error: unknown): string {
31
+ const timestamp = new Date().toISOString();
32
+ const message = error instanceof Error ? error.message : String(error);
33
+ const stack =
34
+ error instanceof Error && error.stack
35
+ ? `\nStack: ${error.stack}`
36
+ : "";
37
+ return `[${timestamp}] [${context}] Error: ${message}${stack}\n`;
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Public API
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /**
45
+ * Logs an error to `projects/<projectId>/analyzer.log`.
46
+ *
47
+ * When `projectId` is null (e.g. session_start failed before project detection),
48
+ * falls back to `console.warn` only.
49
+ *
50
+ * Never throws - all I/O failures are silently swallowed.
51
+ */
52
+ export function logError(
53
+ projectId: string | null,
54
+ context: string,
55
+ error: unknown,
56
+ baseDir?: string
57
+ ): void {
58
+ const line = formatError(context, error);
59
+
60
+ if (projectId === null) {
61
+ console.warn(`${PREFIX} ${context}: ${line.trim()}`);
62
+ return;
63
+ }
64
+
65
+ const logPath = getLogPath(projectId, baseDir);
66
+ try {
67
+ mkdirSync(dirname(logPath), { recursive: true });
68
+ appendFileSync(logPath, line, "utf-8");
69
+ } catch {
70
+ // Cannot log the logger failing - fall back to console
71
+ console.warn(`${PREFIX} ${context}: ${line.trim()}`);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Logs a warning (non-error) message to the analyzer log.
77
+ * Used for subprocess stderr output and other non-fatal warnings.
78
+ *
79
+ * Never throws - all I/O failures are silently swallowed.
80
+ */
81
+ export function logWarning(
82
+ projectId: string | null,
83
+ context: string,
84
+ message: string,
85
+ baseDir?: string
86
+ ): void {
87
+ const timestamp = new Date().toISOString();
88
+ const line = `[${timestamp}] [${context}] Warning: ${message}\n`;
89
+
90
+ if (projectId === null) {
91
+ console.warn(`${PREFIX} ${context}: ${message}`);
92
+ return;
93
+ }
94
+
95
+ const logPath = getLogPath(projectId, baseDir);
96
+ try {
97
+ mkdirSync(dirname(logPath), { recursive: true });
98
+ appendFileSync(logPath, line, "utf-8");
99
+ } catch {
100
+ console.warn(`${PREFIX} ${context}: ${message}`);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Logs an informational message to the analyzer log.
106
+ * Used for tracking analyzer lifecycle events (started, completed, skipped).
107
+ *
108
+ * Never throws - all I/O failures are silently swallowed.
109
+ */
110
+ export function logInfo(
111
+ projectId: string | null,
112
+ context: string,
113
+ message: string,
114
+ baseDir?: string
115
+ ): void {
116
+ const timestamp = new Date().toISOString();
117
+ const line = `[${timestamp}] [${context}] Info: ${message}\n`;
118
+
119
+ if (projectId === null) {
120
+ return;
121
+ }
122
+
123
+ const logPath = getLogPath(projectId, baseDir);
124
+ try {
125
+ mkdirSync(dirname(logPath), { recursive: true });
126
+ appendFileSync(logPath, line, "utf-8");
127
+ } catch {
128
+ // silently swallow
129
+ }
130
+ }
package/src/index.ts ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Pi Continuous Learning Extension - Entry Point
3
+ *
4
+ * Observes coding sessions, records events as observations, and injects
5
+ * learned instincts into the agent's system prompt. Background analysis
6
+ * runs via a separate standalone script (src/cli/analyze.ts).
7
+ */
8
+
9
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
10
+ import { loadSkills } from "@mariozechner/pi-coding-agent";
11
+
12
+ import { loadConfig } from "./config.js";
13
+ import { detectProject } from "./project.js";
14
+ import { ensureStorageLayout } from "./storage.js";
15
+ import { cleanOldArchives } from "./observations.js";
16
+ import { handleToolStart, handleToolEnd } from "./tool-observer.js";
17
+ import { handleBeforeAgentStart, handleAgentEnd } from "./prompt-observer.js";
18
+ import {
19
+ handleBeforeAgentStartInjection,
20
+ handleAgentEndClearInstincts,
21
+ } from "./instinct-injector.js";
22
+ import { handleInstinctStatus, COMMAND_NAME as STATUS_CMD } from "./instinct-status.js";
23
+ import { handleInstinctExport, COMMAND_NAME as EXPORT_CMD } from "./instinct-export.js";
24
+ import { handleInstinctImport, COMMAND_NAME as IMPORT_CMD } from "./instinct-import.js";
25
+ import { handleInstinctPromote, COMMAND_NAME as PROMOTE_CMD } from "./instinct-promote.js";
26
+ import { handleInstinctEvolve, COMMAND_NAME as EVOLVE_CMD } from "./instinct-evolve.js";
27
+ import { handleInstinctProjects, COMMAND_NAME as PROJECTS_CMD } from "./instinct-projects.js";
28
+ import { registerAllTools } from "./instinct-tools.js";
29
+ import { logError } from "./error-logger.js";
30
+ import type { Config, InstalledSkill, ProjectEntry } from "./types.js";
31
+
32
+ export default function (pi: ExtensionAPI): void {
33
+ let config: Config | null = null;
34
+ let project: ProjectEntry | null = null;
35
+ let installedSkills: InstalledSkill[] = [];
36
+
37
+ pi.on("session_start", async (_event, ctx) => {
38
+ try {
39
+ config = loadConfig();
40
+ project = await detectProject(pi, ctx.cwd);
41
+ ensureStorageLayout(project);
42
+ cleanOldArchives(project.id);
43
+
44
+ try {
45
+ const result = loadSkills({ cwd: ctx.cwd });
46
+ installedSkills = result.skills.map((s) => ({ name: s.name, description: s.description }));
47
+ } catch {
48
+ installedSkills = [];
49
+ }
50
+
51
+ registerAllTools(pi, project.id, project.name);
52
+ } catch (err) {
53
+ logError(project?.id ?? null, "session_start", err);
54
+ }
55
+ });
56
+
57
+ pi.on("session_shutdown", (_event, _ctx) => {
58
+ // No cleanup needed — analyzer runs as external process
59
+ });
60
+
61
+ pi.on("before_agent_start", (event, ctx) => {
62
+ try {
63
+ if (!project || !config) return;
64
+ handleBeforeAgentStart(event, ctx, project);
65
+ return handleBeforeAgentStartInjection(event, ctx, config, project.id) ?? undefined;
66
+ } catch (err) {
67
+ logError(project?.id ?? null, "before_agent_start", err);
68
+ }
69
+ });
70
+
71
+ pi.on("agent_start", (_event, _ctx) => {});
72
+
73
+ pi.on("agent_end", (event, ctx) => {
74
+ try {
75
+ if (!project) return;
76
+ handleAgentEnd(event, ctx, project);
77
+ handleAgentEndClearInstincts(event, ctx);
78
+ } catch (err) {
79
+ logError(project?.id ?? null, "agent_end", err);
80
+ }
81
+ });
82
+
83
+ pi.on("tool_execution_start", (event, ctx) => {
84
+ try {
85
+ if (!project) return;
86
+ handleToolStart(event, ctx, project);
87
+ } catch (err) {
88
+ logError(project?.id ?? null, "tool_execution_start", err);
89
+ }
90
+ });
91
+
92
+ pi.on("tool_execution_end", (event, ctx) => {
93
+ try {
94
+ if (!project) return;
95
+ handleToolEnd(event, ctx, project);
96
+ } catch (err) {
97
+ logError(project?.id ?? null, "tool_execution_end", err);
98
+ }
99
+ });
100
+
101
+ pi.registerCommand(STATUS_CMD, {
102
+ description: "Show all instincts grouped by domain with confidence scores",
103
+ handler: (args: string, ctx: ExtensionCommandContext) =>
104
+ handleInstinctStatus(args, ctx, project?.id),
105
+ });
106
+
107
+ pi.registerCommand(EXPORT_CMD, {
108
+ description: "Export instincts to a JSON file",
109
+ handler: (args: string, ctx: ExtensionCommandContext) =>
110
+ handleInstinctExport(args, ctx, project?.id),
111
+ });
112
+
113
+ pi.registerCommand(IMPORT_CMD, {
114
+ description: "Import instincts from a JSON file",
115
+ handler: (args: string, ctx: ExtensionCommandContext) =>
116
+ handleInstinctImport(args, ctx, project?.id),
117
+ });
118
+
119
+ pi.registerCommand(PROMOTE_CMD, {
120
+ description: "Promote project instincts to global scope",
121
+ handler: (args: string, ctx: ExtensionCommandContext) =>
122
+ handleInstinctPromote(args, ctx, project?.id),
123
+ });
124
+
125
+ pi.registerCommand(EVOLVE_CMD, {
126
+ description: "Analyze instincts and suggest improvements (LLM-powered)",
127
+ handler: (args: string, ctx: ExtensionCommandContext) =>
128
+ handleInstinctEvolve(
129
+ args,
130
+ ctx,
131
+ pi,
132
+ project?.id,
133
+ undefined,
134
+ project?.root ?? null,
135
+ installedSkills
136
+ ),
137
+ });
138
+
139
+ pi.registerCommand(PROJECTS_CMD, {
140
+ description: "List all known projects and their instinct counts",
141
+ handler: (args: string, ctx: ExtensionCommandContext) =>
142
+ handleInstinctProjects(args, ctx),
143
+ });
144
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Passive confidence decay for instincts.
3
+ * Applied at the start of each analysis run to age out stale instincts.
4
+ *
5
+ * Decay: -0.02 per week since updated_at, clamped to [0.1, 0.9].
6
+ * Instincts dropping below 0.1 are flagged for removal.
7
+ *
8
+ * US-031: Passive Confidence Decay
9
+ */
10
+
11
+ import { applyPassiveDecay } from "./confidence.js";
12
+ import { listInstincts, saveInstinct } from "./instinct-store.js";
13
+ import {
14
+ getBaseDir,
15
+ getProjectInstinctsDir,
16
+ getGlobalInstinctsDir,
17
+ } from "./storage.js";
18
+ import type { Instinct } from "./types.js";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Constants
22
+ // ---------------------------------------------------------------------------
23
+
24
+ /**
25
+ * Minimum confidence change required to persist the instinct back to disk.
26
+ * Prevents excessive writes for negligibly small elapsed times.
27
+ * At 5-minute analysis intervals, elapsed decay is ~0.000002 - well below this.
28
+ */
29
+ const DECAY_CHANGE_THRESHOLD = 0.001;
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Public API
33
+ // ---------------------------------------------------------------------------
34
+
35
+ /**
36
+ * Applies passive decay to a single instinct.
37
+ *
38
+ * Returns the updated instinct (with adjusted confidence and refreshed
39
+ * updated_at) when the decay is significant, or null if no meaningful
40
+ * change occurred.
41
+ *
42
+ * Does not mutate the input instinct.
43
+ */
44
+ export function applyDecayToInstinct(instinct: Instinct): Instinct | null {
45
+ const result = applyPassiveDecay(instinct.confidence, instinct.updated_at);
46
+
47
+ const decayAmount = Math.abs(result.confidence - instinct.confidence);
48
+ const flagChanged =
49
+ Boolean(result.flaggedForRemoval) !== Boolean(instinct.flagged_for_removal);
50
+
51
+ if (decayAmount < DECAY_CHANGE_THRESHOLD && !flagChanged) {
52
+ return null;
53
+ }
54
+
55
+ const updated: Instinct = {
56
+ ...instinct,
57
+ confidence: result.confidence,
58
+ updated_at: new Date().toISOString(),
59
+ };
60
+
61
+ // Set or clear flagged_for_removal using delete pattern (exactOptionalPropertyTypes)
62
+ if (result.flaggedForRemoval) {
63
+ (updated as Partial<Instinct>).flagged_for_removal = true;
64
+ } else {
65
+ delete (updated as Partial<Instinct>).flagged_for_removal;
66
+ }
67
+
68
+ return updated;
69
+ }
70
+
71
+ /**
72
+ * Applies decay to all instincts found in a directory.
73
+ * Saves any instincts with meaningful confidence changes.
74
+ *
75
+ * @param dir - Directory containing .md instinct files
76
+ * @returns Number of instincts updated on disk
77
+ */
78
+ export function applyDecayInDir(dir: string): number {
79
+ const instincts = listInstincts(dir);
80
+ let updatedCount = 0;
81
+
82
+ for (const instinct of instincts) {
83
+ const updated = applyDecayToInstinct(instinct);
84
+ if (updated !== null) {
85
+ saveInstinct(updated, dir);
86
+ updatedCount++;
87
+ }
88
+ }
89
+
90
+ return updatedCount;
91
+ }
92
+
93
+ /**
94
+ * Runs a full decay pass over personal instincts for a project and globally.
95
+ * Called at the start of each analysis run, before the Haiku subprocess
96
+ * applies feedback adjustments.
97
+ *
98
+ * @param projectId - Project ID to decay (skipped when null/undefined)
99
+ * @param baseDir - Base storage directory (defaults to ~/.pi/continuous-learning/)
100
+ * @returns Total number of instincts updated across both scopes
101
+ */
102
+ export function runDecayPass(
103
+ projectId?: string | null,
104
+ baseDir = getBaseDir()
105
+ ): number {
106
+ let total = 0;
107
+
108
+ if (projectId) {
109
+ const projectDir = getProjectInstinctsDir(projectId, "personal", baseDir);
110
+ total += applyDecayInDir(projectDir);
111
+ }
112
+
113
+ const globalDir = getGlobalInstinctsDir("personal", baseDir);
114
+ total += applyDecayInDir(globalDir);
115
+
116
+ return total;
117
+ }
@@ -0,0 +1,44 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ import type { InstalledSkill } from "./types.js";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { getBaseDir } from "./storage.js";
6
+ import { readAgentsMd } from "./agents-md.js";
7
+ import { loadProjectInstincts, loadGlobalInstincts } from "./instinct-store.js";
8
+ import { filterInstincts } from "./instinct-loader.js";
9
+ import { buildEvolvePrompt } from "./prompts/evolve-prompt.js";
10
+
11
+ const MAX_EVOLVE_INSTINCTS = 100;
12
+
13
+ export const COMMAND_NAME = "instinct-evolve";
14
+
15
+ export async function handleInstinctEvolve(
16
+ _args: string,
17
+ ctx: ExtensionCommandContext,
18
+ pi: ExtensionAPI,
19
+ projectId?: string | null,
20
+ baseDir?: string,
21
+ projectRoot?: string | null,
22
+ installedSkills?: InstalledSkill[]
23
+ ): Promise<void> {
24
+ const effectiveBase = baseDir ?? getBaseDir();
25
+ const projectInstincts = projectId ? loadProjectInstincts(projectId, effectiveBase) : [];
26
+ const globalInstincts = loadGlobalInstincts(effectiveBase);
27
+ const allInstincts = filterInstincts(
28
+ [...projectInstincts, ...globalInstincts],
29
+ 0.1,
30
+ MAX_EVOLVE_INSTINCTS
31
+ );
32
+
33
+ if (allInstincts.length === 0) {
34
+ ctx.ui.notify("No instincts to analyze. Keep using pi to accumulate instincts first.", "info");
35
+ return;
36
+ }
37
+
38
+ const agentsMdProject =
39
+ projectRoot != null ? readAgentsMd(join(projectRoot, "AGENTS.md")) : null;
40
+ const agentsMdGlobal = readAgentsMd(join(homedir(), ".pi", "agent", "AGENTS.md"));
41
+
42
+ const prompt = buildEvolvePrompt(allInstincts, agentsMdProject, agentsMdGlobal, installedSkills);
43
+ pi.sendUserMessage(prompt, { deliverAs: "followUp" });
44
+ }