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,138 @@
1
+ /**
2
+ * /instinct-export command for pi-continuous-learning.
3
+ * Exports instincts to a JSON file in the current directory.
4
+ * Optional args: scope (project|global) and domain filter.
5
+ */
6
+
7
+ import { writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
10
+ import type { Instinct, InstinctScope } from "./types.js";
11
+ import { loadProjectInstincts, loadGlobalInstincts } from "./instinct-store.js";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Constants
15
+ // ---------------------------------------------------------------------------
16
+
17
+ const COMMAND_NAME = "instinct-export";
18
+ const SCOPE_PROJECT: InstinctScope = "project";
19
+ const SCOPE_GLOBAL: InstinctScope = "global";
20
+ const VALID_SCOPES: readonly string[] = [SCOPE_PROJECT, SCOPE_GLOBAL];
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Arg parsing
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export interface ExportArgs {
27
+ scope: InstinctScope | null;
28
+ domain: string | null;
29
+ }
30
+
31
+ /**
32
+ * Parses space-separated args string.
33
+ * If first token is "project" or "global", it is treated as scope filter.
34
+ * Remaining tokens (if any) are joined as domain filter.
35
+ */
36
+ export function parseExportArgs(args: string): ExportArgs {
37
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
38
+ if (tokens.length === 0) {
39
+ return { scope: null, domain: null };
40
+ }
41
+
42
+ const first = tokens[0] ?? "";
43
+ const isScope = VALID_SCOPES.includes(first);
44
+
45
+ const scope = isScope ? (first as InstinctScope) : null;
46
+ const domainTokens = isScope ? tokens.slice(1) : tokens;
47
+ const domain = domainTokens.length > 0 ? domainTokens.join(" ") : null;
48
+
49
+ return { scope, domain };
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Filtering
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /**
57
+ * Filters instincts by optional scope and domain.
58
+ * Immutable - returns a new array.
59
+ */
60
+ export function filterInstinctsForExport(
61
+ instincts: Instinct[],
62
+ scope: InstinctScope | null,
63
+ domain: string | null
64
+ ): Instinct[] {
65
+ return instincts.filter((instinct) => {
66
+ if (scope !== null && instinct.scope !== scope) return false;
67
+ if (domain !== null && instinct.domain !== domain) return false;
68
+ return true;
69
+ });
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Filename generation
74
+ // ---------------------------------------------------------------------------
75
+
76
+ /**
77
+ * Generates an export filename with timestamp.
78
+ * Format: instincts-export-<YYYYMMDDTHHmmss>.json
79
+ */
80
+ export function buildExportFilename(now: Date = new Date()): string {
81
+ const iso = now.toISOString(); // "2026-03-26T17:12:20.216Z"
82
+ // Compact: remove dashes, colons, milliseconds, and trailing Z
83
+ const compact = iso
84
+ .replace(/-/g, "")
85
+ .replace(/:/g, "")
86
+ .replace(/\.\d+Z$/, "");
87
+ return `instincts-export-${compact}.json`;
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // loadAllInstinctsForExport
92
+ // ---------------------------------------------------------------------------
93
+
94
+ /**
95
+ * Loads instincts from disk (project + global) for export.
96
+ * Does NOT apply confidence filtering - export includes everything.
97
+ */
98
+ export function loadAllInstinctsForExport(
99
+ projectId?: string | null,
100
+ baseDir?: string
101
+ ): Instinct[] {
102
+ const projectInstincts =
103
+ projectId != null ? loadProjectInstincts(projectId, baseDir) : [];
104
+ const globalInstincts = loadGlobalInstincts(baseDir);
105
+ return [...projectInstincts, ...globalInstincts];
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // handleInstinctExport
110
+ // ---------------------------------------------------------------------------
111
+
112
+ /**
113
+ * Command handler for /instinct-export.
114
+ * Loads instincts, applies optional filters, writes JSON file to cwd.
115
+ */
116
+ export async function handleInstinctExport(
117
+ args: string,
118
+ ctx: ExtensionCommandContext,
119
+ projectId?: string | null,
120
+ baseDir?: string
121
+ ): Promise<void> {
122
+ const { scope, domain } = parseExportArgs(args);
123
+
124
+ const all = loadAllInstinctsForExport(projectId, baseDir);
125
+ const filtered = filterInstinctsForExport(all, scope, domain);
126
+
127
+ const filename = buildExportFilename();
128
+ const outputPath = join(ctx.cwd, filename);
129
+
130
+ writeFileSync(outputPath, JSON.stringify(filtered, null, 2), "utf-8");
131
+
132
+ ctx.ui.notify(
133
+ `Exported ${filtered.length} instinct${filtered.length !== 1 ? "s" : ""} to ${outputPath}`,
134
+ "info"
135
+ );
136
+ }
137
+
138
+ export { COMMAND_NAME };
@@ -0,0 +1,260 @@
1
+ /**
2
+ * /instinct-import command for pi-continuous-learning.
3
+ * Imports instincts from a JSON file into the inherited instincts directory.
4
+ * Destination is determined by each instinct's scope field:
5
+ * - scope "project" -> projects/<id>/instincts/inherited/
6
+ * - scope "global" -> instincts/inherited/
7
+ */
8
+
9
+ import { readFileSync, mkdirSync, existsSync } from "node:fs";
10
+ import { resolve } from "node:path";
11
+ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
12
+ import type { Instinct } from "./types.js";
13
+ import { saveInstinct, listInstincts } from "./instinct-store.js";
14
+ import {
15
+ getProjectInstinctsDir,
16
+ getGlobalInstinctsDir,
17
+ getBaseDir,
18
+ } from "./storage.js";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Constants
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export const COMMAND_NAME = "instinct-import";
25
+
26
+ const KEBAB_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
27
+ const REQUIRED_FIELDS = [
28
+ "id",
29
+ "title",
30
+ "trigger",
31
+ "action",
32
+ "confidence",
33
+ "domain",
34
+ "source",
35
+ "scope",
36
+ "created_at",
37
+ "updated_at",
38
+ "observation_count",
39
+ "confirmed_count",
40
+ "contradicted_count",
41
+ "inactive_count",
42
+ ] as const;
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Validation
46
+ // ---------------------------------------------------------------------------
47
+
48
+ export interface ValidationError {
49
+ index: number;
50
+ reason: string;
51
+ }
52
+
53
+ /**
54
+ * Validates a raw JSON object as an Instinct.
55
+ * Returns an error string if invalid, null if valid.
56
+ */
57
+ export function validateImportObject(
58
+ obj: unknown,
59
+ index: number
60
+ ): ValidationError | null {
61
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
62
+ return { index, reason: "not an object" };
63
+ }
64
+
65
+ const record = obj as Record<string, unknown>;
66
+
67
+ for (const field of REQUIRED_FIELDS) {
68
+ if (record[field] === undefined || record[field] === null) {
69
+ return { index, reason: `missing required field "${field}"` };
70
+ }
71
+ }
72
+
73
+ const id = String(record["id"]);
74
+ if (!KEBAB_RE.test(id)) {
75
+ return { index, reason: `invalid id "${id}" - must be kebab-case` };
76
+ }
77
+
78
+ return null;
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // File loading
83
+ // ---------------------------------------------------------------------------
84
+
85
+ export interface LoadResult {
86
+ valid: Instinct[];
87
+ invalid: ValidationError[];
88
+ }
89
+
90
+ /**
91
+ * Reads and parses the import JSON file.
92
+ * Returns valid instincts and validation errors separately.
93
+ */
94
+ export function loadImportFile(filePath: string): LoadResult {
95
+ const content = readFileSync(filePath, "utf-8");
96
+ const parsed: unknown = JSON.parse(content);
97
+
98
+ if (!Array.isArray(parsed)) {
99
+ throw new Error("Import file must contain a JSON array of instinct objects.");
100
+ }
101
+
102
+ const valid: Instinct[] = [];
103
+ const invalid: ValidationError[] = [];
104
+
105
+ for (let i = 0; i < parsed.length; i++) {
106
+ const err = validateImportObject(parsed[i], i);
107
+ if (err) {
108
+ invalid.push(err);
109
+ } else {
110
+ valid.push(parsed[i] as Instinct);
111
+ }
112
+ }
113
+
114
+ return { valid, invalid };
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Duplicate detection
119
+ // ---------------------------------------------------------------------------
120
+
121
+ export interface PartitionResult {
122
+ toImport: Instinct[];
123
+ duplicates: string[];
124
+ }
125
+
126
+ /**
127
+ * Partitions instincts into those to import and those skipped as duplicates.
128
+ * Checks existing inherited instincts in both project and global directories.
129
+ */
130
+ export function partitionByDuplicates(
131
+ instincts: Instinct[],
132
+ projectId: string | null | undefined,
133
+ baseDir: string
134
+ ): PartitionResult {
135
+ const existingIds = new Set<string>();
136
+
137
+ const globalDir = getGlobalInstinctsDir("inherited", baseDir);
138
+ for (const inst of listInstincts(globalDir)) {
139
+ existingIds.add(inst.id);
140
+ }
141
+
142
+ if (projectId != null) {
143
+ const projectDir = getProjectInstinctsDir(projectId, "inherited", baseDir);
144
+ for (const inst of listInstincts(projectDir)) {
145
+ existingIds.add(inst.id);
146
+ }
147
+ }
148
+
149
+ const toImport: Instinct[] = [];
150
+ const duplicates: string[] = [];
151
+
152
+ for (const inst of instincts) {
153
+ if (existingIds.has(inst.id)) {
154
+ duplicates.push(inst.id);
155
+ } else {
156
+ toImport.push(inst);
157
+ }
158
+ }
159
+
160
+ return { toImport, duplicates };
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Target directory resolution
165
+ // ---------------------------------------------------------------------------
166
+
167
+ /**
168
+ * Returns the inherited instincts directory for the given instinct.
169
+ * Project-scoped instincts go into the project's inherited dir.
170
+ * Global instincts go into the global inherited dir.
171
+ */
172
+ export function getTargetDir(
173
+ instinct: Instinct,
174
+ projectId: string | null | undefined,
175
+ baseDir: string
176
+ ): string {
177
+ if (instinct.scope === "project" && projectId != null) {
178
+ return getProjectInstinctsDir(projectId, "inherited", baseDir);
179
+ }
180
+ return getGlobalInstinctsDir("inherited", baseDir);
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // handleInstinctImport
185
+ // ---------------------------------------------------------------------------
186
+
187
+ /**
188
+ * Command handler for /instinct-import.
189
+ * Reads the JSON file at the given path, validates each instinct,
190
+ * skips duplicates, and saves valid instincts to inherited/ directories.
191
+ */
192
+ export async function handleInstinctImport(
193
+ args: string,
194
+ ctx: ExtensionCommandContext,
195
+ projectId?: string | null,
196
+ baseDir?: string
197
+ ): Promise<void> {
198
+ const effectiveBase = baseDir ?? getBaseDir();
199
+ const filePath = resolve(ctx.cwd, args.trim());
200
+
201
+ if (!existsSync(filePath)) {
202
+ ctx.ui.notify(`Import failed: file not found: ${filePath}`, "error");
203
+ return;
204
+ }
205
+
206
+ let loadResult: LoadResult;
207
+ try {
208
+ loadResult = loadImportFile(filePath);
209
+ } catch (err) {
210
+ const msg = err instanceof Error ? err.message : String(err);
211
+ ctx.ui.notify(`Import failed: ${msg}`, "error");
212
+ return;
213
+ }
214
+
215
+ const { valid, invalid } = loadResult;
216
+
217
+ const { toImport, duplicates } = partitionByDuplicates(
218
+ valid,
219
+ projectId,
220
+ effectiveBase
221
+ );
222
+
223
+ // Ensure target dirs exist before writing
224
+ const globalInheritedDir = getGlobalInstinctsDir("inherited", effectiveBase);
225
+ mkdirSync(globalInheritedDir, { recursive: true });
226
+
227
+ if (projectId != null) {
228
+ const projectInheritedDir = getProjectInstinctsDir(
229
+ projectId,
230
+ "inherited",
231
+ effectiveBase
232
+ );
233
+ mkdirSync(projectInheritedDir, { recursive: true });
234
+ }
235
+
236
+ // Save each instinct to the correct inherited directory
237
+ for (const instinct of toImport) {
238
+ const targetDir = getTargetDir(instinct, projectId, effectiveBase);
239
+ saveInstinct(instinct, targetDir);
240
+ }
241
+
242
+ // Build summary message
243
+ const lines: string[] = [
244
+ `Imported ${toImport.length} instinct${toImport.length !== 1 ? "s" : ""} from ${filePath}`,
245
+ ];
246
+
247
+ if (duplicates.length > 0) {
248
+ lines.push(
249
+ `Skipped ${duplicates.length} duplicate${duplicates.length !== 1 ? "s" : ""}: ${duplicates.join(", ")}`
250
+ );
251
+ }
252
+
253
+ if (invalid.length > 0) {
254
+ lines.push(
255
+ `Skipped ${invalid.length} invalid entr${invalid.length !== 1 ? "ies" : "y"}: ${invalid.map((e) => `[${e.index}] ${e.reason}`).join("; ")}`
256
+ );
257
+ }
258
+
259
+ ctx.ui.notify(lines.join("\n"), "info");
260
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * System prompt injection for pi-continuous-learning.
3
+ * Loads filtered instincts and appends them to the system prompt on each
4
+ * before_agent_start event so the agent benefits from learned behaviors.
5
+ * Also bridges injected instinct IDs to shared active-instincts state (US-023).
6
+ */
7
+
8
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
9
+ import type { BeforeAgentStartEvent, AgentEndEvent } from "./prompt-observer.js";
10
+ import type { Config, Instinct } from "./types.js";
11
+
12
+ /** Subset of BeforeAgentStartEventResult used by this module. */
13
+ export interface InjectionResult {
14
+ /** Replacement system prompt to use for this turn. */
15
+ systemPrompt?: string;
16
+ }
17
+ import { loadAndFilterFromConfig, inferDomains } from "./instinct-loader.js";
18
+ import {
19
+ setCurrentActiveInstincts,
20
+ clearActiveInstincts,
21
+ } from "./active-instincts.js";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Constants
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export const INSTINCTS_HEADER = "## Learned Behaviors (Instincts)";
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // buildInjectionBlock
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /**
34
+ * Builds the injection block string from a list of instincts.
35
+ * Returns null when the list is empty (no block needed).
36
+ */
37
+ export function buildInjectionBlock(instincts: Instinct[], maxChars?: number): string | null {
38
+ if (instincts.length === 0) return null;
39
+
40
+ const headerLen = `\n\n${INSTINCTS_HEADER}\n`.length;
41
+ const allBullets: string[] = [];
42
+ let charCount = headerLen;
43
+ let omitted = 0;
44
+
45
+ for (const i of instincts) {
46
+ const bullet = `- [${i.confidence.toFixed(2)}] ${i.trigger}: ${i.action}`;
47
+ const bulletLen = bullet.length + 1; // +1 for newline
48
+
49
+ if (maxChars && charCount + bulletLen > maxChars) {
50
+ omitted = instincts.length - allBullets.length;
51
+ break;
52
+ }
53
+
54
+ allBullets.push(bullet);
55
+ charCount += bulletLen;
56
+ }
57
+
58
+ if (allBullets.length === 0) return null;
59
+
60
+ let result = `\n\n${INSTINCTS_HEADER}\n${allBullets.join("\n")}`;
61
+ if (omitted > 0) {
62
+ result += `\n(${omitted} lower-confidence instinct${omitted > 1 ? "s" : ""} omitted)`;
63
+ }
64
+ return result;
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // injectInstincts (pure, for testing)
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /**
72
+ * Returns a modified system prompt string with injected instincts,
73
+ * or null when no qualifying instincts were found.
74
+ * Pure function - no I/O.
75
+ */
76
+ export function injectInstincts(
77
+ systemPrompt: string,
78
+ instincts: Instinct[]
79
+ ): string | null {
80
+ const block = buildInjectionBlock(instincts);
81
+ if (block === null) return null;
82
+ return systemPrompt + block;
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // handleBeforeAgentStartInjection
87
+ // ---------------------------------------------------------------------------
88
+
89
+ /**
90
+ * Handles before_agent_start events.
91
+ * Loads qualifying instincts, appends them to the system prompt, and stores
92
+ * their IDs in shared active-instincts state for observation tagging (US-023).
93
+ * Returns undefined when no instincts qualify (no-op).
94
+ */
95
+ export function handleBeforeAgentStartInjection(
96
+ event: BeforeAgentStartEvent,
97
+ _ctx: ExtensionContext,
98
+ config: Config,
99
+ projectId?: string | null,
100
+ baseDir?: string
101
+ ): InjectionResult | void {
102
+ const relevantDomains = inferDomains(event.prompt);
103
+ const instincts = loadAndFilterFromConfig(config, projectId, baseDir, relevantDomains);
104
+
105
+ const block = buildInjectionBlock(instincts, config.max_injection_chars);
106
+ if (block === null) {
107
+ setCurrentActiveInstincts([]);
108
+ return undefined;
109
+ }
110
+
111
+ setCurrentActiveInstincts(instincts.map((i) => i.id));
112
+ return { systemPrompt: event.systemPrompt + block };
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // handleAgentEndClearInstincts
117
+ // ---------------------------------------------------------------------------
118
+
119
+ /**
120
+ * Handles agent_end events.
121
+ * Clears active instincts state so the next prompt starts clean (US-023).
122
+ */
123
+ export function handleAgentEndClearInstincts(
124
+ _event: AgentEndEvent,
125
+ _ctx: ExtensionContext
126
+ ): void {
127
+ clearActiveInstincts();
128
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Instinct loading and filtering for the injector.
3
+ * Loads project and global instincts, filters by confidence threshold,
4
+ * sorts by confidence descending, and caps to max_instincts.
5
+ */
6
+
7
+ import type { Instinct, Config } from "./types.js";
8
+ import { loadProjectInstincts, loadGlobalInstincts } from "./instinct-store.js";
9
+ import { DEFAULT_CONFIG } from "./config.js";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Domain inference
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const UNIVERSAL_DOMAINS = new Set(["workflow", "git"]);
16
+
17
+ const DOMAIN_KEYWORDS: Record<string, string[]> = {
18
+ typescript: ["typescript", ".ts", "type ", "interface ", "generic"],
19
+ css: ["css", "style", "tailwind", "classname", "scss", "sass"],
20
+ testing: ["test", "spec", "vitest", "jest", "coverage", "assert"],
21
+ git: ["git", "commit", "branch", "merge", "rebase", "stash"],
22
+ debugging: ["debug", "error", "stack trace", "exception", "breakpoint"],
23
+ performance: ["performance", "slow", "memory", "profil", "latency", "cache"],
24
+ security: ["security", "auth", "token", "secret", "csrf", "xss", "injection"],
25
+ documentation: ["documentation", "readme", "jsdoc", "docstring"],
26
+ design: ["component", "ui ", "layout", "responsive", "accessibility"],
27
+ workflow: ["workflow", "ci", "pipeline", "deploy", "automat"],
28
+ };
29
+
30
+ export function inferDomains(userPrompt: string): Set<string> {
31
+ const lower = userPrompt.toLowerCase();
32
+ const matched = new Set<string>();
33
+ for (const [domain, keywords] of Object.entries(DOMAIN_KEYWORDS)) {
34
+ if (keywords.some((kw) => lower.includes(kw))) {
35
+ matched.add(domain);
36
+ }
37
+ }
38
+ return matched;
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Types
43
+ // ---------------------------------------------------------------------------
44
+
45
+ export interface LoadInstinctsOptions {
46
+ /** Project ID, or undefined/null when running outside a project. */
47
+ projectId?: string | null;
48
+ /** Minimum confidence threshold (default: DEFAULT_CONFIG.min_confidence). */
49
+ minConfidence?: number;
50
+ /** Maximum number of instincts to return (default: DEFAULT_CONFIG.max_instincts). */
51
+ maxInstincts?: number;
52
+ /** Optional base directory for storage (used in tests). */
53
+ baseDir?: string;
54
+ /** Domains relevant to the current context — matched instincts sort first. */
55
+ relevantDomains?: Set<string>;
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // filterInstincts
60
+ // ---------------------------------------------------------------------------
61
+
62
+ /**
63
+ * Filters, sorts, and caps a flat list of instincts.
64
+ * Pure function - no I/O.
65
+ */
66
+ export function filterInstincts(
67
+ instincts: Instinct[],
68
+ minConfidence: number,
69
+ maxInstincts: number,
70
+ relevantDomains?: Set<string>
71
+ ): Instinct[] {
72
+ const eligible = instincts.filter(
73
+ (i) => !i.flagged_for_removal && i.confidence >= minConfidence
74
+ );
75
+
76
+ const sorted = [...eligible].sort((a, b) => {
77
+ // When relevantDomains are provided, prioritize domain-matched instincts
78
+ if (relevantDomains && relevantDomains.size > 0) {
79
+ const aRelevant = relevantDomains.has(a.domain) || UNIVERSAL_DOMAINS.has(a.domain);
80
+ const bRelevant = relevantDomains.has(b.domain) || UNIVERSAL_DOMAINS.has(b.domain);
81
+ if (aRelevant && !bRelevant) return -1;
82
+ if (!aRelevant && bRelevant) return 1;
83
+ }
84
+ return b.confidence - a.confidence;
85
+ });
86
+
87
+ return sorted.slice(0, maxInstincts);
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // loadAndFilterInstincts
92
+ // ---------------------------------------------------------------------------
93
+
94
+ /**
95
+ * Loads instincts from disk, filters by confidence threshold, sorts by
96
+ * confidence descending, and caps to max_instincts.
97
+ *
98
+ * When projectId is provided (and non-null), loads both project-scoped
99
+ * instincts and global instincts. Otherwise loads only global instincts.
100
+ */
101
+ export function loadAndFilterInstincts(
102
+ options: LoadInstinctsOptions = {}
103
+ ): Instinct[] {
104
+ const {
105
+ projectId,
106
+ minConfidence = DEFAULT_CONFIG.min_confidence,
107
+ maxInstincts = DEFAULT_CONFIG.max_instincts,
108
+ baseDir,
109
+ relevantDomains,
110
+ } = options;
111
+
112
+ const projectInstincts =
113
+ projectId != null
114
+ ? loadProjectInstincts(projectId, baseDir)
115
+ : [];
116
+
117
+ const globalInstincts = loadGlobalInstincts(baseDir);
118
+
119
+ // Combine: project instincts first, then global (project-scoped are more specific)
120
+ const all = [...projectInstincts, ...globalInstincts];
121
+
122
+ return filterInstincts(all, minConfidence, maxInstincts, relevantDomains);
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // loadAndFilterFromConfig
127
+ // ---------------------------------------------------------------------------
128
+
129
+ /**
130
+ * Convenience wrapper - uses thresholds from a Config object.
131
+ */
132
+ export function loadAndFilterFromConfig(
133
+ config: Config,
134
+ projectId?: string | null,
135
+ baseDir?: string,
136
+ relevantDomains?: Set<string>
137
+ ): Instinct[] {
138
+ const opts: LoadInstinctsOptions = {
139
+ minConfidence: config.min_confidence,
140
+ maxInstincts: config.max_instincts,
141
+ };
142
+ if (projectId !== undefined) opts.projectId = projectId;
143
+ if (baseDir !== undefined) opts.baseDir = baseDir;
144
+ if (relevantDomains !== undefined) opts.relevantDomains = relevantDomains;
145
+ return loadAndFilterInstincts(opts);
146
+ }