openhermes 4.12.1 → 4.13.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.
- package/CONTEXT.md +6 -6
- package/ETHOS.md +2 -2
- package/README.md +11 -17
- package/bootstrap.ts +118 -126
- package/docs/HOW-IT-WORKS.md +162 -0
- package/docs/adr/ADR-0001-rebuild-vs-increment.md +30 -0
- package/docs/adr/ADR-0002-routing-graph-vs-linear-chain.md +36 -0
- package/docs/adr/ADR-0003-per-directory-plan-storage.md +34 -0
- package/docs/adr/ADR-0004-composer-fragment-architecture.md +42 -0
- package/docs/adr/ADR-0005-hook-system-design.md +42 -0
- package/docs/adr/README.md +9 -0
- package/harness/codex/AUTOPILOT.md +35 -40
- package/harness/codex/CHARTER.md +3 -3
- package/harness/lib/composer/compose.test.ts +29 -29
- package/harness/lib/composer/fragments/02-delegation.md +5 -5
- package/harness/lib/composer/fragments/04-task-flow.md +13 -13
- package/harness/lib/composer/fragments/08-routing.md +1 -1
- package/harness/lib/composer/fragments/09-guardrails.md +25 -25
- package/harness/lib/composer/index.ts +1 -1
- package/harness/lib/guards/guard-config.ts +72 -72
- package/harness/lib/hooks/builtins/confidence-gate-hook.ts +9 -9
- package/harness/lib/hooks/builtins/delegation-depth-hook.ts +1 -1
- package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -99
- package/harness/lib/hooks/builtins/next-route-hook.ts +24 -24
- package/harness/lib/hooks/builtins/plan-check-hook.ts +5 -5
- package/harness/lib/hooks/builtins/route-tracking-hook.ts +1 -1
- package/harness/lib/hooks/hooks.test.ts +160 -324
- package/harness/lib/hooks/index.ts +38 -42
- package/harness/lib/hooks/registry.ts +309 -416
- package/harness/lib/hooks/types.ts +116 -119
- package/harness/lib/plans/plan-location.ts +134 -134
- package/harness/lib/routing/index.ts +21 -21
- package/harness/lib/routing/route-guidance.ts +147 -147
- package/harness/lib/routing/route-resolver.ts +58 -58
- package/harness/lib/routing/routing.test.ts +195 -195
- package/harness/lib/routing/skill-frontmatter.ts +125 -125
- package/harness/lib/routing/types.ts +52 -52
- package/harness/skills/oh-ascii/SKILL.md +1 -1
- package/harness/skills/oh-fusion/DEEP.md +109 -109
- package/harness/skills/oh-fusion/SKILL.md +47 -47
- package/harness/skills/oh-init/DEEP.md +2 -2
- package/harness/skills/oh-plan-review/DEEP.md +1 -1
- package/harness/skills/oh-planner/DEEP.md +3 -3
- package/harness/skills/oh-review/DEEP.md +5 -5
- package/package.json +56 -53
- package/harness/lib/background/background.test.ts +0 -216
- package/harness/lib/background/index.ts +0 -7
- package/harness/lib/background/interfaces.ts +0 -31
- package/harness/lib/background/manager.ts +0 -320
- package/harness/lib/hooks/builtins/error-recovery-hook.ts +0 -107
- package/harness/lib/hooks/builtins/memory-sync-hook.ts +0 -73
- package/harness/lib/hooks/builtins/sanity-check-hook.ts +0 -52
- package/harness/lib/hooks/builtins/subagent-failure-hook.ts +0 -93
- package/harness/lib/memory/index.ts +0 -18
- package/harness/lib/memory/interfaces.ts +0 -53
- package/harness/lib/memory/memory-manager.ts +0 -205
- package/harness/lib/memory/memory.test.ts +0 -485
- package/harness/lib/memory/plan-store.ts +0 -346
- package/harness/lib/recovery/handler.ts +0 -243
- package/harness/lib/recovery/index.ts +0 -14
- package/harness/lib/recovery/interfaces.ts +0 -48
- package/harness/lib/recovery/patterns.ts +0 -149
- package/harness/lib/recovery/recovery.test.ts +0 -312
- package/harness/lib/sanity/anomaly-tracker.ts +0 -127
- package/harness/lib/sanity/checker.ts +0 -189
- package/harness/lib/sanity/index.ts +0 -13
- package/harness/lib/sanity/interfaces.ts +0 -24
- package/harness/lib/sanity/sanity.test.ts +0 -472
- package/harness/lib/sync/file-watcher.ts +0 -175
- package/harness/lib/sync/index.ts +0 -11
- package/harness/lib/sync/interfaces.ts +0 -27
- package/harness/lib/sync/plan-sync.ts +0 -533
- package/harness/lib/sync/sync.test.ts +0 -858
|
@@ -1,485 +0,0 @@
|
|
|
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
|
-
});
|
|
414
|
-
|
|
415
|
-
// ---------------------------------------------------------------------------
|
|
416
|
-
// Integration: MemoryManager + PlanStore
|
|
417
|
-
// ---------------------------------------------------------------------------
|
|
418
|
-
|
|
419
|
-
describe("Memory integration", () => {
|
|
420
|
-
let testDir: string;
|
|
421
|
-
|
|
422
|
-
beforeEach(() => {
|
|
423
|
-
testDir = tmpdir();
|
|
424
|
-
MemoryManager.resetInstance();
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
afterEach(() => {
|
|
428
|
-
fs.rmSync(testDir, { recursive: true, force: true });
|
|
429
|
-
MemoryManager.resetInstance();
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
it("can persist memory entries through PlanStore", async () => {
|
|
433
|
-
const planPath = path.join(testDir, "plan-integration.md");
|
|
434
|
-
const mm = MemoryManager.getInstance();
|
|
435
|
-
|
|
436
|
-
mm.add(MemoryLevel.PROJECT, "Project convention: use Bun", 0.9);
|
|
437
|
-
mm.add(MemoryLevel.MISSION, "Implement memory system", 0.8);
|
|
438
|
-
|
|
439
|
-
// Export memory and write to plan
|
|
440
|
-
const snapshot = mm.export();
|
|
441
|
-
const allMemory = [
|
|
442
|
-
...snapshot.system,
|
|
443
|
-
...snapshot.project,
|
|
444
|
-
...snapshot.mission,
|
|
445
|
-
...snapshot.task,
|
|
446
|
-
];
|
|
447
|
-
|
|
448
|
-
await PlanStore.writePlan(planPath, {
|
|
449
|
-
tasks: [],
|
|
450
|
-
memory: allMemory,
|
|
451
|
-
findings: [],
|
|
452
|
-
decisions: [],
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
// Read back
|
|
456
|
-
const loaded = await PlanStore.readPlan(planPath);
|
|
457
|
-
assert.equal(loaded.memory.length, 2);
|
|
458
|
-
|
|
459
|
-
const contents = loaded.memory.map((m) => m.content);
|
|
460
|
-
assert.ok(contents.includes("Project convention: use Bun"));
|
|
461
|
-
assert.ok(contents.includes("Implement memory system"));
|
|
462
|
-
|
|
463
|
-
// Import back into fresh MemoryManager
|
|
464
|
-
MemoryManager.resetInstance();
|
|
465
|
-
const mm2 = MemoryManager.getInstance();
|
|
466
|
-
const restored: MemoryEntry[] = [];
|
|
467
|
-
|
|
468
|
-
// Split back into levels
|
|
469
|
-
for (const entry of loaded.memory) {
|
|
470
|
-
restored.push(entry);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Group by level and import
|
|
474
|
-
const importSnapshot = {
|
|
475
|
-
system: restored.filter((e) => e.level === MemoryLevel.SYSTEM),
|
|
476
|
-
project: restored.filter((e) => e.level === MemoryLevel.PROJECT),
|
|
477
|
-
mission: restored.filter((e) => e.level === MemoryLevel.MISSION),
|
|
478
|
-
task: restored.filter((e) => e.level === MemoryLevel.TASK),
|
|
479
|
-
};
|
|
480
|
-
mm2.import(importSnapshot);
|
|
481
|
-
|
|
482
|
-
assert.equal(mm2.getEntryCount(MemoryLevel.PROJECT), 1);
|
|
483
|
-
assert.equal(mm2.getEntryCount(MemoryLevel.MISSION), 1);
|
|
484
|
-
});
|
|
485
|
-
});
|