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.
- package/README.md +808 -0
- package/agents/api-documenter.md +32 -0
- package/agents/bash-expert.md +53 -0
- package/agents/code-reviewer-docs.md +145 -0
- package/agents/code-reviewer-guidelines.md +97 -0
- package/agents/code-reviewer-quality.md +142 -0
- package/agents/code-reviewer-security.md +107 -0
- package/agents/code-reviewer-spec.md +164 -0
- package/agents/debugger.md +63 -0
- package/agents/docker-expert.md +40 -0
- package/agents/docs-fetcher.md +48 -0
- package/agents/git-expert.md +63 -0
- package/agents/github-expert.md +83 -0
- package/agents/go-expert.md +62 -0
- package/agents/issue-reviewer-feasibility.md +60 -0
- package/agents/issue-reviewer-scope.md +63 -0
- package/agents/issue-reviewer-spec.md +77 -0
- package/agents/java-expert.md +31 -0
- package/agents/jenkins-expert.md +38 -0
- package/agents/kubernetes-expert.md +40 -0
- package/agents/planner.md +42 -0
- package/agents/python-expert.md +61 -0
- package/agents/reviewer.md +26 -0
- package/agents/scout.md +40 -0
- package/agents/security-auditor.md +254 -0
- package/agents/technical-documentation-writer.md +37 -0
- package/agents/test-automator.md +55 -0
- package/agents/test-runner.md +47 -0
- package/agents/ts-expert.md +23 -0
- package/agents/worker.md +30 -0
- package/extensions/acpx-provider/index.ts +104 -0
- package/extensions/acpx-provider/load-runtime.ts +99 -0
- package/extensions/acpx-provider/runtime-models.ts +40 -0
- package/extensions/cli-provider/agents/claude.ts +128 -0
- package/extensions/cli-provider/agents/cursor.ts +84 -0
- package/extensions/cli-provider/agents/gemini.ts +100 -0
- package/extensions/cli-provider/discover.ts +77 -0
- package/extensions/cli-provider/index.ts +57 -0
- package/extensions/cli-provider/parsers.ts +304 -0
- package/extensions/cli-provider/providers.ts +58 -0
- package/extensions/cli-provider/runner.ts +225 -0
- package/extensions/cli-provider/runtime-models.ts +32 -0
- package/extensions/cli-provider/session-reaper.ts +122 -0
- package/extensions/cli-provider/sessions.ts +533 -0
- package/extensions/cli-provider/shared/discover-cache.ts +235 -0
- package/extensions/cli-provider/types.ts +48 -0
- package/extensions/coms/coms-net-server.ts +1642 -0
- package/extensions/coms/coms-net-wrapper.ts +521 -0
- package/extensions/coms/coms-net.ts +1589 -0
- package/extensions/coms/coms-p2p.ts +1879 -0
- package/extensions/coms/coms-shared.ts +510 -0
- package/extensions/coms/coms-wrapper.ts +138 -0
- package/extensions/coms/index.ts +20 -0
- package/extensions/coms/themeMap.ts +147 -0
- package/extensions/image-gen/image-gen.ts +332 -0
- package/extensions/image-gen/index.ts +14 -0
- package/extensions/orchestrator/agents.ts +134 -0
- package/extensions/orchestrator/ask-user.ts +293 -0
- package/extensions/orchestrator/async-agents.ts +1037 -0
- package/extensions/orchestrator/async-capability.ts +129 -0
- package/extensions/orchestrator/async-runner.ts +324 -0
- package/extensions/orchestrator/async-status-parse.ts +31 -0
- package/extensions/orchestrator/async-status-ui.ts +201 -0
- package/extensions/orchestrator/async-wait.ts +36 -0
- package/extensions/orchestrator/btw.ts +210 -0
- package/extensions/orchestrator/cron-status-format.ts +126 -0
- package/extensions/orchestrator/cron-status-ui.ts +132 -0
- package/extensions/orchestrator/cron.ts +730 -0
- package/extensions/orchestrator/dreaming.ts +355 -0
- package/extensions/orchestrator/enforcement-helpers.ts +530 -0
- package/extensions/orchestrator/enforcement-rules.ts +329 -0
- package/extensions/orchestrator/enforcement.ts +828 -0
- package/extensions/orchestrator/extended-autocomplete.ts +593 -0
- package/extensions/orchestrator/git-helpers.ts +420 -0
- package/extensions/orchestrator/github-autocomplete.ts +250 -0
- package/extensions/orchestrator/icons.ts +17 -0
- package/extensions/orchestrator/index.ts +100 -0
- package/extensions/orchestrator/memory-embeddings.ts +281 -0
- package/extensions/orchestrator/memory-promotion.ts +449 -0
- package/extensions/orchestrator/memory-provenance.ts +80 -0
- package/extensions/orchestrator/memory-query-class.ts +90 -0
- package/extensions/orchestrator/memory-scoring.ts +434 -0
- package/extensions/orchestrator/memory-tools.ts +883 -0
- package/extensions/orchestrator/memory-tree.ts +212 -0
- package/extensions/orchestrator/nvim.ts +169 -0
- package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
- package/extensions/orchestrator/overlay-dashboard.ts +512 -0
- package/extensions/orchestrator/pi-config-review-state.ts +303 -0
- package/extensions/orchestrator/preference-extractor.ts +106 -0
- package/extensions/orchestrator/project-settings.ts +468 -0
- package/extensions/orchestrator/promotion-queue.ts +311 -0
- package/extensions/orchestrator/resolve-agent-model.ts +32 -0
- package/extensions/orchestrator/review-ui.ts +204 -0
- package/extensions/orchestrator/rule-placeholders.ts +12 -0
- package/extensions/orchestrator/rules.ts +481 -0
- package/extensions/orchestrator/session-search.ts +243 -0
- package/extensions/orchestrator/session-validation.ts +374 -0
- package/extensions/orchestrator/situation-report.ts +370 -0
- package/extensions/orchestrator/status-bar.ts +31 -0
- package/extensions/orchestrator/status-line.ts +192 -0
- package/extensions/orchestrator/status.ts +110 -0
- package/extensions/orchestrator/subagent-tool.ts +1748 -0
- package/extensions/orchestrator/task-lifecycle.ts +88 -0
- package/extensions/orchestrator/utils.ts +227 -0
- package/extensions/pidash/index.ts +15 -0
- package/extensions/pidash/pidash.ts +941 -0
- package/extensions/pidiff/index.ts +13 -0
- package/extensions/pidiff/pidiff.ts +369 -0
- package/extensions/providers/acpx-driver.ts +479 -0
- package/extensions/providers/built-in-drivers.ts +52 -0
- package/extensions/providers/claude-driver.ts +319 -0
- package/extensions/providers/cursor-acpx-driver.ts +398 -0
- package/extensions/providers/cursor-cli-driver.ts +484 -0
- package/extensions/providers/gemini-driver.ts +302 -0
- package/extensions/providers/index.ts +513 -0
- package/extensions/shared/build-system-prompt.ts +57 -0
- package/extensions/shared/create-runtime-provider.ts +190 -0
- package/extensions/shared/daemon-manager.ts +248 -0
- package/extensions/shared/file-logger.ts +115 -0
- package/extensions/shared/managed-refresh.ts +211 -0
- package/extensions/shared/provider-driver.ts +295 -0
- package/extensions/shared/provider-errors.ts +305 -0
- package/extensions/shared/provider-registry.ts +388 -0
- package/extensions/shared/stream-builder.ts +275 -0
- package/extensions/shared/types.ts +20 -0
- package/extensions/shared/ui/README.md +10 -0
- package/extensions/shared/ui/badge.tsx +52 -0
- package/extensions/shared/ui/button.tsx +58 -0
- package/extensions/shared/ui/collapsible.tsx +21 -0
- package/extensions/shared/ui/dropdown-menu.tsx +266 -0
- package/extensions/shared/ui/input.tsx +20 -0
- package/extensions/shared/ui/popover.tsx +114 -0
- package/extensions/shared/ui/resizable.tsx +48 -0
- package/extensions/shared/ui/scroll-area.tsx +53 -0
- package/extensions/shared/ui/select.tsx +201 -0
- package/extensions/shared/ui/separator.tsx +25 -0
- package/extensions/shared/ui/tooltip.tsx +64 -0
- package/extensions/shared/ws-client.ts +74 -0
- package/package.json +59 -0
- package/prompts/coderabbit-rate-limit.md +95 -0
- package/prompts/create-coms-feature-manager.md +186 -0
- package/prompts/create-skill.md +128 -0
- package/prompts/domain-model.md +109 -0
- package/prompts/external-ai.md +665 -0
- package/prompts/implement-and-review.md +25 -0
- package/prompts/implement.md +26 -0
- package/prompts/issue-review.md +256 -0
- package/prompts/pr-review.md +753 -0
- package/prompts/query-db.md +154 -0
- package/prompts/refine-review.md +208 -0
- package/prompts/release.md +235 -0
- package/prompts/remember.md +33 -0
- package/prompts/review-handler-status.md +27 -0
- package/prompts/review-handler.md +677 -0
- package/prompts/review-local.md +159 -0
- package/prompts/scout-and-plan.md +21 -0
- package/rules/00-orchestrator-core.md +42 -0
- package/rules/05-issue-first-workflow.md +119 -0
- package/rules/10-agent-routing.md +58 -0
- package/rules/15-mcp-launchpad.md +46 -0
- package/rules/20-code-review-loop.md +234 -0
- package/rules/25-documentation-updates.md +15 -0
- package/rules/30-prompt-templates.md +19 -0
- package/rules/35-memory.md +240 -0
- package/rules/40-critical-rules.md +124 -0
- package/rules/45-file-preview.md +29 -0
- package/rules/50-agent-bug-reporting.md +90 -0
- package/rules/55-coms-protocol.md +63 -0
- package/rules/60-task-tracking.md +66 -0
- package/skills/conflict-resolver/SKILL.md +213 -0
- package/skills/skill-creator/SKILL.md +85 -0
- package/skills/systematic-debugging/SKILL.md +353 -0
- package/skills/test-driven-development/SKILL.md +335 -0
- package/skills/update-agents-md/SKILL.md +282 -0
- 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
|
+
}
|