pi-continuous-learning 0.7.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 (48) 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 +112 -8
  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/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/instinct-parser.d.ts.map +1 -1
  23. package/dist/instinct-parser.js +6 -0
  24. package/dist/instinct-parser.js.map +1 -1
  25. package/dist/observation-signal.d.ts +34 -0
  26. package/dist/observation-signal.d.ts.map +1 -0
  27. package/dist/observation-signal.js +66 -0
  28. package/dist/observation-signal.js.map +1 -0
  29. package/dist/prompts/analyzer-system-single-shot.d.ts.map +1 -1
  30. package/dist/prompts/analyzer-system-single-shot.js +41 -2
  31. package/dist/prompts/analyzer-system-single-shot.js.map +1 -1
  32. package/dist/prompts/analyzer-user-single-shot.d.ts.map +1 -1
  33. package/dist/prompts/analyzer-user-single-shot.js +4 -2
  34. package/dist/prompts/analyzer-user-single-shot.js.map +1 -1
  35. package/dist/types.d.ts +1 -0
  36. package/dist/types.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/analysis-event-log.ts +171 -0
  39. package/src/analysis-notification.ts +79 -0
  40. package/src/cli/analyze-single-shot.ts +98 -2
  41. package/src/cli/analyze.ts +138 -7
  42. package/src/confidence.ts +33 -7
  43. package/src/index.ts +2 -0
  44. package/src/instinct-parser.ts +6 -0
  45. package/src/observation-signal.ts +80 -0
  46. package/src/prompts/analyzer-system-single-shot.ts +41 -2
  47. package/src/prompts/analyzer-user-single-shot.ts +5 -2
  48. package/src/types.ts +1 -0
@@ -0,0 +1,50 @@
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
+ export interface InstinctChangeSummary {
14
+ readonly id: string;
15
+ readonly title: string;
16
+ readonly scope: "project" | "global";
17
+ readonly trigger?: string;
18
+ readonly action?: string;
19
+ readonly confidence_delta?: number;
20
+ }
21
+ export interface AnalysisEvent {
22
+ readonly timestamp: string;
23
+ readonly project_id: string;
24
+ readonly project_name: string;
25
+ readonly created: readonly InstinctChangeSummary[];
26
+ readonly updated: readonly InstinctChangeSummary[];
27
+ readonly deleted: readonly InstinctChangeSummary[];
28
+ }
29
+ export declare function getEventsPath(projectId: string, baseDir?: string): string;
30
+ export declare function getConsumedPath(projectId: string, baseDir?: string): string;
31
+ /**
32
+ * Appends an analysis event to the project's event log.
33
+ * Skips writing if nothing changed (all arrays empty).
34
+ * Creates the parent directory if needed.
35
+ */
36
+ export declare function appendAnalysisEvent(event: AnalysisEvent, baseDir?: string): void;
37
+ /**
38
+ * Atomically consumes all pending analysis events for a project.
39
+ *
40
+ * Strategy:
41
+ * 1. Check for orphaned `.consumed` file from a prior crash - read it first
42
+ * 2. Rename `analysis-events.jsonl` to `.consumed` (atomic on POSIX)
43
+ * 3. Read and parse all lines from `.consumed`
44
+ * 4. Delete `.consumed`
45
+ *
46
+ * Returns an empty array if no events exist or rename fails (e.g. file
47
+ * doesn't exist, or another consumer raced us).
48
+ */
49
+ export declare function consumeAnalysisEvents(projectId: string, baseDir?: string): readonly AnalysisEvent[];
50
+ //# sourceMappingURL=analysis-event-log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analysis-event-log.d.ts","sourceRoot":"","sources":["../src/analysis-event-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAwBH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAC;IACrC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACnD,QAAQ,CAAC,OAAO,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACnD,QAAQ,CAAC,OAAO,EAAE,SAAS,qBAAqB,EAAE,CAAC;CACpD;AAMD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEzE;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3E;AAMD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAYhF;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,MAAM,GACf,SAAS,aAAa,EAAE,CA8B1B"}
@@ -0,0 +1,120 @@
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
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, } from "node:fs";
14
+ import { dirname, join } from "node:path";
15
+ import { getProjectDir } from "./storage.js";
16
+ // ---------------------------------------------------------------------------
17
+ // Constants
18
+ // ---------------------------------------------------------------------------
19
+ const EVENTS_FILENAME = "analysis-events.jsonl";
20
+ const CONSUMED_FILENAME = "analysis-events.consumed";
21
+ // ---------------------------------------------------------------------------
22
+ // Paths
23
+ // ---------------------------------------------------------------------------
24
+ export function getEventsPath(projectId, baseDir) {
25
+ return join(getProjectDir(projectId, baseDir), EVENTS_FILENAME);
26
+ }
27
+ export function getConsumedPath(projectId, baseDir) {
28
+ return join(getProjectDir(projectId, baseDir), CONSUMED_FILENAME);
29
+ }
30
+ // ---------------------------------------------------------------------------
31
+ // Write (analyzer side)
32
+ // ---------------------------------------------------------------------------
33
+ /**
34
+ * Appends an analysis event to the project's event log.
35
+ * Skips writing if nothing changed (all arrays empty).
36
+ * Creates the parent directory if needed.
37
+ */
38
+ export function appendAnalysisEvent(event, baseDir) {
39
+ if (event.created.length === 0 &&
40
+ event.updated.length === 0 &&
41
+ event.deleted.length === 0) {
42
+ return;
43
+ }
44
+ const eventsPath = getEventsPath(event.project_id, baseDir);
45
+ mkdirSync(dirname(eventsPath), { recursive: true });
46
+ appendFileSync(eventsPath, JSON.stringify(event) + "\n", "utf-8");
47
+ }
48
+ // ---------------------------------------------------------------------------
49
+ // Read and clear (extension side)
50
+ // ---------------------------------------------------------------------------
51
+ /**
52
+ * Atomically consumes all pending analysis events for a project.
53
+ *
54
+ * Strategy:
55
+ * 1. Check for orphaned `.consumed` file from a prior crash - read it first
56
+ * 2. Rename `analysis-events.jsonl` to `.consumed` (atomic on POSIX)
57
+ * 3. Read and parse all lines from `.consumed`
58
+ * 4. Delete `.consumed`
59
+ *
60
+ * Returns an empty array if no events exist or rename fails (e.g. file
61
+ * doesn't exist, or another consumer raced us).
62
+ */
63
+ export function consumeAnalysisEvents(projectId, baseDir) {
64
+ const eventsPath = getEventsPath(projectId, baseDir);
65
+ const consumedPath = getConsumedPath(projectId, baseDir);
66
+ const allEvents = [];
67
+ // Step 1: recover orphaned consumed file from prior crash
68
+ if (existsSync(consumedPath)) {
69
+ allEvents.push(...parseEventsFile(consumedPath));
70
+ safeUnlink(consumedPath);
71
+ }
72
+ // Step 2: atomically rename the events file
73
+ if (existsSync(eventsPath)) {
74
+ try {
75
+ renameSync(eventsPath, consumedPath);
76
+ }
77
+ catch {
78
+ // Rename failed (race with another consumer, or OS issue).
79
+ // Return whatever we recovered from step 1.
80
+ return allEvents;
81
+ }
82
+ // Step 3: read the renamed file
83
+ allEvents.push(...parseEventsFile(consumedPath));
84
+ // Step 4: delete consumed file
85
+ safeUnlink(consumedPath);
86
+ }
87
+ return allEvents;
88
+ }
89
+ // ---------------------------------------------------------------------------
90
+ // Helpers
91
+ // ---------------------------------------------------------------------------
92
+ function parseEventsFile(filePath) {
93
+ const events = [];
94
+ try {
95
+ const content = readFileSync(filePath, "utf-8");
96
+ const lines = content.split("\n").filter((line) => line.trim().length > 0);
97
+ for (const line of lines) {
98
+ try {
99
+ events.push(JSON.parse(line));
100
+ }
101
+ catch {
102
+ // Skip malformed lines - don't lose other events
103
+ }
104
+ }
105
+ }
106
+ catch {
107
+ // File read failed - return empty
108
+ }
109
+ return events;
110
+ }
111
+ function safeUnlink(filePath) {
112
+ try {
113
+ if (existsSync(filePath))
114
+ unlinkSync(filePath);
115
+ }
116
+ catch {
117
+ // Best effort cleanup
118
+ }
119
+ }
120
+ //# sourceMappingURL=analysis-event-log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analysis-event-log.js","sourceRoot":"","sources":["../src/analysis-event-log.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
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
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
9
+ import { type AnalysisEvent } from "./analysis-event-log.js";
10
+ /**
11
+ * Aggregates multiple analysis events into a single summary line.
12
+ * Returns null when no changes occurred.
13
+ */
14
+ export declare function formatNotification(events: readonly AnalysisEvent[]): string | null;
15
+ /**
16
+ * Checks for pending analysis events and shows a notification if any exist.
17
+ * Safe to call on every `before_agent_start` - no-ops when there's nothing.
18
+ */
19
+ export declare function checkAnalysisNotifications(ctx: ExtensionContext, projectId: string | null, baseDir?: string): void;
20
+ //# sourceMappingURL=analysis-notification.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analysis-notification.d.ts","sourceRoot":"","sources":["../src/analysis-notification.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,yBAAyB,CAAC;AAMjC;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,GAAG,MAAM,GAAG,IAAI,CAiClF;AAMD;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,gBAAgB,EACrB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,OAAO,CAAC,EAAE,MAAM,GACf,IAAI,CASN"}
@@ -0,0 +1,63 @@
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
+ import { consumeAnalysisEvents, } from "./analysis-event-log.js";
9
+ // ---------------------------------------------------------------------------
10
+ // Formatting
11
+ // ---------------------------------------------------------------------------
12
+ /**
13
+ * Aggregates multiple analysis events into a single summary line.
14
+ * Returns null when no changes occurred.
15
+ */
16
+ export function formatNotification(events) {
17
+ if (events.length === 0)
18
+ return null;
19
+ let created = 0;
20
+ let updated = 0;
21
+ let deleted = 0;
22
+ const createdIds = [];
23
+ for (const event of events) {
24
+ created += event.created.length;
25
+ updated += event.updated.length;
26
+ deleted += event.deleted.length;
27
+ for (const c of event.created) {
28
+ createdIds.push(c.id);
29
+ }
30
+ }
31
+ if (created === 0 && updated === 0 && deleted === 0)
32
+ return null;
33
+ const parts = [];
34
+ if (created > 0) {
35
+ const idList = createdIds.slice(0, 3).join(", ");
36
+ const suffix = createdIds.length > 3 ? ", ..." : "";
37
+ parts.push(`+${created} new (${idList}${suffix})`);
38
+ }
39
+ if (updated > 0) {
40
+ parts.push(`${updated} updated`);
41
+ }
42
+ if (deleted > 0) {
43
+ parts.push(`${deleted} deleted`);
44
+ }
45
+ return `[instincts] Background analysis: ${parts.join(", ")}`;
46
+ }
47
+ // ---------------------------------------------------------------------------
48
+ // Handler
49
+ // ---------------------------------------------------------------------------
50
+ /**
51
+ * Checks for pending analysis events and shows a notification if any exist.
52
+ * Safe to call on every `before_agent_start` - no-ops when there's nothing.
53
+ */
54
+ export function checkAnalysisNotifications(ctx, projectId, baseDir) {
55
+ if (!projectId)
56
+ return;
57
+ const events = consumeAnalysisEvents(projectId, baseDir);
58
+ const message = formatNotification(events);
59
+ if (message) {
60
+ ctx.ui.notify(message, "info");
61
+ }
62
+ }
63
+ //# sourceMappingURL=analysis-notification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analysis-notification.js","sourceRoot":"","sources":["../src/analysis-notification.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EACL,qBAAqB,GAEtB,MAAM,yBAAyB,CAAC;AAEjC,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAgC;IACjE,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAErC,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,MAAM,UAAU,GAAa,EAAE,CAAC;IAEhC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAChC,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAChC,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAChC,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;YAC9B,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;IAED,IAAI,OAAO,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEjE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QAChB,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjD,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QACpD,KAAK,CAAC,IAAI,CAAC,IAAI,OAAO,SAAS,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC;IACrD,CAAC;IACD,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QAChB,KAAK,CAAC,IAAI,CAAC,GAAG,OAAO,UAAU,CAAC,CAAC;IACnC,CAAC;IACD,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QAChB,KAAK,CAAC,IAAI,CAAC,GAAG,OAAO,UAAU,CAAC,CAAC;IACnC,CAAC;IAED,OAAO,oCAAoC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;AAChE,CAAC;AAED,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E;;;GAGG;AACH,MAAM,UAAU,0BAA0B,CACxC,GAAqB,EACrB,SAAwB,EACxB,OAAgB;IAEhB,IAAI,CAAC,SAAS;QAAE,OAAO;IAEvB,MAAM,MAAM,GAAG,qBAAqB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACzD,MAAM,OAAO,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAE3C,IAAI,OAAO,EAAE,CAAC;QACZ,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACjC,CAAC;AACH,CAAC"}
@@ -21,6 +21,7 @@ export interface InstinctChangePayload {
21
21
  contradicted_count?: number;
22
22
  inactive_count?: number;
23
23
  evidence?: string[];
24
+ last_confirmed_session?: string;
24
25
  }
25
26
  export interface InstinctChange {
26
27
  action: "create" | "update" | "delete";
@@ -50,8 +51,19 @@ export declare function parseChanges(raw: string): InstinctChange[];
50
51
  * @param allInstincts - All current instincts, used for dedup check on creates
51
52
  */
52
53
  export declare function buildInstinctFromChange(change: InstinctChange, existing: Instinct | null, projectId: string, allInstincts?: Instinct[]): Instinct | null;
54
+ /**
55
+ * Formats existing instincts as a compact JSON array for inline context.
56
+ * Reduces token usage by ~70% compared to full YAML+markdown serialization.
57
+ * Includes only the fields the analyzer needs to make decisions.
58
+ */
59
+ export declare function formatInstinctsCompact(instincts: Instinct[]): string;
60
+ /**
61
+ * Estimates the token count of a text string using a chars/token heuristic.
62
+ */
63
+ export declare function estimateTokens(text: string): number;
53
64
  /**
54
65
  * Formats existing instincts as serialized markdown blocks for inline context.
66
+ * @deprecated Use formatInstinctsCompact for lower token usage.
55
67
  */
56
68
  export declare function formatInstinctsForPrompt(instincts: Instinct[]): string;
57
69
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"analyze-single-shot.d.ts","sourceRoot":"","sources":["../../src/cli/analyze-single-shot.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAI5C,MAAM,WAAW,qBAAqB;IACpC,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,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACvC,QAAQ,CAAC,EAAE,qBAAqB,CAAC;IACjC,6CAA6C;IAC7C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,uCAAuC;IACvC,KAAK,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;CAC9B;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,OAAO,EAAE,gBAAgB,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,EAAE,CA0B1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,QAAQ,GAAG,IAAI,EACzB,SAAS,EAAE,MAAM,EACjB,YAAY,GAAE,QAAQ,EAAO,GAC5B,QAAQ,GAAG,IAAI,CAgDjB;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,MAAM,CAKtE;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,EACrC,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,gBAAgB,CAAC,CAgB3B"}
1
+ {"version":3,"file":"analyze-single-shot.d.ts","sourceRoot":"","sources":["../../src/cli/analyze-single-shot.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAQ5C,MAAM,WAAW,qBAAqB;IACpC,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,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACvC,QAAQ,CAAC,EAAE,qBAAqB,CAAC;IACjC,6CAA6C;IAC7C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,uCAAuC;IACvC,KAAK,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;CAC9B;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,OAAO,EAAE,gBAAgB,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,EAAE,CA0B1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,QAAQ,GAAG,IAAI,EACzB,SAAS,EAAE,MAAM,EACjB,YAAY,GAAE,QAAQ,EAAO,GAC5B,QAAQ,GAAG,IAAI,CAgGjB;AAUD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,MAAM,CAoBpE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,MAAM,CAKtE;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,EACrC,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,gBAAgB,CAAC,CAgB3B"}
@@ -1,6 +1,9 @@
1
1
  import { complete } from "@mariozechner/pi-ai";
2
2
  import { serializeInstinct } from "../instinct-parser.js";
3
+ /** Chars-per-token heuristic for prompt size estimation. */
4
+ const CHARS_PER_TOKEN = 4;
3
5
  import { validateInstinct, findSimilarInstinct } from "../instinct-validator.js";
6
+ import { confirmationDelta } from "../confidence.js";
4
7
  /**
5
8
  * Parses the model's raw text response into an array of InstinctChange.
6
9
  * Strips markdown code fences if present. Throws on invalid JSON or schema.
@@ -55,12 +58,48 @@ export function buildInstinctFromChange(change, existing, projectId, allInstinct
55
58
  }
56
59
  }
57
60
  const now = new Date().toISOString();
61
+ // For updates, recompute confidence client-side to enforce:
62
+ // 1. Per-session deduplication: only one confirmation per unique session_id
63
+ // 2. Diminishing returns: each additional confirmation yields a smaller delta
64
+ let resolvedConfidence;
65
+ let resolvedConfirmedCount = payload.confirmed_count ?? existing?.confirmed_count ?? 0;
66
+ let resolvedLastConfirmedSession = payload.last_confirmed_session ?? existing?.last_confirmed_session;
67
+ if (change.action === "update" && existing !== null) {
68
+ const prevConfirmedCount = existing.confirmed_count;
69
+ const newConfirmedCount = payload.confirmed_count ?? prevConfirmedCount;
70
+ const contradictionsAdded = Math.max(0, (payload.contradicted_count ?? 0) - existing.contradicted_count);
71
+ // Detect whether the LLM intends to add a confirmation
72
+ const wantsToConfirm = newConfirmedCount > prevConfirmedCount;
73
+ // Session dedup: reject the confirmation if the confirming session is the
74
+ // same as the one that last confirmed this instinct.
75
+ const sessionDuplicate = wantsToConfirm &&
76
+ resolvedLastConfirmedSession !== undefined &&
77
+ payload.last_confirmed_session !== undefined &&
78
+ payload.last_confirmed_session === existing.last_confirmed_session;
79
+ if (sessionDuplicate) {
80
+ // Revert to existing count - this session already confirmed the instinct
81
+ resolvedConfirmedCount = prevConfirmedCount;
82
+ }
83
+ // Recompute confidence from existing + explicit deltas (don't trust LLM arithmetic)
84
+ resolvedConfidence = existing.confidence;
85
+ if (wantsToConfirm && !sessionDuplicate) {
86
+ resolvedConfidence += confirmationDelta(prevConfirmedCount);
87
+ }
88
+ if (contradictionsAdded > 0) {
89
+ resolvedConfidence -= 0.15 * contradictionsAdded;
90
+ }
91
+ resolvedConfidence = Math.max(0.1, Math.min(0.9, resolvedConfidence));
92
+ }
93
+ else {
94
+ // For creates, trust the LLM's initial confidence (no prior state to base delta on)
95
+ resolvedConfidence = Math.max(0.1, Math.min(0.9, payload.confidence));
96
+ }
58
97
  return {
59
98
  id: payload.id,
60
99
  title: payload.title,
61
100
  trigger: payload.trigger,
62
101
  action: payload.action,
63
- confidence: Math.max(0.1, Math.min(0.9, payload.confidence)),
102
+ confidence: resolvedConfidence,
64
103
  domain: payload.domain,
65
104
  scope: payload.scope,
66
105
  source: "personal",
@@ -68,14 +107,57 @@ export function buildInstinctFromChange(change, existing, projectId, allInstinct
68
107
  created_at: existing?.created_at ?? now,
69
108
  updated_at: now,
70
109
  observation_count: payload.observation_count ?? 1,
71
- confirmed_count: payload.confirmed_count ?? 0,
110
+ confirmed_count: resolvedConfirmedCount,
72
111
  contradicted_count: payload.contradicted_count ?? 0,
73
112
  inactive_count: payload.inactive_count ?? 0,
74
113
  ...(payload.evidence !== undefined ? { evidence: payload.evidence } : {}),
114
+ ...(resolvedLastConfirmedSession !== undefined
115
+ ? { last_confirmed_session: resolvedLastConfirmedSession }
116
+ : {}),
75
117
  };
76
118
  }
119
+ /**
120
+ * Returns days elapsed since the given ISO 8601 date string.
121
+ */
122
+ function daysSince(dateStr) {
123
+ const ms = Date.now() - new Date(dateStr).getTime();
124
+ return Math.max(0, Math.floor(ms / (1000 * 60 * 60 * 24)));
125
+ }
126
+ /**
127
+ * Formats existing instincts as a compact JSON array for inline context.
128
+ * Reduces token usage by ~70% compared to full YAML+markdown serialization.
129
+ * Includes only the fields the analyzer needs to make decisions.
130
+ */
131
+ export function formatInstinctsCompact(instincts) {
132
+ if (instincts.length === 0) {
133
+ return "[]";
134
+ }
135
+ const summaries = instincts.map((i) => ({
136
+ id: i.id,
137
+ trigger: i.trigger,
138
+ action: i.action,
139
+ confidence: i.confidence,
140
+ domain: i.domain,
141
+ scope: i.scope,
142
+ confirmed: i.confirmed_count,
143
+ contradicted: i.contradicted_count,
144
+ inactive: i.inactive_count,
145
+ age_days: daysSince(i.created_at),
146
+ ...(i.last_confirmed_session !== undefined
147
+ ? { last_confirmed_session: i.last_confirmed_session }
148
+ : {}),
149
+ }));
150
+ return JSON.stringify(summaries);
151
+ }
152
+ /**
153
+ * Estimates the token count of a text string using a chars/token heuristic.
154
+ */
155
+ export function estimateTokens(text) {
156
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
157
+ }
77
158
  /**
78
159
  * Formats existing instincts as serialized markdown blocks for inline context.
160
+ * @deprecated Use formatInstinctsCompact for lower token usage.
79
161
  */
80
162
  export function formatInstinctsForPrompt(instincts) {
81
163
  if (instincts.length === 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"analyze-single-shot.js","sourceRoot":"","sources":["../../src/cli/analyze-single-shot.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"analyze-single-shot.js","sourceRoot":"","sources":["../../src/cli/analyze-single-shot.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE1D,4DAA4D;AAC5D,MAAM,eAAe,GAAG,CAAC,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAgCrD;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,MAAM,QAAQ,GAAG,GAAG;SACjB,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC;SAChC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC;SACzB,IAAI,EAAE,CAAC;IAEV,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAChC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,mCAAmC,MAAM,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAC1E,CAAC;IACJ,CAAC;IAED,IACE,OAAO,MAAM,KAAK,QAAQ;QAC1B,MAAM,KAAK,IAAI;QACf,CAAC,KAAK,CAAC,OAAO,CAAE,MAAgC,CAAC,OAAO,CAAC,EACzD,CAAC;QACD,MAAM,IAAI,KAAK,CACb,mDAAmD,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAC1F,CAAC;IACJ,CAAC;IAED,OAAQ,MAAwC,CAAC,OAAO,CAAC;AAC3D,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAsB,EACtB,QAAyB,EACzB,SAAiB,EACjB,eAA2B,EAAE;IAE7B,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;QACnD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC;IAEhC,MAAM,UAAU,GAAG,gBAAgB,CAAC;QAClC,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,MAAM,EAAE,OAAO,CAAC,MAAM;KACvB,CAAC,CAAC;IACH,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QACtB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,0FAA0F;IAC1F,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,mBAAmB,CACjC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,EACpD,YAAY,EACZ,OAAO,CAAC,EAAE,CACX,CAAC;QACF,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAErC,4DAA4D;IAC5D,4EAA4E;IAC5E,8EAA8E;IAC9E,IAAI,kBAA0B,CAAC;IAC/B,IAAI,sBAAsB,GAAG,OAAO,CAAC,eAAe,IAAI,QAAQ,EAAE,eAAe,IAAI,CAAC,CAAC;IACvF,IAAI,4BAA4B,GAAG,OAAO,CAAC,sBAAsB,IAAI,QAAQ,EAAE,sBAAsB,CAAC;IAEtG,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACpD,MAAM,kBAAkB,GAAG,QAAQ,CAAC,eAAe,CAAC;QACpD,MAAM,iBAAiB,GAAG,OAAO,CAAC,eAAe,IAAI,kBAAkB,CAAC;QACxE,MAAM,mBAAmB,GAAG,IAAI,CAAC,GAAG,CAClC,CAAC,EACD,CAAC,OAAO,CAAC,kBAAkB,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,kBAAkB,CAChE,CAAC;QAEF,uDAAuD;QACvD,MAAM,cAAc,GAAG,iBAAiB,GAAG,kBAAkB,CAAC;QAE9D,0EAA0E;QAC1E,qDAAqD;QACrD,MAAM,gBAAgB,GACpB,cAAc;YACd,4BAA4B,KAAK,SAAS;YAC1C,OAAO,CAAC,sBAAsB,KAAK,SAAS;YAC5C,OAAO,CAAC,sBAAsB,KAAK,QAAQ,CAAC,sBAAsB,CAAC;QAErE,IAAI,gBAAgB,EAAE,CAAC;YACrB,yEAAyE;YACzE,sBAAsB,GAAG,kBAAkB,CAAC;QAC9C,CAAC;QAED,oFAAoF;QACpF,kBAAkB,GAAG,QAAQ,CAAC,UAAU,CAAC;QACzC,IAAI,cAAc,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACxC,kBAAkB,IAAI,iBAAiB,CAAC,kBAAkB,CAAC,CAAC;QAC9D,CAAC;QACD,IAAI,mBAAmB,GAAG,CAAC,EAAE,CAAC;YAC5B,kBAAkB,IAAI,IAAI,GAAG,mBAAmB,CAAC;QACnD,CAAC;QACD,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC,CAAC;IACxE,CAAC;SAAM,CAAC;QACN,oFAAoF;QACpF,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;IACxE,CAAC;IAED,OAAO;QACL,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,UAAU,EAAE,kBAAkB;QAC9B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,EAAE,UAAU;QAClB,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjE,UAAU,EAAE,QAAQ,EAAE,UAAU,IAAI,GAAG;QACvC,UAAU,EAAE,GAAG;QACf,iBAAiB,EAAE,OAAO,CAAC,iBAAiB,IAAI,CAAC;QACjD,eAAe,EAAE,sBAAsB;QACvC,kBAAkB,EAAE,OAAO,CAAC,kBAAkB,IAAI,CAAC;QACnD,cAAc,EAAE,OAAO,CAAC,cAAc,IAAI,CAAC;QAC3C,GAAG,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzE,GAAG,CAAC,4BAA4B,KAAK,SAAS;YAC5C,CAAC,CAAC,EAAE,sBAAsB,EAAE,4BAA4B,EAAE;YAC1D,CAAC,CAAC,EAAE,CAAC;KACR,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,SAAS,SAAS,CAAC,OAAe;IAChC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC;IACpD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;AAC7D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CAAC,SAAqB;IAC1D,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,SAAS,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACtC,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,UAAU,EAAE,CAAC,CAAC,UAAU;QACxB,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,SAAS,EAAE,CAAC,CAAC,eAAe;QAC5B,YAAY,EAAE,CAAC,CAAC,kBAAkB;QAClC,QAAQ,EAAE,CAAC,CAAC,cAAc;QAC1B,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC;QACjC,GAAG,CAAC,CAAC,CAAC,sBAAsB,KAAK,SAAS;YACxC,CAAC,CAAC,EAAE,sBAAsB,EAAE,CAAC,CAAC,sBAAsB,EAAE;YACtD,CAAC,CAAC,EAAE,CAAC;KACR,CAAC,CAAC,CAAC;IACJ,OAAO,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;AACnC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,eAAe,CAAC,CAAC;AAClD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CAAC,SAAqB;IAC5D,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,yBAAyB,CAAC;IACnC,CAAC;IACD,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAgB,EAChB,KAAqC,EACrC,MAAc,EACd,MAAoB;IAEpB,MAAM,IAAI,GAAmC,EAAE,MAAM,EAAE,CAAC;IACxD,IAAI,MAAM,KAAK,SAAS;QAAE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IAC/C,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAErD,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO;SAChC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;SAChC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAE,CAAoC,CAAC,IAAI,CAAC;SACtD,IAAI,CAAC,EAAE,CAAC,CAAC;IAEZ,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACtD,CAAC;IAED,MAAM,OAAO,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC;IAC1C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAC9B,CAAC"}
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, readFileSync, statSync, writeFileSync, unlinkSync, } from "node:fs";
3
+ import { createHash } from "node:crypto";
3
4
  import { join } from "node:path";
4
5
  import { AuthStorage } from "@mariozechner/pi-coding-agent";
5
6
  import { getModel } from "@mariozechner/pi-ai";
@@ -11,7 +12,9 @@ import { runCleanupPass } from "../instinct-cleanup.js";
11
12
  import { tailObservationsSince } from "../prompts/analyzer-user.js";
12
13
  import { buildSingleShotSystemPrompt } from "../prompts/analyzer-system-single-shot.js";
13
14
  import { buildSingleShotUserPrompt } from "../prompts/analyzer-user-single-shot.js";
14
- import { runSingleShot, buildInstinctFromChange, } from "./analyze-single-shot.js";
15
+ import { runSingleShot, buildInstinctFromChange, estimateTokens, } from "./analyze-single-shot.js";
16
+ import { isLowSignalBatch } from "../observation-signal.js";
17
+ import { appendAnalysisEvent, } from "../analysis-event-log.js";
15
18
  import { loadProjectInstincts, loadGlobalInstincts, saveInstinct, } from "../instinct-store.js";
16
19
  import { readAgentsMd } from "../agents-md.js";
17
20
  import { homedir } from "node:os";
@@ -69,6 +72,24 @@ function startGlobalTimeout(timeoutMs, logger) {
69
72
  process.exit(2);
70
73
  }, timeoutMs).unref();
71
74
  }
75
+ // ---------------------------------------------------------------------------
76
+ // Per-project analysis
77
+ // ---------------------------------------------------------------------------
78
+ /** Max estimated tokens before fallback strategies are applied. */
79
+ const PROMPT_TOKEN_BUDGET = 40_000;
80
+ function hashContent(content) {
81
+ return createHash("sha256").update(content).digest("hex");
82
+ }
83
+ /**
84
+ * Truncates AGENTS.md content to section headers only (lines starting with #).
85
+ * Used as a fallback when the prompt is over the token budget.
86
+ */
87
+ function truncateAgentsMdToHeaders(content) {
88
+ return content
89
+ .split("\n")
90
+ .filter((line) => line.startsWith("#"))
91
+ .join("\n");
92
+ }
72
93
  function loadProjectsRegistry(baseDir) {
73
94
  const path = getProjectsRegistryPath(baseDir);
74
95
  if (!existsSync(path))
@@ -118,6 +139,9 @@ async function analyzeProject(project, config, baseDir, logger) {
118
139
  if (newObsLines.length === 0) {
119
140
  return { ran: false, skippedReason: "no new observation lines after preprocessing" };
120
141
  }
142
+ if (isLowSignalBatch(newObsLines)) {
143
+ return { ran: false, skippedReason: "low-signal batch (no errors, corrections, or user redirections)" };
144
+ }
121
145
  const obsCount = countObservations(project.id, baseDir);
122
146
  if (obsCount < config.min_observations_to_analyze) {
123
147
  return { ran: false, skippedReason: `below threshold (${obsCount}/${config.min_observations_to_analyze})` };
@@ -130,8 +154,17 @@ async function analyzeProject(project, config, baseDir, logger) {
130
154
  const projectInstincts = loadProjectInstincts(project.id, baseDir);
131
155
  const globalInstincts = loadGlobalInstincts(baseDir);
132
156
  const allInstincts = [...projectInstincts, ...globalInstincts];
133
- const agentsMdProject = readAgentsMd(join(project.root, "AGENTS.md"));
134
- const agentsMdGlobal = readAgentsMd(join(homedir(), ".pi", "agent", "AGENTS.md"));
157
+ // Load AGENTS.md, skipping if content hash is unchanged since last run.
158
+ const rawAgentsMdProject = readAgentsMd(join(project.root, "AGENTS.md"));
159
+ const rawAgentsMdGlobal = readAgentsMd(join(homedir(), ".pi", "agent", "AGENTS.md"));
160
+ const projectMdHash = rawAgentsMdProject ? hashContent(rawAgentsMdProject) : null;
161
+ const globalMdHash = rawAgentsMdGlobal ? hashContent(rawAgentsMdGlobal) : null;
162
+ const agentsMdProject = rawAgentsMdProject && projectMdHash !== meta.agents_md_project_hash
163
+ ? rawAgentsMdProject
164
+ : null;
165
+ const agentsMdGlobal = rawAgentsMdGlobal && globalMdHash !== meta.agents_md_global_hash
166
+ ? rawAgentsMdGlobal
167
+ : null;
135
168
  let installedSkills = [];
136
169
  try {
137
170
  const { loadSkills } = await import("@mariozechner/pi-coding-agent");
@@ -144,9 +177,39 @@ async function analyzeProject(project, config, baseDir, logger) {
144
177
  catch {
145
178
  // Skills loading is best-effort - continue without them
146
179
  }
147
- const userPrompt = buildSingleShotUserPrompt(project, allInstincts, newObsLines, {
148
- agentsMdProject,
149
- agentsMdGlobal,
180
+ let promptObsLines = newObsLines;
181
+ let promptAgentsMdProject = agentsMdProject;
182
+ let promptAgentsMdGlobal = agentsMdGlobal;
183
+ const userPrompt = buildSingleShotUserPrompt(project, allInstincts, promptObsLines, {
184
+ agentsMdProject: promptAgentsMdProject,
185
+ agentsMdGlobal: promptAgentsMdGlobal,
186
+ installedSkills,
187
+ });
188
+ // Estimate token budget and apply fallbacks if over limit.
189
+ const systemPromptTokens = estimateTokens(buildSingleShotSystemPrompt());
190
+ let estimatedTotal = systemPromptTokens + estimateTokens(userPrompt);
191
+ if (estimatedTotal > PROMPT_TOKEN_BUDGET) {
192
+ logger.warn(`Prompt over budget (${estimatedTotal} est. tokens > ${PROMPT_TOKEN_BUDGET}). Applying fallbacks.`);
193
+ // Fallback 1: truncate AGENTS.md to headers only.
194
+ if (promptAgentsMdProject) {
195
+ promptAgentsMdProject = truncateAgentsMdToHeaders(promptAgentsMdProject);
196
+ }
197
+ if (promptAgentsMdGlobal) {
198
+ promptAgentsMdGlobal = truncateAgentsMdToHeaders(promptAgentsMdGlobal);
199
+ }
200
+ // Fallback 2: reduce observation lines to fit budget.
201
+ // Use binary-search-like reduction: keep halving until under budget.
202
+ while (promptObsLines.length > 1) {
203
+ const trimmedPrompt = buildSingleShotUserPrompt(project, allInstincts, promptObsLines, { agentsMdProject: promptAgentsMdProject, agentsMdGlobal: promptAgentsMdGlobal, installedSkills });
204
+ estimatedTotal = systemPromptTokens + estimateTokens(trimmedPrompt);
205
+ if (estimatedTotal <= PROMPT_TOKEN_BUDGET)
206
+ break;
207
+ promptObsLines = promptObsLines.slice(Math.floor(promptObsLines.length / 2));
208
+ }
209
+ }
210
+ const finalUserPrompt = buildSingleShotUserPrompt(project, allInstincts, promptObsLines, {
211
+ agentsMdProject: promptAgentsMdProject,
212
+ agentsMdGlobal: promptAgentsMdGlobal,
150
213
  installedSkills,
151
214
  });
152
215
  const authStorage = AuthStorage.create();
@@ -159,13 +222,16 @@ async function analyzeProject(project, config, baseDir, logger) {
159
222
  const context = {
160
223
  systemPrompt: buildSingleShotSystemPrompt(),
161
224
  messages: [
162
- { role: "user", content: userPrompt, timestamp: Date.now() },
225
+ { role: "user", content: finalUserPrompt, timestamp: Date.now() },
163
226
  ],
164
227
  };
165
228
  const timeoutMs = (config.timeout_seconds ?? DEFAULT_CONFIG.timeout_seconds) * 1000;
166
229
  const abortController = new AbortController();
167
230
  const timeoutHandle = setTimeout(() => abortController.abort(), timeoutMs);
168
231
  const instinctCounts = { created: 0, updated: 0, deleted: 0 };
232
+ const createdSummaries = [];
233
+ const updatedSummaries = [];
234
+ const deletedSummaries = [];
169
235
  const projectInstinctsDir = getProjectInstinctsDir(project.id, "personal", baseDir);
170
236
  const globalInstinctsDir = getGlobalInstinctsDir("personal", baseDir);
171
237
  let singleShotMessage;
@@ -185,6 +251,11 @@ async function analyzeProject(project, config, baseDir, logger) {
185
251
  if (existsSync(filePath)) {
186
252
  unlinkSync(filePath);
187
253
  instinctCounts.deleted++;
254
+ deletedSummaries.push({
255
+ id,
256
+ title: id,
257
+ scope: change.scope ?? "project",
258
+ });
188
259
  }
189
260
  }
190
261
  else if (change.action === "create") {
@@ -198,6 +269,13 @@ async function analyzeProject(project, config, baseDir, logger) {
198
269
  saveInstinct(instinct, dir);
199
270
  instinctCounts.created++;
200
271
  createsRemaining--;
272
+ createdSummaries.push({
273
+ id: instinct.id,
274
+ title: instinct.title,
275
+ scope: instinct.scope,
276
+ trigger: instinct.trigger,
277
+ action: instinct.action,
278
+ });
201
279
  }
202
280
  else {
203
281
  // update
@@ -208,6 +286,15 @@ async function analyzeProject(project, config, baseDir, logger) {
208
286
  const dir = instinct.scope === "global" ? globalInstinctsDir : projectInstinctsDir;
209
287
  saveInstinct(instinct, dir);
210
288
  instinctCounts.updated++;
289
+ const delta = existing
290
+ ? instinct.confidence - existing.confidence
291
+ : undefined;
292
+ updatedSummaries.push({
293
+ id: instinct.id,
294
+ title: instinct.title,
295
+ scope: instinct.scope,
296
+ ...(delta !== undefined ? { confidence_delta: delta } : {}),
297
+ });
211
298
  }
212
299
  }
213
300
  }
@@ -234,7 +321,24 @@ async function analyzeProject(project, config, baseDir, logger) {
234
321
  model: modelId,
235
322
  };
236
323
  logger.projectComplete(stats);
237
- saveProjectMeta(project.id, { ...meta, last_analyzed_at: new Date().toISOString(), last_observation_line_count: totalLineCount }, baseDir);
324
+ // Write analysis event for extension notification
325
+ const analysisEvent = {
326
+ timestamp: new Date().toISOString(),
327
+ project_id: project.id,
328
+ project_name: project.name,
329
+ created: createdSummaries,
330
+ updated: updatedSummaries,
331
+ deleted: deletedSummaries,
332
+ };
333
+ appendAnalysisEvent(analysisEvent, baseDir);
334
+ saveProjectMeta(project.id, {
335
+ ...meta,
336
+ last_analyzed_at: new Date().toISOString(),
337
+ last_observation_line_count: totalLineCount,
338
+ // Update AGENTS.md hashes only when the content was actually sent.
339
+ ...(agentsMdProject && projectMdHash ? { agents_md_project_hash: projectMdHash } : {}),
340
+ ...(agentsMdGlobal && globalMdHash ? { agents_md_global_hash: globalMdHash } : {}),
341
+ }, baseDir);
238
342
  return { ran: true, stats };
239
343
  }
240
344
  // ---------------------------------------------------------------------------