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
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Skill scaffolding from instinct clusters.
3
+ *
4
+ * When 3+ related instincts in the same domain form a cohesive topic,
5
+ * generates a SKILL.md file that can be installed as a Pi skill.
6
+ */
7
+ import type { DomainCluster } from "./graduation.js";
8
+ export interface SkillScaffold {
9
+ name: string;
10
+ description: string;
11
+ domain: string;
12
+ content: string;
13
+ sourceInstinctIds: string[];
14
+ }
15
+ /**
16
+ * Generates a SKILL.md scaffold from a domain cluster of instincts.
17
+ */
18
+ export declare function generateSkillScaffold(cluster: DomainCluster): SkillScaffold;
19
+ /**
20
+ * Generates skill scaffolds for all qualifying clusters.
21
+ */
22
+ export declare function generateAllSkillScaffolds(clusters: DomainCluster[]): SkillScaffold[];
23
+ //# sourceMappingURL=skill-scaffold.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-scaffold.d.ts","sourceRoot":"","sources":["../src/skill-scaffold.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAMrD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAyBD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,aAAa,GAAG,aAAa,CAgC3E;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,aAAa,EAAE,GACxB,aAAa,EAAE,CAEjB"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Skill scaffolding from instinct clusters.
3
+ *
4
+ * When 3+ related instincts in the same domain form a cohesive topic,
5
+ * generates a SKILL.md file that can be installed as a Pi skill.
6
+ */
7
+ // ---------------------------------------------------------------------------
8
+ // Helpers
9
+ // ---------------------------------------------------------------------------
10
+ function toSkillName(domain) {
11
+ return domain.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
12
+ }
13
+ function formatInstinctAsSection(instinct, index) {
14
+ return [
15
+ `### ${index + 1}. ${instinct.title}`,
16
+ "",
17
+ `**When:** ${instinct.trigger}`,
18
+ "",
19
+ instinct.action,
20
+ "",
21
+ ].join("\n");
22
+ }
23
+ // ---------------------------------------------------------------------------
24
+ // Public API
25
+ // ---------------------------------------------------------------------------
26
+ /**
27
+ * Generates a SKILL.md scaffold from a domain cluster of instincts.
28
+ */
29
+ export function generateSkillScaffold(cluster) {
30
+ const name = toSkillName(cluster.domain);
31
+ const sortedInstincts = [...cluster.instincts].sort((a, b) => b.confidence - a.confidence);
32
+ const description = `Learned ${cluster.domain} patterns from coding sessions. ` +
33
+ `Covers ${sortedInstincts.length} practices distilled from instinct observations.`;
34
+ const sections = sortedInstincts.map((inst, i) => formatInstinctAsSection(inst, i));
35
+ const content = [
36
+ `# ${cluster.domain} Skill`,
37
+ "",
38
+ `> Auto-generated from ${sortedInstincts.length} graduated instincts in the "${cluster.domain}" domain.`,
39
+ "",
40
+ `## Description`,
41
+ "",
42
+ description,
43
+ "",
44
+ `## Practices`,
45
+ "",
46
+ ...sections,
47
+ ].join("\n");
48
+ return {
49
+ name,
50
+ description,
51
+ domain: cluster.domain,
52
+ content,
53
+ sourceInstinctIds: sortedInstincts.map((i) => i.id),
54
+ };
55
+ }
56
+ /**
57
+ * Generates skill scaffolds for all qualifying clusters.
58
+ */
59
+ export function generateAllSkillScaffolds(clusters) {
60
+ return clusters.map(generateSkillScaffold);
61
+ }
62
+ //# sourceMappingURL=skill-scaffold.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-scaffold.js","sourceRoot":"","sources":["../src/skill-scaffold.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiBH,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E,SAAS,WAAW,CAAC,MAAc;IACjC,OAAO,MAAM,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AAChF,CAAC;AAED,SAAS,uBAAuB,CAAC,QAAkB,EAAE,KAAa;IAChE,OAAO;QACL,OAAO,KAAK,GAAG,CAAC,KAAK,QAAQ,CAAC,KAAK,EAAE;QACrC,EAAE;QACF,aAAa,QAAQ,CAAC,OAAO,EAAE;QAC/B,EAAE;QACF,QAAQ,CAAC,MAAM;QACf,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;GAEG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAsB;IAC1D,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACzC,MAAM,eAAe,GAAG,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,IAAI,CACjD,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CACtC,CAAC;IAEF,MAAM,WAAW,GAAG,WAAW,OAAO,CAAC,MAAM,kCAAkC;QAC7E,UAAU,eAAe,CAAC,MAAM,kDAAkD,CAAC;IAErF,MAAM,QAAQ,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,uBAAuB,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAEpF,MAAM,OAAO,GAAG;QACd,KAAK,OAAO,CAAC,MAAM,QAAQ;QAC3B,EAAE;QACF,yBAAyB,eAAe,CAAC,MAAM,gCAAgC,OAAO,CAAC,MAAM,WAAW;QACxG,EAAE;QACF,gBAAgB;QAChB,EAAE;QACF,WAAW;QACX,EAAE;QACF,cAAc;QACd,EAAE;QACF,GAAG,QAAQ;KACZ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEb,OAAO;QACL,IAAI;QACJ,WAAW;QACX,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,OAAO;QACP,iBAAiB,EAAE,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpD,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,yBAAyB,CACvC,QAAyB;IAEzB,OAAO,QAAQ,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;AAC7C,CAAC"}
package/dist/types.d.ts CHANGED
@@ -46,7 +46,11 @@ export interface Instinct {
46
46
  inactive_count: number;
47
47
  evidence?: string[];
48
48
  flagged_for_removal?: boolean;
49
+ graduated_to?: GraduationTarget;
50
+ graduated_at?: string;
51
+ last_confirmed_session?: string;
49
52
  }
53
+ export type GraduationTarget = "agents-md" | "skill" | "command";
50
54
  export interface ProjectEntry {
51
55
  id: string;
52
56
  name: string;
@@ -71,5 +75,10 @@ export interface Config {
71
75
  active_hours_end: number;
72
76
  max_idle_seconds: number;
73
77
  log_path?: string;
78
+ max_total_instincts_per_project: number;
79
+ max_total_instincts_global: number;
80
+ max_new_instincts_per_run: number;
81
+ flagged_cleanup_days: number;
82
+ instinct_ttl_days: number;
74
83
  }
75
84
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,MAAM,MAAM,gBAAgB,GACxB,YAAY,GACZ,eAAe,GACf,aAAa,GACb,WAAW,GACX,YAAY,GACZ,UAAU,GACV,WAAW,GACX,iBAAiB,GACjB,cAAc,CAAC;AAEnB,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAMD,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,QAAQ,CAAC;AACjD,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,WAAW,CAAC;AAEtD,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,aAAa,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAMD,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,MAAM;IACrB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,2BAA2B,EAAE,MAAM,CAAC;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-continuous-learning",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "A Pi extension that observes coding sessions and distills patterns into reusable instincts.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/agents-md.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  /**
2
- * Utility for reading AGENTS.md files.
3
- * Provides a safe wrapper around filesystem access that returns null on any failure.
2
+ * Utility for reading and writing AGENTS.md files.
3
+ * Provides safe wrappers around filesystem access.
4
4
  */
5
5
 
6
- import { existsSync, readFileSync } from "node:fs";
6
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
7
+ import { dirname } from "node:path";
8
+ import type { Instinct } from "./types.js";
7
9
 
8
10
  /**
9
11
  * Reads an AGENTS.md file and returns its content.
@@ -21,3 +23,71 @@ export function readAgentsMd(filePath: string): string | null {
21
23
  return null;
22
24
  }
23
25
  }
26
+
27
+ /**
28
+ * Formats an instinct as an AGENTS.md section entry.
29
+ * Produces a markdown block with the instinct title as heading and
30
+ * trigger/action as content.
31
+ */
32
+ export function formatInstinctAsAgentsMdEntry(instinct: Instinct): string {
33
+ const lines = [
34
+ `### ${instinct.title}`,
35
+ "",
36
+ `**When:** ${instinct.trigger}`,
37
+ "",
38
+ instinct.action,
39
+ "",
40
+ ];
41
+ return lines.join("\n");
42
+ }
43
+
44
+ /**
45
+ * Generates a complete AGENTS.md diff showing proposed additions.
46
+ * Returns the full new content that would result from appending the entries.
47
+ */
48
+ export function generateAgentsMdDiff(
49
+ currentContent: string | null,
50
+ instincts: Instinct[]
51
+ ): string {
52
+ const entries = instincts.map(formatInstinctAsAgentsMdEntry);
53
+ const graduatedSection = [
54
+ "",
55
+ "## Graduated Instincts",
56
+ "",
57
+ ...entries,
58
+ ].join("\n");
59
+
60
+ if (currentContent === null || currentContent.trim().length === 0) {
61
+ return `# Project Guidelines\n${graduatedSection}\n`;
62
+ }
63
+
64
+ // If the section already exists, append to it; otherwise add a new section
65
+ if (currentContent.includes("## Graduated Instincts")) {
66
+ return `${currentContent.trimEnd()}\n\n${entries.join("\n")}\n`;
67
+ }
68
+
69
+ return `${currentContent.trimEnd()}\n${graduatedSection}\n`;
70
+ }
71
+
72
+ /**
73
+ * Appends graduated instinct entries to an AGENTS.md file.
74
+ * Creates the file and parent directories if they don't exist.
75
+ *
76
+ * @param filePath - Absolute path to AGENTS.md
77
+ * @param instincts - Instincts to append as entries
78
+ * @returns The new file content that was written
79
+ */
80
+ export function appendToAgentsMd(
81
+ filePath: string,
82
+ instincts: Instinct[]
83
+ ): string {
84
+ if (instincts.length === 0) return readAgentsMd(filePath) ?? "";
85
+
86
+ const currentContent = readAgentsMd(filePath);
87
+ const newContent = generateAgentsMdDiff(currentContent, instincts);
88
+
89
+ mkdirSync(dirname(filePath), { recursive: true });
90
+ writeFileSync(filePath, newContent, "utf-8");
91
+
92
+ return newContent;
93
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Append-only analysis event log with atomic rename for safe consumption.
3
+ *
4
+ * The background analyzer appends events to `analysis-events.jsonl`.
5
+ * The extension consumes events by atomically renaming the file to
6
+ * `.consumed`, reading it, then deleting it. On POSIX, rename is atomic -
7
+ * any in-flight appends follow the inode to the renamed file.
8
+ *
9
+ * Multiple analyzer runs can append before the extension reads. No events
10
+ * are lost because each run only appends; the file is never truncated by
11
+ * the analyzer.
12
+ */
13
+
14
+ import {
15
+ appendFileSync,
16
+ existsSync,
17
+ mkdirSync,
18
+ readFileSync,
19
+ renameSync,
20
+ unlinkSync,
21
+ } from "node:fs";
22
+ import { dirname, join } from "node:path";
23
+ import { getProjectDir } from "./storage.js";
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Constants
27
+ // ---------------------------------------------------------------------------
28
+
29
+ const EVENTS_FILENAME = "analysis-events.jsonl";
30
+ const CONSUMED_FILENAME = "analysis-events.consumed";
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Types
34
+ // ---------------------------------------------------------------------------
35
+
36
+ export interface InstinctChangeSummary {
37
+ readonly id: string;
38
+ readonly title: string;
39
+ readonly scope: "project" | "global";
40
+ readonly trigger?: string;
41
+ readonly action?: string;
42
+ readonly confidence_delta?: number;
43
+ }
44
+
45
+ export interface AnalysisEvent {
46
+ readonly timestamp: string;
47
+ readonly project_id: string;
48
+ readonly project_name: string;
49
+ readonly created: readonly InstinctChangeSummary[];
50
+ readonly updated: readonly InstinctChangeSummary[];
51
+ readonly deleted: readonly InstinctChangeSummary[];
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Paths
56
+ // ---------------------------------------------------------------------------
57
+
58
+ export function getEventsPath(projectId: string, baseDir?: string): string {
59
+ return join(getProjectDir(projectId, baseDir), EVENTS_FILENAME);
60
+ }
61
+
62
+ export function getConsumedPath(projectId: string, baseDir?: string): string {
63
+ return join(getProjectDir(projectId, baseDir), CONSUMED_FILENAME);
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Write (analyzer side)
68
+ // ---------------------------------------------------------------------------
69
+
70
+ /**
71
+ * Appends an analysis event to the project's event log.
72
+ * Skips writing if nothing changed (all arrays empty).
73
+ * Creates the parent directory if needed.
74
+ */
75
+ export function appendAnalysisEvent(event: AnalysisEvent, baseDir?: string): void {
76
+ if (
77
+ event.created.length === 0 &&
78
+ event.updated.length === 0 &&
79
+ event.deleted.length === 0
80
+ ) {
81
+ return;
82
+ }
83
+
84
+ const eventsPath = getEventsPath(event.project_id, baseDir);
85
+ mkdirSync(dirname(eventsPath), { recursive: true });
86
+ appendFileSync(eventsPath, JSON.stringify(event) + "\n", "utf-8");
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Read and clear (extension side)
91
+ // ---------------------------------------------------------------------------
92
+
93
+ /**
94
+ * Atomically consumes all pending analysis events for a project.
95
+ *
96
+ * Strategy:
97
+ * 1. Check for orphaned `.consumed` file from a prior crash - read it first
98
+ * 2. Rename `analysis-events.jsonl` to `.consumed` (atomic on POSIX)
99
+ * 3. Read and parse all lines from `.consumed`
100
+ * 4. Delete `.consumed`
101
+ *
102
+ * Returns an empty array if no events exist or rename fails (e.g. file
103
+ * doesn't exist, or another consumer raced us).
104
+ */
105
+ export function consumeAnalysisEvents(
106
+ projectId: string,
107
+ baseDir?: string
108
+ ): readonly AnalysisEvent[] {
109
+ const eventsPath = getEventsPath(projectId, baseDir);
110
+ const consumedPath = getConsumedPath(projectId, baseDir);
111
+
112
+ const allEvents: AnalysisEvent[] = [];
113
+
114
+ // Step 1: recover orphaned consumed file from prior crash
115
+ if (existsSync(consumedPath)) {
116
+ allEvents.push(...parseEventsFile(consumedPath));
117
+ safeUnlink(consumedPath);
118
+ }
119
+
120
+ // Step 2: atomically rename the events file
121
+ if (existsSync(eventsPath)) {
122
+ try {
123
+ renameSync(eventsPath, consumedPath);
124
+ } catch {
125
+ // Rename failed (race with another consumer, or OS issue).
126
+ // Return whatever we recovered from step 1.
127
+ return allEvents;
128
+ }
129
+
130
+ // Step 3: read the renamed file
131
+ allEvents.push(...parseEventsFile(consumedPath));
132
+
133
+ // Step 4: delete consumed file
134
+ safeUnlink(consumedPath);
135
+ }
136
+
137
+ return allEvents;
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Helpers
142
+ // ---------------------------------------------------------------------------
143
+
144
+ function parseEventsFile(filePath: string): AnalysisEvent[] {
145
+ const events: AnalysisEvent[] = [];
146
+
147
+ try {
148
+ const content = readFileSync(filePath, "utf-8");
149
+ const lines = content.split("\n").filter((line) => line.trim().length > 0);
150
+
151
+ for (const line of lines) {
152
+ try {
153
+ events.push(JSON.parse(line) as AnalysisEvent);
154
+ } catch {
155
+ // Skip malformed lines - don't lose other events
156
+ }
157
+ }
158
+ } catch {
159
+ // File read failed - return empty
160
+ }
161
+
162
+ return events;
163
+ }
164
+
165
+ function safeUnlink(filePath: string): void {
166
+ try {
167
+ if (existsSync(filePath)) unlinkSync(filePath);
168
+ } catch {
169
+ // Best effort cleanup
170
+ }
171
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Extension-side notification for analysis events.
3
+ *
4
+ * On `before_agent_start`, consumes pending analysis events and shows
5
+ * a brief one-line notification summarizing instinct changes since the
6
+ * last session interaction.
7
+ */
8
+
9
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
10
+ import {
11
+ consumeAnalysisEvents,
12
+ type AnalysisEvent,
13
+ } from "./analysis-event-log.js";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Formatting
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /**
20
+ * Aggregates multiple analysis events into a single summary line.
21
+ * Returns null when no changes occurred.
22
+ */
23
+ export function formatNotification(events: readonly AnalysisEvent[]): string | null {
24
+ if (events.length === 0) return null;
25
+
26
+ let created = 0;
27
+ let updated = 0;
28
+ let deleted = 0;
29
+ const createdIds: string[] = [];
30
+
31
+ for (const event of events) {
32
+ created += event.created.length;
33
+ updated += event.updated.length;
34
+ deleted += event.deleted.length;
35
+ for (const c of event.created) {
36
+ createdIds.push(c.id);
37
+ }
38
+ }
39
+
40
+ if (created === 0 && updated === 0 && deleted === 0) return null;
41
+
42
+ const parts: string[] = [];
43
+ if (created > 0) {
44
+ const idList = createdIds.slice(0, 3).join(", ");
45
+ const suffix = createdIds.length > 3 ? ", ..." : "";
46
+ parts.push(`+${created} new (${idList}${suffix})`);
47
+ }
48
+ if (updated > 0) {
49
+ parts.push(`${updated} updated`);
50
+ }
51
+ if (deleted > 0) {
52
+ parts.push(`${deleted} deleted`);
53
+ }
54
+
55
+ return `[instincts] Background analysis: ${parts.join(", ")}`;
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Handler
60
+ // ---------------------------------------------------------------------------
61
+
62
+ /**
63
+ * Checks for pending analysis events and shows a notification if any exist.
64
+ * Safe to call on every `before_agent_start` - no-ops when there's nothing.
65
+ */
66
+ export function checkAnalysisNotifications(
67
+ ctx: ExtensionContext,
68
+ projectId: string | null,
69
+ baseDir?: string
70
+ ): void {
71
+ if (!projectId) return;
72
+
73
+ const events = consumeAnalysisEvents(projectId, baseDir);
74
+ const message = formatNotification(events);
75
+
76
+ if (message) {
77
+ ctx.ui.notify(message, "info");
78
+ }
79
+ }
@@ -10,6 +10,11 @@ import { complete } from "@mariozechner/pi-ai";
10
10
  import type { Instinct } from "../types.js";
11
11
  import { serializeInstinct } from "../instinct-parser.js";
12
12
 
13
+ /** Chars-per-token heuristic for prompt size estimation. */
14
+ const CHARS_PER_TOKEN = 4;
15
+ import { validateInstinct, findSimilarInstinct } from "../instinct-validator.js";
16
+ import { confirmationDelta } from "../confidence.js";
17
+
13
18
  export interface InstinctChangePayload {
14
19
  id: string;
15
20
  title: string;
@@ -23,6 +28,7 @@ export interface InstinctChangePayload {
23
28
  contradicted_count?: number;
24
29
  inactive_count?: number;
25
30
  evidence?: string[];
31
+ last_confirmed_session?: string;
26
32
  }
27
33
 
28
34
  export interface InstinctChange {
@@ -73,26 +79,100 @@ export function parseChanges(raw: string): InstinctChange[] {
73
79
 
74
80
  /**
75
81
  * Builds a full Instinct from a create/update change.
76
- * Returns null for delete changes or changes with missing instinct data.
82
+ * Returns null for delete changes, changes with missing instinct data,
83
+ * invalid fields, or semantically duplicate actions.
84
+ *
85
+ * @param change - The change to apply
86
+ * @param existing - The existing instinct with this ID, if any
87
+ * @param projectId - Project ID for scoping
88
+ * @param allInstincts - All current instincts, used for dedup check on creates
77
89
  */
78
90
  export function buildInstinctFromChange(
79
91
  change: InstinctChange,
80
92
  existing: Instinct | null,
81
- projectId: string
93
+ projectId: string,
94
+ allInstincts: Instinct[] = []
82
95
  ): Instinct | null {
83
96
  if (change.action === "delete" || !change.instinct) {
84
97
  return null;
85
98
  }
86
99
 
87
- const now = new Date().toISOString();
88
100
  const payload = change.instinct;
89
101
 
102
+ const validation = validateInstinct({
103
+ action: payload.action,
104
+ trigger: payload.trigger,
105
+ domain: payload.domain,
106
+ });
107
+ if (!validation.valid) {
108
+ return null;
109
+ }
110
+
111
+ // On create, reject if semantically similar to an existing instinct (skip self on update)
112
+ if (change.action === "create") {
113
+ const similar = findSimilarInstinct(
114
+ { trigger: payload.trigger, action: payload.action },
115
+ allInstincts,
116
+ payload.id
117
+ );
118
+ if (similar) {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ const now = new Date().toISOString();
124
+
125
+ // For updates, recompute confidence client-side to enforce:
126
+ // 1. Per-session deduplication: only one confirmation per unique session_id
127
+ // 2. Diminishing returns: each additional confirmation yields a smaller delta
128
+ let resolvedConfidence: number;
129
+ let resolvedConfirmedCount = payload.confirmed_count ?? existing?.confirmed_count ?? 0;
130
+ let resolvedLastConfirmedSession = payload.last_confirmed_session ?? existing?.last_confirmed_session;
131
+
132
+ if (change.action === "update" && existing !== null) {
133
+ const prevConfirmedCount = existing.confirmed_count;
134
+ const newConfirmedCount = payload.confirmed_count ?? prevConfirmedCount;
135
+ const contradictionsAdded = Math.max(
136
+ 0,
137
+ (payload.contradicted_count ?? 0) - existing.contradicted_count,
138
+ );
139
+
140
+ // Detect whether the LLM intends to add a confirmation
141
+ const wantsToConfirm = newConfirmedCount > prevConfirmedCount;
142
+
143
+ // Session dedup: reject the confirmation if the confirming session is the
144
+ // same as the one that last confirmed this instinct.
145
+ const sessionDuplicate =
146
+ wantsToConfirm &&
147
+ resolvedLastConfirmedSession !== undefined &&
148
+ payload.last_confirmed_session !== undefined &&
149
+ payload.last_confirmed_session === existing.last_confirmed_session;
150
+
151
+ if (sessionDuplicate) {
152
+ // Revert to existing count - this session already confirmed the instinct
153
+ resolvedConfirmedCount = prevConfirmedCount;
154
+ }
155
+
156
+ // Recompute confidence from existing + explicit deltas (don't trust LLM arithmetic)
157
+ resolvedConfidence = existing.confidence;
158
+ if (wantsToConfirm && !sessionDuplicate) {
159
+ resolvedConfidence += confirmationDelta(prevConfirmedCount);
160
+ }
161
+ if (contradictionsAdded > 0) {
162
+ resolvedConfidence -= 0.15 * contradictionsAdded;
163
+ }
164
+ resolvedConfidence = Math.max(0.1, Math.min(0.9, resolvedConfidence));
165
+ } else {
166
+ // For creates, trust the LLM's initial confidence (no prior state to base delta on)
167
+ resolvedConfidence = Math.max(0.1, Math.min(0.9, payload.confidence));
168
+ }
169
+
90
170
  return {
91
171
  id: payload.id,
92
172
  title: payload.title,
93
173
  trigger: payload.trigger,
94
174
  action: payload.action,
95
- confidence: Math.max(0.1, Math.min(0.9, payload.confidence)),
175
+ confidence: resolvedConfidence,
96
176
  domain: payload.domain,
97
177
  scope: payload.scope,
98
178
  source: "personal",
@@ -100,15 +180,61 @@ export function buildInstinctFromChange(
100
180
  created_at: existing?.created_at ?? now,
101
181
  updated_at: now,
102
182
  observation_count: payload.observation_count ?? 1,
103
- confirmed_count: payload.confirmed_count ?? 0,
183
+ confirmed_count: resolvedConfirmedCount,
104
184
  contradicted_count: payload.contradicted_count ?? 0,
105
185
  inactive_count: payload.inactive_count ?? 0,
106
186
  ...(payload.evidence !== undefined ? { evidence: payload.evidence } : {}),
187
+ ...(resolvedLastConfirmedSession !== undefined
188
+ ? { last_confirmed_session: resolvedLastConfirmedSession }
189
+ : {}),
107
190
  };
108
191
  }
109
192
 
193
+ /**
194
+ * Returns days elapsed since the given ISO 8601 date string.
195
+ */
196
+ function daysSince(dateStr: string): number {
197
+ const ms = Date.now() - new Date(dateStr).getTime();
198
+ return Math.max(0, Math.floor(ms / (1000 * 60 * 60 * 24)));
199
+ }
200
+
201
+ /**
202
+ * Formats existing instincts as a compact JSON array for inline context.
203
+ * Reduces token usage by ~70% compared to full YAML+markdown serialization.
204
+ * Includes only the fields the analyzer needs to make decisions.
205
+ */
206
+ export function formatInstinctsCompact(instincts: Instinct[]): string {
207
+ if (instincts.length === 0) {
208
+ return "[]";
209
+ }
210
+ const summaries = instincts.map((i) => ({
211
+ id: i.id,
212
+ trigger: i.trigger,
213
+ action: i.action,
214
+ confidence: i.confidence,
215
+ domain: i.domain,
216
+ scope: i.scope,
217
+ confirmed: i.confirmed_count,
218
+ contradicted: i.contradicted_count,
219
+ inactive: i.inactive_count,
220
+ age_days: daysSince(i.created_at),
221
+ ...(i.last_confirmed_session !== undefined
222
+ ? { last_confirmed_session: i.last_confirmed_session }
223
+ : {}),
224
+ }));
225
+ return JSON.stringify(summaries);
226
+ }
227
+
228
+ /**
229
+ * Estimates the token count of a text string using a chars/token heuristic.
230
+ */
231
+ export function estimateTokens(text: string): number {
232
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
233
+ }
234
+
110
235
  /**
111
236
  * Formats existing instincts as serialized markdown blocks for inline context.
237
+ * @deprecated Use formatInstinctsCompact for lower token usage.
112
238
  */
113
239
  export function formatInstinctsForPrompt(instincts: Instinct[]): string {
114
240
  if (instincts.length === 0) {