pi-continuous-learning 0.7.0 → 0.9.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 (91) hide show
  1. package/dist/analysis-event-log.d.ts +50 -0
  2. package/dist/analysis-event-log.d.ts.map +1 -0
  3. package/dist/analysis-event-log.js +120 -0
  4. package/dist/analysis-event-log.js.map +1 -0
  5. package/dist/analysis-notification.d.ts +20 -0
  6. package/dist/analysis-notification.d.ts.map +1 -0
  7. package/dist/analysis-notification.js +63 -0
  8. package/dist/analysis-notification.js.map +1 -0
  9. package/dist/cli/analyze-single-shot.d.ts +12 -0
  10. package/dist/cli/analyze-single-shot.d.ts.map +1 -1
  11. package/dist/cli/analyze-single-shot.js +84 -2
  12. package/dist/cli/analyze-single-shot.js.map +1 -1
  13. package/dist/cli/analyze.js +349 -21
  14. package/dist/cli/analyze.js.map +1 -1
  15. package/dist/confidence.d.ts +12 -1
  16. package/dist/confidence.d.ts.map +1 -1
  17. package/dist/confidence.js +35 -8
  18. package/dist/confidence.js.map +1 -1
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +7 -0
  21. package/dist/config.js.map +1 -1
  22. package/dist/consolidation.d.ts +43 -0
  23. package/dist/consolidation.d.ts.map +1 -0
  24. package/dist/consolidation.js +104 -0
  25. package/dist/consolidation.js.map +1 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +7 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/instinct-cleanup.d.ts +14 -0
  30. package/dist/instinct-cleanup.d.ts.map +1 -1
  31. package/dist/instinct-cleanup.js +59 -3
  32. package/dist/instinct-cleanup.js.map +1 -1
  33. package/dist/instinct-contradiction.d.ts +42 -0
  34. package/dist/instinct-contradiction.d.ts.map +1 -0
  35. package/dist/instinct-contradiction.js +164 -0
  36. package/dist/instinct-contradiction.js.map +1 -0
  37. package/dist/instinct-dream.d.ts +12 -0
  38. package/dist/instinct-dream.d.ts.map +1 -0
  39. package/dist/instinct-dream.js +33 -0
  40. package/dist/instinct-dream.js.map +1 -0
  41. package/dist/instinct-parser.d.ts.map +1 -1
  42. package/dist/instinct-parser.js +6 -0
  43. package/dist/instinct-parser.js.map +1 -1
  44. package/dist/observation-signal.d.ts +34 -0
  45. package/dist/observation-signal.d.ts.map +1 -0
  46. package/dist/observation-signal.js +66 -0
  47. package/dist/observation-signal.js.map +1 -0
  48. package/dist/prompts/analyzer-system-single-shot.d.ts.map +1 -1
  49. package/dist/prompts/analyzer-system-single-shot.js +57 -2
  50. package/dist/prompts/analyzer-system-single-shot.js.map +1 -1
  51. package/dist/prompts/analyzer-user-single-shot.d.ts.map +1 -1
  52. package/dist/prompts/analyzer-user-single-shot.js +5 -3
  53. package/dist/prompts/analyzer-user-single-shot.js.map +1 -1
  54. package/dist/prompts/consolidate-system.d.ts +6 -0
  55. package/dist/prompts/consolidate-system.d.ts.map +1 -0
  56. package/dist/prompts/consolidate-system.js +102 -0
  57. package/dist/prompts/consolidate-system.js.map +1 -0
  58. package/dist/prompts/consolidate-user.d.ts +19 -0
  59. package/dist/prompts/consolidate-user.d.ts.map +1 -0
  60. package/dist/prompts/consolidate-user.js +45 -0
  61. package/dist/prompts/consolidate-user.js.map +1 -0
  62. package/dist/prompts/dream-prompt.d.ts +7 -0
  63. package/dist/prompts/dream-prompt.d.ts.map +1 -0
  64. package/dist/prompts/dream-prompt.js +64 -0
  65. package/dist/prompts/dream-prompt.js.map +1 -0
  66. package/dist/prompts/evolve-prompt.d.ts.map +1 -1
  67. package/dist/prompts/evolve-prompt.js +6 -5
  68. package/dist/prompts/evolve-prompt.js.map +1 -1
  69. package/dist/types.d.ts +4 -0
  70. package/dist/types.d.ts.map +1 -1
  71. package/package.json +1 -1
  72. package/src/analysis-event-log.ts +171 -0
  73. package/src/analysis-notification.ts +79 -0
  74. package/src/cli/analyze-single-shot.ts +98 -2
  75. package/src/cli/analyze.ts +406 -20
  76. package/src/confidence.ts +33 -7
  77. package/src/config.ts +10 -0
  78. package/src/consolidation.ts +162 -0
  79. package/src/index.ts +17 -0
  80. package/src/instinct-cleanup.ts +62 -3
  81. package/src/instinct-contradiction.ts +202 -0
  82. package/src/instinct-dream.ts +62 -0
  83. package/src/instinct-parser.ts +6 -0
  84. package/src/observation-signal.ts +80 -0
  85. package/src/prompts/analyzer-system-single-shot.ts +57 -2
  86. package/src/prompts/analyzer-user-single-shot.ts +7 -2
  87. package/src/prompts/consolidate-system.ts +101 -0
  88. package/src/prompts/consolidate-user.ts +88 -0
  89. package/src/prompts/dream-prompt.ts +88 -0
  90. package/src/prompts/evolve-prompt.ts +6 -5
  91. package/src/types.ts +5 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Pure consolidation gate logic for the "instinct-dream" holistic review.
3
+ *
4
+ * Determines whether enough time and sessions have elapsed since the last
5
+ * consolidation to justify a new pass. No I/O - all inputs are passed in.
6
+ */
7
+
8
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
9
+ import { dirname, join } from "node:path";
10
+ import { getBaseDir } from "./storage.js";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Constants (defaults - overridable via config)
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /** Minimum days between consolidation runs. */
17
+ export const DEFAULT_CONSOLIDATION_INTERVAL_DAYS = 7;
18
+
19
+ /** Minimum distinct sessions since last consolidation. */
20
+ export const DEFAULT_CONSOLIDATION_MIN_SESSIONS = 10;
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Types
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export interface ConsolidationMeta {
27
+ last_consolidation_at?: string; // ISO 8601
28
+ last_consolidation_session_count?: number;
29
+ }
30
+
31
+ export interface ConsolidationGateInput {
32
+ meta: ConsolidationMeta;
33
+ currentSessionCount: number;
34
+ now?: Date;
35
+ intervalDays?: number;
36
+ minSessions?: number;
37
+ }
38
+
39
+ export interface ConsolidationGateResult {
40
+ eligible: boolean;
41
+ reason: string;
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Gate check (pure)
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /**
49
+ * Determines whether a consolidation pass should run based on
50
+ * elapsed time and session count since the last consolidation.
51
+ *
52
+ * Both conditions must be met (dual-gate):
53
+ * 1. At least `intervalDays` since last consolidation
54
+ * 2. At least `minSessions` new sessions since last consolidation
55
+ */
56
+ export function checkConsolidationGate(
57
+ input: ConsolidationGateInput
58
+ ): ConsolidationGateResult {
59
+ const {
60
+ meta,
61
+ currentSessionCount,
62
+ now = new Date(),
63
+ intervalDays = DEFAULT_CONSOLIDATION_INTERVAL_DAYS,
64
+ minSessions = DEFAULT_CONSOLIDATION_MIN_SESSIONS,
65
+ } = input;
66
+
67
+ // First run - no prior consolidation
68
+ if (!meta.last_consolidation_at) {
69
+ const sessionsSinceStart = currentSessionCount;
70
+ if (sessionsSinceStart < minSessions) {
71
+ return {
72
+ eligible: false,
73
+ reason: `only ${sessionsSinceStart} sessions recorded (need ${minSessions})`,
74
+ };
75
+ }
76
+ return { eligible: true, reason: "first consolidation run" };
77
+ }
78
+
79
+ const lastRun = new Date(meta.last_consolidation_at);
80
+ const daysSince = (now.getTime() - lastRun.getTime()) / (1000 * 60 * 60 * 24);
81
+
82
+ if (daysSince < intervalDays) {
83
+ return {
84
+ eligible: false,
85
+ reason: `only ${daysSince.toFixed(1)} days since last consolidation (need ${intervalDays})`,
86
+ };
87
+ }
88
+
89
+ const sessionsSinceLast =
90
+ currentSessionCount - (meta.last_consolidation_session_count ?? 0);
91
+
92
+ if (sessionsSinceLast < minSessions) {
93
+ return {
94
+ eligible: false,
95
+ reason: `only ${sessionsSinceLast} sessions since last consolidation (need ${minSessions})`,
96
+ };
97
+ }
98
+
99
+ return { eligible: true, reason: "gate conditions met" };
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Session counting (from observations)
104
+ // ---------------------------------------------------------------------------
105
+
106
+ /**
107
+ * Counts distinct session IDs in a JSONL observations file.
108
+ * Scans all lines and extracts unique `"session":"..."` values.
109
+ */
110
+ export function countDistinctSessions(obsPath: string): number {
111
+ if (!existsSync(obsPath)) return 0;
112
+
113
+ const content = readFileSync(obsPath, "utf-8");
114
+ const sessions = new Set<string>();
115
+
116
+ for (const line of content.split("\n")) {
117
+ if (!line.trim()) continue;
118
+ // Fast regex extraction avoids full JSON parse per line
119
+ const match = /"session"\s*:\s*"([^"]+)"/.exec(line);
120
+ if (match?.[1]) {
121
+ sessions.add(match[1]);
122
+ }
123
+ }
124
+
125
+ return sessions.size;
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Consolidation meta persistence
130
+ // ---------------------------------------------------------------------------
131
+
132
+ const CONSOLIDATION_META_FILENAME = "consolidation.json";
133
+
134
+ export function getConsolidationMetaPath(
135
+ projectId: string,
136
+ baseDir = getBaseDir()
137
+ ): string {
138
+ return join(baseDir, "projects", projectId, CONSOLIDATION_META_FILENAME);
139
+ }
140
+
141
+ export function loadConsolidationMeta(
142
+ projectId: string,
143
+ baseDir = getBaseDir()
144
+ ): ConsolidationMeta {
145
+ const metaPath = getConsolidationMetaPath(projectId, baseDir);
146
+ if (!existsSync(metaPath)) return {};
147
+ try {
148
+ return JSON.parse(readFileSync(metaPath, "utf-8")) as ConsolidationMeta;
149
+ } catch {
150
+ return {};
151
+ }
152
+ }
153
+
154
+ export function saveConsolidationMeta(
155
+ projectId: string,
156
+ meta: ConsolidationMeta,
157
+ baseDir = getBaseDir()
158
+ ): void {
159
+ const metaPath = getConsolidationMetaPath(projectId, baseDir);
160
+ mkdirSync(dirname(metaPath), { recursive: true });
161
+ writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf-8");
162
+ }
package/src/index.ts CHANGED
@@ -33,8 +33,10 @@ import { handleInstinctPromote, COMMAND_NAME as PROMOTE_CMD } from "./instinct-p
33
33
  import { handleInstinctEvolve, COMMAND_NAME as EVOLVE_CMD } from "./instinct-evolve.js";
34
34
  import { handleInstinctProjects, COMMAND_NAME as PROJECTS_CMD } from "./instinct-projects.js";
35
35
  import { handleInstinctGraduate, COMMAND_NAME as GRADUATE_CMD } from "./instinct-graduate.js";
36
+ import { handleInstinctDream, COMMAND_NAME as DREAM_CMD } from "./instinct-dream.js";
36
37
  import { registerAllTools } from "./instinct-tools.js";
37
38
  import { logError } from "./error-logger.js";
39
+ import { checkAnalysisNotifications } from "./analysis-notification.js";
38
40
  import type { Config, InstalledSkill, ProjectEntry } from "./types.js";
39
41
 
40
42
  export default function (pi: ExtensionAPI): void {
@@ -70,6 +72,7 @@ export default function (pi: ExtensionAPI): void {
70
72
  try {
71
73
  if (!project || !config) return;
72
74
  handleBeforeAgentStart(event, ctx, project);
75
+ checkAnalysisNotifications(ctx, project.id);
73
76
  return handleBeforeAgentStartInjection(event, ctx, config, project.id) ?? undefined;
74
77
  } catch (err) {
75
78
  logError(project?.id ?? null, "before_agent_start", err);
@@ -207,4 +210,18 @@ export default function (pi: ExtensionAPI): void {
207
210
  project?.root ?? null
208
211
  ),
209
212
  });
213
+
214
+ pi.registerCommand(DREAM_CMD, {
215
+ description: "Holistic consolidation review of all instincts (merge, deduplicate, resolve contradictions)",
216
+ handler: (args: string, ctx: ExtensionCommandContext) =>
217
+ handleInstinctDream(
218
+ args,
219
+ ctx,
220
+ pi,
221
+ project?.id,
222
+ undefined,
223
+ project?.root ?? null,
224
+ installedSkills
225
+ ),
226
+ });
210
227
  }
@@ -11,12 +11,13 @@
11
11
  import { unlinkSync, existsSync } from "node:fs";
12
12
  import { join } from "node:path";
13
13
  import type { Instinct, Config } from "./types.js";
14
- import { listInstincts, invalidateCache } from "./instinct-store.js";
14
+ import { listInstincts, saveInstinct, invalidateCache } from "./instinct-store.js";
15
15
  import {
16
16
  getBaseDir,
17
17
  getProjectInstinctsDir,
18
18
  getGlobalInstinctsDir,
19
19
  } from "./storage.js";
20
+ import { findContradictions } from "./instinct-contradiction.js";
20
21
 
21
22
  // ---------------------------------------------------------------------------
22
23
  // Helpers
@@ -124,9 +125,58 @@ export function enforceInstinctCap(dir: string, maxCount: number): number {
124
125
  // Result type
125
126
  // ---------------------------------------------------------------------------
126
127
 
128
+ /**
129
+ * Flags the lower-confidence instinct in each contradictory pair.
130
+ * When confidence is equal, both are flagged.
131
+ * Already-flagged instincts are excluded from contradiction detection.
132
+ *
133
+ * @returns Number of instincts newly flagged.
134
+ */
135
+ export function cleanupContradictions(dir: string): number {
136
+ const instincts = listInstincts(dir);
137
+ const matches = findContradictions(instincts);
138
+ if (matches.length === 0) return 0;
139
+
140
+ const toFlag = new Set<string>();
141
+
142
+ for (const match of matches) {
143
+ const { instinctA, instinctB } = match;
144
+ if (instinctA.confidence > instinctB.confidence) {
145
+ toFlag.add(instinctB.id);
146
+ } else if (instinctB.confidence > instinctA.confidence) {
147
+ toFlag.add(instinctA.id);
148
+ } else {
149
+ // Equal confidence - flag both for user review
150
+ toFlag.add(instinctA.id);
151
+ toFlag.add(instinctB.id);
152
+ }
153
+ }
154
+
155
+ let flagged = 0;
156
+ for (const instinct of instincts) {
157
+ if (toFlag.has(instinct.id)) {
158
+ const updated: Instinct = {
159
+ ...instinct,
160
+ flagged_for_removal: true,
161
+ updated_at: new Date().toISOString(),
162
+ };
163
+ saveInstinct(updated, dir);
164
+ flagged++;
165
+ }
166
+ }
167
+
168
+ if (flagged > 0) invalidateCache(dir);
169
+ return flagged;
170
+ }
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // Result type
174
+ // ---------------------------------------------------------------------------
175
+
127
176
  export interface CleanupResult {
128
177
  flaggedDeleted: number;
129
178
  zeroConfirmedDeleted: number;
179
+ contradictionsFlagged: number;
130
180
  capDeleted: number;
131
181
  total: number;
132
182
  }
@@ -140,6 +190,11 @@ export interface CleanupResult {
140
190
  * Order: flagged → zero-confirmed → cap enforcement (cap runs last so it
141
191
  * accounts for deletions made by the earlier rules).
142
192
  */
193
+ /**
194
+ * Runs all cleanup rules against a single directory.
195
+ * Order: flagged → zero-confirmed → contradictions → cap enforcement
196
+ * (cap runs last so it accounts for deletions/flags from earlier rules).
197
+ */
143
198
  export function cleanupDir(
144
199
  dir: string,
145
200
  config: Config,
@@ -150,9 +205,10 @@ export function cleanupDir(
150
205
  dir,
151
206
  config.instinct_ttl_days
152
207
  );
208
+ const contradictionsFlagged = cleanupContradictions(dir);
153
209
  const capDeleted = enforceInstinctCap(dir, maxCount);
154
- const total = flaggedDeleted + zeroConfirmedDeleted + capDeleted;
155
- return { flaggedDeleted, zeroConfirmedDeleted, capDeleted, total };
210
+ const total = flaggedDeleted + zeroConfirmedDeleted + contradictionsFlagged + capDeleted;
211
+ return { flaggedDeleted, zeroConfirmedDeleted, contradictionsFlagged, capDeleted, total };
156
212
  }
157
213
 
158
214
  /**
@@ -172,6 +228,7 @@ export function runCleanupPass(
172
228
  const result: CleanupResult = {
173
229
  flaggedDeleted: 0,
174
230
  zeroConfirmedDeleted: 0,
231
+ contradictionsFlagged: 0,
175
232
  capDeleted: 0,
176
233
  total: 0,
177
234
  };
@@ -185,6 +242,7 @@ export function runCleanupPass(
185
242
  );
186
243
  result.flaggedDeleted += projectResult.flaggedDeleted;
187
244
  result.zeroConfirmedDeleted += projectResult.zeroConfirmedDeleted;
245
+ result.contradictionsFlagged += projectResult.contradictionsFlagged;
188
246
  result.capDeleted += projectResult.capDeleted;
189
247
  result.total += projectResult.total;
190
248
  }
@@ -197,6 +255,7 @@ export function runCleanupPass(
197
255
  );
198
256
  result.flaggedDeleted += globalResult.flaggedDeleted;
199
257
  result.zeroConfirmedDeleted += globalResult.zeroConfirmedDeleted;
258
+ result.contradictionsFlagged += globalResult.contradictionsFlagged;
200
259
  result.capDeleted += globalResult.capDeleted;
201
260
  result.total += globalResult.total;
202
261
 
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Contradiction detection for instincts with opposing actions.
3
+ *
4
+ * Detects instincts that have similar triggers but semantically opposed actions
5
+ * using pattern-based heuristics (negation words, antonym verb pairs).
6
+ * No LLM cost - purely deterministic.
7
+ */
8
+
9
+ import type { Instinct } from "./types.js";
10
+ import { tokenize, jaccardSimilarity } from "./instinct-validator.js";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Constants
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /** Default Jaccard similarity threshold for trigger comparison. */
17
+ const DEFAULT_TRIGGER_THRESHOLD = 0.4;
18
+
19
+ /**
20
+ * Pairs of verbs/keywords that indicate opposing intent when one appears
21
+ * in each action. Order within each pair does not matter.
22
+ */
23
+ export const OPPOSING_VERB_PAIRS: ReadonlyArray<readonly [string, string]> = [
24
+ ["avoid", "prefer"],
25
+ ["avoid", "use"],
26
+ ["avoid", "always"],
27
+ ["avoid", "ensure"],
28
+ ["never", "always"],
29
+ ["never", "prefer"],
30
+ ["never", "use"],
31
+ ["never", "ensure"],
32
+ ["skip", "always"],
33
+ ["skip", "ensure"],
34
+ ["skip", "require"],
35
+ ["reject", "prefer"],
36
+ ["reject", "use"],
37
+ ["reject", "accept"],
38
+ ] as const;
39
+
40
+ /**
41
+ * Negation prefixes that invert the meaning of a following verb.
42
+ * Matched as word boundaries in lowercase text.
43
+ */
44
+ const NEGATION_PATTERNS: ReadonlyArray<string> = [
45
+ "do not ",
46
+ "don't ",
47
+ "do not",
48
+ "don't",
49
+ ];
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Helpers
53
+ // ---------------------------------------------------------------------------
54
+
55
+ /**
56
+ * Extracts the set of action-relevant keywords from an action string.
57
+ * Lowercases and splits on word boundaries.
58
+ */
59
+ function extractActionWords(action: string): Set<string> {
60
+ return new Set(
61
+ action
62
+ .toLowerCase()
63
+ .split(/[^a-z']+/)
64
+ .filter((w) => w.length > 0)
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Checks whether a negation prefix appears in the action text,
70
+ * followed by a verb that appears in the other action.
71
+ */
72
+ function hasNegationConflict(actionA: string, actionB: string): string | null {
73
+ const lowerA = actionA.toLowerCase();
74
+ const lowerB = actionB.toLowerCase();
75
+ const wordsA = extractActionWords(actionA);
76
+ const wordsB = extractActionWords(actionB);
77
+
78
+ for (const neg of NEGATION_PATTERNS) {
79
+ // Check if A has negation + verb that B uses affirmatively
80
+ const idxA = lowerA.indexOf(neg);
81
+ if (idxA !== -1) {
82
+ const afterNeg = lowerA.slice(idxA + neg.length).trim();
83
+ const negatedVerb = afterNeg.split(/[^a-z]+/)[0];
84
+ if (negatedVerb && negatedVerb.length > 1 && wordsB.has(negatedVerb)) {
85
+ return `"${neg}${negatedVerb}" vs "${negatedVerb}"`;
86
+ }
87
+ }
88
+
89
+ // Check if B has negation + verb that A uses affirmatively
90
+ const idxB = lowerB.indexOf(neg);
91
+ if (idxB !== -1) {
92
+ const afterNeg = lowerB.slice(idxB + neg.length).trim();
93
+ const negatedVerb = afterNeg.split(/[^a-z]+/)[0];
94
+ if (negatedVerb && negatedVerb.length > 1 && wordsA.has(negatedVerb)) {
95
+ return `"${neg}${negatedVerb}" vs "${negatedVerb}"`;
96
+ }
97
+ }
98
+ }
99
+
100
+ return null;
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Public API
105
+ // ---------------------------------------------------------------------------
106
+
107
+ export interface ContradictionMatch {
108
+ instinctA: Instinct;
109
+ instinctB: Instinct;
110
+ triggerSimilarity: number;
111
+ reason: string;
112
+ }
113
+
114
+ /**
115
+ * Checks whether two actions are semantically opposing using verb pair matching
116
+ * and negation pattern detection.
117
+ *
118
+ * @returns A reason string if opposing, null otherwise.
119
+ */
120
+ export function hasOpposingAction(actionA: string, actionB: string): string | null {
121
+ if (!actionA || !actionB) return null;
122
+ if (actionA === actionB) return null;
123
+
124
+ const wordsA = extractActionWords(actionA);
125
+ const wordsB = extractActionWords(actionB);
126
+
127
+ // Check opposing verb pairs
128
+ for (const [verbX, verbY] of OPPOSING_VERB_PAIRS) {
129
+ if (
130
+ (wordsA.has(verbX) && wordsB.has(verbY)) ||
131
+ (wordsA.has(verbY) && wordsB.has(verbX))
132
+ ) {
133
+ return `opposing verbs: "${verbX}" vs "${verbY}"`;
134
+ }
135
+ }
136
+
137
+ // Check negation patterns (e.g., "do not use" vs "use")
138
+ const negationResult = hasNegationConflict(actionA, actionB);
139
+ if (negationResult) {
140
+ return `negation conflict: ${negationResult}`;
141
+ }
142
+
143
+ return null;
144
+ }
145
+
146
+ /**
147
+ * Finds all contradictory pairs in a set of instincts.
148
+ *
149
+ * A contradiction is defined as:
150
+ * 1. Similar triggers (Jaccard similarity >= threshold on trigger tokens)
151
+ * 2. Opposing actions (detected via verb pairs or negation patterns)
152
+ *
153
+ * Instincts with `flagged_for_removal` are excluded.
154
+ * Each pair is reported once (no duplicates).
155
+ *
156
+ * @param instincts - All instincts to check
157
+ * @param triggerThreshold - Jaccard similarity threshold for triggers (default 0.4)
158
+ * @returns Array of contradiction matches
159
+ */
160
+ export function findContradictions(
161
+ instincts: readonly Instinct[],
162
+ triggerThreshold = DEFAULT_TRIGGER_THRESHOLD
163
+ ): ContradictionMatch[] {
164
+ const active = instincts.filter((i) => !i.flagged_for_removal);
165
+ if (active.length < 2) return [];
166
+
167
+ const matches: ContradictionMatch[] = [];
168
+
169
+ // Pre-compute trigger tokens
170
+ const triggerTokens = new Map<string, Set<string>>();
171
+ for (const inst of active) {
172
+ triggerTokens.set(inst.id, tokenize(inst.trigger));
173
+ }
174
+
175
+ // Compare all unique pairs
176
+ for (let i = 0; i < active.length; i++) {
177
+ for (let j = i + 1; j < active.length; j++) {
178
+ const a = active[i]!;
179
+ const b = active[j]!;
180
+
181
+ // Step 1: Check trigger similarity
182
+ const tokensA = triggerTokens.get(a.id)!;
183
+ const tokensB = triggerTokens.get(b.id)!;
184
+ const similarity = jaccardSimilarity(tokensA, tokensB);
185
+
186
+ if (similarity < triggerThreshold) continue;
187
+
188
+ // Step 2: Check action opposition
189
+ const reason = hasOpposingAction(a.action, b.action);
190
+ if (reason) {
191
+ matches.push({
192
+ instinctA: a,
193
+ instinctB: b,
194
+ triggerSimilarity: similarity,
195
+ reason,
196
+ });
197
+ }
198
+ }
199
+ }
200
+
201
+ return matches;
202
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * /instinct-dream slash command handler.
3
+ *
4
+ * Interactive version of consolidation that runs inside a Pi session.
5
+ * Loads all instincts, builds a consolidation prompt, and sends it as
6
+ * a followUp message for the LLM to review with the user.
7
+ */
8
+
9
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
10
+ import type { InstalledSkill } from "./types.js";
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { getBaseDir } from "./storage.js";
14
+ import { readAgentsMd } from "./agents-md.js";
15
+ import { loadProjectInstincts, loadGlobalInstincts } from "./instinct-store.js";
16
+ import { filterInstincts } from "./instinct-loader.js";
17
+ import { buildDreamPrompt } from "./prompts/dream-prompt.js";
18
+
19
+ const MAX_DREAM_INSTINCTS = 100;
20
+
21
+ export const COMMAND_NAME = "instinct-dream";
22
+
23
+ export async function handleInstinctDream(
24
+ _args: string,
25
+ ctx: ExtensionCommandContext,
26
+ pi: ExtensionAPI,
27
+ projectId?: string | null,
28
+ baseDir?: string,
29
+ projectRoot?: string | null,
30
+ installedSkills?: InstalledSkill[]
31
+ ): Promise<void> {
32
+ const effectiveBase = baseDir ?? getBaseDir();
33
+ const projectInstincts = projectId
34
+ ? loadProjectInstincts(projectId, effectiveBase)
35
+ : [];
36
+ const globalInstincts = loadGlobalInstincts(effectiveBase);
37
+ const allInstincts = filterInstincts(
38
+ [...projectInstincts, ...globalInstincts],
39
+ 0.1,
40
+ MAX_DREAM_INSTINCTS
41
+ );
42
+
43
+ if (allInstincts.length === 0) {
44
+ ctx.ui.notify(
45
+ "No instincts to consolidate. Keep using pi to accumulate instincts first.",
46
+ "info"
47
+ );
48
+ return;
49
+ }
50
+
51
+ const agentsMdProject =
52
+ projectRoot != null ? readAgentsMd(join(projectRoot, "AGENTS.md")) : null;
53
+ const agentsMdGlobal = readAgentsMd(join(homedir(), ".pi", "agent", "AGENTS.md"));
54
+
55
+ const prompt = buildDreamPrompt(
56
+ allInstincts,
57
+ agentsMdProject,
58
+ agentsMdGlobal,
59
+ installedSkills
60
+ );
61
+ pi.sendUserMessage(prompt, { deliverAs: "followUp" });
62
+ }
@@ -132,6 +132,9 @@ export function parseInstinct(content: string): Instinct {
132
132
  if (fm["graduated_at"] !== undefined && fm["graduated_at"] !== null) {
133
133
  instinct.graduated_at = String(fm["graduated_at"]);
134
134
  }
135
+ if (fm["last_confirmed_session"] !== undefined && fm["last_confirmed_session"] !== null) {
136
+ instinct.last_confirmed_session = String(fm["last_confirmed_session"]);
137
+ }
135
138
 
136
139
  return instinct;
137
140
  }
@@ -177,6 +180,9 @@ export function serializeInstinct(instinct: Instinct): string {
177
180
  if (instinct.graduated_at !== undefined) {
178
181
  frontmatter["graduated_at"] = instinct.graduated_at;
179
182
  }
183
+ if (instinct.last_confirmed_session !== undefined) {
184
+ frontmatter["last_confirmed_session"] = instinct.last_confirmed_session;
185
+ }
180
186
 
181
187
  const yamlStr = stringifyYaml(frontmatter);
182
188
  return `---\n${yamlStr}---\n\n${instinct.action}\n`;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Observation batch signal scoring.
3
+ * Determines whether a batch of observations contains enough signal
4
+ * to warrant running the analyzer (and spending tokens).
5
+ */
6
+
7
+ import type { Observation } from "./types.js";
8
+
9
+ /**
10
+ * Score threshold below which a batch is considered low-signal.
11
+ * Batches scoring below this are skipped with a log entry.
12
+ */
13
+ export const LOW_SIGNAL_THRESHOLD = 3;
14
+
15
+ interface ScoreResult {
16
+ readonly score: number;
17
+ readonly errors: number;
18
+ readonly corrections: number;
19
+ readonly userPrompts: number;
20
+ }
21
+
22
+ /**
23
+ * Scores an observation batch for signal richness.
24
+ *
25
+ * Scoring rules:
26
+ * - Error observation (is_error: true): +2 points
27
+ * - user_prompt after an error (user correction): +3 points
28
+ * - Other user_prompt events (potential corrections/redirections): +1 point
29
+ *
30
+ * @param lines - Raw JSONL observation lines (preprocessed or raw)
31
+ * @returns Score result with breakdown
32
+ */
33
+ export function scoreObservationBatch(lines: string[]): ScoreResult {
34
+ let score = 0;
35
+ let errors = 0;
36
+ let corrections = 0;
37
+ let userPrompts = 0;
38
+ let lastWasError = false;
39
+
40
+ for (const line of lines) {
41
+ const trimmed = line.trim();
42
+ if (!trimmed) continue;
43
+
44
+ let obs: Partial<Observation>;
45
+ try {
46
+ obs = JSON.parse(trimmed) as Partial<Observation>;
47
+ } catch {
48
+ continue; // Skip malformed lines
49
+ }
50
+
51
+ if (obs.is_error) {
52
+ score += 2;
53
+ errors++;
54
+ lastWasError = true;
55
+ continue;
56
+ }
57
+
58
+ if (obs.event === "user_prompt") {
59
+ userPrompts++;
60
+ if (lastWasError) {
61
+ score += 3;
62
+ corrections++;
63
+ } else {
64
+ score += 1;
65
+ }
66
+ }
67
+
68
+ lastWasError = false;
69
+ }
70
+
71
+ return { score, errors, corrections, userPrompts };
72
+ }
73
+
74
+ /**
75
+ * Returns true if the batch is low-signal and analysis should be skipped.
76
+ */
77
+ export function isLowSignalBatch(lines: string[]): boolean {
78
+ const { score } = scoreObservationBatch(lines);
79
+ return score < LOW_SIGNAL_THRESHOLD;
80
+ }