pi-continuous-learning 0.6.0 → 0.8.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 (93) 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/analysis-event-log.d.ts +50 -0
  7. package/dist/analysis-event-log.d.ts.map +1 -0
  8. package/dist/analysis-event-log.js +120 -0
  9. package/dist/analysis-event-log.js.map +1 -0
  10. package/dist/analysis-notification.d.ts +20 -0
  11. package/dist/analysis-notification.d.ts.map +1 -0
  12. package/dist/analysis-notification.js +63 -0
  13. package/dist/analysis-notification.js.map +1 -0
  14. package/dist/cli/analyze-single-shot.d.ts +20 -2
  15. package/dist/cli/analyze-single-shot.d.ts.map +1 -1
  16. package/dist/cli/analyze-single-shot.js +109 -5
  17. package/dist/cli/analyze-single-shot.js.map +1 -1
  18. package/dist/cli/analyze.js +132 -16
  19. package/dist/cli/analyze.js.map +1 -1
  20. package/dist/command-scaffold.d.ts +25 -0
  21. package/dist/command-scaffold.d.ts.map +1 -0
  22. package/dist/command-scaffold.js +77 -0
  23. package/dist/command-scaffold.js.map +1 -0
  24. package/dist/confidence.d.ts +12 -1
  25. package/dist/confidence.d.ts.map +1 -1
  26. package/dist/confidence.js +37 -9
  27. package/dist/confidence.js.map +1 -1
  28. package/dist/config.d.ts +16 -0
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +31 -0
  31. package/dist/config.js.map +1 -1
  32. package/dist/graduation.d.ts +63 -0
  33. package/dist/graduation.d.ts.map +1 -0
  34. package/dist/graduation.js +155 -0
  35. package/dist/graduation.js.map +1 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +7 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/instinct-cleanup.d.ts +57 -0
  40. package/dist/instinct-cleanup.d.ts.map +1 -0
  41. package/dist/instinct-cleanup.js +150 -0
  42. package/dist/instinct-cleanup.js.map +1 -0
  43. package/dist/instinct-graduate.d.ts +43 -0
  44. package/dist/instinct-graduate.d.ts.map +1 -0
  45. package/dist/instinct-graduate.js +253 -0
  46. package/dist/instinct-graduate.js.map +1 -0
  47. package/dist/instinct-parser.d.ts.map +1 -1
  48. package/dist/instinct-parser.js +18 -0
  49. package/dist/instinct-parser.js.map +1 -1
  50. package/dist/instinct-tools.d.ts.map +1 -1
  51. package/dist/instinct-tools.js +19 -0
  52. package/dist/instinct-tools.js.map +1 -1
  53. package/dist/instinct-validator.d.ts +61 -0
  54. package/dist/instinct-validator.d.ts.map +1 -0
  55. package/dist/instinct-validator.js +235 -0
  56. package/dist/instinct-validator.js.map +1 -0
  57. package/dist/observation-signal.d.ts +34 -0
  58. package/dist/observation-signal.d.ts.map +1 -0
  59. package/dist/observation-signal.js +66 -0
  60. package/dist/observation-signal.js.map +1 -0
  61. package/dist/prompts/analyzer-system-single-shot.d.ts.map +1 -1
  62. package/dist/prompts/analyzer-system-single-shot.js +82 -3
  63. package/dist/prompts/analyzer-system-single-shot.js.map +1 -1
  64. package/dist/prompts/analyzer-user-single-shot.d.ts.map +1 -1
  65. package/dist/prompts/analyzer-user-single-shot.js +5 -3
  66. package/dist/prompts/analyzer-user-single-shot.js.map +1 -1
  67. package/dist/skill-scaffold.d.ts +23 -0
  68. package/dist/skill-scaffold.d.ts.map +1 -0
  69. package/dist/skill-scaffold.js +62 -0
  70. package/dist/skill-scaffold.js.map +1 -0
  71. package/dist/types.d.ts +9 -0
  72. package/dist/types.d.ts.map +1 -1
  73. package/package.json +1 -1
  74. package/src/agents-md.ts +73 -3
  75. package/src/analysis-event-log.ts +171 -0
  76. package/src/analysis-notification.ts +79 -0
  77. package/src/cli/analyze-single-shot.ts +131 -5
  78. package/src/cli/analyze.ts +157 -15
  79. package/src/command-scaffold.ts +105 -0
  80. package/src/confidence.ts +35 -8
  81. package/src/config.ts +40 -0
  82. package/src/graduation.ts +243 -0
  83. package/src/index.ts +16 -0
  84. package/src/instinct-cleanup.ts +204 -0
  85. package/src/instinct-graduate.ts +377 -0
  86. package/src/instinct-parser.ts +18 -0
  87. package/src/instinct-tools.ts +26 -0
  88. package/src/instinct-validator.ts +287 -0
  89. package/src/observation-signal.ts +80 -0
  90. package/src/prompts/analyzer-system-single-shot.ts +82 -3
  91. package/src/prompts/analyzer-user-single-shot.ts +11 -2
  92. package/src/skill-scaffold.ts +90 -0
  93. package/src/types.ts +11 -0
@@ -6,6 +6,7 @@ import {
6
6
  writeFileSync,
7
7
  unlinkSync,
8
8
  } from "node:fs";
9
+ import { createHash } from "node:crypto";
9
10
  import { join } from "node:path";
10
11
  import { AuthStorage } from "@mariozechner/pi-coding-agent";
11
12
  import { getModel } from "@mariozechner/pi-ai";
@@ -22,13 +23,21 @@ import {
22
23
  } from "../storage.js";
23
24
  import { countObservations } from "../observations.js";
24
25
  import { runDecayPass } from "../instinct-decay.js";
26
+ import { runCleanupPass } from "../instinct-cleanup.js";
25
27
  import { tailObservationsSince } from "../prompts/analyzer-user.js";
26
28
  import { buildSingleShotSystemPrompt } from "../prompts/analyzer-system-single-shot.js";
27
29
  import { buildSingleShotUserPrompt } from "../prompts/analyzer-user-single-shot.js";
28
30
  import {
29
31
  runSingleShot,
30
32
  buildInstinctFromChange,
33
+ estimateTokens,
31
34
  } from "./analyze-single-shot.js";
35
+ import { isLowSignalBatch } from "../observation-signal.js";
36
+ import {
37
+ appendAnalysisEvent,
38
+ type InstinctChangeSummary,
39
+ type AnalysisEvent,
40
+ } from "../analysis-event-log.js";
32
41
  import {
33
42
  loadProjectInstincts,
34
43
  loadGlobalInstincts,
@@ -106,9 +115,31 @@ function startGlobalTimeout(timeoutMs: number, logger: AnalyzeLogger): void {
106
115
  // Per-project analysis
107
116
  // ---------------------------------------------------------------------------
108
117
 
118
+ /** Max estimated tokens before fallback strategies are applied. */
119
+ const PROMPT_TOKEN_BUDGET = 40_000;
120
+
109
121
  interface ProjectMeta {
110
122
  last_analyzed_at?: string;
111
123
  last_observation_line_count?: number;
124
+ /** SHA-256 hash of the last AGENTS.md content sent for this project (project-level file). */
125
+ agents_md_project_hash?: string;
126
+ /** SHA-256 hash of the last AGENTS.md content sent (global file). */
127
+ agents_md_global_hash?: string;
128
+ }
129
+
130
+ function hashContent(content: string): string {
131
+ return createHash("sha256").update(content).digest("hex");
132
+ }
133
+
134
+ /**
135
+ * Truncates AGENTS.md content to section headers only (lines starting with #).
136
+ * Used as a fallback when the prompt is over the token budget.
137
+ */
138
+ function truncateAgentsMdToHeaders(content: string): string {
139
+ return content
140
+ .split("\n")
141
+ .filter((line) => line.startsWith("#"))
142
+ .join("\n");
112
143
  }
113
144
 
114
145
  function loadProjectsRegistry(baseDir: string): Record<string, ProjectEntry> {
@@ -178,6 +209,10 @@ async function analyzeProject(
178
209
  return { ran: false, skippedReason: "no new observation lines after preprocessing" };
179
210
  }
180
211
 
212
+ if (isLowSignalBatch(newObsLines)) {
213
+ return { ran: false, skippedReason: "low-signal batch (no errors, corrections, or user redirections)" };
214
+ }
215
+
181
216
  const obsCount = countObservations(project.id, baseDir);
182
217
  if (obsCount < config.min_observations_to_analyze) {
183
218
  return { ran: false, skippedReason: `below threshold (${obsCount}/${config.min_observations_to_analyze})` };
@@ -186,6 +221,7 @@ async function analyzeProject(
186
221
  const startTime = Date.now();
187
222
  logger.projectStart(project.id, project.name, rawLineCount, obsCount);
188
223
 
224
+ runCleanupPass(project.id, config, baseDir);
189
225
  runDecayPass(project.id, baseDir);
190
226
 
191
227
  // Load current instincts inline - no tool calls needed
@@ -193,8 +229,21 @@ async function analyzeProject(
193
229
  const globalInstincts = loadGlobalInstincts(baseDir);
194
230
  const allInstincts = [...projectInstincts, ...globalInstincts];
195
231
 
196
- const agentsMdProject = readAgentsMd(join(project.root, "AGENTS.md"));
197
- const agentsMdGlobal = readAgentsMd(join(homedir(), ".pi", "agent", "AGENTS.md"));
232
+ // Load AGENTS.md, skipping if content hash is unchanged since last run.
233
+ const rawAgentsMdProject = readAgentsMd(join(project.root, "AGENTS.md"));
234
+ const rawAgentsMdGlobal = readAgentsMd(join(homedir(), ".pi", "agent", "AGENTS.md"));
235
+
236
+ const projectMdHash = rawAgentsMdProject ? hashContent(rawAgentsMdProject) : null;
237
+ const globalMdHash = rawAgentsMdGlobal ? hashContent(rawAgentsMdGlobal) : null;
238
+
239
+ const agentsMdProject =
240
+ rawAgentsMdProject && projectMdHash !== meta.agents_md_project_hash
241
+ ? rawAgentsMdProject
242
+ : null;
243
+ const agentsMdGlobal =
244
+ rawAgentsMdGlobal && globalMdHash !== meta.agents_md_global_hash
245
+ ? rawAgentsMdGlobal
246
+ : null;
198
247
 
199
248
  let installedSkills: InstalledSkill[] = [];
200
249
  try {
@@ -208,9 +257,51 @@ async function analyzeProject(
208
257
  // Skills loading is best-effort - continue without them
209
258
  }
210
259
 
211
- const userPrompt = buildSingleShotUserPrompt(project, allInstincts, newObsLines, {
212
- agentsMdProject,
213
- agentsMdGlobal,
260
+ let promptObsLines = newObsLines;
261
+ let promptAgentsMdProject = agentsMdProject;
262
+ let promptAgentsMdGlobal = agentsMdGlobal;
263
+
264
+ const userPrompt = buildSingleShotUserPrompt(project, allInstincts, promptObsLines, {
265
+ agentsMdProject: promptAgentsMdProject,
266
+ agentsMdGlobal: promptAgentsMdGlobal,
267
+ installedSkills,
268
+ });
269
+
270
+ // Estimate token budget and apply fallbacks if over limit.
271
+ const systemPromptTokens = estimateTokens(buildSingleShotSystemPrompt());
272
+ let estimatedTotal = systemPromptTokens + estimateTokens(userPrompt);
273
+
274
+ if (estimatedTotal > PROMPT_TOKEN_BUDGET) {
275
+ logger.warn(
276
+ `Prompt over budget (${estimatedTotal} est. tokens > ${PROMPT_TOKEN_BUDGET}). Applying fallbacks.`
277
+ );
278
+
279
+ // Fallback 1: truncate AGENTS.md to headers only.
280
+ if (promptAgentsMdProject) {
281
+ promptAgentsMdProject = truncateAgentsMdToHeaders(promptAgentsMdProject);
282
+ }
283
+ if (promptAgentsMdGlobal) {
284
+ promptAgentsMdGlobal = truncateAgentsMdToHeaders(promptAgentsMdGlobal);
285
+ }
286
+
287
+ // Fallback 2: reduce observation lines to fit budget.
288
+ // Use binary-search-like reduction: keep halving until under budget.
289
+ while (promptObsLines.length > 1) {
290
+ const trimmedPrompt = buildSingleShotUserPrompt(
291
+ project,
292
+ allInstincts,
293
+ promptObsLines,
294
+ { agentsMdProject: promptAgentsMdProject, agentsMdGlobal: promptAgentsMdGlobal, installedSkills }
295
+ );
296
+ estimatedTotal = systemPromptTokens + estimateTokens(trimmedPrompt);
297
+ if (estimatedTotal <= PROMPT_TOKEN_BUDGET) break;
298
+ promptObsLines = promptObsLines.slice(Math.floor(promptObsLines.length / 2));
299
+ }
300
+ }
301
+
302
+ const finalUserPrompt = buildSingleShotUserPrompt(project, allInstincts, promptObsLines, {
303
+ agentsMdProject: promptAgentsMdProject,
304
+ agentsMdGlobal: promptAgentsMdGlobal,
214
305
  installedSkills,
215
306
  });
216
307
 
@@ -226,7 +317,7 @@ async function analyzeProject(
226
317
  const context = {
227
318
  systemPrompt: buildSingleShotSystemPrompt(),
228
319
  messages: [
229
- { role: "user" as const, content: userPrompt, timestamp: Date.now() },
320
+ { role: "user" as const, content: finalUserPrompt, timestamp: Date.now() },
230
321
  ],
231
322
  };
232
323
 
@@ -235,6 +326,9 @@ async function analyzeProject(
235
326
  const timeoutHandle = setTimeout(() => abortController.abort(), timeoutMs);
236
327
 
237
328
  const instinctCounts = { created: 0, updated: 0, deleted: 0 };
329
+ const createdSummaries: InstinctChangeSummary[] = [];
330
+ const updatedSummaries: InstinctChangeSummary[] = [];
331
+ const deletedSummaries: InstinctChangeSummary[] = [];
238
332
  const projectInstinctsDir = getProjectInstinctsDir(project.id, "personal", baseDir);
239
333
  const globalInstinctsDir = getGlobalInstinctsDir("personal", baseDir);
240
334
 
@@ -243,6 +337,10 @@ async function analyzeProject(
243
337
  const result = await runSingleShot(context, model, apiKey, abortController.signal);
244
338
  singleShotMessage = result.message;
245
339
 
340
+ // Enforce creation rate limit: only the first N create actions per run are applied.
341
+ const maxNewInstincts = config.max_new_instincts_per_run ?? DEFAULT_CONFIG.max_new_instincts_per_run;
342
+ let createsRemaining = maxNewInstincts;
343
+
246
344
  for (const change of result.changes) {
247
345
  if (change.action === "delete") {
248
346
  const id = change.id;
@@ -252,21 +350,47 @@ async function analyzeProject(
252
350
  if (existsSync(filePath)) {
253
351
  unlinkSync(filePath);
254
352
  instinctCounts.deleted++;
353
+ deletedSummaries.push({
354
+ id,
355
+ title: id,
356
+ scope: change.scope ?? "project",
357
+ });
255
358
  }
256
- } else {
257
- // create or update
359
+ } else if (change.action === "create") {
360
+ if (createsRemaining <= 0) continue; // rate limit reached
258
361
  const existing = allInstincts.find((i) => i.id === change.instinct?.id) ?? null;
259
- const instinct = buildInstinctFromChange(change, existing, project.id);
362
+ const instinct = buildInstinctFromChange(change, existing, project.id, allInstincts);
260
363
  if (!instinct) continue;
261
364
 
262
365
  const dir = instinct.scope === "global" ? globalInstinctsDir : projectInstinctsDir;
263
366
  saveInstinct(instinct, dir);
367
+ instinctCounts.created++;
368
+ createsRemaining--;
369
+ createdSummaries.push({
370
+ id: instinct.id,
371
+ title: instinct.title,
372
+ scope: instinct.scope,
373
+ trigger: instinct.trigger,
374
+ action: instinct.action,
375
+ });
376
+ } else {
377
+ // update
378
+ const existing = allInstincts.find((i) => i.id === change.instinct?.id) ?? null;
379
+ const instinct = buildInstinctFromChange(change, existing, project.id, allInstincts);
380
+ if (!instinct) continue;
264
381
 
265
- if (change.action === "create") {
266
- instinctCounts.created++;
267
- } else {
268
- instinctCounts.updated++;
269
- }
382
+ const dir = instinct.scope === "global" ? globalInstinctsDir : projectInstinctsDir;
383
+ saveInstinct(instinct, dir);
384
+ instinctCounts.updated++;
385
+ const delta = existing
386
+ ? instinct.confidence - existing.confidence
387
+ : undefined;
388
+ updatedSummaries.push({
389
+ id: instinct.id,
390
+ title: instinct.title,
391
+ scope: instinct.scope,
392
+ ...(delta !== undefined ? { confidence_delta: delta } : {}),
393
+ });
270
394
  }
271
395
  }
272
396
  } finally {
@@ -296,9 +420,27 @@ async function analyzeProject(
296
420
 
297
421
  logger.projectComplete(stats);
298
422
 
423
+ // Write analysis event for extension notification
424
+ const analysisEvent: AnalysisEvent = {
425
+ timestamp: new Date().toISOString(),
426
+ project_id: project.id,
427
+ project_name: project.name,
428
+ created: createdSummaries,
429
+ updated: updatedSummaries,
430
+ deleted: deletedSummaries,
431
+ };
432
+ appendAnalysisEvent(analysisEvent, baseDir);
433
+
299
434
  saveProjectMeta(
300
435
  project.id,
301
- { ...meta, last_analyzed_at: new Date().toISOString(), last_observation_line_count: totalLineCount },
436
+ {
437
+ ...meta,
438
+ last_analyzed_at: new Date().toISOString(),
439
+ last_observation_line_count: totalLineCount,
440
+ // Update AGENTS.md hashes only when the content was actually sent.
441
+ ...(agentsMdProject && projectMdHash ? { agents_md_project_hash: projectMdHash } : {}),
442
+ ...(agentsMdGlobal && globalMdHash ? { agents_md_global_hash: globalMdHash } : {}),
443
+ },
302
444
  baseDir
303
445
  );
304
446
 
@@ -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
@@ -21,12 +21,19 @@ const OBS_BRACKET_MED_MAX = 5;
21
21
  const OBS_BRACKET_HIGH_MAX = 10;
22
22
 
23
23
  // adjustConfidence deltas
24
- const DELTA_CONFIRMED = 0.05;
24
+ // Confirmation uses diminishing returns to prevent runaway confidence on trivially easy-to-confirm instincts.
25
+ const DELTA_CONFIRMED_TIER1 = 0.05; // 1st–3rd confirmation
26
+ const DELTA_CONFIRMED_TIER2 = 0.03; // 4th–6th confirmation
27
+ const DELTA_CONFIRMED_TIER3 = 0.01; // 7th+ confirmation
25
28
  const DELTA_CONTRADICTED = -0.15;
26
29
  const DELTA_INACTIVE = 0;
27
30
 
31
+ const CONFIRMED_TIER1_MAX = 3;
32
+ const CONFIRMED_TIER2_MAX = 6;
33
+
28
34
  // applyPassiveDecay
29
- const DECAY_PER_WEEK = 0.02;
35
+ // Increased from 0.02 to 0.05: at 0.5 confidence, reaches 0.1 in ~8 weeks instead of 20.
36
+ const DECAY_PER_WEEK = 0.05;
30
37
  const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000;
31
38
 
32
39
  // ---------------------------------------------------------------------------
@@ -35,6 +42,16 @@ const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000;
35
42
 
36
43
  export type FeedbackOutcome = "confirmed" | "contradicted" | "inactive";
37
44
 
45
+ /**
46
+ * Returns the confirmation confidence delta using diminishing returns.
47
+ * Higher confirmed_count yields smaller increments to prevent runaway scores.
48
+ */
49
+ export function confirmationDelta(confirmedCount: number): number {
50
+ if (confirmedCount <= CONFIRMED_TIER1_MAX) return DELTA_CONFIRMED_TIER1;
51
+ if (confirmedCount <= CONFIRMED_TIER2_MAX) return DELTA_CONFIRMED_TIER2;
52
+ return DELTA_CONFIRMED_TIER3;
53
+ }
54
+
38
55
  export interface ConfidenceResult {
39
56
  confidence: number;
40
57
  flaggedForRemoval: boolean;
@@ -70,18 +87,28 @@ export function initialConfidence(observationCount: number): number {
70
87
 
71
88
  /**
72
89
  * Adjusts confidence based on a feedback outcome from the observer loop.
90
+ * For "confirmed" outcomes, applies diminishing returns based on how many
91
+ * times the instinct has already been confirmed (higher count = smaller delta).
73
92
  * Returns the clamped confidence and a flag indicating if removal is warranted.
93
+ *
94
+ * @param current - Current confidence value
95
+ * @param outcome - Feedback outcome type
96
+ * @param confirmedCount - Current confirmed_count (used for diminishing returns on confirmations)
74
97
  */
75
98
  export function adjustConfidence(
76
99
  current: number,
77
100
  outcome: FeedbackOutcome,
101
+ confirmedCount = 0,
78
102
  ): 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];
103
+ let delta: number;
104
+ if (outcome === "confirmed") {
105
+ delta = confirmationDelta(confirmedCount);
106
+ } else if (outcome === "contradicted") {
107
+ delta = DELTA_CONTRADICTED;
108
+ } else {
109
+ delta = DELTA_INACTIVE;
110
+ }
111
+ const raw = current + delta;
85
112
  return toResult(raw);
86
113
  }
87
114
 
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