pi-continuous-learning 0.5.1 → 0.7.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 (89) hide show
  1. package/README.md +78 -0
  2. package/dist/agents-md.d.ts +23 -2
  3. package/dist/agents-md.d.ts.map +1 -1
  4. package/dist/agents-md.js +58 -3
  5. package/dist/agents-md.js.map +1 -1
  6. package/dist/cli/analyze-single-shot.d.ts +62 -0
  7. package/dist/cli/analyze-single-shot.d.ts.map +1 -0
  8. package/dist/cli/analyze-single-shot.js +105 -0
  9. package/dist/cli/analyze-single-shot.js.map +1 -0
  10. package/dist/cli/analyze.js +82 -81
  11. package/dist/cli/analyze.js.map +1 -1
  12. package/dist/command-scaffold.d.ts +25 -0
  13. package/dist/command-scaffold.d.ts.map +1 -0
  14. package/dist/command-scaffold.js +77 -0
  15. package/dist/command-scaffold.js.map +1 -0
  16. package/dist/confidence.d.ts.map +1 -1
  17. package/dist/confidence.js +2 -1
  18. package/dist/confidence.js.map +1 -1
  19. package/dist/config.d.ts +16 -0
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +31 -0
  22. package/dist/config.js.map +1 -1
  23. package/dist/graduation.d.ts +63 -0
  24. package/dist/graduation.d.ts.map +1 -0
  25. package/dist/graduation.js +155 -0
  26. package/dist/graduation.js.map +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +5 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/instinct-cleanup.d.ts +57 -0
  31. package/dist/instinct-cleanup.d.ts.map +1 -0
  32. package/dist/instinct-cleanup.js +150 -0
  33. package/dist/instinct-cleanup.js.map +1 -0
  34. package/dist/instinct-graduate.d.ts +43 -0
  35. package/dist/instinct-graduate.d.ts.map +1 -0
  36. package/dist/instinct-graduate.js +253 -0
  37. package/dist/instinct-graduate.js.map +1 -0
  38. package/dist/instinct-parser.d.ts.map +1 -1
  39. package/dist/instinct-parser.js +12 -0
  40. package/dist/instinct-parser.js.map +1 -1
  41. package/dist/instinct-tools.d.ts.map +1 -1
  42. package/dist/instinct-tools.js +19 -0
  43. package/dist/instinct-tools.js.map +1 -1
  44. package/dist/instinct-validator.d.ts +61 -0
  45. package/dist/instinct-validator.d.ts.map +1 -0
  46. package/dist/instinct-validator.js +235 -0
  47. package/dist/instinct-validator.js.map +1 -0
  48. package/dist/observation-preprocessor.d.ts +26 -0
  49. package/dist/observation-preprocessor.d.ts.map +1 -0
  50. package/dist/observation-preprocessor.js +31 -0
  51. package/dist/observation-preprocessor.js.map +1 -0
  52. package/dist/prompts/analyzer-system-single-shot.d.ts +6 -0
  53. package/dist/prompts/analyzer-system-single-shot.d.ts.map +1 -0
  54. package/dist/prompts/analyzer-system-single-shot.js +164 -0
  55. package/dist/prompts/analyzer-system-single-shot.js.map +1 -0
  56. package/dist/prompts/analyzer-user-single-shot.d.ts +22 -0
  57. package/dist/prompts/analyzer-user-single-shot.d.ts.map +1 -0
  58. package/dist/prompts/analyzer-user-single-shot.js +53 -0
  59. package/dist/prompts/analyzer-user-single-shot.js.map +1 -0
  60. package/dist/prompts/analyzer-user.d.ts +3 -1
  61. package/dist/prompts/analyzer-user.d.ts.map +1 -1
  62. package/dist/prompts/analyzer-user.js +20 -7
  63. package/dist/prompts/analyzer-user.js.map +1 -1
  64. package/dist/skill-scaffold.d.ts +23 -0
  65. package/dist/skill-scaffold.d.ts.map +1 -0
  66. package/dist/skill-scaffold.js +62 -0
  67. package/dist/skill-scaffold.js.map +1 -0
  68. package/dist/types.d.ts +8 -0
  69. package/dist/types.d.ts.map +1 -1
  70. package/package.json +1 -1
  71. package/src/agents-md.ts +73 -3
  72. package/src/cli/analyze-single-shot.ts +175 -0
  73. package/src/cli/analyze.ts +93 -124
  74. package/src/command-scaffold.ts +105 -0
  75. package/src/confidence.ts +2 -1
  76. package/src/config.ts +40 -0
  77. package/src/graduation.ts +243 -0
  78. package/src/index.ts +14 -0
  79. package/src/instinct-cleanup.ts +204 -0
  80. package/src/instinct-graduate.ts +377 -0
  81. package/src/instinct-parser.ts +12 -0
  82. package/src/instinct-tools.ts +26 -0
  83. package/src/instinct-validator.ts +287 -0
  84. package/src/observation-preprocessor.ts +48 -0
  85. package/src/prompts/analyzer-system-single-shot.ts +163 -0
  86. package/src/prompts/analyzer-user-single-shot.ts +94 -0
  87. package/src/prompts/analyzer-user.ts +26 -8
  88. package/src/skill-scaffold.ts +90 -0
  89. package/src/types.ts +10 -0
package/README.md CHANGED
@@ -57,6 +57,7 @@ To analyze observations and create/update instincts, you need to run the analyze
57
57
  | `/instinct-export` | Export instincts to a JSON file (filterable by scope/domain) |
58
58
  | `/instinct-import <path>` | Import instincts from a JSON file |
59
59
  | `/instinct-promote [id]` | Promote project instincts to global scope |
60
+ | `/instinct-graduate` | Graduate mature instincts to AGENTS.md, skills, or commands |
60
61
  | `/instinct-projects` | List all known projects and their instinct counts |
61
62
 
62
63
  ### LLM Tools
@@ -244,6 +245,46 @@ inactive_count: 12
244
245
  Always search with grep to find relevant context before editing files.
245
246
  ```
246
247
 
248
+ Graduated instincts include additional fields:
249
+
250
+ ```yaml
251
+ ---
252
+ id: grep-before-edit
253
+ # ...other fields...
254
+ graduated_to: agents-md
255
+ graduated_at: "2026-03-27T12:00:00.000Z"
256
+ ---
257
+ ```
258
+
259
+ ## Instinct Quality Control
260
+
261
+ Every instinct write (from the LLM tools or the background analyzer) is validated and deduplicated before being saved.
262
+
263
+ ### Content Validation
264
+
265
+ | Rule | Details |
266
+ |---|---|
267
+ | Non-empty fields | `action` and `trigger` cannot be `undefined`, `null`, `"null"`, `"none"`, or empty |
268
+ | Minimum length | Both fields must be >= 10 characters |
269
+ | Known domain | `domain` must be in the known set: `git`, `testing`, `debugging`, `workflow`, `typescript`, `javascript`, `python`, `go`, `css`, `design`, `security`, `performance`, `documentation`, `react`, `node`, `database`, `api`, `devops`, `architecture`, or `other` |
270
+ | Verb heuristic | `action` should start with an imperative verb - a warning is logged but the write is not rejected |
271
+
272
+ ### Semantic Deduplication
273
+
274
+ Before a new instinct is created, a Jaccard similarity check runs against all existing instincts. Tokenize `trigger + action`, compute `|intersection| / |union|`, and block the write if any existing instinct scores >= 0.6.
275
+
276
+ This prevents near-duplicate instincts from accumulating. When a similar instinct exists, the LLM is told to update the existing one instead.
277
+
278
+ ### Analyzer Quality Tiers
279
+
280
+ The background analyzer is instructed to classify patterns before recording them:
281
+
282
+ - **Project Conventions** (Tier 1): Project-specific patterns like "use Result<T,E> for errors in this codebase" → record as project-scoped instinct
283
+ - **Workflow Patterns** (Tier 2): Universal multi-step workflows → record as global-scoped instinct
284
+ - **Generic Agent Behavior** (Tier 3): Read-before-edit, clarify-before-implement, check-errors-after-tool-calls → **skip entirely**, these are fundamental behaviors not learned patterns
285
+
286
+ The analyzer also checks AGENTS.md content before creating instincts - if a pattern is already covered by AGENTS.md, it is skipped.
287
+
247
288
  ## Confidence Scoring
248
289
 
249
290
  Confidence comes from two sources:
@@ -263,6 +304,39 @@ Confidence comes from two sources:
263
304
 
264
305
  This means an instinct observed 20 times but consistently contradicted in practice will lose confidence. Frequency alone doesn't equal correctness.
265
306
 
307
+ ## Instinct Graduation
308
+
309
+ Instincts are designed to be short-lived - they should graduate into permanent knowledge within a few weeks. The graduation pipeline (`/instinct-graduate`) handles this lifecycle:
310
+
311
+ ```
312
+ Observation -> Instinct (days) -> AGENTS.md / Skill / Command (1-2 weeks)
313
+ ```
314
+
315
+ ### Graduation Targets
316
+
317
+ | Target | When | What happens |
318
+ |--------|------|--------------|
319
+ | **AGENTS.md** | Single mature instinct | Appended as a guideline entry to your project or global AGENTS.md |
320
+ | **Skill** | 3+ related instincts in the same domain | Scaffolded into a `SKILL.md` file |
321
+ | **Command** | 3+ workflow instincts in the same domain | Scaffolded into a slash command specification |
322
+
323
+ ### Maturity Criteria
324
+
325
+ An instinct qualifies for graduation when all of these are met:
326
+ - Age >= 7 days
327
+ - Confidence >= 0.75
328
+ - Confirmed >= 3 times
329
+ - Contradicted <= 1 time
330
+ - Not a duplicate of existing AGENTS.md content
331
+
332
+ ### TTL Enforcement
333
+
334
+ Instincts that don't graduate within 28 days are subject to TTL enforcement:
335
+ - **Confidence < 0.3**: Deleted outright
336
+ - **Confidence >= 0.3**: Aggressively decayed (confidence halved, flagged for removal)
337
+
338
+ Graduated instincts are tracked with `graduated_to` and `graduated_at` fields so they aren't left as duplicates of the knowledge they graduated into.
339
+
266
340
  ## Updating
267
341
 
268
342
  ```bash
@@ -296,12 +370,16 @@ Only include the fields you want to change — missing fields use the defaults a
296
370
 
297
371
  | Field | Default | Description |
298
372
  |-------|---------|-------------|
373
+ | `run_interval_minutes` | 5 | How often the analyzer is expected to run (informational, used for decay calculations) |
299
374
  | `min_observations_to_analyze` | 20 | Minimum observations before analysis triggers |
300
375
  | `min_confidence` | 0.5 | Instincts below this are not injected into prompts |
301
376
  | `max_instincts` | 20 | Maximum instincts injected per turn |
302
377
  | `max_injection_chars` | 4000 | Character budget for the injection block (~1000 tokens) |
303
378
  | `model` | `claude-haiku-4-5` | Model for the background analyzer (lightweight models recommended to minimize cost) |
304
379
  | `timeout_seconds` | 120 | Per-project timeout for the analyzer LLM session |
380
+ | `active_hours_start` | 8 | Hour (0-23) at which the active observation window starts |
381
+ | `active_hours_end` | 23 | Hour (0-23) at which the active observation window ends |
382
+ | `max_idle_seconds` | 1800 | Seconds of inactivity before a session is considered idle |
305
383
  | `log_path` | `~/.pi/continuous-learning/analyzer.log` | Path to the analyzer log file |
306
384
 
307
385
  ## Storage
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Utility for reading AGENTS.md files.
3
- * Provides a safe wrapper around filesystem access that returns null on any failure.
2
+ * Utility for reading and writing AGENTS.md files.
3
+ * Provides safe wrappers around filesystem access.
4
4
  */
5
+ import type { Instinct } from "./types.js";
5
6
  /**
6
7
  * Reads an AGENTS.md file and returns its content.
7
8
  * Returns null if the file does not exist or cannot be read.
@@ -9,4 +10,24 @@
9
10
  * @param filePath - Absolute path to the AGENTS.md file
10
11
  */
11
12
  export declare function readAgentsMd(filePath: string): string | null;
13
+ /**
14
+ * Formats an instinct as an AGENTS.md section entry.
15
+ * Produces a markdown block with the instinct title as heading and
16
+ * trigger/action as content.
17
+ */
18
+ export declare function formatInstinctAsAgentsMdEntry(instinct: Instinct): string;
19
+ /**
20
+ * Generates a complete AGENTS.md diff showing proposed additions.
21
+ * Returns the full new content that would result from appending the entries.
22
+ */
23
+ export declare function generateAgentsMdDiff(currentContent: string | null, instincts: Instinct[]): string;
24
+ /**
25
+ * Appends graduated instinct entries to an AGENTS.md file.
26
+ * Creates the file and parent directories if they don't exist.
27
+ *
28
+ * @param filePath - Absolute path to AGENTS.md
29
+ * @param instincts - Instincts to append as entries
30
+ * @returns The new file content that was written
31
+ */
32
+ export declare function appendToAgentsMd(filePath: string, instincts: Instinct[]): string;
12
33
  //# sourceMappingURL=agents-md.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agents-md.d.ts","sourceRoot":"","sources":["../src/agents-md.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS5D"}
1
+ {"version":3,"file":"agents-md.d.ts","sourceRoot":"","sources":["../src/agents-md.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS5D;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAUxE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,cAAc,EAAE,MAAM,GAAG,IAAI,EAC7B,SAAS,EAAE,QAAQ,EAAE,GACpB,MAAM,CAmBR;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,QAAQ,EAAE,GACpB,MAAM,CAUR"}
package/dist/agents-md.js CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Utility for reading AGENTS.md files.
3
- * Provides a safe wrapper around filesystem access that returns null on any failure.
2
+ * Utility for reading and writing AGENTS.md files.
3
+ * Provides safe wrappers around filesystem access.
4
4
  */
5
- import { existsSync, readFileSync } from "node:fs";
5
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
6
+ import { dirname } from "node:path";
6
7
  /**
7
8
  * Reads an AGENTS.md file and returns its content.
8
9
  * Returns null if the file does not exist or cannot be read.
@@ -20,4 +21,58 @@ export function readAgentsMd(filePath) {
20
21
  return null;
21
22
  }
22
23
  }
24
+ /**
25
+ * Formats an instinct as an AGENTS.md section entry.
26
+ * Produces a markdown block with the instinct title as heading and
27
+ * trigger/action as content.
28
+ */
29
+ export function formatInstinctAsAgentsMdEntry(instinct) {
30
+ const lines = [
31
+ `### ${instinct.title}`,
32
+ "",
33
+ `**When:** ${instinct.trigger}`,
34
+ "",
35
+ instinct.action,
36
+ "",
37
+ ];
38
+ return lines.join("\n");
39
+ }
40
+ /**
41
+ * Generates a complete AGENTS.md diff showing proposed additions.
42
+ * Returns the full new content that would result from appending the entries.
43
+ */
44
+ export function generateAgentsMdDiff(currentContent, instincts) {
45
+ const entries = instincts.map(formatInstinctAsAgentsMdEntry);
46
+ const graduatedSection = [
47
+ "",
48
+ "## Graduated Instincts",
49
+ "",
50
+ ...entries,
51
+ ].join("\n");
52
+ if (currentContent === null || currentContent.trim().length === 0) {
53
+ return `# Project Guidelines\n${graduatedSection}\n`;
54
+ }
55
+ // If the section already exists, append to it; otherwise add a new section
56
+ if (currentContent.includes("## Graduated Instincts")) {
57
+ return `${currentContent.trimEnd()}\n\n${entries.join("\n")}\n`;
58
+ }
59
+ return `${currentContent.trimEnd()}\n${graduatedSection}\n`;
60
+ }
61
+ /**
62
+ * Appends graduated instinct entries to an AGENTS.md file.
63
+ * Creates the file and parent directories if they don't exist.
64
+ *
65
+ * @param filePath - Absolute path to AGENTS.md
66
+ * @param instincts - Instincts to append as entries
67
+ * @returns The new file content that was written
68
+ */
69
+ export function appendToAgentsMd(filePath, instincts) {
70
+ if (instincts.length === 0)
71
+ return readAgentsMd(filePath) ?? "";
72
+ const currentContent = readAgentsMd(filePath);
73
+ const newContent = generateAgentsMdDiff(currentContent, instincts);
74
+ mkdirSync(dirname(filePath), { recursive: true });
75
+ writeFileSync(filePath, newContent, "utf-8");
76
+ return newContent;
77
+ }
23
78
  //# sourceMappingURL=agents-md.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"agents-md.js","sourceRoot":"","sources":["../src/agents-md.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"agents-md.js","sourceRoot":"","sources":["../src/agents-md.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAAC,QAAkB;IAC9D,MAAM,KAAK,GAAG;QACZ,OAAO,QAAQ,CAAC,KAAK,EAAE;QACvB,EAAE;QACF,aAAa,QAAQ,CAAC,OAAO,EAAE;QAC/B,EAAE;QACF,QAAQ,CAAC,MAAM;QACf,EAAE;KACH,CAAC;IACF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,cAA6B,EAC7B,SAAqB;IAErB,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC;IAC7D,MAAM,gBAAgB,GAAG;QACvB,EAAE;QACF,wBAAwB;QACxB,EAAE;QACF,GAAG,OAAO;KACX,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEb,IAAI,cAAc,KAAK,IAAI,IAAI,cAAc,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClE,OAAO,yBAAyB,gBAAgB,IAAI,CAAC;IACvD,CAAC;IAED,2EAA2E;IAC3E,IAAI,cAAc,CAAC,QAAQ,CAAC,wBAAwB,CAAC,EAAE,CAAC;QACtD,OAAO,GAAG,cAAc,CAAC,OAAO,EAAE,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;IAClE,CAAC;IAED,OAAO,GAAG,cAAc,CAAC,OAAO,EAAE,KAAK,gBAAgB,IAAI,CAAC;AAC9D,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAAgB,EAChB,SAAqB;IAErB,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,YAAY,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;IAEhE,MAAM,cAAc,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC9C,MAAM,UAAU,GAAG,oBAAoB,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;IAEnE,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,aAAa,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;IAE7C,OAAO,UAAU,CAAC;AACpB,CAAC"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Single-shot (non-agentic) analyzer core.
3
+ *
4
+ * Replaces the multi-turn agentic session with a single complete() call.
5
+ * The model receives all current instincts inline and returns a JSON change-set.
6
+ * Changes are applied client-side, eliminating the ~16x cache-read multiplier.
7
+ */
8
+ import type { AssistantMessage, Context } from "@mariozechner/pi-ai";
9
+ import { complete } from "@mariozechner/pi-ai";
10
+ import type { Instinct } from "../types.js";
11
+ export interface InstinctChangePayload {
12
+ id: string;
13
+ title: string;
14
+ trigger: string;
15
+ action: string;
16
+ confidence: number;
17
+ domain: string;
18
+ scope: "project" | "global";
19
+ observation_count?: number;
20
+ confirmed_count?: number;
21
+ contradicted_count?: number;
22
+ inactive_count?: number;
23
+ evidence?: string[];
24
+ }
25
+ export interface InstinctChange {
26
+ action: "create" | "update" | "delete";
27
+ instinct?: InstinctChangePayload;
28
+ /** For delete: the instinct ID to remove. */
29
+ id?: string;
30
+ /** For delete: the scope to target. */
31
+ scope?: "project" | "global";
32
+ }
33
+ export interface SingleShotResult {
34
+ changes: InstinctChange[];
35
+ message: AssistantMessage;
36
+ }
37
+ /**
38
+ * Parses the model's raw text response into an array of InstinctChange.
39
+ * Strips markdown code fences if present. Throws on invalid JSON or schema.
40
+ */
41
+ export declare function parseChanges(raw: string): InstinctChange[];
42
+ /**
43
+ * Builds a full Instinct from a create/update change.
44
+ * Returns null for delete changes, changes with missing instinct data,
45
+ * invalid fields, or semantically duplicate actions.
46
+ *
47
+ * @param change - The change to apply
48
+ * @param existing - The existing instinct with this ID, if any
49
+ * @param projectId - Project ID for scoping
50
+ * @param allInstincts - All current instincts, used for dedup check on creates
51
+ */
52
+ export declare function buildInstinctFromChange(change: InstinctChange, existing: Instinct | null, projectId: string, allInstincts?: Instinct[]): Instinct | null;
53
+ /**
54
+ * Formats existing instincts as serialized markdown blocks for inline context.
55
+ */
56
+ export declare function formatInstinctsForPrompt(instincts: Instinct[]): string;
57
+ /**
58
+ * Runs a single complete() call with the provided context.
59
+ * Returns parsed changes and the raw AssistantMessage (for usage stats).
60
+ */
61
+ export declare function runSingleShot(context: Context, model: Parameters<typeof complete>[0], apiKey: string, signal?: AbortSignal): Promise<SingleShotResult>;
62
+ //# sourceMappingURL=analyze-single-shot.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,105 @@
1
+ import { complete } from "@mariozechner/pi-ai";
2
+ import { serializeInstinct } from "../instinct-parser.js";
3
+ import { validateInstinct, findSimilarInstinct } from "../instinct-validator.js";
4
+ /**
5
+ * Parses the model's raw text response into an array of InstinctChange.
6
+ * Strips markdown code fences if present. Throws on invalid JSON or schema.
7
+ */
8
+ export function parseChanges(raw) {
9
+ const stripped = raw
10
+ .replace(/^```(?:json)?\s*/i, "")
11
+ .replace(/\s*```\s*$/, "")
12
+ .trim();
13
+ let parsed;
14
+ try {
15
+ parsed = JSON.parse(stripped);
16
+ }
17
+ catch (e) {
18
+ throw new Error(`Analyzer returned invalid JSON: ${String(e)}\nRaw: ${raw.slice(0, 200)}`);
19
+ }
20
+ if (typeof parsed !== "object" ||
21
+ parsed === null ||
22
+ !Array.isArray(parsed.changes)) {
23
+ throw new Error(`Analyzer response missing 'changes' array. Got: ${JSON.stringify(parsed).slice(0, 200)}`);
24
+ }
25
+ return parsed.changes;
26
+ }
27
+ /**
28
+ * Builds a full Instinct from a create/update change.
29
+ * Returns null for delete changes, changes with missing instinct data,
30
+ * invalid fields, or semantically duplicate actions.
31
+ *
32
+ * @param change - The change to apply
33
+ * @param existing - The existing instinct with this ID, if any
34
+ * @param projectId - Project ID for scoping
35
+ * @param allInstincts - All current instincts, used for dedup check on creates
36
+ */
37
+ export function buildInstinctFromChange(change, existing, projectId, allInstincts = []) {
38
+ if (change.action === "delete" || !change.instinct) {
39
+ return null;
40
+ }
41
+ const payload = change.instinct;
42
+ const validation = validateInstinct({
43
+ action: payload.action,
44
+ trigger: payload.trigger,
45
+ domain: payload.domain,
46
+ });
47
+ if (!validation.valid) {
48
+ return null;
49
+ }
50
+ // On create, reject if semantically similar to an existing instinct (skip self on update)
51
+ if (change.action === "create") {
52
+ const similar = findSimilarInstinct({ trigger: payload.trigger, action: payload.action }, allInstincts, payload.id);
53
+ if (similar) {
54
+ return null;
55
+ }
56
+ }
57
+ const now = new Date().toISOString();
58
+ return {
59
+ id: payload.id,
60
+ title: payload.title,
61
+ trigger: payload.trigger,
62
+ action: payload.action,
63
+ confidence: Math.max(0.1, Math.min(0.9, payload.confidence)),
64
+ domain: payload.domain,
65
+ scope: payload.scope,
66
+ source: "personal",
67
+ ...(payload.scope === "project" ? { project_id: projectId } : {}),
68
+ created_at: existing?.created_at ?? now,
69
+ updated_at: now,
70
+ observation_count: payload.observation_count ?? 1,
71
+ confirmed_count: payload.confirmed_count ?? 0,
72
+ contradicted_count: payload.contradicted_count ?? 0,
73
+ inactive_count: payload.inactive_count ?? 0,
74
+ ...(payload.evidence !== undefined ? { evidence: payload.evidence } : {}),
75
+ };
76
+ }
77
+ /**
78
+ * Formats existing instincts as serialized markdown blocks for inline context.
79
+ */
80
+ export function formatInstinctsForPrompt(instincts) {
81
+ if (instincts.length === 0) {
82
+ return "(no existing instincts)";
83
+ }
84
+ return instincts.map((i) => serializeInstinct(i)).join("\n---\n");
85
+ }
86
+ /**
87
+ * Runs a single complete() call with the provided context.
88
+ * Returns parsed changes and the raw AssistantMessage (for usage stats).
89
+ */
90
+ export async function runSingleShot(context, model, apiKey, signal) {
91
+ const opts = { apiKey };
92
+ if (signal !== undefined)
93
+ opts.signal = signal;
94
+ const message = await complete(model, context, opts);
95
+ const textContent = message.content
96
+ .filter((c) => c.type === "text")
97
+ .map((c) => c.text)
98
+ .join("");
99
+ if (!textContent.trim()) {
100
+ throw new Error("Analyzer returned empty response");
101
+ }
102
+ const changes = parseChanges(textContent);
103
+ return { changes, message };
104
+ }
105
+ //# sourceMappingURL=analyze-single-shot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze-single-shot.js","sourceRoot":"","sources":["../../src/cli/analyze-single-shot.ts"],"names":[],"mappings":"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"}
@@ -1,15 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, readFileSync, statSync, writeFileSync, unlinkSync, } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { createAgentSession, SessionManager, AuthStorage, ModelRegistry, DefaultResourceLoader, } from "@mariozechner/pi-coding-agent";
4
+ import { AuthStorage } from "@mariozechner/pi-coding-agent";
5
5
  import { getModel } from "@mariozechner/pi-ai";
6
6
  import { loadConfig, DEFAULT_CONFIG } from "../config.js";
7
- import { getBaseDir, getProjectsRegistryPath, getObservationsPath, getProjectDir, } from "../storage.js";
7
+ import { getBaseDir, getProjectsRegistryPath, getObservationsPath, getProjectDir, getProjectInstinctsDir, getGlobalInstinctsDir, } from "../storage.js";
8
8
  import { countObservations } from "../observations.js";
9
9
  import { runDecayPass } from "../instinct-decay.js";
10
- import { buildAnalyzerUserPrompt, tailObservationsSince } from "../prompts/analyzer-user.js";
11
- import { buildAnalyzerSystemPrompt } from "./analyze-prompt.js";
12
- import { createInstinctListTool, createInstinctReadTool, createInstinctWriteTool, createInstinctDeleteTool, } from "../instinct-tools.js";
10
+ import { runCleanupPass } from "../instinct-cleanup.js";
11
+ import { tailObservationsSince } from "../prompts/analyzer-user.js";
12
+ import { buildSingleShotSystemPrompt } from "../prompts/analyzer-system-single-shot.js";
13
+ import { buildSingleShotUserPrompt } from "../prompts/analyzer-user-single-shot.js";
14
+ import { runSingleShot, buildInstinctFromChange, } from "./analyze-single-shot.js";
15
+ import { loadProjectInstincts, loadGlobalInstincts, saveInstinct, } from "../instinct-store.js";
13
16
  import { readAgentsMd } from "../agents-md.js";
14
17
  import { homedir } from "node:os";
15
18
  import { AnalyzeLogger } from "./analyze-logger.js";
@@ -28,7 +31,6 @@ function acquireLock(baseDir) {
28
31
  const content = readFileSync(lockPath, "utf-8");
29
32
  const lock = JSON.parse(content);
30
33
  const age = Date.now() - new Date(lock.started_at).getTime();
31
- // Check if the owning process is still alive
32
34
  try {
33
35
  process.kill(lock.pid, 0); // signal 0 = existence check, no actual signal
34
36
  if (age < LOCK_STALE_MS) {
@@ -67,42 +69,6 @@ function startGlobalTimeout(timeoutMs, logger) {
67
69
  process.exit(2);
68
70
  }, timeoutMs).unref();
69
71
  }
70
- /**
71
- * Wraps instinct tools to count create/update/delete operations.
72
- * Returns new tool instances that increment the provided counts.
73
- */
74
- function wrapInstinctToolsWithTracking(projectId, projectName, baseDir, counts) {
75
- const writeTool = createInstinctWriteTool(projectId, projectName, baseDir);
76
- const deleteTool = createInstinctDeleteTool(projectId, baseDir);
77
- const trackedWrite = {
78
- ...writeTool,
79
- async execute(toolCallId, params, signal, onUpdate, ctx) {
80
- const result = await writeTool.execute(toolCallId, params, signal, onUpdate, ctx);
81
- const details = result.details;
82
- if (details?.action === "created") {
83
- counts.created++;
84
- }
85
- else {
86
- counts.updated++;
87
- }
88
- return result;
89
- },
90
- };
91
- const trackedDelete = {
92
- ...deleteTool,
93
- async execute(toolCallId, params, signal, onUpdate, ctx) {
94
- const result = await deleteTool.execute(toolCallId, params, signal, onUpdate, ctx);
95
- counts.deleted++;
96
- return result;
97
- },
98
- };
99
- return {
100
- listTool: createInstinctListTool(projectId, baseDir),
101
- readTool: createInstinctReadTool(projectId, baseDir),
102
- writeTool: trackedWrite,
103
- deleteTool: trackedDelete,
104
- };
105
- }
106
72
  function loadProjectsRegistry(baseDir) {
107
73
  const path = getProjectsRegistryPath(baseDir);
108
74
  if (!existsSync(path))
@@ -148,18 +114,22 @@ async function analyzeProject(project, config, baseDir, logger) {
148
114
  }
149
115
  const obsPath = getObservationsPath(project.id, baseDir);
150
116
  const sinceLineCount = meta.last_observation_line_count ?? 0;
151
- const { lines: newObsLines, totalLineCount } = tailObservationsSince(obsPath, sinceLineCount);
117
+ const { lines: newObsLines, totalLineCount, rawLineCount } = tailObservationsSince(obsPath, sinceLineCount);
152
118
  if (newObsLines.length === 0) {
153
- return { ran: false, skippedReason: "no new observation lines" };
119
+ return { ran: false, skippedReason: "no new observation lines after preprocessing" };
154
120
  }
155
121
  const obsCount = countObservations(project.id, baseDir);
156
122
  if (obsCount < config.min_observations_to_analyze) {
157
123
  return { ran: false, skippedReason: `below threshold (${obsCount}/${config.min_observations_to_analyze})` };
158
124
  }
159
125
  const startTime = Date.now();
160
- logger.projectStart(project.id, project.name, newObsLines.length, obsCount);
126
+ logger.projectStart(project.id, project.name, rawLineCount, obsCount);
127
+ runCleanupPass(project.id, config, baseDir);
161
128
  runDecayPass(project.id, baseDir);
162
- const instinctsDir = join(getProjectDir(project.id, baseDir), "instincts", "personal");
129
+ // Load current instincts inline - no tool calls needed
130
+ const projectInstincts = loadProjectInstincts(project.id, baseDir);
131
+ const globalInstincts = loadGlobalInstincts(baseDir);
132
+ const allInstincts = [...projectInstincts, ...globalInstincts];
163
133
  const agentsMdProject = readAgentsMd(join(project.root, "AGENTS.md"));
164
134
  const agentsMdGlobal = readAgentsMd(join(homedir(), ".pi", "agent", "AGENTS.md"));
165
135
  let installedSkills = [];
@@ -174,61 +144,93 @@ async function analyzeProject(project, config, baseDir, logger) {
174
144
  catch {
175
145
  // Skills loading is best-effort - continue without them
176
146
  }
177
- const userPrompt = buildAnalyzerUserPrompt(obsPath, instinctsDir, project, {
147
+ const userPrompt = buildSingleShotUserPrompt(project, allInstincts, newObsLines, {
178
148
  agentsMdProject,
179
149
  agentsMdGlobal,
180
150
  installedSkills,
181
- observationLines: newObsLines,
182
151
  });
183
152
  const authStorage = AuthStorage.create();
184
- const modelRegistry = new ModelRegistry(authStorage);
185
153
  const modelId = (config.model || DEFAULT_CONFIG.model);
186
154
  const model = getModel("anthropic", modelId);
187
- // Track instinct operations
155
+ const apiKey = await authStorage.getApiKey("anthropic");
156
+ if (!apiKey) {
157
+ throw new Error("No Anthropic API key configured. Set via auth.json or ANTHROPIC_API_KEY.");
158
+ }
159
+ const context = {
160
+ systemPrompt: buildSingleShotSystemPrompt(),
161
+ messages: [
162
+ { role: "user", content: userPrompt, timestamp: Date.now() },
163
+ ],
164
+ };
165
+ const timeoutMs = (config.timeout_seconds ?? DEFAULT_CONFIG.timeout_seconds) * 1000;
166
+ const abortController = new AbortController();
167
+ const timeoutHandle = setTimeout(() => abortController.abort(), timeoutMs);
188
168
  const instinctCounts = { created: 0, updated: 0, deleted: 0 };
189
- const trackedTools = wrapInstinctToolsWithTracking(project.id, project.name, baseDir, instinctCounts);
190
- const customTools = [
191
- trackedTools.listTool,
192
- trackedTools.readTool,
193
- trackedTools.writeTool,
194
- trackedTools.deleteTool,
195
- ];
196
- const loader = new DefaultResourceLoader({
197
- systemPromptOverride: () => buildAnalyzerSystemPrompt(),
198
- });
199
- await loader.reload();
200
- const { session } = await createAgentSession({
201
- model,
202
- authStorage,
203
- modelRegistry,
204
- sessionManager: SessionManager.inMemory(),
205
- customTools,
206
- resourceLoader: loader,
207
- });
169
+ const projectInstinctsDir = getProjectInstinctsDir(project.id, "personal", baseDir);
170
+ const globalInstinctsDir = getGlobalInstinctsDir("personal", baseDir);
171
+ let singleShotMessage;
208
172
  try {
209
- await session.prompt(userPrompt);
173
+ const result = await runSingleShot(context, model, apiKey, abortController.signal);
174
+ singleShotMessage = result.message;
175
+ // Enforce creation rate limit: only the first N create actions per run are applied.
176
+ const maxNewInstincts = config.max_new_instincts_per_run ?? DEFAULT_CONFIG.max_new_instincts_per_run;
177
+ let createsRemaining = maxNewInstincts;
178
+ for (const change of result.changes) {
179
+ if (change.action === "delete") {
180
+ const id = change.id;
181
+ if (!id)
182
+ continue;
183
+ const dir = change.scope === "global" ? globalInstinctsDir : projectInstinctsDir;
184
+ const filePath = join(dir, `${id}.md`);
185
+ if (existsSync(filePath)) {
186
+ unlinkSync(filePath);
187
+ instinctCounts.deleted++;
188
+ }
189
+ }
190
+ else if (change.action === "create") {
191
+ if (createsRemaining <= 0)
192
+ continue; // rate limit reached
193
+ const existing = allInstincts.find((i) => i.id === change.instinct?.id) ?? null;
194
+ const instinct = buildInstinctFromChange(change, existing, project.id, allInstincts);
195
+ if (!instinct)
196
+ continue;
197
+ const dir = instinct.scope === "global" ? globalInstinctsDir : projectInstinctsDir;
198
+ saveInstinct(instinct, dir);
199
+ instinctCounts.created++;
200
+ createsRemaining--;
201
+ }
202
+ else {
203
+ // update
204
+ const existing = allInstincts.find((i) => i.id === change.instinct?.id) ?? null;
205
+ const instinct = buildInstinctFromChange(change, existing, project.id, allInstincts);
206
+ if (!instinct)
207
+ continue;
208
+ const dir = instinct.scope === "global" ? globalInstinctsDir : projectInstinctsDir;
209
+ saveInstinct(instinct, dir);
210
+ instinctCounts.updated++;
211
+ }
212
+ }
210
213
  }
211
214
  finally {
212
- session.dispose();
215
+ clearTimeout(timeoutHandle);
213
216
  }
214
- // Collect stats after session completes
215
- const sessionStats = session.getSessionStats();
217
+ const usage = singleShotMessage.usage;
216
218
  const durationMs = Date.now() - startTime;
217
219
  const stats = {
218
220
  project_id: project.id,
219
221
  project_name: project.name,
220
222
  duration_ms: durationMs,
221
- observations_processed: newObsLines.length,
223
+ observations_processed: rawLineCount,
222
224
  observations_total: obsCount,
223
225
  instincts_created: instinctCounts.created,
224
226
  instincts_updated: instinctCounts.updated,
225
227
  instincts_deleted: instinctCounts.deleted,
226
- tokens_input: sessionStats.tokens.input,
227
- tokens_output: sessionStats.tokens.output,
228
- tokens_cache_read: sessionStats.tokens.cacheRead,
229
- tokens_cache_write: sessionStats.tokens.cacheWrite,
230
- tokens_total: sessionStats.tokens.total,
231
- cost_usd: sessionStats.cost,
228
+ tokens_input: usage.input,
229
+ tokens_output: usage.output,
230
+ tokens_cache_read: usage.cacheRead,
231
+ tokens_cache_write: usage.cacheWrite,
232
+ tokens_total: usage.totalTokens,
233
+ cost_usd: usage.cost.total,
232
234
  model: modelId,
233
235
  };
234
236
  logger.projectComplete(stats);
@@ -300,7 +302,6 @@ async function main() {
300
302
  }
301
303
  main().catch((err) => {
302
304
  releaseLock(getBaseDir());
303
- // Last-resort logging - config may not have loaded
304
305
  const logger = new AnalyzeLogger();
305
306
  logger.error("Fatal error", err);
306
307
  process.exit(1);