chapterhouse 0.5.2 → 0.7.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.
Files changed (40) hide show
  1. package/.pr-types.json +14 -0
  2. package/README.md +6 -0
  3. package/dist/api/agent-edit-access.js +11 -0
  4. package/dist/api/agents.api.test.js +48 -0
  5. package/dist/api/server.js +182 -11
  6. package/dist/api/server.test.js +334 -3
  7. package/dist/config.test.js +29 -0
  8. package/dist/copilot/agent-event-bus.js +1 -0
  9. package/dist/copilot/agents.js +114 -46
  10. package/dist/copilot/agents.mcp-servers.test.js +87 -0
  11. package/dist/copilot/agents.parse.test.js +69 -0
  12. package/dist/copilot/agents.test.js +125 -1
  13. package/dist/copilot/memory-coordinator.js +234 -0
  14. package/dist/copilot/memory-coordinator.test.js +257 -0
  15. package/dist/copilot/orchestrator.js +81 -221
  16. package/dist/copilot/orchestrator.test.js +238 -1
  17. package/dist/copilot/pr-title.js +92 -0
  18. package/dist/copilot/pr-title.test.js +54 -0
  19. package/dist/copilot/router.test.js +30 -0
  20. package/dist/copilot/session-manager.js +34 -0
  21. package/dist/copilot/threat-model.js +50 -0
  22. package/dist/copilot/threat-model.test.js +129 -0
  23. package/dist/copilot/tools.js +61 -37
  24. package/dist/copilot/tools.wiki.test.js +15 -6
  25. package/dist/setup.js +15 -5
  26. package/dist/setup.test.js +20 -3
  27. package/dist/sprint-merge.js +168 -0
  28. package/dist/sprint-merge.test.js +131 -0
  29. package/dist/store/db.js +63 -0
  30. package/dist/store/db.test.js +279 -0
  31. package/dist/test/setup-env.js +2 -1
  32. package/dist/test/setup-env.test.js +8 -1
  33. package/package.json +8 -1
  34. package/web/dist/assets/index-DuKYxMIR.css +10 -0
  35. package/web/dist/assets/index-DytB69KC.js +223 -0
  36. package/web/dist/assets/index-DytB69KC.js.map +1 -0
  37. package/web/dist/index.html +2 -2
  38. package/web/dist/assets/index-CPaILy2j.js +0 -223
  39. package/web/dist/assets/index-CPaILy2j.js.map +0 -1
  40. package/web/dist/assets/index-Cs7AGeaL.css +0 -10
@@ -0,0 +1,257 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ function makeScope(id, slug, title = slug, description = `${slug} scope`) {
4
+ return {
5
+ id,
6
+ slug,
7
+ title,
8
+ description,
9
+ keywords: [],
10
+ active: true,
11
+ createdAt: "2026-05-14T00:00:00.000Z",
12
+ updatedAt: "2026-05-14T00:00:00.000Z",
13
+ };
14
+ }
15
+ async function loadMemoryCoordinatorModule(t, overrides = {}) {
16
+ const chapterhouseScope = makeScope(1, "chapterhouse", "Chapterhouse", "Core Chapterhouse work.");
17
+ const wikiScope = makeScope(2, "wiki", "Wiki", "Wiki work.");
18
+ const state = {
19
+ config: {
20
+ copilotModel: "claude-sonnet-4.6",
21
+ memoryInjectEnabled: true,
22
+ memoryCheckpointEnabled: true,
23
+ memoryCheckpointTurns: 2,
24
+ memoryCheckpointOnScopeChange: true,
25
+ memoryCheckpointMinTurnsForScopeFire: 2,
26
+ memoryHousekeepingEnabled: true,
27
+ memoryHousekeepingTurns: 2,
28
+ },
29
+ activeScope: chapterhouseScope,
30
+ scopes: new Map([
31
+ [chapterhouseScope.slug, chapterhouseScope],
32
+ [wikiScope.slug, wikiScope],
33
+ ]),
34
+ sessionScopes: new Map([["default", chapterhouseScope]]),
35
+ hotTierXmlByScope: new Map([[chapterhouseScope.slug, "<memory_context scope=\"chapterhouse\">\n <decision>Ship it</decision>\n</memory_context>\n"]]),
36
+ checkpointRuns: [],
37
+ checkpointInFlight: false,
38
+ checkpointTickCalls: 0,
39
+ checkpointMarkFiredCalls: 0,
40
+ checkpointMarkScopeChangeFireCalls: 0,
41
+ checkpointResetCalls: 0,
42
+ housekeepingRuns: [],
43
+ housekeepingInFlight: false,
44
+ eotCalls: [],
45
+ ...overrides,
46
+ };
47
+ t.mock.module("../config.js", {
48
+ namedExports: {
49
+ config: state.config,
50
+ },
51
+ });
52
+ t.mock.module("../memory/active-scope.js", {
53
+ namedExports: {
54
+ getActiveScope: () => state.activeScope,
55
+ },
56
+ });
57
+ t.mock.module("../memory/scopes.js", {
58
+ namedExports: {
59
+ getScope: (slug) => state.scopes.get(slug) ?? null,
60
+ },
61
+ });
62
+ t.mock.module("../memory/hot-tier.js", {
63
+ namedExports: {
64
+ getHotTierEntries: (scopeId) => {
65
+ const scope = Array.from(state.scopes.values()).find((candidate) => candidate.id === scopeId) ?? null;
66
+ return { scope, entities: [], observations: [], decisions: [], actionItems: [] };
67
+ },
68
+ renderHotTierXML: (entries) => entries.scope ? (state.hotTierXmlByScope.get(entries.scope.slug) ?? "") : "",
69
+ renderHotTierForActiveScope: () => state.activeScope ? (state.hotTierXmlByScope.get(state.activeScope.slug) ?? "") : "",
70
+ },
71
+ });
72
+ t.mock.module("../memory/checkpoint.js", {
73
+ namedExports: {
74
+ CheckpointTracker: class {
75
+ turns = 0;
76
+ tickOrchestratorTurn() {
77
+ state.checkpointTickCalls++;
78
+ this.turns++;
79
+ }
80
+ shouldFire() {
81
+ return this.turns >= state.config.memoryCheckpointTurns;
82
+ }
83
+ turnsSinceLastFire() {
84
+ return this.turns;
85
+ }
86
+ markFired() {
87
+ state.checkpointMarkFiredCalls++;
88
+ this.turns = 0;
89
+ }
90
+ markScopeChangeFire() {
91
+ state.checkpointMarkScopeChangeFireCalls++;
92
+ this.turns = 0;
93
+ }
94
+ reset() {
95
+ state.checkpointResetCalls++;
96
+ this.turns = 0;
97
+ }
98
+ },
99
+ isCheckpointInFlight: () => state.checkpointInFlight,
100
+ runCheckpointExtraction: async (input) => {
101
+ state.checkpointRuns.push(input);
102
+ return { written: 0, skipped: 0, errors: [] };
103
+ },
104
+ },
105
+ });
106
+ t.mock.module("../memory/housekeeping.js", {
107
+ namedExports: {
108
+ isHousekeepingInFlight: () => state.housekeepingInFlight,
109
+ runHousekeeping: async (input) => {
110
+ state.housekeepingRuns.push(input);
111
+ return { scopeIds: input.scopeIds ?? [], summaries: [], totalExamined: 0, totalModified: 0, durationMs: 0 };
112
+ },
113
+ },
114
+ });
115
+ t.mock.module("../memory/eot.js", {
116
+ namedExports: {
117
+ runEndOfTaskMemoryHook: async (input) => {
118
+ state.eotCalls.push(input);
119
+ return {
120
+ task_id: String(input.taskId ?? "task"),
121
+ proposals_total: 0,
122
+ accepted: 0,
123
+ rejected: 0,
124
+ implicit_extracted: 0,
125
+ auto_accept: true,
126
+ };
127
+ },
128
+ },
129
+ });
130
+ const module = await import(new URL(`./memory-coordinator.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
131
+ return { module, state };
132
+ }
133
+ test("buildHotTierContext and buildPerTurnHooks inject trimmed scoped hot-tier memory", async (t) => {
134
+ const { module, state } = await loadMemoryCoordinatorModule(t);
135
+ const client = { name: "mock-client" };
136
+ const coordinator = new module.MemoryCoordinator({
137
+ getCopilotClient: () => client,
138
+ config: state.config,
139
+ resolveScopeForSession: (sessionKey) => state.sessionScopes.get(sessionKey) ?? null,
140
+ });
141
+ const hotTierContext = await coordinator.buildHotTierContext("default");
142
+ const hooks = coordinator.buildPerTurnHooks("default");
143
+ const hookResult = await hooks?.onUserPromptSubmitted?.();
144
+ assert.equal(hotTierContext, "<memory_context scope=\"chapterhouse\">\n <decision>Ship it</decision>\n</memory_context>");
145
+ assert.deepEqual(hookResult, {
146
+ additionalContext: "<memory_context scope=\"chapterhouse\">\n <decision>Ship it</decision>\n</memory_context>",
147
+ });
148
+ });
149
+ test("onTurnComplete schedules checkpoint and housekeeping at the configured turn boundary", async (t) => {
150
+ const { module, state } = await loadMemoryCoordinatorModule(t);
151
+ const coordinator = new module.MemoryCoordinator({
152
+ getCopilotClient: () => ({ name: "mock-client" }),
153
+ config: state.config,
154
+ resolveScopeForSession: (sessionKey) => state.sessionScopes.get(sessionKey) ?? null,
155
+ });
156
+ await coordinator.onTurnComplete("default", "First prompt", "First response", "web");
157
+ assert.equal(state.checkpointRuns.length, 0);
158
+ assert.equal(state.housekeepingRuns.length, 0);
159
+ await coordinator.onTurnComplete("default", "Second prompt", "Second response", "web");
160
+ assert.equal(state.checkpointMarkFiredCalls, 1);
161
+ assert.equal(state.checkpointRuns.length, 1);
162
+ assert.equal(state.housekeepingRuns.length, 1);
163
+ assert.deepEqual(state.housekeepingRuns[0]?.scopeIds, [1]);
164
+ assert.equal((state.checkpointRuns[0]?.activeScope).slug, "chapterhouse");
165
+ assert.deepEqual(state.checkpointRuns[0]?.turns, [
166
+ { user: "First prompt", assistant: "First response" },
167
+ { user: "Second prompt", assistant: "Second response" },
168
+ ]);
169
+ await coordinator.onTurnComplete("default", "Background prompt", "Background response", "background");
170
+ assert.equal(state.checkpointRuns.length, 1);
171
+ assert.equal(state.housekeepingRuns.length, 1);
172
+ });
173
+ test("onScopeChange triggers checkpoint extraction for the previous scope", async (t) => {
174
+ const { module, state } = await loadMemoryCoordinatorModule(t, {
175
+ config: {
176
+ copilotModel: "claude-sonnet-4.6",
177
+ memoryInjectEnabled: true,
178
+ memoryCheckpointEnabled: true,
179
+ memoryCheckpointTurns: 5,
180
+ memoryCheckpointOnScopeChange: true,
181
+ memoryCheckpointMinTurnsForScopeFire: 2,
182
+ memoryHousekeepingEnabled: true,
183
+ memoryHousekeepingTurns: 2,
184
+ },
185
+ });
186
+ const coordinator = new module.MemoryCoordinator({
187
+ getCopilotClient: () => ({ name: "mock-client" }),
188
+ config: state.config,
189
+ resolveScopeForSession: (sessionKey) => state.sessionScopes.get(sessionKey) ?? null,
190
+ });
191
+ await coordinator.onTurnComplete("default", "Turn one", "Reply one", "web");
192
+ await coordinator.onTurnComplete("default", "Turn two", "Reply two", "web");
193
+ state.checkpointRuns.length = 0;
194
+ await coordinator.onScopeChange("default", "chapterhouse", "wiki");
195
+ assert.equal(state.checkpointMarkScopeChangeFireCalls, 1);
196
+ assert.equal(state.checkpointRuns.length, 1);
197
+ assert.equal((state.checkpointRuns[0]?.activeScope).slug, "chapterhouse");
198
+ assert.equal(state.checkpointRuns[0]?.trigger, "scope_change");
199
+ assert.deepEqual(state.checkpointRuns[0]?.scopeChangeContext, { from: "chapterhouse", to: "wiki" });
200
+ });
201
+ test("onAgentTaskComplete runs the end-of-task memory hook with the mock client", async (t) => {
202
+ const { module, state } = await loadMemoryCoordinatorModule(t);
203
+ const client = { name: "mock-client" };
204
+ const coordinator = new module.MemoryCoordinator({
205
+ getCopilotClient: () => client,
206
+ config: state.config,
207
+ resolveScopeForSession: (sessionKey) => state.sessionScopes.get(sessionKey) ?? null,
208
+ });
209
+ await coordinator.onAgentTaskComplete("task-42", "Final delegated result");
210
+ assert.equal(state.eotCalls.length, 1);
211
+ assert.equal(state.eotCalls[0]?.taskId, "task-42");
212
+ assert.equal(state.eotCalls[0]?.finalResult, "Final delegated result");
213
+ assert.equal(state.eotCalls[0]?.copilotClient, client);
214
+ });
215
+ test("onAgentTaskComplete is a no-op for a duplicate task ID (double-fire guard)", async (t) => {
216
+ const { module, state } = await loadMemoryCoordinatorModule(t);
217
+ const client = { name: "mock-client" };
218
+ const coordinator = new module.MemoryCoordinator({
219
+ getCopilotClient: () => client,
220
+ config: state.config,
221
+ resolveScopeForSession: (sessionKey) => state.sessionScopes.get(sessionKey) ?? null,
222
+ });
223
+ await coordinator.onAgentTaskComplete("task-99", "result one");
224
+ await coordinator.onAgentTaskComplete("task-99", "result two");
225
+ assert.equal(state.eotCalls.length, 1);
226
+ assert.equal(state.eotCalls[0]?.taskId, "task-99");
227
+ assert.equal(state.eotCalls[0]?.finalResult, "result one");
228
+ });
229
+ test("reset clears turn buffers and tracker state for a session", async (t) => {
230
+ const { module, state } = await loadMemoryCoordinatorModule(t);
231
+ const coordinator = new module.MemoryCoordinator({
232
+ getCopilotClient: () => ({ name: "mock-client" }),
233
+ config: state.config,
234
+ resolveScopeForSession: (sessionKey) => state.sessionScopes.get(sessionKey) ?? null,
235
+ });
236
+ await coordinator.onTurnComplete("default", "Turn one", "Reply one", "web");
237
+ coordinator.reset("default");
238
+ await coordinator.onTurnComplete("default", "Turn two", "Reply two", "web");
239
+ await coordinator.onScopeChange("default", "chapterhouse", "wiki");
240
+ assert.equal(state.checkpointResetCalls, 1);
241
+ assert.equal(state.checkpointRuns.length, 0);
242
+ assert.equal(state.housekeepingRuns.length, 0);
243
+ });
244
+ test("shutdown clears session state across all tracked maps", async (t) => {
245
+ const { module, state } = await loadMemoryCoordinatorModule(t);
246
+ const coordinator = new module.MemoryCoordinator({
247
+ getCopilotClient: () => ({ name: "mock-client" }),
248
+ config: state.config,
249
+ resolveScopeForSession: (sessionKey) => state.sessionScopes.get(sessionKey) ?? null,
250
+ });
251
+ await coordinator.onTurnComplete("default", "Turn one", "Reply one", "web");
252
+ coordinator.shutdown();
253
+ await coordinator.onTurnComplete("default", "Turn two", "Reply two", "web");
254
+ assert.equal(state.checkpointRuns.length, 0);
255
+ assert.equal(state.housekeepingRuns.length, 0);
256
+ });
257
+ //# sourceMappingURL=memory-coordinator.test.js.map