openhermes 4.3.0 → 4.11.2
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/CONTEXT.md +10 -1
- package/README.md +54 -42
- package/bootstrap.ts +396 -142
- package/harness/agents/oh-browser.md +97 -0
- package/harness/agents/oh-builder.md +78 -0
- package/harness/agents/oh-facade.md +75 -0
- package/harness/agents/oh-fusion.md +45 -0
- package/harness/agents/oh-gauntlet.md +71 -0
- package/harness/agents/oh-grill.md +71 -0
- package/harness/agents/oh-investigate.md +60 -0
- package/harness/agents/oh-manifest.md +95 -0
- package/harness/agents/oh-plan-review.md +40 -0
- package/harness/agents/oh-planner.md +50 -0
- package/harness/agents/oh-refactor.md +37 -0
- package/harness/agents/oh-retro.md +46 -0
- package/harness/agents/oh-review.md +85 -0
- package/harness/agents/oh-security.md +83 -0
- package/harness/agents/oh-ship.md +76 -0
- package/harness/agents/oh-skill-craft.md +38 -0
- package/harness/agents/openhermes.md +28 -73
- package/harness/codex/AUTOPILOT.md +235 -87
- package/harness/codex/CHARTER.md +80 -0
- package/harness/instructions/SHELL.md +76 -0
- package/harness/lib/background/background.test.ts +197 -0
- package/harness/lib/background/index.ts +7 -0
- package/harness/lib/background/interfaces.ts +31 -0
- package/harness/lib/background/manager.ts +320 -0
- package/harness/lib/composer/compose.test.ts +168 -0
- package/harness/lib/composer/compose.ts +65 -0
- package/harness/lib/composer/fragments/01-identity.md +1 -0
- package/harness/lib/composer/fragments/02-delegation.md +6 -0
- package/harness/lib/composer/fragments/03-permissions.md +13 -0
- package/harness/lib/composer/fragments/04-task-flow.md +15 -0
- package/harness/lib/composer/fragments/05-confidence.md +5 -0
- package/harness/lib/composer/fragments/06-parallelization.md +17 -0
- package/harness/lib/composer/fragments/07-shell.md +41 -0
- package/harness/lib/composer/fragments/08-routing.md +8 -0
- package/harness/lib/composer/fragments/09-guardrails.md +12 -0
- package/harness/lib/composer/index.ts +1 -0
- package/harness/lib/hooks/builtins/confidence-gate-hook.ts +70 -0
- package/harness/lib/hooks/builtins/delegation-depth-hook.ts +59 -0
- package/harness/lib/hooks/builtins/error-recovery-hook.ts +107 -0
- package/harness/lib/hooks/builtins/memory-sync-hook.ts +73 -0
- package/harness/lib/hooks/builtins/plan-check-hook.ts +43 -0
- package/harness/lib/hooks/builtins/route-tracking-hook.ts +147 -0
- package/harness/lib/hooks/builtins/sanity-check-hook.ts +52 -0
- package/harness/lib/hooks/builtins/shell-detect-hook.ts +96 -0
- package/harness/lib/hooks/hooks.test.ts +1016 -0
- package/harness/lib/hooks/index.ts +30 -0
- package/harness/lib/hooks/registry.ts +416 -0
- package/harness/lib/hooks/types.ts +71 -0
- package/harness/lib/memory/index.ts +18 -0
- package/harness/lib/memory/interfaces.ts +53 -0
- package/harness/lib/memory/memory-manager.ts +205 -0
- package/harness/lib/memory/memory.test.ts +491 -0
- package/harness/lib/memory/plan-store.ts +366 -0
- package/harness/lib/recovery/handler.ts +243 -0
- package/harness/lib/recovery/index.ts +14 -0
- package/harness/lib/recovery/interfaces.ts +48 -0
- package/harness/lib/recovery/patterns.ts +149 -0
- package/harness/lib/recovery/recovery.test.ts +312 -0
- package/harness/lib/sanity/anomaly-tracker.ts +127 -0
- package/harness/lib/sanity/checker.ts +178 -0
- package/harness/lib/sanity/index.ts +13 -0
- package/harness/lib/sanity/interfaces.ts +24 -0
- package/harness/lib/sanity/sanity.test.ts +472 -0
- package/harness/lib/sync/file-watcher.ts +174 -0
- package/harness/lib/sync/index.ts +11 -0
- package/harness/lib/sync/interfaces.ts +27 -0
- package/harness/lib/sync/plan-sync.ts +536 -0
- package/harness/lib/sync/sync.test.ts +832 -0
- package/harness/skills/oh-ascii/DEEP.md +292 -0
- package/harness/skills/oh-ascii/SKILL.md +31 -0
- package/harness/skills/oh-ascii/scripts/check_ascii_alignment.py +596 -0
- package/harness/skills/oh-browser/DEEP.md +54 -0
- package/harness/skills/oh-browser/SKILL.md +30 -0
- package/harness/skills/oh-builder/DEEP.md +63 -0
- package/harness/skills/oh-builder/SKILL.md +12 -90
- package/harness/skills/oh-expert/DEEP.md +85 -0
- package/harness/skills/oh-expert/SKILL.md +13 -106
- package/harness/skills/oh-facade/DEEP.md +182 -0
- package/harness/skills/oh-facade/SKILL.md +15 -279
- package/harness/skills/oh-freeze/DEEP.md +18 -0
- package/harness/skills/oh-freeze/SKILL.md +10 -19
- package/harness/skills/oh-full-output/DEEP.md +25 -0
- package/harness/skills/oh-full-output/SKILL.md +12 -65
- package/harness/skills/oh-fusion/DEEP.md +120 -0
- package/harness/skills/oh-fusion/SKILL.md +17 -295
- package/harness/skills/oh-gauntlet/DEEP.md +77 -0
- package/harness/skills/oh-gauntlet/SKILL.md +13 -105
- package/harness/skills/oh-grill/DEEP.md +51 -0
- package/harness/skills/oh-grill/SKILL.md +12 -63
- package/harness/skills/oh-guard/DEEP.md +19 -0
- package/harness/skills/oh-guard/SKILL.md +10 -24
- package/harness/skills/oh-handoff/DEEP.md +48 -0
- package/harness/skills/oh-handoff/SKILL.md +13 -23
- package/harness/skills/oh-health/DEEP.md +74 -0
- package/harness/skills/oh-health/SKILL.md +13 -76
- package/harness/skills/oh-init/DEEP.md +85 -0
- package/harness/skills/oh-init/SKILL.md +13 -127
- package/harness/skills/oh-investigate/DEEP.md +171 -0
- package/harness/skills/oh-investigate/SKILL.md +13 -66
- package/harness/skills/oh-issue/DEEP.md +21 -0
- package/harness/skills/oh-issue/SKILL.md +11 -27
- package/harness/skills/oh-manifest/DEEP.md +92 -0
- package/harness/skills/oh-manifest/SKILL.md +12 -109
- package/harness/skills/oh-plan-review/DEEP.md +90 -0
- package/harness/skills/oh-plan-review/SKILL.md +13 -115
- package/harness/skills/oh-planner/DEEP.md +172 -0
- package/harness/skills/oh-planner/SKILL.md +12 -149
- package/harness/skills/oh-prd/DEEP.md +45 -0
- package/harness/skills/oh-prd/SKILL.md +10 -26
- package/harness/skills/oh-refactor/DEEP.md +122 -0
- package/harness/skills/oh-refactor/SKILL.md +17 -410
- package/harness/skills/oh-retro/DEEP.md +26 -0
- package/harness/skills/oh-retro/SKILL.md +12 -24
- package/harness/skills/oh-review/DEEP.md +87 -0
- package/harness/skills/oh-review/SKILL.md +11 -97
- package/harness/skills/oh-security/DEEP.md +83 -0
- package/harness/skills/oh-security/SKILL.md +14 -96
- package/harness/skills/oh-ship/DEEP.md +141 -0
- package/harness/skills/oh-ship/SKILL.md +14 -32
- package/harness/skills/oh-skill-craft/DEEP.md +369 -0
- package/harness/skills/oh-skill-craft/SKILL.md +13 -177
- package/harness/skills/oh-skills-link/DEEP.md +16 -0
- package/harness/skills/oh-skills-link/SKILL.md +10 -20
- package/harness/skills/oh-skills-list/DEEP.md +20 -0
- package/harness/skills/oh-skills-list/SKILL.md +9 -22
- package/harness/skills/oh-triage/DEEP.md +23 -0
- package/harness/skills/oh-triage/SKILL.md +8 -24
- package/harness/skills/oh-worktree/DEEP.md +169 -0
- package/harness/skills/oh-worktree/SKILL.md +32 -0
- package/lib/harness-resolver.ts +8 -10
- package/package.json +7 -5
- package/tsconfig.json +1 -1
- package/harness/codex/CONSTITUTION.md +0 -73
- package/harness/codex/ROUTING.md +0 -92
- package/harness/commands/oh-doctor.md +0 -26
- package/harness/commands/oh-log.md +0 -18
- package/harness/instructions/RUNTIME.md +0 -30
- package/harness/skills/oh-caveman/SKILL.md +0 -42
- package/harness/skills/oh-learn/SKILL.md +0 -101
- package/lib/logger.ts +0 -75
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// MemoryManager — singleton 4-tier hierarchical memory store
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import {
|
|
7
|
+
MemoryLevel,
|
|
8
|
+
DEFAULT_BUDGETS,
|
|
9
|
+
} from "./interfaces.ts";
|
|
10
|
+
import type {
|
|
11
|
+
MemoryEntry,
|
|
12
|
+
MemorySnapshot,
|
|
13
|
+
MemoryConfig,
|
|
14
|
+
} from "./interfaces.ts";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Manager
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export class MemoryManager {
|
|
21
|
+
private static instance: MemoryManager;
|
|
22
|
+
|
|
23
|
+
private entries: Map<MemoryLevel, MemoryEntry[]> = new Map();
|
|
24
|
+
private config: MemoryConfig;
|
|
25
|
+
|
|
26
|
+
private constructor(config?: Partial<MemoryConfig>) {
|
|
27
|
+
this.config = {
|
|
28
|
+
budgets: { ...DEFAULT_BUDGETS, ...config?.budgets },
|
|
29
|
+
};
|
|
30
|
+
// Initialise every level so callers never hit undefined
|
|
31
|
+
for (const level of Object.values(MemoryLevel)) {
|
|
32
|
+
this.entries.set(level, []);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// -----------------------------------------------------------------------
|
|
37
|
+
// Singleton
|
|
38
|
+
// -----------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
static getInstance(config?: Partial<MemoryConfig>): MemoryManager {
|
|
41
|
+
if (!MemoryManager.instance) {
|
|
42
|
+
MemoryManager.instance = new MemoryManager(config);
|
|
43
|
+
}
|
|
44
|
+
return MemoryManager.instance;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Reset singleton — used in tests for isolation. */
|
|
48
|
+
static resetInstance(): void {
|
|
49
|
+
MemoryManager.instance = null as unknown as MemoryManager;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// -----------------------------------------------------------------------
|
|
53
|
+
// Public API
|
|
54
|
+
// -----------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Add a memory entry at the given level.
|
|
58
|
+
* Inserts, sorts by importance DESC then timestamp DESC, then prunes.
|
|
59
|
+
*/
|
|
60
|
+
add(
|
|
61
|
+
level: MemoryLevel,
|
|
62
|
+
content: string,
|
|
63
|
+
importance: number,
|
|
64
|
+
metadata?: Record<string, string>,
|
|
65
|
+
): MemoryEntry {
|
|
66
|
+
const entry: MemoryEntry = {
|
|
67
|
+
id: randomUUID(),
|
|
68
|
+
level,
|
|
69
|
+
content,
|
|
70
|
+
timestamp: Date.now(),
|
|
71
|
+
importance: Math.max(0, Math.min(1, importance)), // clamp [0, 1]
|
|
72
|
+
metadata,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const bucket = this.entries.get(level)!;
|
|
76
|
+
bucket.push(entry);
|
|
77
|
+
|
|
78
|
+
// Sort: importance DESC, then timestamp DESC (newer first for ties)
|
|
79
|
+
bucket.sort((a, b) => {
|
|
80
|
+
if (b.importance !== a.importance) return b.importance - a.importance;
|
|
81
|
+
return b.timestamp - a.timestamp;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
this.prune(level);
|
|
85
|
+
return entry;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Return a formatted context string for all levels.
|
|
90
|
+
* If a query string is provided, only entries whose content includes
|
|
91
|
+
* the query (case-insensitive) are returned.
|
|
92
|
+
*/
|
|
93
|
+
getContext(query?: string): string {
|
|
94
|
+
const parts: string[] = [];
|
|
95
|
+
|
|
96
|
+
for (const level of Object.values(MemoryLevel)) {
|
|
97
|
+
const bucket = this.entries.get(level) ?? [];
|
|
98
|
+
let filtered = bucket;
|
|
99
|
+
|
|
100
|
+
if (query && query.length > 0) {
|
|
101
|
+
const q = query.toLowerCase();
|
|
102
|
+
filtered = bucket.filter((e) => e.content.toLowerCase().includes(q));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (filtered.length === 0) continue;
|
|
106
|
+
|
|
107
|
+
const heading = level.toUpperCase();
|
|
108
|
+
const lines = filtered.map((e) => {
|
|
109
|
+
const imp = e.importance.toFixed(2);
|
|
110
|
+
const meta = e.metadata ? ` [${formatMetadata(e.metadata)}]` : "";
|
|
111
|
+
return ` [${imp}] ${e.content}${meta}`;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
parts.push(`== ${heading} ==\n${lines.join("\n")}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return parts.join("\n\n");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Remove the least important entries from a level when budget is exceeded.
|
|
122
|
+
* Falls back to DEFAULT_BUDGETS if no explicit budget is configured.
|
|
123
|
+
*/
|
|
124
|
+
prune(level: MemoryLevel): void {
|
|
125
|
+
const budget = this.config.budgets[level] ?? DEFAULT_BUDGETS[level];
|
|
126
|
+
|
|
127
|
+
// Guard: budget must be a valid non-negative number
|
|
128
|
+
if (typeof budget !== "number" || budget < 0 || !Number.isFinite(budget)) {
|
|
129
|
+
console.warn(
|
|
130
|
+
`[MemoryManager] Invalid budget for level "${level}": ${budget}. Skipping prune.`,
|
|
131
|
+
);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const bucket = this.entries.get(level);
|
|
136
|
+
if (!bucket) return;
|
|
137
|
+
if (bucket.length <= budget) return;
|
|
138
|
+
|
|
139
|
+
// Already sorted: importance DESC → drop from the end
|
|
140
|
+
bucket.splice(budget);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Clear all entries at a given level (used for TASK after iteration). */
|
|
144
|
+
clearLevel(level: MemoryLevel): void {
|
|
145
|
+
this.entries.set(level, []);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Serialise all entries into a snapshot. */
|
|
149
|
+
export(): MemorySnapshot {
|
|
150
|
+
return {
|
|
151
|
+
system: [...(this.entries.get(MemoryLevel.SYSTEM) ?? [])],
|
|
152
|
+
project: [...(this.entries.get(MemoryLevel.PROJECT) ?? [])],
|
|
153
|
+
mission: [...(this.entries.get(MemoryLevel.MISSION) ?? [])],
|
|
154
|
+
task: [...(this.entries.get(MemoryLevel.TASK) ?? [])],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Restore state from a snapshot. */
|
|
159
|
+
import(snapshot: MemorySnapshot): void {
|
|
160
|
+
this.entries.set(MemoryLevel.SYSTEM, [...snapshot.system]);
|
|
161
|
+
this.entries.set(MemoryLevel.PROJECT, [...snapshot.project]);
|
|
162
|
+
this.entries.set(MemoryLevel.MISSION, [...snapshot.mission]);
|
|
163
|
+
this.entries.set(MemoryLevel.TASK, [...snapshot.task]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Get entries, optionally filtered by level. */
|
|
167
|
+
getEntries(level?: MemoryLevel): MemoryEntry[] {
|
|
168
|
+
if (level) {
|
|
169
|
+
return [...(this.entries.get(level) ?? [])];
|
|
170
|
+
}
|
|
171
|
+
const all: MemoryEntry[] = [];
|
|
172
|
+
for (const lvl of Object.values(MemoryLevel)) {
|
|
173
|
+
all.push(...(this.entries.get(lvl) ?? []));
|
|
174
|
+
}
|
|
175
|
+
return all;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Count entries at a given level. */
|
|
179
|
+
getEntryCount(level: MemoryLevel): number {
|
|
180
|
+
return this.entries.get(level)?.length ?? 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Update the budgets after construction. */
|
|
184
|
+
setBudgets(budgets: Partial<Record<MemoryLevel, number>>): void {
|
|
185
|
+
for (const [level, budget] of Object.entries(budgets)) {
|
|
186
|
+
if (budget !== undefined && Object.values(MemoryLevel).includes(level as MemoryLevel)) {
|
|
187
|
+
this.config.budgets[level as MemoryLevel] = budget;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Re-prune all levels with new budgets
|
|
191
|
+
for (const level of Object.values(MemoryLevel)) {
|
|
192
|
+
this.prune(level);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// Helpers
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
function formatMetadata(meta: Record<string, string>): string {
|
|
202
|
+
return Object.entries(meta)
|
|
203
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
204
|
+
.join(", ");
|
|
205
|
+
}
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
import { MemoryManager } from "./memory-manager.ts";
|
|
9
|
+
import { PlanStore } from "./plan-store.ts";
|
|
10
|
+
import { MemoryLevel, DEFAULT_BUDGETS } from "./interfaces.ts";
|
|
11
|
+
import type { MemoryEntry, Finding, Decision } from "./interfaces.ts";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
function tmpdir(): string {
|
|
18
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "memory-test-"));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// MemoryManager tests
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
describe("MemoryManager", () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
MemoryManager.resetInstance();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
MemoryManager.resetInstance();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ---- 1: add() creates entry with correct level ---------------------------
|
|
35
|
+
|
|
36
|
+
it("add() creates an entry at the given level", () => {
|
|
37
|
+
const mm = MemoryManager.getInstance();
|
|
38
|
+
const entry = mm.add(MemoryLevel.MISSION, "Test mission entry", 0.8);
|
|
39
|
+
|
|
40
|
+
assert.ok(entry.id, "entry must have an id");
|
|
41
|
+
assert.equal(entry.level, MemoryLevel.MISSION);
|
|
42
|
+
assert.equal(entry.content, "Test mission entry");
|
|
43
|
+
assert.equal(entry.importance, 0.8);
|
|
44
|
+
assert.ok(entry.timestamp > 0, "timestamp must be set");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ---- 2: getEntries() returns entries at all levels ----------------------
|
|
48
|
+
|
|
49
|
+
it("getEntries() returns all entries when no level filter", () => {
|
|
50
|
+
const mm = MemoryManager.getInstance();
|
|
51
|
+
mm.add(MemoryLevel.SYSTEM, "sys", 1.0);
|
|
52
|
+
mm.add(MemoryLevel.PROJECT, "proj", 0.5);
|
|
53
|
+
mm.add(MemoryLevel.TASK, "task", 0.3);
|
|
54
|
+
|
|
55
|
+
const all = mm.getEntries();
|
|
56
|
+
assert.equal(all.length, 3);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ---- 3: getEntries() filters by level -----------------------------------
|
|
60
|
+
|
|
61
|
+
it("getEntries() filters by level", () => {
|
|
62
|
+
const mm = MemoryManager.getInstance();
|
|
63
|
+
mm.add(MemoryLevel.SYSTEM, "sys", 1.0);
|
|
64
|
+
mm.add(MemoryLevel.PROJECT, "proj", 0.5);
|
|
65
|
+
|
|
66
|
+
const sys = mm.getEntries(MemoryLevel.SYSTEM);
|
|
67
|
+
assert.equal(sys.length, 1);
|
|
68
|
+
assert.equal(sys[0].level, MemoryLevel.SYSTEM);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ---- 4: getEntryCount() returns correct count ---------------------------
|
|
72
|
+
|
|
73
|
+
it("getEntryCount() returns correct count", () => {
|
|
74
|
+
const mm = MemoryManager.getInstance();
|
|
75
|
+
assert.equal(mm.getEntryCount(MemoryLevel.TASK), 0);
|
|
76
|
+
|
|
77
|
+
mm.add(MemoryLevel.TASK, "t1", 0.5);
|
|
78
|
+
mm.add(MemoryLevel.TASK, "t2", 0.3);
|
|
79
|
+
assert.equal(mm.getEntryCount(MemoryLevel.TASK), 2);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ---- 5: entries sorted by importance DESC -------------------------------
|
|
83
|
+
|
|
84
|
+
it("entries are sorted by importance descending", () => {
|
|
85
|
+
const mm = MemoryManager.getInstance();
|
|
86
|
+
mm.add(MemoryLevel.TASK, "low", 0.1);
|
|
87
|
+
mm.add(MemoryLevel.TASK, "high", 0.9);
|
|
88
|
+
mm.add(MemoryLevel.TASK, "mid", 0.5);
|
|
89
|
+
|
|
90
|
+
const entries = mm.getEntries(MemoryLevel.TASK);
|
|
91
|
+
assert.equal(entries[0].content, "high");
|
|
92
|
+
assert.equal(entries[1].content, "mid");
|
|
93
|
+
assert.equal(entries[2].content, "low");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ---- 6: budget enforcement ----------------------------------------------
|
|
97
|
+
|
|
98
|
+
it("prunes when budget is exceeded", () => {
|
|
99
|
+
const mm = MemoryManager.getInstance({
|
|
100
|
+
budgets: { [MemoryLevel.TASK]: 3 },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
mm.add(MemoryLevel.TASK, "keep-1", 0.9);
|
|
104
|
+
mm.add(MemoryLevel.TASK, "keep-2", 0.8);
|
|
105
|
+
mm.add(MemoryLevel.TASK, "keep-3", 0.7);
|
|
106
|
+
mm.add(MemoryLevel.TASK, "drop-1", 0.1);
|
|
107
|
+
mm.add(MemoryLevel.TASK, "drop-2", 0.2);
|
|
108
|
+
|
|
109
|
+
assert.equal(mm.getEntryCount(MemoryLevel.TASK), 3);
|
|
110
|
+
const entries = mm.getEntries(MemoryLevel.TASK);
|
|
111
|
+
const contents = entries.map((e) => e.content);
|
|
112
|
+
assert.ok(contents.includes("keep-1"));
|
|
113
|
+
assert.ok(contents.includes("keep-2"));
|
|
114
|
+
assert.ok(contents.includes("keep-3"));
|
|
115
|
+
assert.ok(!contents.includes("drop-1"));
|
|
116
|
+
assert.ok(!contents.includes("drop-2"));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ---- 7: importance is clamped to [0, 1] ---------------------------------
|
|
120
|
+
|
|
121
|
+
it("clamps importance to [0, 1]", () => {
|
|
122
|
+
const mm = MemoryManager.getInstance();
|
|
123
|
+
const e1 = mm.add(MemoryLevel.TASK, "too-high", 5.0);
|
|
124
|
+
const e2 = mm.add(MemoryLevel.TASK, "too-low", -1.0);
|
|
125
|
+
|
|
126
|
+
assert.equal(e1.importance, 1.0);
|
|
127
|
+
assert.equal(e2.importance, 0.0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ---- 8: clearLevel clears only the specified level ----------------------
|
|
131
|
+
|
|
132
|
+
it("clearLevel clears only the specified level", () => {
|
|
133
|
+
const mm = MemoryManager.getInstance();
|
|
134
|
+
mm.add(MemoryLevel.SYSTEM, "sys", 1.0);
|
|
135
|
+
mm.add(MemoryLevel.MISSION, "mission", 0.5);
|
|
136
|
+
mm.add(MemoryLevel.TASK, "task", 0.3);
|
|
137
|
+
|
|
138
|
+
mm.clearLevel(MemoryLevel.TASK);
|
|
139
|
+
|
|
140
|
+
assert.equal(mm.getEntryCount(MemoryLevel.TASK), 0);
|
|
141
|
+
assert.equal(mm.getEntryCount(MemoryLevel.SYSTEM), 1);
|
|
142
|
+
assert.equal(mm.getEntryCount(MemoryLevel.MISSION), 1);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ---- 9: export / import round-trip --------------------------------------
|
|
146
|
+
|
|
147
|
+
it("export/import round-trip preserves all entries", () => {
|
|
148
|
+
const mm = MemoryManager.getInstance();
|
|
149
|
+
mm.add(MemoryLevel.SYSTEM, "sys-1", 1.0);
|
|
150
|
+
mm.add(MemoryLevel.SYSTEM, "sys-2", 0.9);
|
|
151
|
+
mm.add(MemoryLevel.PROJECT, "proj-1", 0.5);
|
|
152
|
+
mm.add(MemoryLevel.MISSION, "mission-1", 0.8);
|
|
153
|
+
mm.add(MemoryLevel.TASK, "task-1", 0.3);
|
|
154
|
+
|
|
155
|
+
const snapshot = mm.export();
|
|
156
|
+
assert.equal(snapshot.system.length, 2);
|
|
157
|
+
assert.equal(snapshot.project.length, 1);
|
|
158
|
+
assert.equal(snapshot.mission.length, 1);
|
|
159
|
+
assert.equal(snapshot.task.length, 1);
|
|
160
|
+
|
|
161
|
+
// Import into a fresh instance
|
|
162
|
+
MemoryManager.resetInstance();
|
|
163
|
+
const mm2 = MemoryManager.getInstance();
|
|
164
|
+
mm2.import(snapshot);
|
|
165
|
+
|
|
166
|
+
assert.equal(mm2.getEntryCount(MemoryLevel.SYSTEM), 2);
|
|
167
|
+
assert.equal(mm2.getEntryCount(MemoryLevel.PROJECT), 1);
|
|
168
|
+
assert.equal(mm2.getEntryCount(MemoryLevel.MISSION), 1);
|
|
169
|
+
assert.equal(mm2.getEntryCount(MemoryLevel.TASK), 1);
|
|
170
|
+
|
|
171
|
+
// Verify content preserved
|
|
172
|
+
const sysEntries = mm2.getEntries(MemoryLevel.SYSTEM);
|
|
173
|
+
assert.equal(sysEntries[0].content, "sys-1");
|
|
174
|
+
assert.equal(sysEntries[1].content, "sys-2");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ---- 10: getContext produces formatted output ---------------------------
|
|
178
|
+
|
|
179
|
+
it("getContext returns formatted output", () => {
|
|
180
|
+
const mm = MemoryManager.getInstance();
|
|
181
|
+
mm.add(MemoryLevel.SYSTEM, "OpenHermes identity", 1.0);
|
|
182
|
+
mm.add(MemoryLevel.MISSION, "Implement feature X", 0.8);
|
|
183
|
+
|
|
184
|
+
const ctx = mm.getContext();
|
|
185
|
+
assert.ok(ctx.includes("== SYSTEM =="));
|
|
186
|
+
assert.ok(ctx.includes("== MISSION =="));
|
|
187
|
+
assert.ok(ctx.includes("OpenHermes identity"));
|
|
188
|
+
assert.ok(ctx.includes("Implement feature X"));
|
|
189
|
+
|
|
190
|
+
// Verify each line has importance
|
|
191
|
+
const lines = ctx.split("\n");
|
|
192
|
+
const sysLine = lines.find((l) => l.includes("OpenHermes identity"));
|
|
193
|
+
assert.ok(sysLine, "must find system line");
|
|
194
|
+
assert.match(sysLine!, /\[1\.00\]/);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// ---- 11: getContext with query filters by relevance ---------------------
|
|
198
|
+
|
|
199
|
+
it("getContext filters by query string", () => {
|
|
200
|
+
const mm = MemoryManager.getInstance();
|
|
201
|
+
mm.add(MemoryLevel.SYSTEM, "OpenHermes identity", 1.0);
|
|
202
|
+
mm.add(MemoryLevel.MISSION, "Implement feature X", 0.8);
|
|
203
|
+
mm.add(MemoryLevel.PROJECT, "Database schema design", 0.6);
|
|
204
|
+
|
|
205
|
+
const ctx = mm.getContext("feature");
|
|
206
|
+
assert.ok(ctx.includes("Implement feature X"));
|
|
207
|
+
assert.ok(!ctx.includes("OpenHermes identity"));
|
|
208
|
+
assert.ok(!ctx.includes("Database schema"));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ---- 12: getContext with empty query returns all ------------------------
|
|
212
|
+
|
|
213
|
+
it("getContext with empty query returns all", () => {
|
|
214
|
+
const mm = MemoryManager.getInstance();
|
|
215
|
+
mm.add(MemoryLevel.SYSTEM, "entry-1", 1.0);
|
|
216
|
+
mm.add(MemoryLevel.PROJECT, "entry-2", 0.5);
|
|
217
|
+
|
|
218
|
+
const ctx = mm.getContext("");
|
|
219
|
+
assert.ok(ctx.includes("entry-1"));
|
|
220
|
+
assert.ok(ctx.includes("entry-2"));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ---- 13: Default budgets match spec -------------------------------------
|
|
224
|
+
|
|
225
|
+
it("has correct default budgets", () => {
|
|
226
|
+
assert.equal(DEFAULT_BUDGETS[MemoryLevel.SYSTEM], 50);
|
|
227
|
+
assert.equal(DEFAULT_BUDGETS[MemoryLevel.PROJECT], 100);
|
|
228
|
+
assert.equal(DEFAULT_BUDGETS[MemoryLevel.MISSION], 30);
|
|
229
|
+
assert.equal(DEFAULT_BUDGETS[MemoryLevel.TASK], 20);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ---- 14: setBudgets re-prunes -------------------------------------------
|
|
233
|
+
|
|
234
|
+
it("setBudgets re-prunes after changing budgets", () => {
|
|
235
|
+
const mm = MemoryManager.getInstance({
|
|
236
|
+
budgets: { [MemoryLevel.TASK]: 10 },
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
mm.add(MemoryLevel.TASK, "e1", 0.9);
|
|
240
|
+
mm.add(MemoryLevel.TASK, "e2", 0.8);
|
|
241
|
+
mm.add(MemoryLevel.TASK, "e3", 0.7);
|
|
242
|
+
|
|
243
|
+
// Reduce budget to 2 — should prune 1
|
|
244
|
+
mm.setBudgets({ [MemoryLevel.TASK]: 2 });
|
|
245
|
+
assert.equal(mm.getEntryCount(MemoryLevel.TASK), 2);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ---- 15: metadata stored and formatted ----------------------------------
|
|
249
|
+
|
|
250
|
+
it("metadata is stored on entries", () => {
|
|
251
|
+
const mm = MemoryManager.getInstance();
|
|
252
|
+
const entry = mm.add(MemoryLevel.TASK, "with meta", 0.5, {
|
|
253
|
+
source: "test",
|
|
254
|
+
phase: "red",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
assert.deepEqual(entry.metadata, { source: "test", phase: "red" });
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// PlanStore tests
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
describe("PlanStore", () => {
|
|
266
|
+
let testDir: string;
|
|
267
|
+
|
|
268
|
+
beforeEach(() => {
|
|
269
|
+
testDir = tmpdir();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
afterEach(() => {
|
|
273
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ---- 16: readPlan returns defaults for missing file ----------------------
|
|
277
|
+
|
|
278
|
+
it("readPlan returns defaults for non-existent file", async () => {
|
|
279
|
+
const result = await PlanStore.readPlan(
|
|
280
|
+
path.join(testDir, "nonexistent.md"),
|
|
281
|
+
);
|
|
282
|
+
assert.deepEqual(result.tasks, []);
|
|
283
|
+
assert.deepEqual(result.memory, []);
|
|
284
|
+
assert.deepEqual(result.findings, []);
|
|
285
|
+
assert.deepEqual(result.decisions, []);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// ---- 17: writePlan / readPlan round-trip --------------------------------
|
|
289
|
+
|
|
290
|
+
it("writePlan and readPlan round-trip preserves data", async () => {
|
|
291
|
+
const planPath = path.join(testDir, "plan-001.md");
|
|
292
|
+
|
|
293
|
+
const data = {
|
|
294
|
+
tasks: [
|
|
295
|
+
{ id: randomUUID(), description: "Task A", status: "pending" as const, dependsOn: [] },
|
|
296
|
+
{ id: randomUUID(), description: "Task B", status: "completed" as const, dependsOn: ["task-a"] },
|
|
297
|
+
],
|
|
298
|
+
memory: [
|
|
299
|
+
{ id: randomUUID(), level: MemoryLevel.PROJECT, content: "Use TypeScript", importance: 0.9, timestamp: Date.now() },
|
|
300
|
+
],
|
|
301
|
+
findings: [
|
|
302
|
+
{ id: randomUUID(), sessionId: "sess-1", description: "Found bug", severity: "blocker" as const, timestamp: Date.now() },
|
|
303
|
+
],
|
|
304
|
+
decisions: [
|
|
305
|
+
{ id: randomUUID(), sessionId: "sess-1", description: "Use Bun", rationale: "Fastest runtime", timestamp: Date.now() },
|
|
306
|
+
],
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
await PlanStore.writePlan(planPath, data);
|
|
310
|
+
assert.ok(fs.existsSync(planPath));
|
|
311
|
+
|
|
312
|
+
const loaded = await PlanStore.readPlan(planPath);
|
|
313
|
+
|
|
314
|
+
// Tasks
|
|
315
|
+
assert.equal(loaded.tasks.length, 2);
|
|
316
|
+
assert.equal(loaded.tasks[0].description, "Task A");
|
|
317
|
+
assert.equal(loaded.tasks[0].status, "pending");
|
|
318
|
+
assert.equal(loaded.tasks[1].description, "Task B");
|
|
319
|
+
assert.equal(loaded.tasks[1].status, "completed");
|
|
320
|
+
|
|
321
|
+
// Memory
|
|
322
|
+
assert.equal(loaded.memory.length, 1);
|
|
323
|
+
assert.equal(loaded.memory[0].content, "Use TypeScript");
|
|
324
|
+
|
|
325
|
+
// Findings
|
|
326
|
+
assert.equal(loaded.findings.length, 1);
|
|
327
|
+
assert.equal(loaded.findings[0].description, "Found bug");
|
|
328
|
+
|
|
329
|
+
// Decisions
|
|
330
|
+
assert.equal(loaded.decisions.length, 1);
|
|
331
|
+
assert.equal(loaded.decisions[0].description, "Use Bun");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// ---- 18: addFinding appends to file ------------------------------------
|
|
335
|
+
|
|
336
|
+
it("addFinding appends a finding to the plan file", async () => {
|
|
337
|
+
const planPath = path.join(testDir, "plan-002.md");
|
|
338
|
+
const sessionId = "sess-test";
|
|
339
|
+
|
|
340
|
+
// Write initial empty plan
|
|
341
|
+
await PlanStore.writePlan(planPath, {
|
|
342
|
+
tasks: [],
|
|
343
|
+
memory: [],
|
|
344
|
+
findings: [],
|
|
345
|
+
decisions: [],
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
await PlanStore.addFinding(planPath, sessionId, {
|
|
349
|
+
description: "Critical bug discovered",
|
|
350
|
+
severity: "blocker",
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const data = await PlanStore.readPlan(planPath);
|
|
354
|
+
assert.equal(data.findings.length, 1);
|
|
355
|
+
assert.equal(data.findings[0].description, "Critical bug discovered");
|
|
356
|
+
assert.equal(data.findings[0].severity, "blocker");
|
|
357
|
+
assert.equal(data.findings[0].sessionId, sessionId);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// ---- 19: addDecision appends to file ------------------------------------
|
|
361
|
+
|
|
362
|
+
it("addDecision appends a decision to the plan file", async () => {
|
|
363
|
+
const planPath = path.join(testDir, "plan-003.md");
|
|
364
|
+
const sessionId = "sess-dec";
|
|
365
|
+
|
|
366
|
+
await PlanStore.writePlan(planPath, {
|
|
367
|
+
tasks: [],
|
|
368
|
+
memory: [],
|
|
369
|
+
findings: [],
|
|
370
|
+
decisions: [],
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
await PlanStore.addDecision(planPath, sessionId, {
|
|
374
|
+
description: "Use singleton pattern",
|
|
375
|
+
rationale: "Ensures single instance across sessions",
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const data = await PlanStore.readPlan(planPath);
|
|
379
|
+
assert.equal(data.decisions.length, 1);
|
|
380
|
+
assert.equal(data.decisions[0].description, "Use singleton pattern");
|
|
381
|
+
assert.equal(data.decisions[0].rationale, "Ensures single instance across sessions");
|
|
382
|
+
assert.equal(data.decisions[0].sessionId, sessionId);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// ---- 20: writePlan preserves existing header ----------------------------
|
|
386
|
+
|
|
387
|
+
it("writePlan preserves existing header", async () => {
|
|
388
|
+
const planPath = path.join(testDir, "plan-004.md");
|
|
389
|
+
|
|
390
|
+
// Write initial plan with header
|
|
391
|
+
const header = [
|
|
392
|
+
"# PLAN: test-project",
|
|
393
|
+
"Plan ID: test-project/plan-004.md",
|
|
394
|
+
"Status: active",
|
|
395
|
+
"",
|
|
396
|
+
].join("\n");
|
|
397
|
+
fs.writeFileSync(planPath, header, "utf8");
|
|
398
|
+
|
|
399
|
+
await PlanStore.writePlan(planPath, {
|
|
400
|
+
tasks: [{ id: randomUUID(), description: "Do something", status: "pending", dependsOn: [] }],
|
|
401
|
+
memory: [],
|
|
402
|
+
findings: [],
|
|
403
|
+
decisions: [],
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const content = fs.readFileSync(planPath, "utf8");
|
|
407
|
+
assert.ok(content.includes("# PLAN: test-project"));
|
|
408
|
+
assert.ok(content.includes("Status: active"));
|
|
409
|
+
assert.ok(content.includes("## Tasks"));
|
|
410
|
+
assert.ok(content.includes("Do something"));
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// ---- 21: getMerged returns empty for now --------------------------------
|
|
414
|
+
|
|
415
|
+
it("getMerged returns empty for now", async () => {
|
|
416
|
+
const result = await PlanStore.getMerged("session-1");
|
|
417
|
+
assert.deepEqual(result, []);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
// Integration: MemoryManager + PlanStore
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
|
|
425
|
+
describe("Memory integration", () => {
|
|
426
|
+
let testDir: string;
|
|
427
|
+
|
|
428
|
+
beforeEach(() => {
|
|
429
|
+
testDir = tmpdir();
|
|
430
|
+
MemoryManager.resetInstance();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
afterEach(() => {
|
|
434
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
435
|
+
MemoryManager.resetInstance();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("can persist memory entries through PlanStore", async () => {
|
|
439
|
+
const planPath = path.join(testDir, "plan-integration.md");
|
|
440
|
+
const mm = MemoryManager.getInstance();
|
|
441
|
+
|
|
442
|
+
mm.add(MemoryLevel.PROJECT, "Project convention: use Bun", 0.9);
|
|
443
|
+
mm.add(MemoryLevel.MISSION, "Implement memory system", 0.8);
|
|
444
|
+
|
|
445
|
+
// Export memory and write to plan
|
|
446
|
+
const snapshot = mm.export();
|
|
447
|
+
const allMemory = [
|
|
448
|
+
...snapshot.system,
|
|
449
|
+
...snapshot.project,
|
|
450
|
+
...snapshot.mission,
|
|
451
|
+
...snapshot.task,
|
|
452
|
+
];
|
|
453
|
+
|
|
454
|
+
await PlanStore.writePlan(planPath, {
|
|
455
|
+
tasks: [],
|
|
456
|
+
memory: allMemory,
|
|
457
|
+
findings: [],
|
|
458
|
+
decisions: [],
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Read back
|
|
462
|
+
const loaded = await PlanStore.readPlan(planPath);
|
|
463
|
+
assert.equal(loaded.memory.length, 2);
|
|
464
|
+
|
|
465
|
+
const contents = loaded.memory.map((m) => m.content);
|
|
466
|
+
assert.ok(contents.includes("Project convention: use Bun"));
|
|
467
|
+
assert.ok(contents.includes("Implement memory system"));
|
|
468
|
+
|
|
469
|
+
// Import back into fresh MemoryManager
|
|
470
|
+
MemoryManager.resetInstance();
|
|
471
|
+
const mm2 = MemoryManager.getInstance();
|
|
472
|
+
const restored: MemoryEntry[] = [];
|
|
473
|
+
|
|
474
|
+
// Split back into levels
|
|
475
|
+
for (const entry of loaded.memory) {
|
|
476
|
+
restored.push(entry);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Group by level and import
|
|
480
|
+
const importSnapshot = {
|
|
481
|
+
system: restored.filter((e) => e.level === MemoryLevel.SYSTEM),
|
|
482
|
+
project: restored.filter((e) => e.level === MemoryLevel.PROJECT),
|
|
483
|
+
mission: restored.filter((e) => e.level === MemoryLevel.MISSION),
|
|
484
|
+
task: restored.filter((e) => e.level === MemoryLevel.TASK),
|
|
485
|
+
};
|
|
486
|
+
mm2.import(importSnapshot);
|
|
487
|
+
|
|
488
|
+
assert.equal(mm2.getEntryCount(MemoryLevel.PROJECT), 1);
|
|
489
|
+
assert.equal(mm2.getEntryCount(MemoryLevel.MISSION), 1);
|
|
490
|
+
});
|
|
491
|
+
});
|