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.
- package/dist/analysis-event-log.d.ts +50 -0
- package/dist/analysis-event-log.d.ts.map +1 -0
- package/dist/analysis-event-log.js +120 -0
- package/dist/analysis-event-log.js.map +1 -0
- package/dist/analysis-notification.d.ts +20 -0
- package/dist/analysis-notification.d.ts.map +1 -0
- package/dist/analysis-notification.js +63 -0
- package/dist/analysis-notification.js.map +1 -0
- package/dist/cli/analyze-single-shot.d.ts +12 -0
- package/dist/cli/analyze-single-shot.d.ts.map +1 -1
- package/dist/cli/analyze-single-shot.js +84 -2
- package/dist/cli/analyze-single-shot.js.map +1 -1
- package/dist/cli/analyze.js +349 -21
- package/dist/cli/analyze.js.map +1 -1
- package/dist/confidence.d.ts +12 -1
- package/dist/confidence.d.ts.map +1 -1
- package/dist/confidence.js +35 -8
- package/dist/confidence.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/consolidation.d.ts +43 -0
- package/dist/consolidation.d.ts.map +1 -0
- package/dist/consolidation.js +104 -0
- package/dist/consolidation.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/instinct-cleanup.d.ts +14 -0
- package/dist/instinct-cleanup.d.ts.map +1 -1
- package/dist/instinct-cleanup.js +59 -3
- package/dist/instinct-cleanup.js.map +1 -1
- package/dist/instinct-contradiction.d.ts +42 -0
- package/dist/instinct-contradiction.d.ts.map +1 -0
- package/dist/instinct-contradiction.js +164 -0
- package/dist/instinct-contradiction.js.map +1 -0
- package/dist/instinct-dream.d.ts +12 -0
- package/dist/instinct-dream.d.ts.map +1 -0
- package/dist/instinct-dream.js +33 -0
- package/dist/instinct-dream.js.map +1 -0
- package/dist/instinct-parser.d.ts.map +1 -1
- package/dist/instinct-parser.js +6 -0
- package/dist/instinct-parser.js.map +1 -1
- package/dist/observation-signal.d.ts +34 -0
- package/dist/observation-signal.d.ts.map +1 -0
- package/dist/observation-signal.js +66 -0
- package/dist/observation-signal.js.map +1 -0
- package/dist/prompts/analyzer-system-single-shot.d.ts.map +1 -1
- package/dist/prompts/analyzer-system-single-shot.js +57 -2
- package/dist/prompts/analyzer-system-single-shot.js.map +1 -1
- package/dist/prompts/analyzer-user-single-shot.d.ts.map +1 -1
- package/dist/prompts/analyzer-user-single-shot.js +5 -3
- package/dist/prompts/analyzer-user-single-shot.js.map +1 -1
- package/dist/prompts/consolidate-system.d.ts +6 -0
- package/dist/prompts/consolidate-system.d.ts.map +1 -0
- package/dist/prompts/consolidate-system.js +102 -0
- package/dist/prompts/consolidate-system.js.map +1 -0
- package/dist/prompts/consolidate-user.d.ts +19 -0
- package/dist/prompts/consolidate-user.d.ts.map +1 -0
- package/dist/prompts/consolidate-user.js +45 -0
- package/dist/prompts/consolidate-user.js.map +1 -0
- package/dist/prompts/dream-prompt.d.ts +7 -0
- package/dist/prompts/dream-prompt.d.ts.map +1 -0
- package/dist/prompts/dream-prompt.js +64 -0
- package/dist/prompts/dream-prompt.js.map +1 -0
- package/dist/prompts/evolve-prompt.d.ts.map +1 -1
- package/dist/prompts/evolve-prompt.js +6 -5
- package/dist/prompts/evolve-prompt.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/analysis-event-log.ts +171 -0
- package/src/analysis-notification.ts +79 -0
- package/src/cli/analyze-single-shot.ts +98 -2
- package/src/cli/analyze.ts +406 -20
- package/src/confidence.ts +33 -7
- package/src/config.ts +10 -0
- package/src/consolidation.ts +162 -0
- package/src/index.ts +17 -0
- package/src/instinct-cleanup.ts +62 -3
- package/src/instinct-contradiction.ts +202 -0
- package/src/instinct-dream.ts +62 -0
- package/src/instinct-parser.ts +6 -0
- package/src/observation-signal.ts +80 -0
- package/src/prompts/analyzer-system-single-shot.ts +57 -2
- package/src/prompts/analyzer-user-single-shot.ts +7 -2
- package/src/prompts/consolidate-system.ts +101 -0
- package/src/prompts/consolidate-user.ts +88 -0
- package/src/prompts/dream-prompt.ts +88 -0
- package/src/prompts/evolve-prompt.ts +6 -5
- 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
|
}
|
package/src/instinct-cleanup.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/instinct-parser.ts
CHANGED
|
@@ -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
|
+
}
|