pi-orchestrator-config 4.1.5

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 (175) hide show
  1. package/README.md +808 -0
  2. package/agents/api-documenter.md +32 -0
  3. package/agents/bash-expert.md +53 -0
  4. package/agents/code-reviewer-docs.md +145 -0
  5. package/agents/code-reviewer-guidelines.md +97 -0
  6. package/agents/code-reviewer-quality.md +142 -0
  7. package/agents/code-reviewer-security.md +107 -0
  8. package/agents/code-reviewer-spec.md +164 -0
  9. package/agents/debugger.md +63 -0
  10. package/agents/docker-expert.md +40 -0
  11. package/agents/docs-fetcher.md +48 -0
  12. package/agents/git-expert.md +63 -0
  13. package/agents/github-expert.md +83 -0
  14. package/agents/go-expert.md +62 -0
  15. package/agents/issue-reviewer-feasibility.md +60 -0
  16. package/agents/issue-reviewer-scope.md +63 -0
  17. package/agents/issue-reviewer-spec.md +77 -0
  18. package/agents/java-expert.md +31 -0
  19. package/agents/jenkins-expert.md +38 -0
  20. package/agents/kubernetes-expert.md +40 -0
  21. package/agents/planner.md +42 -0
  22. package/agents/python-expert.md +61 -0
  23. package/agents/reviewer.md +26 -0
  24. package/agents/scout.md +40 -0
  25. package/agents/security-auditor.md +254 -0
  26. package/agents/technical-documentation-writer.md +37 -0
  27. package/agents/test-automator.md +55 -0
  28. package/agents/test-runner.md +47 -0
  29. package/agents/ts-expert.md +23 -0
  30. package/agents/worker.md +30 -0
  31. package/extensions/acpx-provider/index.ts +104 -0
  32. package/extensions/acpx-provider/load-runtime.ts +99 -0
  33. package/extensions/acpx-provider/runtime-models.ts +40 -0
  34. package/extensions/cli-provider/agents/claude.ts +128 -0
  35. package/extensions/cli-provider/agents/cursor.ts +84 -0
  36. package/extensions/cli-provider/agents/gemini.ts +100 -0
  37. package/extensions/cli-provider/discover.ts +77 -0
  38. package/extensions/cli-provider/index.ts +57 -0
  39. package/extensions/cli-provider/parsers.ts +304 -0
  40. package/extensions/cli-provider/providers.ts +58 -0
  41. package/extensions/cli-provider/runner.ts +225 -0
  42. package/extensions/cli-provider/runtime-models.ts +32 -0
  43. package/extensions/cli-provider/session-reaper.ts +122 -0
  44. package/extensions/cli-provider/sessions.ts +533 -0
  45. package/extensions/cli-provider/shared/discover-cache.ts +235 -0
  46. package/extensions/cli-provider/types.ts +48 -0
  47. package/extensions/coms/coms-net-server.ts +1642 -0
  48. package/extensions/coms/coms-net-wrapper.ts +521 -0
  49. package/extensions/coms/coms-net.ts +1589 -0
  50. package/extensions/coms/coms-p2p.ts +1879 -0
  51. package/extensions/coms/coms-shared.ts +510 -0
  52. package/extensions/coms/coms-wrapper.ts +138 -0
  53. package/extensions/coms/index.ts +20 -0
  54. package/extensions/coms/themeMap.ts +147 -0
  55. package/extensions/image-gen/image-gen.ts +332 -0
  56. package/extensions/image-gen/index.ts +14 -0
  57. package/extensions/orchestrator/agents.ts +134 -0
  58. package/extensions/orchestrator/ask-user.ts +293 -0
  59. package/extensions/orchestrator/async-agents.ts +1037 -0
  60. package/extensions/orchestrator/async-capability.ts +129 -0
  61. package/extensions/orchestrator/async-runner.ts +324 -0
  62. package/extensions/orchestrator/async-status-parse.ts +31 -0
  63. package/extensions/orchestrator/async-status-ui.ts +201 -0
  64. package/extensions/orchestrator/async-wait.ts +36 -0
  65. package/extensions/orchestrator/btw.ts +210 -0
  66. package/extensions/orchestrator/cron-status-format.ts +126 -0
  67. package/extensions/orchestrator/cron-status-ui.ts +132 -0
  68. package/extensions/orchestrator/cron.ts +730 -0
  69. package/extensions/orchestrator/dreaming.ts +355 -0
  70. package/extensions/orchestrator/enforcement-helpers.ts +530 -0
  71. package/extensions/orchestrator/enforcement-rules.ts +329 -0
  72. package/extensions/orchestrator/enforcement.ts +828 -0
  73. package/extensions/orchestrator/extended-autocomplete.ts +593 -0
  74. package/extensions/orchestrator/git-helpers.ts +420 -0
  75. package/extensions/orchestrator/github-autocomplete.ts +250 -0
  76. package/extensions/orchestrator/icons.ts +17 -0
  77. package/extensions/orchestrator/index.ts +100 -0
  78. package/extensions/orchestrator/memory-embeddings.ts +281 -0
  79. package/extensions/orchestrator/memory-promotion.ts +449 -0
  80. package/extensions/orchestrator/memory-provenance.ts +80 -0
  81. package/extensions/orchestrator/memory-query-class.ts +90 -0
  82. package/extensions/orchestrator/memory-scoring.ts +434 -0
  83. package/extensions/orchestrator/memory-tools.ts +883 -0
  84. package/extensions/orchestrator/memory-tree.ts +212 -0
  85. package/extensions/orchestrator/nvim.ts +169 -0
  86. package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
  87. package/extensions/orchestrator/overlay-dashboard.ts +512 -0
  88. package/extensions/orchestrator/pi-config-review-state.ts +303 -0
  89. package/extensions/orchestrator/preference-extractor.ts +106 -0
  90. package/extensions/orchestrator/project-settings.ts +468 -0
  91. package/extensions/orchestrator/promotion-queue.ts +311 -0
  92. package/extensions/orchestrator/resolve-agent-model.ts +32 -0
  93. package/extensions/orchestrator/review-ui.ts +204 -0
  94. package/extensions/orchestrator/rule-placeholders.ts +12 -0
  95. package/extensions/orchestrator/rules.ts +481 -0
  96. package/extensions/orchestrator/session-search.ts +243 -0
  97. package/extensions/orchestrator/session-validation.ts +374 -0
  98. package/extensions/orchestrator/situation-report.ts +370 -0
  99. package/extensions/orchestrator/status-bar.ts +31 -0
  100. package/extensions/orchestrator/status-line.ts +192 -0
  101. package/extensions/orchestrator/status.ts +110 -0
  102. package/extensions/orchestrator/subagent-tool.ts +1748 -0
  103. package/extensions/orchestrator/task-lifecycle.ts +88 -0
  104. package/extensions/orchestrator/utils.ts +227 -0
  105. package/extensions/pidash/index.ts +15 -0
  106. package/extensions/pidash/pidash.ts +941 -0
  107. package/extensions/pidiff/index.ts +13 -0
  108. package/extensions/pidiff/pidiff.ts +369 -0
  109. package/extensions/providers/acpx-driver.ts +479 -0
  110. package/extensions/providers/built-in-drivers.ts +52 -0
  111. package/extensions/providers/claude-driver.ts +319 -0
  112. package/extensions/providers/cursor-acpx-driver.ts +398 -0
  113. package/extensions/providers/cursor-cli-driver.ts +484 -0
  114. package/extensions/providers/gemini-driver.ts +302 -0
  115. package/extensions/providers/index.ts +513 -0
  116. package/extensions/shared/build-system-prompt.ts +57 -0
  117. package/extensions/shared/create-runtime-provider.ts +190 -0
  118. package/extensions/shared/daemon-manager.ts +248 -0
  119. package/extensions/shared/file-logger.ts +115 -0
  120. package/extensions/shared/managed-refresh.ts +211 -0
  121. package/extensions/shared/provider-driver.ts +295 -0
  122. package/extensions/shared/provider-errors.ts +305 -0
  123. package/extensions/shared/provider-registry.ts +388 -0
  124. package/extensions/shared/stream-builder.ts +275 -0
  125. package/extensions/shared/types.ts +20 -0
  126. package/extensions/shared/ui/README.md +10 -0
  127. package/extensions/shared/ui/badge.tsx +52 -0
  128. package/extensions/shared/ui/button.tsx +58 -0
  129. package/extensions/shared/ui/collapsible.tsx +21 -0
  130. package/extensions/shared/ui/dropdown-menu.tsx +266 -0
  131. package/extensions/shared/ui/input.tsx +20 -0
  132. package/extensions/shared/ui/popover.tsx +114 -0
  133. package/extensions/shared/ui/resizable.tsx +48 -0
  134. package/extensions/shared/ui/scroll-area.tsx +53 -0
  135. package/extensions/shared/ui/select.tsx +201 -0
  136. package/extensions/shared/ui/separator.tsx +25 -0
  137. package/extensions/shared/ui/tooltip.tsx +64 -0
  138. package/extensions/shared/ws-client.ts +74 -0
  139. package/package.json +59 -0
  140. package/prompts/coderabbit-rate-limit.md +95 -0
  141. package/prompts/create-coms-feature-manager.md +186 -0
  142. package/prompts/create-skill.md +128 -0
  143. package/prompts/domain-model.md +109 -0
  144. package/prompts/external-ai.md +665 -0
  145. package/prompts/implement-and-review.md +25 -0
  146. package/prompts/implement.md +26 -0
  147. package/prompts/issue-review.md +256 -0
  148. package/prompts/pr-review.md +753 -0
  149. package/prompts/query-db.md +154 -0
  150. package/prompts/refine-review.md +208 -0
  151. package/prompts/release.md +235 -0
  152. package/prompts/remember.md +33 -0
  153. package/prompts/review-handler-status.md +27 -0
  154. package/prompts/review-handler.md +677 -0
  155. package/prompts/review-local.md +159 -0
  156. package/prompts/scout-and-plan.md +21 -0
  157. package/rules/00-orchestrator-core.md +42 -0
  158. package/rules/05-issue-first-workflow.md +119 -0
  159. package/rules/10-agent-routing.md +58 -0
  160. package/rules/15-mcp-launchpad.md +46 -0
  161. package/rules/20-code-review-loop.md +234 -0
  162. package/rules/25-documentation-updates.md +15 -0
  163. package/rules/30-prompt-templates.md +19 -0
  164. package/rules/35-memory.md +240 -0
  165. package/rules/40-critical-rules.md +124 -0
  166. package/rules/45-file-preview.md +29 -0
  167. package/rules/50-agent-bug-reporting.md +90 -0
  168. package/rules/55-coms-protocol.md +63 -0
  169. package/rules/60-task-tracking.md +66 -0
  170. package/skills/conflict-resolver/SKILL.md +213 -0
  171. package/skills/skill-creator/SKILL.md +85 -0
  172. package/skills/systematic-debugging/SKILL.md +353 -0
  173. package/skills/test-driven-development/SKILL.md +335 -0
  174. package/skills/update-agents-md/SKILL.md +282 -0
  175. package/templates/coms-feature-manager-prompt.md +313 -0
@@ -0,0 +1,434 @@
1
+ /**
2
+ * Memory Scoring Engine — stability-based memory learning.
3
+ *
4
+ * Scores memory entries using a stability formula with decay, evidence
5
+ * weighting, and per-category budgets. Entries that aren't reinforced
6
+ * fade over time. Frequently reinforced entries become stronger.
7
+ *
8
+ * Architecture inspired by OpenHuman (https://github.com/tinyhumansai/openhuman).
9
+ * Clean-room TypeScript implementation under MIT — not a code translation.
10
+ */
11
+
12
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
13
+ import { join, dirname } from "node:path";
14
+
15
+ // ── Types ──────────────────────────────────────────────────────────────────
16
+
17
+ export type MemoryCategory = "preference" | "lesson" | "pattern" | "decision" | "done" | "mistake";
18
+ export type CueType = "explicit" | "structural" | "behavioral" | "recurrence";
19
+ export type UserState = "auto" | "pinned" | "forgotten";
20
+ export type LifecycleState = "active" | "provisional" | "candidate" | "dropped";
21
+
22
+ /** Trigger types for enforcement rules */
23
+ export type EnforcementTrigger =
24
+ | `bash_contains ${string}` // matches if bash command contains the string
25
+ | `bash_regex ${string}` // matches if bash command matches the regex
26
+ | `tool_name ${string}` // matches if tool name equals the string
27
+ | `file_modified ${string}`; // matches if a write/edit targets a matching path glob
28
+
29
+ /** Action types for enforcement rules */
30
+ export type EnforcementAction =
31
+ | "block" // block the tool call, return error
32
+ | "run_after" // run a command after the tool succeeds
33
+ | "warn"; // append warning to tool result
34
+
35
+ export interface ScoredEntry {
36
+ /** Category of the memory */
37
+ class: MemoryCategory;
38
+ /** Stability score (higher = more stable) */
39
+ score: number;
40
+ /** Number of times this memory has been reinforced */
41
+ evidenceCount: number;
42
+ /** How this memory was produced */
43
+ cue: CueType;
44
+ /** When first observed (ISO string) */
45
+ firstSeen: string;
46
+ /** When last reinforced (ISO string) */
47
+ lastReinforced: string;
48
+ /** User override state */
49
+ userState: UserState;
50
+ /** Current lifecycle state */
51
+ lifecycle: LifecycleState;
52
+
53
+ // ── Enforcement fields (optional) ──────────────────────────────────
54
+ /** What activates this rule (e.g., 'bash_contains git add .') */
55
+ trigger?: EnforcementTrigger;
56
+ /** What to do when triggered: block, run_after, warn */
57
+ action?: EnforcementAction;
58
+ /** Command to run (for run_after action) */
59
+ actionCommand?: string;
60
+ /** Semantic verifier — condition to check (e.g., 'tool_called ask_user before gh pr merge') */
61
+ verifier?: string;
62
+
63
+ // ── Provenance fields (optional; not injected into situation report) ──
64
+ /** Session id or path basename this memory came from */
65
+ sourceSession?: string;
66
+ /** Short note or prior entry hash this was derived from */
67
+ derivedFrom?: string;
68
+ /** Free-text targets this memory informs (e.g. pr-review, git, skill name) */
69
+ informs?: string[];
70
+ }
71
+
72
+ export interface ScoresFile {
73
+ entries: Record<string, ScoredEntry>;
74
+ lastRebuild: string;
75
+ }
76
+
77
+ // ── Constants ──────────────────────────────────────────────────────────────
78
+
79
+ /** Cue type weights — how strongly each evidence source contributes */
80
+ const CUE_WEIGHTS: Record<CueType, number> = {
81
+ explicit: 1.0,
82
+ structural: 0.9,
83
+ behavioral: 0.7,
84
+ recurrence: 0.6,
85
+ };
86
+
87
+ /** Half-lives in seconds per category — how fast memories decay */
88
+ const HALF_LIVES: Record<MemoryCategory, number> = {
89
+ preference: 90 * 86400, // 90 days
90
+ lesson: 60 * 86400, // 60 days
91
+ pattern: 30 * 86400, // 30 days
92
+ decision: 30 * 86400, // 30 days
93
+ done: 14 * 86400, // 14 days
94
+ mistake: 14 * 86400, // 14 days
95
+ };
96
+
97
+ /** Per-category budget caps for Active entries */
98
+ const CATEGORY_BUDGETS: Record<MemoryCategory, number> = {
99
+ preference: 8,
100
+ lesson: 8,
101
+ pattern: 6,
102
+ decision: 4,
103
+ done: 4,
104
+ mistake: 4,
105
+ };
106
+
107
+ /** Cross-category overflow pool for Provisional entries */
108
+ const OVERFLOW_BUDGET = 6;
109
+
110
+ /** Total max active entries */
111
+ const MAX_ACTIVE_ENTRIES = 40;
112
+
113
+ /** Lifecycle thresholds */
114
+ const TAU_PROMOTE = 1.5;
115
+ const TAU_PROVISIONAL = 0.7;
116
+ const TAU_EVICT = 0.4;
117
+
118
+ // ── Scoring Formula ────────────────────────────────────────────────────────
119
+
120
+ /**
121
+ * Calculate stability score for a memory entry.
122
+ *
123
+ * Formula: cue_weight × exp(-Δt / half_life) × ln(1 + evidence_count)
124
+ *
125
+ * - Pinned entries → Infinity (never decay)
126
+ * - Forgotten entries → 0 (always dropped)
127
+ */
128
+ /** Score value representing pinned entries (JSON-safe alternative to Infinity) */
129
+ export const PINNED_SCORE = 9999;
130
+
131
+ export function calculateStability(
132
+ cue: CueType,
133
+ evidenceCount: number,
134
+ lastReinforcedAt: number,
135
+ nowMs: number,
136
+ category: MemoryCategory,
137
+ userState: UserState,
138
+ ): number {
139
+ if (userState === "pinned") return PINNED_SCORE;
140
+ if (userState === "forgotten") return 0;
141
+
142
+ const dtSeconds = Math.max(0, (nowMs - lastReinforcedAt) / 1000);
143
+ const halfLife = HALF_LIVES[category];
144
+ const recency = Math.exp(-dtSeconds / halfLife);
145
+ const base = CUE_WEIGHTS[cue] * recency * Math.log(1 + evidenceCount);
146
+ return base;
147
+ }
148
+
149
+ /**
150
+ * Determine lifecycle state from a stability score.
151
+ */
152
+ export function lifecycleFromScore(score: number, userState: UserState, entry?: ScoredEntry): LifecycleState {
153
+ if (userState === "pinned") return "active";
154
+ if (userState === "forgotten") return "dropped";
155
+ // Enforced entries never decay below active — require trigger+action or verifier
156
+ if (entry && ((entry.trigger && entry.action) || entry.verifier)) return "active";
157
+ if (score >= PINNED_SCORE) return "active";
158
+ if (score >= TAU_PROMOTE) return "active";
159
+ if (score >= TAU_PROVISIONAL) return "provisional";
160
+ if (score >= TAU_EVICT) return "candidate";
161
+ return "dropped";
162
+ }
163
+
164
+ // ── Hash Utility ───────────────────────────────────────────────────────────
165
+
166
+ /** Generate a simple hash key for a memory entry text */
167
+ export function entryHash(text: string): string {
168
+ // Simple FNV-1a hash — deterministic, no crypto dependency needed
169
+ let hash = 0x811c9dc5;
170
+ for (let i = 0; i < text.length; i++) {
171
+ hash ^= text.charCodeAt(i);
172
+ hash = (hash * 0x01000193) >>> 0;
173
+ }
174
+ return hash.toString(16).padStart(8, "0");
175
+ }
176
+
177
+ // ── Scores File I/O ────────────────────────────────────────────────────────
178
+
179
+ const SCORES_FILENAME = "memory-scores.json";
180
+
181
+ export function getScoresPath(cwd: string): string {
182
+ return join(cwd, ".pi", "memory", SCORES_FILENAME);
183
+ }
184
+
185
+ export function loadScores(cwd: string): ScoresFile {
186
+ const path = getScoresPath(cwd);
187
+ if (!existsSync(path)) {
188
+ return { entries: {}, lastRebuild: new Date().toISOString() };
189
+ }
190
+ try {
191
+ const raw = readFileSync(path, "utf-8");
192
+ return JSON.parse(raw) as ScoresFile;
193
+ } catch {
194
+ return { entries: {}, lastRebuild: new Date().toISOString() };
195
+ }
196
+ }
197
+
198
+ export function saveScores(cwd: string, scores: ScoresFile): void {
199
+ const path = getScoresPath(cwd);
200
+ const dir = dirname(path);
201
+ if (!existsSync(dir)) {
202
+ mkdirSync(dir, { recursive: true });
203
+ }
204
+ writeFileSync(path, JSON.stringify(scores, null, 2) + "\n", "utf-8");
205
+ }
206
+
207
+ // ── Internal Types ─────────────────────────────────────────────────────────
208
+
209
+ interface ParsedEntry {
210
+ section: "pinned" | "learned";
211
+ category: MemoryCategory;
212
+ text: string;
213
+ fullLine: string;
214
+ }
215
+
216
+ // ── Rebuild Cycle ──────────────────────────────────────────────────────────
217
+
218
+ export interface RebuildResult {
219
+ active: number;
220
+ provisional: number;
221
+ dropped: number;
222
+ total: number;
223
+ }
224
+
225
+ /**
226
+ * Run a full rebuild cycle: score all entries, apply budgets, update lifecycle states.
227
+ * Takes entries from topic files (the sole source of truth).
228
+ */
229
+ export function rebuild(
230
+ cwd: string,
231
+ entries: { category: MemoryCategory; text: string; pinned: boolean }[],
232
+ ): RebuildResult {
233
+ const parsed = entries.map((e) => ({
234
+ section: (e.pinned ? "pinned" : "learned") as "pinned" | "learned",
235
+ category: e.category,
236
+ text: e.text,
237
+ fullLine: `- [${e.category}] ${e.text}`,
238
+ }));
239
+ return rebuildFromParsed(cwd, parsed);
240
+ }
241
+
242
+ function rebuildFromParsed(cwd: string, parsed: ParsedEntry[]): RebuildResult {
243
+ if (parsed.length === 0) {
244
+ return { active: 0, provisional: 0, dropped: 0, total: 0 };
245
+ }
246
+ const scores = loadScores(cwd);
247
+ const now = Date.now();
248
+
249
+ // Step 1: Score all entries
250
+ for (const entry of parsed) {
251
+ const hash = entryHash(entry.fullLine);
252
+ const existing = scores.entries[hash];
253
+
254
+ if (existing) {
255
+ // Recalculate stability with current time
256
+ existing.score = calculateStability(
257
+ existing.cue,
258
+ existing.evidenceCount,
259
+ new Date(existing.lastReinforced).getTime(),
260
+ now,
261
+ existing.class,
262
+ existing.userState,
263
+ );
264
+ existing.lifecycle = lifecycleFromScore(existing.score, existing.userState, existing);
265
+ } else {
266
+ // New entry — initialize
267
+ const userState: UserState = entry.section === "pinned" ? "pinned" : "auto";
268
+ const cue: CueType = entry.section === "pinned" ? "explicit" : "behavioral";
269
+ const score = calculateStability(cue, 1, now, now, entry.category, userState);
270
+
271
+ scores.entries[hash] = {
272
+ class: entry.category,
273
+ score,
274
+ evidenceCount: 1,
275
+ cue,
276
+ firstSeen: new Date(now).toISOString(),
277
+ lastReinforced: new Date(now).toISOString(),
278
+ userState,
279
+ lifecycle: lifecycleFromScore(score, userState),
280
+ };
281
+ }
282
+ }
283
+
284
+ // Step 2: Remove scores for entries that no longer exist in topics
285
+ // Preserve entries with enforcement fields — they must not be deleted even
286
+ // if dreaming rewrites the topic file text (which changes the hash).
287
+ const currentHashes = new Set(parsed.map((e) => entryHash(e.fullLine)));
288
+ for (const hash of Object.keys(scores.entries)) {
289
+ if (!currentHashes.has(hash)) {
290
+ const entry = scores.entries[hash];
291
+ if (entry && ((entry.trigger && entry.action) || entry.verifier)) {
292
+ // Enforced entry (trigger+action or verifier) — keep it
293
+ // Mark as orphaned so budgeting excludes it
294
+ (entry as any)._orphaned = true;
295
+ continue;
296
+ }
297
+ delete scores.entries[hash];
298
+ }
299
+ }
300
+
301
+ // Step 3: Apply per-category budgets
302
+ const byCategory: Record<string, { hash: string; entry: ScoredEntry }[]> = {};
303
+ for (const [hash, entry] of Object.entries(scores.entries)) {
304
+ // Skip orphaned enforced entries from budgeting — they don't have topic file backing
305
+ if ((entry as any)._orphaned) continue;
306
+ if (entry.lifecycle === "active") {
307
+ if (!byCategory[entry.class]) byCategory[entry.class] = [];
308
+ byCategory[entry.class]!.push({ hash, entry });
309
+ }
310
+ }
311
+
312
+ const overflow: { hash: string; entry: ScoredEntry }[] = [];
313
+ for (const [category, entries] of Object.entries(byCategory)) {
314
+ const budget = CATEGORY_BUDGETS[category as MemoryCategory] || 4;
315
+ // Sort by score descending
316
+ entries.sort((a, b) => b.entry.score - a.entry.score);
317
+ // Demote excess to provisional
318
+ for (let i = budget; i < entries.length; i++) {
319
+ entries[i]!.entry.lifecycle = "provisional";
320
+ overflow.push(entries[i]!);
321
+ }
322
+ }
323
+
324
+ // Step 4: Apply overflow budget for provisional entries
325
+ overflow.sort((a, b) => b.entry.score - a.entry.score);
326
+ for (let i = OVERFLOW_BUDGET; i < overflow.length; i++) {
327
+ overflow[i]!.entry.lifecycle = "candidate";
328
+ }
329
+
330
+ // Step 5: Enforce total cap
331
+ const allActive = Object.values(scores.entries).filter((e) => e.lifecycle === "active");
332
+ if (allActive.length > MAX_ACTIVE_ENTRIES) {
333
+ allActive.sort((a, b) => b.score - a.score);
334
+ for (let i = MAX_ACTIVE_ENTRIES; i < allActive.length; i++) {
335
+ allActive[i]!.lifecycle = "provisional";
336
+ }
337
+ }
338
+
339
+ // Step 6: Clean up internal markers and save
340
+ for (const entry of Object.values(scores.entries)) {
341
+ delete (entry as any)._orphaned;
342
+ }
343
+ scores.lastRebuild = new Date(now).toISOString();
344
+ saveScores(cwd, scores);
345
+
346
+ // Count results
347
+ const values = Object.values(scores.entries);
348
+ return {
349
+ active: values.filter((e) => e.lifecycle === "active").length,
350
+ provisional: values.filter((e) => e.lifecycle === "provisional").length,
351
+ dropped: values.filter((e) => e.lifecycle === "dropped").length,
352
+ total: values.length,
353
+ };
354
+ }
355
+
356
+ // ── Reinforcement ──────────────────────────────────────────────────────────
357
+
358
+ /**
359
+ * Reinforce an existing memory entry (bump evidence count + last reinforced).
360
+ * Returns true if the entry was found and reinforced.
361
+ */
362
+ export function reinforce(cwd: string, entryLine: string): boolean {
363
+ const scores = loadScores(cwd);
364
+ const hash = entryHash(entryLine);
365
+ const entry = scores.entries[hash];
366
+ if (!entry) return false;
367
+
368
+ entry.evidenceCount += 1;
369
+ entry.lastReinforced = new Date().toISOString();
370
+ entry.score = calculateStability(
371
+ entry.cue,
372
+ entry.evidenceCount,
373
+ Date.now(),
374
+ Date.now(),
375
+ entry.class,
376
+ entry.userState,
377
+ );
378
+ entry.lifecycle = lifecycleFromScore(entry.score, entry.userState, entry);
379
+ saveScores(cwd, scores);
380
+ return true;
381
+ }
382
+
383
+ // ── Query ──────────────────────────────────────────────────────────────────
384
+
385
+ /**
386
+ * Get all active entries (for system prompt injection).
387
+ * Returns entries sorted by score descending.
388
+ */
389
+ export function getActiveEntries(cwd: string): { hash: string; entry: ScoredEntry }[] {
390
+ const scores = loadScores(cwd);
391
+ return Object.entries(scores.entries)
392
+ .filter(([, e]) => e.lifecycle === "active" || e.lifecycle === "provisional")
393
+ .sort(([, a], [, b]) => b.score - a.score)
394
+ .map(([hash, entry]) => ({ hash, entry }));
395
+ }
396
+
397
+ // ── Preference Auto-Extraction ─────────────────────────────────────────────
398
+
399
+ /** Patterns that signal explicit user preferences in conversation */
400
+ const PREFERENCE_PATTERNS: RegExp[] = [
401
+ /\bi prefer\b/i,
402
+ /\bi always\b/i,
403
+ /\balways use\b/i,
404
+ /\bnever use\b/i,
405
+ /\bmy timezone\b/i,
406
+ /\bmy language\b/i,
407
+ /\bplease always\b/i,
408
+ /\bplease never\b/i,
409
+ /\bfrom now on\b/i,
410
+ /\bgoing forward\b/i,
411
+ /\bmy name is\b/i,
412
+ /\bi work\b/i,
413
+ /\bmy role\b/i,
414
+ /\bmy stack\b/i,
415
+ ];
416
+
417
+ /**
418
+ * Extract preference statements from user text.
419
+ * Returns sentences that match preference patterns.
420
+ */
421
+ export function extractPreferences(text: string): string[] {
422
+ const found: string[] = [];
423
+ const sentences = text.split(/[.!?\n]/).map((s) => s.trim()).filter((s) => s.length >= 15);
424
+
425
+ for (const sentence of sentences) {
426
+ if (PREFERENCE_PATTERNS.some((p) => p.test(sentence))) {
427
+ // Normalize to a clean one-liner
428
+ const clean = sentence.replace(/\s+/g, " ").slice(0, 100);
429
+ found.push(clean);
430
+ }
431
+ }
432
+
433
+ return found.slice(0, 3); // Cap at 3 per message
434
+ }