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.
Files changed (143) hide show
  1. package/CONTEXT.md +10 -1
  2. package/README.md +54 -42
  3. package/bootstrap.ts +396 -142
  4. package/harness/agents/oh-browser.md +97 -0
  5. package/harness/agents/oh-builder.md +78 -0
  6. package/harness/agents/oh-facade.md +75 -0
  7. package/harness/agents/oh-fusion.md +45 -0
  8. package/harness/agents/oh-gauntlet.md +71 -0
  9. package/harness/agents/oh-grill.md +71 -0
  10. package/harness/agents/oh-investigate.md +60 -0
  11. package/harness/agents/oh-manifest.md +95 -0
  12. package/harness/agents/oh-plan-review.md +40 -0
  13. package/harness/agents/oh-planner.md +50 -0
  14. package/harness/agents/oh-refactor.md +37 -0
  15. package/harness/agents/oh-retro.md +46 -0
  16. package/harness/agents/oh-review.md +85 -0
  17. package/harness/agents/oh-security.md +83 -0
  18. package/harness/agents/oh-ship.md +76 -0
  19. package/harness/agents/oh-skill-craft.md +38 -0
  20. package/harness/agents/openhermes.md +28 -73
  21. package/harness/codex/AUTOPILOT.md +235 -87
  22. package/harness/codex/CHARTER.md +80 -0
  23. package/harness/instructions/SHELL.md +76 -0
  24. package/harness/lib/background/background.test.ts +197 -0
  25. package/harness/lib/background/index.ts +7 -0
  26. package/harness/lib/background/interfaces.ts +31 -0
  27. package/harness/lib/background/manager.ts +320 -0
  28. package/harness/lib/composer/compose.test.ts +168 -0
  29. package/harness/lib/composer/compose.ts +65 -0
  30. package/harness/lib/composer/fragments/01-identity.md +1 -0
  31. package/harness/lib/composer/fragments/02-delegation.md +6 -0
  32. package/harness/lib/composer/fragments/03-permissions.md +13 -0
  33. package/harness/lib/composer/fragments/04-task-flow.md +15 -0
  34. package/harness/lib/composer/fragments/05-confidence.md +5 -0
  35. package/harness/lib/composer/fragments/06-parallelization.md +17 -0
  36. package/harness/lib/composer/fragments/07-shell.md +41 -0
  37. package/harness/lib/composer/fragments/08-routing.md +8 -0
  38. package/harness/lib/composer/fragments/09-guardrails.md +12 -0
  39. package/harness/lib/composer/index.ts +1 -0
  40. package/harness/lib/hooks/builtins/confidence-gate-hook.ts +70 -0
  41. package/harness/lib/hooks/builtins/delegation-depth-hook.ts +59 -0
  42. package/harness/lib/hooks/builtins/error-recovery-hook.ts +107 -0
  43. package/harness/lib/hooks/builtins/memory-sync-hook.ts +73 -0
  44. package/harness/lib/hooks/builtins/plan-check-hook.ts +43 -0
  45. package/harness/lib/hooks/builtins/route-tracking-hook.ts +147 -0
  46. package/harness/lib/hooks/builtins/sanity-check-hook.ts +52 -0
  47. package/harness/lib/hooks/builtins/shell-detect-hook.ts +96 -0
  48. package/harness/lib/hooks/hooks.test.ts +1016 -0
  49. package/harness/lib/hooks/index.ts +30 -0
  50. package/harness/lib/hooks/registry.ts +416 -0
  51. package/harness/lib/hooks/types.ts +71 -0
  52. package/harness/lib/memory/index.ts +18 -0
  53. package/harness/lib/memory/interfaces.ts +53 -0
  54. package/harness/lib/memory/memory-manager.ts +205 -0
  55. package/harness/lib/memory/memory.test.ts +491 -0
  56. package/harness/lib/memory/plan-store.ts +366 -0
  57. package/harness/lib/recovery/handler.ts +243 -0
  58. package/harness/lib/recovery/index.ts +14 -0
  59. package/harness/lib/recovery/interfaces.ts +48 -0
  60. package/harness/lib/recovery/patterns.ts +149 -0
  61. package/harness/lib/recovery/recovery.test.ts +312 -0
  62. package/harness/lib/sanity/anomaly-tracker.ts +127 -0
  63. package/harness/lib/sanity/checker.ts +178 -0
  64. package/harness/lib/sanity/index.ts +13 -0
  65. package/harness/lib/sanity/interfaces.ts +24 -0
  66. package/harness/lib/sanity/sanity.test.ts +472 -0
  67. package/harness/lib/sync/file-watcher.ts +174 -0
  68. package/harness/lib/sync/index.ts +11 -0
  69. package/harness/lib/sync/interfaces.ts +27 -0
  70. package/harness/lib/sync/plan-sync.ts +536 -0
  71. package/harness/lib/sync/sync.test.ts +832 -0
  72. package/harness/skills/oh-ascii/DEEP.md +292 -0
  73. package/harness/skills/oh-ascii/SKILL.md +31 -0
  74. package/harness/skills/oh-ascii/scripts/check_ascii_alignment.py +596 -0
  75. package/harness/skills/oh-browser/DEEP.md +54 -0
  76. package/harness/skills/oh-browser/SKILL.md +30 -0
  77. package/harness/skills/oh-builder/DEEP.md +63 -0
  78. package/harness/skills/oh-builder/SKILL.md +12 -90
  79. package/harness/skills/oh-expert/DEEP.md +85 -0
  80. package/harness/skills/oh-expert/SKILL.md +13 -106
  81. package/harness/skills/oh-facade/DEEP.md +182 -0
  82. package/harness/skills/oh-facade/SKILL.md +15 -279
  83. package/harness/skills/oh-freeze/DEEP.md +18 -0
  84. package/harness/skills/oh-freeze/SKILL.md +10 -19
  85. package/harness/skills/oh-full-output/DEEP.md +25 -0
  86. package/harness/skills/oh-full-output/SKILL.md +12 -65
  87. package/harness/skills/oh-fusion/DEEP.md +120 -0
  88. package/harness/skills/oh-fusion/SKILL.md +17 -295
  89. package/harness/skills/oh-gauntlet/DEEP.md +77 -0
  90. package/harness/skills/oh-gauntlet/SKILL.md +13 -105
  91. package/harness/skills/oh-grill/DEEP.md +51 -0
  92. package/harness/skills/oh-grill/SKILL.md +12 -63
  93. package/harness/skills/oh-guard/DEEP.md +19 -0
  94. package/harness/skills/oh-guard/SKILL.md +10 -24
  95. package/harness/skills/oh-handoff/DEEP.md +48 -0
  96. package/harness/skills/oh-handoff/SKILL.md +13 -23
  97. package/harness/skills/oh-health/DEEP.md +74 -0
  98. package/harness/skills/oh-health/SKILL.md +13 -76
  99. package/harness/skills/oh-init/DEEP.md +85 -0
  100. package/harness/skills/oh-init/SKILL.md +13 -127
  101. package/harness/skills/oh-investigate/DEEP.md +171 -0
  102. package/harness/skills/oh-investigate/SKILL.md +13 -66
  103. package/harness/skills/oh-issue/DEEP.md +21 -0
  104. package/harness/skills/oh-issue/SKILL.md +11 -27
  105. package/harness/skills/oh-manifest/DEEP.md +92 -0
  106. package/harness/skills/oh-manifest/SKILL.md +12 -109
  107. package/harness/skills/oh-plan-review/DEEP.md +90 -0
  108. package/harness/skills/oh-plan-review/SKILL.md +13 -115
  109. package/harness/skills/oh-planner/DEEP.md +172 -0
  110. package/harness/skills/oh-planner/SKILL.md +12 -149
  111. package/harness/skills/oh-prd/DEEP.md +45 -0
  112. package/harness/skills/oh-prd/SKILL.md +10 -26
  113. package/harness/skills/oh-refactor/DEEP.md +122 -0
  114. package/harness/skills/oh-refactor/SKILL.md +17 -410
  115. package/harness/skills/oh-retro/DEEP.md +26 -0
  116. package/harness/skills/oh-retro/SKILL.md +12 -24
  117. package/harness/skills/oh-review/DEEP.md +87 -0
  118. package/harness/skills/oh-review/SKILL.md +11 -97
  119. package/harness/skills/oh-security/DEEP.md +83 -0
  120. package/harness/skills/oh-security/SKILL.md +14 -96
  121. package/harness/skills/oh-ship/DEEP.md +141 -0
  122. package/harness/skills/oh-ship/SKILL.md +14 -32
  123. package/harness/skills/oh-skill-craft/DEEP.md +369 -0
  124. package/harness/skills/oh-skill-craft/SKILL.md +13 -177
  125. package/harness/skills/oh-skills-link/DEEP.md +16 -0
  126. package/harness/skills/oh-skills-link/SKILL.md +10 -20
  127. package/harness/skills/oh-skills-list/DEEP.md +20 -0
  128. package/harness/skills/oh-skills-list/SKILL.md +9 -22
  129. package/harness/skills/oh-triage/DEEP.md +23 -0
  130. package/harness/skills/oh-triage/SKILL.md +8 -24
  131. package/harness/skills/oh-worktree/DEEP.md +169 -0
  132. package/harness/skills/oh-worktree/SKILL.md +32 -0
  133. package/lib/harness-resolver.ts +8 -10
  134. package/package.json +7 -5
  135. package/tsconfig.json +1 -1
  136. package/harness/codex/CONSTITUTION.md +0 -73
  137. package/harness/codex/ROUTING.md +0 -92
  138. package/harness/commands/oh-doctor.md +0 -26
  139. package/harness/commands/oh-log.md +0 -18
  140. package/harness/instructions/RUNTIME.md +0 -30
  141. package/harness/skills/oh-caveman/SKILL.md +0 -42
  142. package/harness/skills/oh-learn/SKILL.md +0 -101
  143. 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
+ });