pi-continuous-learning 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +78 -0
  2. package/dist/agents-md.d.ts +23 -2
  3. package/dist/agents-md.d.ts.map +1 -1
  4. package/dist/agents-md.js +58 -3
  5. package/dist/agents-md.js.map +1 -1
  6. package/dist/cli/analyze-single-shot.d.ts +62 -0
  7. package/dist/cli/analyze-single-shot.d.ts.map +1 -0
  8. package/dist/cli/analyze-single-shot.js +105 -0
  9. package/dist/cli/analyze-single-shot.js.map +1 -0
  10. package/dist/cli/analyze.js +82 -81
  11. package/dist/cli/analyze.js.map +1 -1
  12. package/dist/command-scaffold.d.ts +25 -0
  13. package/dist/command-scaffold.d.ts.map +1 -0
  14. package/dist/command-scaffold.js +77 -0
  15. package/dist/command-scaffold.js.map +1 -0
  16. package/dist/confidence.d.ts.map +1 -1
  17. package/dist/confidence.js +2 -1
  18. package/dist/confidence.js.map +1 -1
  19. package/dist/config.d.ts +16 -0
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +31 -0
  22. package/dist/config.js.map +1 -1
  23. package/dist/graduation.d.ts +63 -0
  24. package/dist/graduation.d.ts.map +1 -0
  25. package/dist/graduation.js +155 -0
  26. package/dist/graduation.js.map +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +5 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/instinct-cleanup.d.ts +57 -0
  31. package/dist/instinct-cleanup.d.ts.map +1 -0
  32. package/dist/instinct-cleanup.js +150 -0
  33. package/dist/instinct-cleanup.js.map +1 -0
  34. package/dist/instinct-graduate.d.ts +43 -0
  35. package/dist/instinct-graduate.d.ts.map +1 -0
  36. package/dist/instinct-graduate.js +253 -0
  37. package/dist/instinct-graduate.js.map +1 -0
  38. package/dist/instinct-parser.d.ts.map +1 -1
  39. package/dist/instinct-parser.js +12 -0
  40. package/dist/instinct-parser.js.map +1 -1
  41. package/dist/instinct-tools.d.ts.map +1 -1
  42. package/dist/instinct-tools.js +19 -0
  43. package/dist/instinct-tools.js.map +1 -1
  44. package/dist/instinct-validator.d.ts +61 -0
  45. package/dist/instinct-validator.d.ts.map +1 -0
  46. package/dist/instinct-validator.js +235 -0
  47. package/dist/instinct-validator.js.map +1 -0
  48. package/dist/observation-preprocessor.d.ts +26 -0
  49. package/dist/observation-preprocessor.d.ts.map +1 -0
  50. package/dist/observation-preprocessor.js +31 -0
  51. package/dist/observation-preprocessor.js.map +1 -0
  52. package/dist/prompts/analyzer-system-single-shot.d.ts +6 -0
  53. package/dist/prompts/analyzer-system-single-shot.d.ts.map +1 -0
  54. package/dist/prompts/analyzer-system-single-shot.js +164 -0
  55. package/dist/prompts/analyzer-system-single-shot.js.map +1 -0
  56. package/dist/prompts/analyzer-user-single-shot.d.ts +22 -0
  57. package/dist/prompts/analyzer-user-single-shot.d.ts.map +1 -0
  58. package/dist/prompts/analyzer-user-single-shot.js +53 -0
  59. package/dist/prompts/analyzer-user-single-shot.js.map +1 -0
  60. package/dist/prompts/analyzer-user.d.ts +3 -1
  61. package/dist/prompts/analyzer-user.d.ts.map +1 -1
  62. package/dist/prompts/analyzer-user.js +20 -7
  63. package/dist/prompts/analyzer-user.js.map +1 -1
  64. package/dist/skill-scaffold.d.ts +23 -0
  65. package/dist/skill-scaffold.d.ts.map +1 -0
  66. package/dist/skill-scaffold.js +62 -0
  67. package/dist/skill-scaffold.js.map +1 -0
  68. package/dist/types.d.ts +8 -0
  69. package/dist/types.d.ts.map +1 -1
  70. package/package.json +1 -1
  71. package/src/agents-md.ts +73 -3
  72. package/src/cli/analyze-single-shot.ts +175 -0
  73. package/src/cli/analyze.ts +93 -124
  74. package/src/command-scaffold.ts +105 -0
  75. package/src/confidence.ts +2 -1
  76. package/src/config.ts +40 -0
  77. package/src/graduation.ts +243 -0
  78. package/src/index.ts +14 -0
  79. package/src/instinct-cleanup.ts +204 -0
  80. package/src/instinct-graduate.ts +377 -0
  81. package/src/instinct-parser.ts +12 -0
  82. package/src/instinct-tools.ts +26 -0
  83. package/src/instinct-validator.ts +287 -0
  84. package/src/observation-preprocessor.ts +48 -0
  85. package/src/prompts/analyzer-system-single-shot.ts +163 -0
  86. package/src/prompts/analyzer-user-single-shot.ts +94 -0
  87. package/src/prompts/analyzer-user.ts +26 -8
  88. package/src/skill-scaffold.ts +90 -0
  89. package/src/types.ts +10 -0
@@ -7,36 +7,36 @@ import {
7
7
  unlinkSync,
8
8
  } from "node:fs";
9
9
  import { join } from "node:path";
10
- import {
11
- createAgentSession,
12
- SessionManager,
13
- AuthStorage,
14
- ModelRegistry,
15
- DefaultResourceLoader,
16
- } from "@mariozechner/pi-coding-agent";
10
+ import { AuthStorage } from "@mariozechner/pi-coding-agent";
17
11
  import { getModel } from "@mariozechner/pi-ai";
18
12
 
19
13
  import { loadConfig, DEFAULT_CONFIG } from "../config.js";
20
- import type { ProjectEntry } from "../types.js";
14
+ import type { InstalledSkill, ProjectEntry } from "../types.js";
21
15
  import {
22
16
  getBaseDir,
23
17
  getProjectsRegistryPath,
24
18
  getObservationsPath,
25
19
  getProjectDir,
20
+ getProjectInstinctsDir,
21
+ getGlobalInstinctsDir,
26
22
  } from "../storage.js";
27
23
  import { countObservations } from "../observations.js";
28
24
  import { runDecayPass } from "../instinct-decay.js";
29
- import { buildAnalyzerUserPrompt, tailObservationsSince } from "../prompts/analyzer-user.js";
30
- import { buildAnalyzerSystemPrompt } from "./analyze-prompt.js";
25
+ import { runCleanupPass } from "../instinct-cleanup.js";
26
+ import { tailObservationsSince } from "../prompts/analyzer-user.js";
27
+ import { buildSingleShotSystemPrompt } from "../prompts/analyzer-system-single-shot.js";
28
+ import { buildSingleShotUserPrompt } from "../prompts/analyzer-user-single-shot.js";
29
+ import {
30
+ runSingleShot,
31
+ buildInstinctFromChange,
32
+ } from "./analyze-single-shot.js";
31
33
  import {
32
- createInstinctListTool,
33
- createInstinctReadTool,
34
- createInstinctWriteTool,
35
- createInstinctDeleteTool,
36
- } from "../instinct-tools.js";
34
+ loadProjectInstincts,
35
+ loadGlobalInstincts,
36
+ saveInstinct,
37
+ } from "../instinct-store.js";
37
38
  import { readAgentsMd } from "../agents-md.js";
38
39
  import { homedir } from "node:os";
39
- import type { InstalledSkill } from "../types.js";
40
40
  import { AnalyzeLogger, type ProjectRunStats, type RunSummary } from "./analyze-logger.js";
41
41
 
42
42
  // ---------------------------------------------------------------------------
@@ -59,7 +59,6 @@ function acquireLock(baseDir: string): boolean {
59
59
  const lock = JSON.parse(content) as { pid: number; started_at: string };
60
60
  const age = Date.now() - new Date(lock.started_at).getTime();
61
61
 
62
- // Check if the owning process is still alive
63
62
  try {
64
63
  process.kill(lock.pid, 0); // signal 0 = existence check, no actual signal
65
64
  if (age < LOCK_STALE_MS) {
@@ -104,72 +103,6 @@ function startGlobalTimeout(timeoutMs: number, logger: AnalyzeLogger): void {
104
103
  }, timeoutMs).unref();
105
104
  }
106
105
 
107
- // ---------------------------------------------------------------------------
108
- // Instinct operation tracking
109
- // ---------------------------------------------------------------------------
110
-
111
- interface InstinctOpCounts {
112
- created: number;
113
- updated: number;
114
- deleted: number;
115
- }
116
-
117
- /**
118
- * Wraps instinct tools to count create/update/delete operations.
119
- * Returns new tool instances that increment the provided counts.
120
- */
121
- function wrapInstinctToolsWithTracking(
122
- projectId: string,
123
- projectName: string,
124
- baseDir: string,
125
- counts: InstinctOpCounts
126
- ) {
127
- const writeTool = createInstinctWriteTool(projectId, projectName, baseDir);
128
- const deleteTool = createInstinctDeleteTool(projectId, baseDir);
129
-
130
- const trackedWrite = {
131
- ...writeTool,
132
- async execute(
133
- toolCallId: string,
134
- params: Parameters<typeof writeTool.execute>[1],
135
- signal: AbortSignal | undefined,
136
- onUpdate: unknown,
137
- ctx: unknown
138
- ) {
139
- const result = await writeTool.execute(toolCallId, params, signal, onUpdate, ctx);
140
- const details = result.details as { action?: string } | undefined;
141
- if (details?.action === "created") {
142
- counts.created++;
143
- } else {
144
- counts.updated++;
145
- }
146
- return result;
147
- },
148
- };
149
-
150
- const trackedDelete = {
151
- ...deleteTool,
152
- async execute(
153
- toolCallId: string,
154
- params: Parameters<typeof deleteTool.execute>[1],
155
- signal: AbortSignal | undefined,
156
- onUpdate: unknown,
157
- ctx: unknown
158
- ) {
159
- const result = await deleteTool.execute(toolCallId, params, signal, onUpdate, ctx);
160
- counts.deleted++;
161
- return result;
162
- },
163
- };
164
-
165
- return {
166
- listTool: createInstinctListTool(projectId, baseDir),
167
- readTool: createInstinctReadTool(projectId, baseDir),
168
- writeTool: trackedWrite,
169
- deleteTool: trackedDelete,
170
- };
171
- }
172
-
173
106
  // ---------------------------------------------------------------------------
174
107
  // Per-project analysis
175
108
  // ---------------------------------------------------------------------------
@@ -237,10 +170,13 @@ async function analyzeProject(
237
170
 
238
171
  const obsPath = getObservationsPath(project.id, baseDir);
239
172
  const sinceLineCount = meta.last_observation_line_count ?? 0;
240
- const { lines: newObsLines, totalLineCount } = tailObservationsSince(obsPath, sinceLineCount);
173
+ const { lines: newObsLines, totalLineCount, rawLineCount } = tailObservationsSince(
174
+ obsPath,
175
+ sinceLineCount
176
+ );
241
177
 
242
178
  if (newObsLines.length === 0) {
243
- return { ran: false, skippedReason: "no new observation lines" };
179
+ return { ran: false, skippedReason: "no new observation lines after preprocessing" };
244
180
  }
245
181
 
246
182
  const obsCount = countObservations(project.id, baseDir);
@@ -249,11 +185,15 @@ async function analyzeProject(
249
185
  }
250
186
 
251
187
  const startTime = Date.now();
252
- logger.projectStart(project.id, project.name, newObsLines.length, obsCount);
188
+ logger.projectStart(project.id, project.name, rawLineCount, obsCount);
253
189
 
190
+ runCleanupPass(project.id, config, baseDir);
254
191
  runDecayPass(project.id, baseDir);
255
192
 
256
- const instinctsDir = join(getProjectDir(project.id, baseDir), "instincts", "personal");
193
+ // Load current instincts inline - no tool calls needed
194
+ const projectInstincts = loadProjectInstincts(project.id, baseDir);
195
+ const globalInstincts = loadGlobalInstincts(baseDir);
196
+ const allInstincts = [...projectInstincts, ...globalInstincts];
257
197
 
258
198
  const agentsMdProject = readAgentsMd(join(project.root, "AGENTS.md"));
259
199
  const agentsMdGlobal = readAgentsMd(join(homedir(), ".pi", "agent", "AGENTS.md"));
@@ -270,68 +210,98 @@ async function analyzeProject(
270
210
  // Skills loading is best-effort - continue without them
271
211
  }
272
212
 
273
- const userPrompt = buildAnalyzerUserPrompt(obsPath, instinctsDir, project, {
213
+ const userPrompt = buildSingleShotUserPrompt(project, allInstincts, newObsLines, {
274
214
  agentsMdProject,
275
215
  agentsMdGlobal,
276
216
  installedSkills,
277
- observationLines: newObsLines,
278
217
  });
279
218
 
280
219
  const authStorage = AuthStorage.create();
281
- const modelRegistry = new ModelRegistry(authStorage);
282
220
  const modelId = (config.model || DEFAULT_CONFIG.model) as Parameters<typeof getModel>[1];
283
221
  const model = getModel("anthropic", modelId);
222
+ const apiKey = await authStorage.getApiKey("anthropic");
284
223
 
285
- // Track instinct operations
286
- const instinctCounts: InstinctOpCounts = { created: 0, updated: 0, deleted: 0 };
287
- const trackedTools = wrapInstinctToolsWithTracking(project.id, project.name, baseDir, instinctCounts);
224
+ if (!apiKey) {
225
+ throw new Error("No Anthropic API key configured. Set via auth.json or ANTHROPIC_API_KEY.");
226
+ }
288
227
 
289
- const customTools = [
290
- trackedTools.listTool,
291
- trackedTools.readTool,
292
- trackedTools.writeTool,
293
- trackedTools.deleteTool,
294
- ];
228
+ const context = {
229
+ systemPrompt: buildSingleShotSystemPrompt(),
230
+ messages: [
231
+ { role: "user" as const, content: userPrompt, timestamp: Date.now() },
232
+ ],
233
+ };
295
234
 
296
- const loader = new DefaultResourceLoader({
297
- systemPromptOverride: () => buildAnalyzerSystemPrompt(),
298
- });
299
- await loader.reload();
300
-
301
- const { session } = await createAgentSession({
302
- model,
303
- authStorage,
304
- modelRegistry,
305
- sessionManager: SessionManager.inMemory(),
306
- customTools,
307
- resourceLoader: loader,
308
- });
235
+ const timeoutMs = (config.timeout_seconds ?? DEFAULT_CONFIG.timeout_seconds) * 1000;
236
+ const abortController = new AbortController();
237
+ const timeoutHandle = setTimeout(() => abortController.abort(), timeoutMs);
238
+
239
+ const instinctCounts = { created: 0, updated: 0, deleted: 0 };
240
+ const projectInstinctsDir = getProjectInstinctsDir(project.id, "personal", baseDir);
241
+ const globalInstinctsDir = getGlobalInstinctsDir("personal", baseDir);
309
242
 
243
+ let singleShotMessage;
310
244
  try {
311
- await session.prompt(userPrompt);
245
+ const result = await runSingleShot(context, model, apiKey, abortController.signal);
246
+ singleShotMessage = result.message;
247
+
248
+ // Enforce creation rate limit: only the first N create actions per run are applied.
249
+ const maxNewInstincts = config.max_new_instincts_per_run ?? DEFAULT_CONFIG.max_new_instincts_per_run;
250
+ let createsRemaining = maxNewInstincts;
251
+
252
+ for (const change of result.changes) {
253
+ if (change.action === "delete") {
254
+ const id = change.id;
255
+ if (!id) continue;
256
+ const dir = change.scope === "global" ? globalInstinctsDir : projectInstinctsDir;
257
+ const filePath = join(dir, `${id}.md`);
258
+ if (existsSync(filePath)) {
259
+ unlinkSync(filePath);
260
+ instinctCounts.deleted++;
261
+ }
262
+ } else if (change.action === "create") {
263
+ if (createsRemaining <= 0) continue; // rate limit reached
264
+ const existing = allInstincts.find((i) => i.id === change.instinct?.id) ?? null;
265
+ const instinct = buildInstinctFromChange(change, existing, project.id, allInstincts);
266
+ if (!instinct) continue;
267
+
268
+ const dir = instinct.scope === "global" ? globalInstinctsDir : projectInstinctsDir;
269
+ saveInstinct(instinct, dir);
270
+ instinctCounts.created++;
271
+ createsRemaining--;
272
+ } else {
273
+ // update
274
+ const existing = allInstincts.find((i) => i.id === change.instinct?.id) ?? null;
275
+ const instinct = buildInstinctFromChange(change, existing, project.id, allInstincts);
276
+ if (!instinct) continue;
277
+
278
+ const dir = instinct.scope === "global" ? globalInstinctsDir : projectInstinctsDir;
279
+ saveInstinct(instinct, dir);
280
+ instinctCounts.updated++;
281
+ }
282
+ }
312
283
  } finally {
313
- session.dispose();
284
+ clearTimeout(timeoutHandle);
314
285
  }
315
286
 
316
- // Collect stats after session completes
317
- const sessionStats = session.getSessionStats();
287
+ const usage = singleShotMessage!.usage;
318
288
  const durationMs = Date.now() - startTime;
319
289
 
320
290
  const stats: ProjectRunStats = {
321
291
  project_id: project.id,
322
292
  project_name: project.name,
323
293
  duration_ms: durationMs,
324
- observations_processed: newObsLines.length,
294
+ observations_processed: rawLineCount,
325
295
  observations_total: obsCount,
326
296
  instincts_created: instinctCounts.created,
327
297
  instincts_updated: instinctCounts.updated,
328
298
  instincts_deleted: instinctCounts.deleted,
329
- tokens_input: sessionStats.tokens.input,
330
- tokens_output: sessionStats.tokens.output,
331
- tokens_cache_read: sessionStats.tokens.cacheRead,
332
- tokens_cache_write: sessionStats.tokens.cacheWrite,
333
- tokens_total: sessionStats.tokens.total,
334
- cost_usd: sessionStats.cost,
299
+ tokens_input: usage.input,
300
+ tokens_output: usage.output,
301
+ tokens_cache_read: usage.cacheRead,
302
+ tokens_cache_write: usage.cacheWrite,
303
+ tokens_total: usage.totalTokens,
304
+ cost_usd: usage.cost.total,
335
305
  model: modelId,
336
306
  };
337
307
 
@@ -420,7 +390,6 @@ async function main(): Promise<void> {
420
390
 
421
391
  main().catch((err) => {
422
392
  releaseLock(getBaseDir());
423
- // Last-resort logging - config may not have loaded
424
393
  const logger = new AnalyzeLogger();
425
394
  logger.error("Fatal error", err);
426
395
  process.exit(1);
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Command scaffolding from instinct clusters.
3
+ *
4
+ * When 3+ related instincts in the same domain form an actionable workflow,
5
+ * generates a Pi slash command scaffold that codifies the pattern.
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 CommandScaffold {
16
+ name: string;
17
+ description: string;
18
+ domain: string;
19
+ content: string;
20
+ sourceInstinctIds: string[];
21
+ }
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function toCommandName(domain: string): string {
28
+ return domain.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
29
+ }
30
+
31
+ function formatInstinctAsStep(instinct: Instinct, index: number): string {
32
+ return [
33
+ `${index + 1}. **${instinct.title}**`,
34
+ ` - Trigger: ${instinct.trigger}`,
35
+ ` - Action: ${instinct.action}`,
36
+ "",
37
+ ].join("\n");
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Public API
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /**
45
+ * Generates a command scaffold from a domain cluster of instincts.
46
+ * The scaffold describes a slash command that encodes the workflow
47
+ * distilled from the instinct cluster.
48
+ */
49
+ export function generateCommandScaffold(cluster: DomainCluster): CommandScaffold {
50
+ const name = toCommandName(cluster.domain);
51
+ const sortedInstincts = [...cluster.instincts].sort(
52
+ (a, b) => b.confidence - a.confidence
53
+ );
54
+
55
+ const description = `Learned ${cluster.domain} workflow from coding sessions. ` +
56
+ `Encodes ${sortedInstincts.length} steps distilled from instinct observations.`;
57
+
58
+ const steps = sortedInstincts.map((inst, i) => formatInstinctAsStep(inst, i));
59
+
60
+ const content = [
61
+ `# /${name} Command`,
62
+ "",
63
+ `> Auto-generated from ${sortedInstincts.length} graduated instincts in the "${cluster.domain}" domain.`,
64
+ "",
65
+ `## Description`,
66
+ "",
67
+ description,
68
+ "",
69
+ `## Command: \`/${name}\``,
70
+ "",
71
+ `When invoked, this command should guide the agent through these steps:`,
72
+ "",
73
+ ...steps,
74
+ `## Implementation Notes`,
75
+ "",
76
+ `Register this command in your Pi extension's \`index.ts\`:`,
77
+ "",
78
+ "```typescript",
79
+ `pi.registerCommand("${name}", {`,
80
+ ` description: "${description.replace(/"/g, '\\"')}",`,
81
+ ` handler: async (args, ctx) => {`,
82
+ ` // TODO: Implement ${name} workflow`,
83
+ ` },`,
84
+ `});`,
85
+ "```",
86
+ "",
87
+ ].join("\n");
88
+
89
+ return {
90
+ name,
91
+ description,
92
+ domain: cluster.domain,
93
+ content,
94
+ sourceInstinctIds: sortedInstincts.map((i) => i.id),
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Generates command scaffolds for all qualifying clusters.
100
+ */
101
+ export function generateAllCommandScaffolds(
102
+ clusters: DomainCluster[]
103
+ ): CommandScaffold[] {
104
+ return clusters.map(generateCommandScaffold);
105
+ }
package/src/confidence.ts CHANGED
@@ -26,7 +26,8 @@ const DELTA_CONTRADICTED = -0.15;
26
26
  const DELTA_INACTIVE = 0;
27
27
 
28
28
  // applyPassiveDecay
29
- const DECAY_PER_WEEK = 0.02;
29
+ // Increased from 0.02 to 0.05: at 0.5 confidence, reaches 0.1 in ~8 weeks instead of 20.
30
+ const DECAY_PER_WEEK = 0.05;
30
31
  const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000;
31
32
 
32
33
  // ---------------------------------------------------------------------------
package/src/config.ts CHANGED
@@ -38,6 +38,34 @@ export const CONFIG_PATH = path.join(
38
38
  "config.json"
39
39
  );
40
40
 
41
+ // ---------------------------------------------------------------------------
42
+ // Graduation maturity criteria
43
+ // ---------------------------------------------------------------------------
44
+
45
+ /** Minimum age in days before an instinct is eligible for graduation. */
46
+ export const GRADUATION_MIN_AGE_DAYS = 7;
47
+
48
+ /** Minimum confidence to qualify for graduation. */
49
+ export const GRADUATION_MIN_CONFIDENCE = 0.75;
50
+
51
+ /** Minimum confirmed_count to qualify for graduation. */
52
+ export const GRADUATION_MIN_CONFIRMED = 3;
53
+
54
+ /** Maximum contradicted_count allowed for graduation. */
55
+ export const GRADUATION_MAX_CONTRADICTED = 1;
56
+
57
+ /** Minimum related instincts in same domain to propose a skill scaffold. */
58
+ export const GRADUATION_SKILL_CLUSTER_SIZE = 3;
59
+
60
+ /** Minimum related instincts in same domain to propose a command scaffold. */
61
+ export const GRADUATION_COMMAND_CLUSTER_SIZE = 3;
62
+
63
+ /** Maximum instinct age in days before TTL cull (aggressive decay / deletion). */
64
+ export const GRADUATION_TTL_MAX_DAYS = 28;
65
+
66
+ /** Confidence threshold below which TTL-expired instincts are deleted outright. */
67
+ export const GRADUATION_TTL_CULL_CONFIDENCE = 0.3;
68
+
41
69
  export const DEFAULT_CONFIG: Config = {
42
70
  run_interval_minutes: 5,
43
71
  min_observations_to_analyze: 20,
@@ -49,6 +77,12 @@ export const DEFAULT_CONFIG: Config = {
49
77
  active_hours_start: 8,
50
78
  active_hours_end: 23,
51
79
  max_idle_seconds: 1800,
80
+ // Volume control defaults
81
+ max_total_instincts_per_project: 30,
82
+ max_total_instincts_global: 20,
83
+ max_new_instincts_per_run: 3,
84
+ flagged_cleanup_days: 7,
85
+ instinct_ttl_days: 28,
52
86
  };
53
87
 
54
88
  // ---------------------------------------------------------------------------
@@ -68,6 +102,12 @@ const PartialConfigSchema = Type.Partial(
68
102
  active_hours_end: Type.Number(),
69
103
  max_idle_seconds: Type.Number(),
70
104
  log_path: Type.String(),
105
+ // Volume control
106
+ max_total_instincts_per_project: Type.Number(),
107
+ max_total_instincts_global: Type.Number(),
108
+ max_new_instincts_per_run: Type.Number(),
109
+ flagged_cleanup_days: Type.Number(),
110
+ instinct_ttl_days: Type.Number(),
71
111
  })
72
112
  );
73
113