chapterhouse 0.9.1 → 0.10.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 (112) hide show
  1. package/README.md +1 -1
  2. package/agents/korg.agent.md +20 -0
  3. package/dist/api/auth.js +11 -1
  4. package/dist/api/auth.test.js +29 -0
  5. package/dist/api/errors.js +23 -0
  6. package/dist/api/route-coverage.test.js +61 -21
  7. package/dist/api/routes/agents.js +472 -0
  8. package/dist/api/routes/memory.js +299 -0
  9. package/dist/api/routes/projects.js +170 -0
  10. package/dist/api/routes/sessions.js +347 -0
  11. package/dist/api/routes/system.js +82 -0
  12. package/dist/api/routes/wiki.js +455 -0
  13. package/dist/api/routes/wiki.test.js +49 -0
  14. package/dist/api/send-json.js +16 -0
  15. package/dist/api/send-json.test.js +18 -0
  16. package/dist/api/server-runtime.js +45 -3
  17. package/dist/api/server.js +34 -1764
  18. package/dist/api/server.test.js +239 -8
  19. package/dist/api/sse-hub.js +37 -0
  20. package/dist/cli.js +1 -1
  21. package/dist/config.js +151 -58
  22. package/dist/config.test.js +29 -0
  23. package/dist/copilot/okr-mapper.js +2 -11
  24. package/dist/copilot/orchestrator.js +358 -352
  25. package/dist/copilot/orchestrator.test.js +139 -4
  26. package/dist/copilot/prompt-date.js +2 -1
  27. package/dist/copilot/session-manager.js +25 -23
  28. package/dist/copilot/session-manager.test.js +35 -1
  29. package/dist/copilot/standup.js +2 -2
  30. package/dist/copilot/task-event-log.js +7 -1
  31. package/dist/copilot/task-event-log.test.js +13 -0
  32. package/dist/copilot/tools/agent.js +608 -0
  33. package/dist/copilot/tools/index.js +19 -0
  34. package/dist/copilot/tools/memory.js +678 -0
  35. package/dist/copilot/tools/models.js +2 -0
  36. package/dist/copilot/tools/okr.js +171 -0
  37. package/dist/copilot/tools/wiki.js +333 -0
  38. package/dist/copilot/tools-deps.js +4 -0
  39. package/dist/copilot/tools.agent.test.js +10 -8
  40. package/dist/copilot/tools.inventory.test.js +76 -0
  41. package/dist/copilot/tools.js +1 -1725
  42. package/dist/copilot/tools.okr.test.js +31 -0
  43. package/dist/copilot/tools.wiki.test.js +358 -6
  44. package/dist/copilot/turn-event-log.js +31 -4
  45. package/dist/copilot/turn-event-log.test.js +24 -2
  46. package/dist/copilot/workiq-installer.test.js +2 -2
  47. package/dist/daemon-install.js +3 -2
  48. package/dist/daemon.js +9 -17
  49. package/dist/integrations/ado-client.js +90 -9
  50. package/dist/integrations/ado-client.test.js +56 -0
  51. package/dist/integrations/team-push.js +1 -0
  52. package/dist/integrations/team-push.test.js +6 -0
  53. package/dist/integrations/teams-notify.js +1 -0
  54. package/dist/integrations/teams-notify.test.js +5 -0
  55. package/dist/memory/active-scope.test.js +0 -1
  56. package/dist/memory/checkpoint.js +89 -72
  57. package/dist/memory/checkpoint.test.js +23 -3
  58. package/dist/memory/eot.js +194 -89
  59. package/dist/memory/eot.test.js +186 -3
  60. package/dist/memory/hooks.js +2 -4
  61. package/dist/memory/housekeeping-scheduler.js +1 -1
  62. package/dist/memory/housekeeping-scheduler.test.js +1 -2
  63. package/dist/memory/housekeeping.js +100 -3
  64. package/dist/memory/housekeeping.test.js +33 -2
  65. package/dist/memory/reflect.test.js +2 -0
  66. package/dist/memory/scope-lock.js +26 -0
  67. package/dist/memory/scope-lock.test.js +118 -0
  68. package/dist/memory/scopes.test.js +0 -1
  69. package/dist/mode-context.js +58 -5
  70. package/dist/mode-context.test.js +68 -0
  71. package/dist/paths.js +1 -0
  72. package/dist/setup.js +3 -2
  73. package/dist/shared/api-schemas.js +48 -5
  74. package/dist/store/connection.js +96 -0
  75. package/dist/store/db.js +5 -1498
  76. package/dist/store/db.test.js +182 -1
  77. package/dist/store/migrations.js +460 -0
  78. package/dist/store/repositories/memory.js +281 -0
  79. package/dist/store/repositories/okr.js +3 -0
  80. package/dist/store/repositories/projects.js +5 -0
  81. package/dist/store/repositories/sessions.js +284 -0
  82. package/dist/store/repositories/wiki.js +60 -0
  83. package/dist/store/schema.js +501 -0
  84. package/dist/util/logger.js +3 -2
  85. package/dist/wiki/consolidation.js +50 -9
  86. package/dist/wiki/consolidation.test.js +45 -0
  87. package/dist/wiki/frontmatter.js +45 -14
  88. package/dist/wiki/frontmatter.test.js +26 -1
  89. package/dist/wiki/fs.js +16 -4
  90. package/dist/wiki/fs.test.js +84 -0
  91. package/dist/wiki/index-manager.js +30 -2
  92. package/dist/wiki/index-manager.test.js +43 -12
  93. package/dist/wiki/ingest.js +17 -1
  94. package/dist/wiki/lock.js +11 -1
  95. package/dist/wiki/log-manager.js +2 -7
  96. package/dist/wiki/migrate.js +44 -17
  97. package/dist/wiki/project-registry.js +10 -5
  98. package/dist/wiki/project-registry.test.js +14 -0
  99. package/dist/wiki/scheduler.js +1 -1
  100. package/dist/wiki/seed-team-wiki.js +2 -1
  101. package/dist/wiki/team-sync.js +31 -6
  102. package/dist/wiki/team-sync.test.js +81 -0
  103. package/package.json +1 -1
  104. package/web/dist/assets/WikiEdit-BZXAdarz.js +30 -0
  105. package/web/dist/assets/WikiEdit-BZXAdarz.js.map +1 -0
  106. package/web/dist/assets/WikiGraph-KrCYco4v.js +2 -0
  107. package/web/dist/assets/WikiGraph-KrCYco4v.js.map +1 -0
  108. package/web/dist/assets/index-CUm2Wbuh.js +250 -0
  109. package/web/dist/assets/index-CUm2Wbuh.js.map +1 -0
  110. package/web/dist/index.html +1 -1
  111. package/web/dist/assets/index-iQrv3lQN.js +0 -286
  112. package/web/dist/assets/index-iQrv3lQN.js.map +0 -1
@@ -14,6 +14,7 @@ test("supports API_TOKEN env var and personal health route helpers", async () =>
14
14
  assert.equal(typeof runtime.createPublicConfigPayload, "function", "createPublicConfigPayload should be exported");
15
15
  assert.equal(typeof runtime.getDisplayHost, "function", "getDisplayHost should be exported");
16
16
  assert.equal(typeof runtime.assertAuthenticationConfigured, "function", "assertAuthenticationConfigured should be exported");
17
+ assert.equal(typeof runtime.setupSseCleanup, "function", "setupSseCleanup should be exported");
17
18
  const token = runtime.resolveApiToken({
18
19
  envToken: "personal-token",
19
20
  tokenPath: "/tmp/chapterhouse-api-token",
@@ -25,6 +26,10 @@ test("supports API_TOKEN env var and personal health route helpers", async () =>
25
26
  status: "ok",
26
27
  timestamp: "2026-05-06T00:00:00.000Z",
27
28
  });
29
+ assert.deepEqual(runtime.createHealthPayload(new Date("2026-05-06T00:00:00.000Z"), { fts5Ready: false }), {
30
+ status: "initializing",
31
+ timestamp: "2026-05-06T00:00:00.000Z",
32
+ });
28
33
  assert.deepEqual(runtime.createPublicConfigPayload({
29
34
  entraAuthEnabled: true,
30
35
  standaloneMode: false,
@@ -50,19 +55,51 @@ test("supports API_TOKEN env var and personal health route helpers", async () =>
50
55
  standalone: false,
51
56
  chatSseEnabled: true,
52
57
  });
53
- assert.doesNotThrow(() => runtime.assertAuthenticationConfigured({
58
+ const assertAuthenticationConfigured = runtime.assertAuthenticationConfigured;
59
+ assert.doesNotThrow(() => assertAuthenticationConfigured({
54
60
  entraAuthEnabled: false,
55
61
  apiToken: null,
62
+ apiHost: "127.0.0.1",
56
63
  }));
57
- assert.doesNotThrow(() => runtime.assertAuthenticationConfigured({
64
+ assert.throws(() => assertAuthenticationConfigured({
65
+ entraAuthEnabled: false,
66
+ apiToken: null,
67
+ apiHost: "0.0.0.0",
68
+ }), /Refusing to start without authentication on non-loopback bind 0\.0\.0\.0.*API_TOKEN.*Entra auth/s);
69
+ assert.doesNotThrow(() => assertAuthenticationConfigured({
58
70
  entraAuthEnabled: false,
59
71
  apiToken: "personal-token",
72
+ apiHost: "0.0.0.0",
60
73
  }));
61
- assert.doesNotThrow(() => runtime.assertAuthenticationConfigured({
74
+ assert.doesNotThrow(() => assertAuthenticationConfigured({
62
75
  entraAuthEnabled: true,
63
76
  apiToken: null,
77
+ apiHost: "0.0.0.0",
64
78
  }));
65
79
  });
80
+ test("setupSseCleanup cleans already-registered callbacks when setup throws", async () => {
81
+ const runtime = await import("./server-runtime.js");
82
+ const cleanupCalls = [];
83
+ const setupSseCleanup = runtime.setupSseCleanup;
84
+ assert.throws(() => setupSseCleanup((registerCleanup) => {
85
+ registerCleanup(() => cleanupCalls.push("heartbeat"));
86
+ registerCleanup(() => cleanupCalls.push("task-log"));
87
+ throw new Error("boom");
88
+ }), /boom/);
89
+ assert.deepEqual(cleanupCalls, ["task-log", "heartbeat"]);
90
+ });
91
+ test("setupSseCleanup only runs cleanup once after successful setup", async () => {
92
+ const runtime = await import("./server-runtime.js");
93
+ const cleanupCalls = [];
94
+ const setupSseCleanup = runtime.setupSseCleanup;
95
+ const cleanup = setupSseCleanup((registerCleanup) => {
96
+ registerCleanup(() => cleanupCalls.push("heartbeat"));
97
+ registerCleanup(() => cleanupCalls.push("task-log"));
98
+ });
99
+ cleanup();
100
+ cleanup();
101
+ assert.deepEqual(cleanupCalls, ["task-log", "heartbeat"]);
102
+ });
66
103
  test("createPublicConfigPayload defaults chat SSE on when not explicitly provided", async () => {
67
104
  const runtime = await import("./server-runtime.js");
68
105
  assert.equal(typeof runtime.createPublicConfigPayload, "function", "createPublicConfigPayload should be exported");
@@ -219,7 +256,7 @@ async function withStartedServer(run, extraEnv = {}, timeoutMs = DEFAULT_API_SER
219
256
  const baseUrl = `http://127.0.0.1:${port}`;
220
257
  try {
221
258
  await waitForApiServerReady({ child, baseUrl, logs, timeoutMs });
222
- await run({ baseUrl, authHeader: "Bearer route-token", testRoot });
259
+ await run({ baseUrl, authHeader: "Bearer route-token", testRoot, logs });
223
260
  }
224
261
  finally {
225
262
  await stopChild(child);
@@ -564,6 +601,44 @@ test("server returns 400 when PATCH /api/agents/:slug cannot parse malformed age
564
601
  assert.match((await response.json()).error, /^Invalid content:/);
565
602
  });
566
603
  });
604
+ test("server rejects unknown fields in PATCH /api/agents/:slug", async () => {
605
+ await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
606
+ writeAgentFile(testRoot, "scribe", [
607
+ "---",
608
+ "name: Scribe",
609
+ "description: Drafts release notes",
610
+ "model: claude-sonnet-4.6",
611
+ "---",
612
+ "",
613
+ "Original system prompt.",
614
+ ].join("\n"));
615
+ const response = await fetch(`${baseUrl}/api/agents/scribe`, {
616
+ method: "PATCH",
617
+ headers: {
618
+ authorization: authHeader,
619
+ "content-type": "application/json",
620
+ },
621
+ body: JSON.stringify({ description: "Updated", slug: "scribe" }),
622
+ });
623
+ assert.equal(response.status, 400);
624
+ assert.match((await response.json()).error, /slug|unrecognized/i);
625
+ });
626
+ });
627
+ test("server honors CHAPTERHOUSE_JSON_LIMIT for request bodies", async () => {
628
+ await withStartedServer(async ({ baseUrl, authHeader }) => {
629
+ const response = await fetch(`${baseUrl}/api/model`, {
630
+ method: "POST",
631
+ headers: {
632
+ authorization: authHeader,
633
+ "content-type": "application/json",
634
+ },
635
+ body: JSON.stringify({ model: "x".repeat(2_000) }),
636
+ });
637
+ assert.equal(response.status, 413);
638
+ }, {
639
+ CHAPTERHOUSE_JSON_LIMIT: "1kb",
640
+ });
641
+ });
567
642
  test("server returns 404 when confirming reload for an unknown agent", async () => {
568
643
  await withStartedServer(async ({ baseUrl, authHeader }) => {
569
644
  const response = await fetch(`${baseUrl}/api/agents/missing/reload-confirm`, {
@@ -574,8 +649,8 @@ test("server returns 404 when confirming reload for an unknown agent", async ()
574
649
  assert.match(await response.text(), /Agent not found/i);
575
650
  });
576
651
  });
577
- test("server runs in standalone mode without auth", async () => {
578
- await withStartedServer(async ({ baseUrl }) => {
652
+ test("server runs in standalone mode without auth on loopback and logs a warning", async () => {
653
+ await withStartedServer(async ({ baseUrl, logs }) => {
579
654
  const bootstrap = await fetch(`${baseUrl}/api/bootstrap`);
580
655
  assert.equal(bootstrap.status, 200);
581
656
  assert.deepEqual(await bootstrap.json(), { authMode: "standalone" });
@@ -590,10 +665,46 @@ test("server runs in standalone mode without auth", async () => {
590
665
  const model = await fetch(`${baseUrl}/api/model`);
591
666
  assert.equal(model.status, 200);
592
667
  assert.deepEqual(await model.json(), { model: "claude-sonnet-4.6" });
668
+ assert.match(logs.join(""), /Running without authentication on loopback/);
593
669
  }, {
594
670
  API_TOKEN: "",
671
+ LOG_LEVEL: "warn",
595
672
  }, STANDALONE_API_SERVER_STARTUP_TIMEOUT_MS);
596
673
  });
674
+ test("server refuses standalone mode on non-loopback binds", async () => {
675
+ const testRoot = join(repoRoot, ".test-work", `server-routes-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
676
+ mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
677
+ rmSync(testRoot, { recursive: true, force: true });
678
+ mkdirSync(testRoot, { recursive: true });
679
+ const port = await getFreePort();
680
+ const logs = [];
681
+ const child = spawn(process.execPath, [
682
+ "--input-type=module",
683
+ "-e",
684
+ "import { startApiServer } from './dist/api/server.js'; await startApiServer();",
685
+ ], {
686
+ cwd: repoRoot,
687
+ env: {
688
+ ...Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith("COPILOT_"))),
689
+ CHAPTERHOUSE_DISABLE_DOTENV: "1",
690
+ CHAPTERHOUSE_HOME: testRoot,
691
+ API_HOST: "0.0.0.0",
692
+ API_PORT: String(port),
693
+ API_TOKEN: "",
694
+ LOG_LEVEL: "warn",
695
+ },
696
+ stdio: ["ignore", "pipe", "pipe"],
697
+ });
698
+ child.stdout?.on("data", (chunk) => logs.push(String(chunk)));
699
+ child.stderr?.on("data", (chunk) => logs.push(String(chunk)));
700
+ const exitCode = await new Promise((resolve) => {
701
+ child.once("exit", (code) => resolve(code));
702
+ });
703
+ assert.equal(exitCode, 1);
704
+ assert.match(logs.join(""), /Refusing to start without authentication on non-loopback bind 0\.0\.0\.0/);
705
+ assert.match(logs.join(""), /Set API_TOKEN or configure Entra auth/);
706
+ rmSync(testRoot, { recursive: true, force: true });
707
+ });
597
708
  test("server exposes the active memory scope API and requires auth", async () => {
598
709
  await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
599
710
  const unauthorized = await fetch(`${baseUrl}/api/memory/active-scope`);
@@ -774,6 +885,30 @@ test("POST /api/wiki/page/pin rejects path traversal slugs", async () => {
774
885
  assert.match(body.error ?? "", /unsafe wiki path/i);
775
886
  });
776
887
  });
888
+ test("wiki explicit auth routes reject requests without a valid token", async () => {
889
+ await withStartedServer(async ({ baseUrl }) => {
890
+ const requests = [
891
+ fetch(`${baseUrl}/api/wiki/update`, {
892
+ method: "POST",
893
+ headers: { "content-type": "application/json" },
894
+ body: JSON.stringify({ slug: "topics/auth-required", compiled_truth: "# Auth required" }),
895
+ }),
896
+ fetch(`${baseUrl}/api/wiki/ingest`, {
897
+ method: "POST",
898
+ headers: { "content-type": "application/json" },
899
+ body: JSON.stringify({ path: "raw source" }),
900
+ }),
901
+ fetch(`${baseUrl}/api/wiki/search?q=chapterhouse`),
902
+ fetch(`${baseUrl}/api/wiki/page/pin`, {
903
+ method: "POST",
904
+ headers: { "content-type": "application/json" },
905
+ body: JSON.stringify({ slug: "topics/auth-required", pinned: true }),
906
+ }),
907
+ ];
908
+ const responses = await Promise.all(requests);
909
+ assert.deepEqual(responses.map((response) => response.status), [401, 401, 401, 401]);
910
+ });
911
+ });
777
912
  test("server worker detail returns the stored dispatched prompt", async () => {
778
913
  await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
779
914
  const db = new Database(join(testRoot, ".chapterhouse", "chapterhouse.db"));
@@ -804,7 +939,7 @@ test("server worker detail returns the stored dispatched prompt", async () => {
804
939
  assert.equal(body.completedAt, null);
805
940
  });
806
941
  });
807
- test("server session message hydration returns current run by default and include=all returns history", async () => {
942
+ test("server session message hydration returns full history by default and include=current scopes to this run", async () => {
808
943
  await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
809
944
  const db = new Database(join(testRoot, ".chapterhouse", "chapterhouse.db"));
810
945
  try {
@@ -817,7 +952,15 @@ test("server session message hydration returns current run by default and includ
817
952
  finally {
818
953
  db.close();
819
954
  }
820
- const currentOnly = await fetch(`${baseUrl}/api/session/hydration-session/messages`, {
955
+ const defaultHistory = await fetch(`${baseUrl}/api/session/hydration-session/messages`, {
956
+ headers: { authorization: authHeader },
957
+ });
958
+ assert.equal(defaultHistory.status, 200);
959
+ assert.deepEqual((await defaultHistory.json()).messages.map((message) => message.content), [
960
+ "previous run",
961
+ "current run",
962
+ ]);
963
+ const currentOnly = await fetch(`${baseUrl}/api/session/hydration-session/messages?include=current`, {
821
964
  headers: { authorization: authHeader },
822
965
  });
823
966
  assert.equal(currentOnly.status, 200);
@@ -1152,6 +1295,59 @@ test("server projects create route rejects a duplicate slug", async () => {
1152
1295
  ]);
1153
1296
  }, {}, 60_000);
1154
1297
  });
1298
+ test("server projects create route rejects paths that do not exist or are not directories", async () => {
1299
+ await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
1300
+ const missingResponse = await fetch(`${baseUrl}/api/projects`, {
1301
+ method: "POST",
1302
+ headers: {
1303
+ authorization: authHeader,
1304
+ "content-type": "application/json",
1305
+ },
1306
+ body: JSON.stringify({
1307
+ slug: "missing",
1308
+ cwd: join(testRoot, "missing-project"),
1309
+ }),
1310
+ });
1311
+ assert.equal(missingResponse.status, 400);
1312
+ assert.deepEqual(await missingResponse.json(), { error: "Project cwd must exist and be a directory" });
1313
+ const filePath = join(testRoot, "not-a-directory");
1314
+ writeFileSync(filePath, "not a project", "utf-8");
1315
+ const fileResponse = await fetch(`${baseUrl}/api/projects`, {
1316
+ method: "POST",
1317
+ headers: {
1318
+ authorization: authHeader,
1319
+ "content-type": "application/json",
1320
+ },
1321
+ body: JSON.stringify({
1322
+ slug: "file-path",
1323
+ cwd: filePath,
1324
+ }),
1325
+ });
1326
+ assert.equal(fileResponse.status, 400);
1327
+ assert.deepEqual(await fileResponse.json(), { error: "Project cwd must exist and be a directory" });
1328
+ assert.deepEqual(readProjectRegistryRows(testRoot), []);
1329
+ }, {}, 60_000);
1330
+ });
1331
+ test("server projects create route requires a team lead in team mode", async () => {
1332
+ await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
1333
+ const projectDir = join(testRoot, "team-project");
1334
+ mkdirSync(projectDir, { recursive: true });
1335
+ const response = await fetch(`${baseUrl}/api/projects`, {
1336
+ method: "POST",
1337
+ headers: {
1338
+ authorization: authHeader,
1339
+ "content-type": "application/json",
1340
+ },
1341
+ body: JSON.stringify({
1342
+ slug: "team-project",
1343
+ cwd: projectDir,
1344
+ }),
1345
+ });
1346
+ assert.equal(response.status, 403);
1347
+ assert.deepEqual(await response.json(), { error: "Forbidden" });
1348
+ assert.deepEqual(readProjectRegistryRows(testRoot), []);
1349
+ }, { CHAPTERHOUSE_MODE: "team" }, 60_000);
1350
+ });
1155
1351
  test("server projects delete route removes the registry entry and rules page", async () => {
1156
1352
  await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
1157
1353
  seedProjectRegistry(testRoot, {
@@ -1515,6 +1711,41 @@ test("GET /api/memory/:scope returns entries for a scope and 404 for unknown", a
1515
1711
  assert.deepEqual(await notFound.json(), { error: "Memory scope 'no-such-scope' not found" });
1516
1712
  });
1517
1713
  });
1714
+ test("GET /api/memory/:scope returns a cursor for additional pages", async () => {
1715
+ await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
1716
+ const db = new Database(getProjectDbPath(testRoot));
1717
+ try {
1718
+ const scope = db.prepare(`SELECT id FROM mem_scopes WHERE slug = ?`).get("chapterhouse");
1719
+ const insert = db.prepare(`
1720
+ INSERT INTO mem_observations (scope_id, content, source, tier, created_at)
1721
+ VALUES (?, ?, 'test', 'hot', ?)
1722
+ `);
1723
+ for (let i = 0; i < 105; i++) {
1724
+ insert.run(scope.id, `Paginated observation ${i}`, `2026-05-15T00:${String(i).padStart(2, "0")}:00.000Z`);
1725
+ }
1726
+ }
1727
+ finally {
1728
+ db.close();
1729
+ }
1730
+ const first = await fetch(`${baseUrl}/api/memory/chapterhouse?store=observations&tier=hot`, {
1731
+ headers: { authorization: authHeader },
1732
+ });
1733
+ assert.equal(first.status, 200);
1734
+ const firstBody = await first.json();
1735
+ assert.equal(firstBody.entries.length, 100);
1736
+ assert.ok(firstBody.total >= 105);
1737
+ assert.ok(firstBody.nextCursor);
1738
+ const second = await fetch(`${baseUrl}/api/memory/chapterhouse?store=observations&tier=hot&cursor=${encodeURIComponent(firstBody.nextCursor)}`, {
1739
+ headers: { authorization: authHeader },
1740
+ });
1741
+ assert.equal(second.status, 200);
1742
+ const secondBody = await second.json();
1743
+ assert.ok(secondBody.entries.length >= 5);
1744
+ assert.equal(secondBody.total, firstBody.total);
1745
+ assert.equal(secondBody.nextCursor, undefined);
1746
+ assert.ok(Math.max(...secondBody.entries.map((entry) => entry.id)) < Math.min(...firstBody.entries.map((entry) => entry.id)));
1747
+ });
1748
+ });
1518
1749
  test("POST /api/memory/:scope/remember writes an observation or decision", async () => {
1519
1750
  await withStartedServer(async ({ baseUrl, authHeader }) => {
1520
1751
  const unauthorized = await fetch(`${baseUrl}/api/memory/chapterhouse/remember`, {
@@ -0,0 +1,37 @@
1
+ import { formatSseData } from "./sse.js";
2
+ export const sseClients = new Map();
3
+ export const pendingSseMessages = [];
4
+ let connectionCounter = 0;
5
+ export function nextSseConnectionId() {
6
+ connectionCounter += 1;
7
+ return `web-${connectionCounter}`;
8
+ }
9
+ export function broadcastSsePayload(payload) {
10
+ for (const [, client] of sseClients) {
11
+ client.res.write(formatSseData(payload));
12
+ }
13
+ }
14
+ export function associateSseClient(connectionId, sessionKey) {
15
+ const client = sseClients.get(connectionId);
16
+ if (client) {
17
+ client.sessionKey = sessionKey;
18
+ }
19
+ }
20
+ export function broadcastSsePayloadToSession(sessionKey, payload) {
21
+ for (const [, client] of sseClients) {
22
+ if (client.sessionKey === sessionKey) {
23
+ client.res.write(formatSseData(payload));
24
+ }
25
+ }
26
+ }
27
+ /** Broadcast a proactive message to all connected SSE clients (for background task completions). */
28
+ export function broadcastToSSE(text) {
29
+ if (sseClients.size === 0) {
30
+ pendingSseMessages.push(text);
31
+ return;
32
+ }
33
+ for (const [, client] of sseClients) {
34
+ client.res.write(formatSseData({ type: "message", content: text }));
35
+ }
36
+ }
37
+ //# sourceMappingURL=sse-hub.js.map
package/dist/cli.js CHANGED
@@ -72,7 +72,7 @@ switch (command) {
72
72
  const force = updateFlags.includes("--force");
73
73
  const refIdx = updateFlags.indexOf("--ref");
74
74
  const ref = refIdx !== -1 ? (updateFlags[refIdx + 1] ?? null) : null;
75
- const { checkForUpdate, performUpdate, detectInstallSource, checkPreconditions, buildLegacyGitInstallCommand, } = await import("./update.js");
75
+ const { checkForUpdate, performUpdate, detectInstallSource, checkPreconditions, } = await import("./update.js");
76
76
  const source = detectInstallSource();
77
77
  // Precondition check (bypass with --force)
78
78
  if (!force) {