chapterhouse 0.9.2 → 0.11.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 (121) hide show
  1. package/README.md +1 -1
  2. package/dist/api/auth.js +11 -1
  3. package/dist/api/auth.test.js +29 -0
  4. package/dist/api/errors.js +23 -0
  5. package/dist/api/route-coverage.test.js +61 -21
  6. package/dist/api/routes/agents.js +472 -0
  7. package/dist/api/routes/memory.js +299 -0
  8. package/dist/api/routes/projects.js +170 -0
  9. package/dist/api/routes/sessions.js +347 -0
  10. package/dist/api/routes/system.js +82 -0
  11. package/dist/api/routes/wiki.js +455 -0
  12. package/dist/api/routes/wiki.test.js +49 -0
  13. package/dist/api/send-json.js +16 -0
  14. package/dist/api/send-json.test.js +18 -0
  15. package/dist/api/server-runtime.js +45 -3
  16. package/dist/api/server.js +34 -1764
  17. package/dist/api/server.test.js +239 -8
  18. package/dist/api/sse-hub.js +37 -0
  19. package/dist/cli.js +1 -1
  20. package/dist/config.js +151 -58
  21. package/dist/config.test.js +29 -0
  22. package/dist/copilot/okr-mapper.js +2 -11
  23. package/dist/copilot/orchestrator.js +358 -352
  24. package/dist/copilot/orchestrator.test.js +139 -4
  25. package/dist/copilot/prompt-date.js +2 -1
  26. package/dist/copilot/session-manager.js +25 -23
  27. package/dist/copilot/session-manager.test.js +35 -1
  28. package/dist/copilot/standup.js +2 -2
  29. package/dist/copilot/task-event-log.js +7 -1
  30. package/dist/copilot/task-event-log.test.js +13 -0
  31. package/dist/copilot/tools/agent.js +608 -0
  32. package/dist/copilot/tools/index.js +19 -0
  33. package/dist/copilot/tools/memory.js +678 -0
  34. package/dist/copilot/tools/models.js +2 -0
  35. package/dist/copilot/tools/okr.js +171 -0
  36. package/dist/copilot/tools/wiki.js +333 -0
  37. package/dist/copilot/tools-deps.js +4 -0
  38. package/dist/copilot/tools.agent.test.js +10 -8
  39. package/dist/copilot/tools.inventory.test.js +76 -0
  40. package/dist/copilot/tools.js +1 -1780
  41. package/dist/copilot/tools.okr.test.js +31 -0
  42. package/dist/copilot/tools.wiki.test.js +6 -3
  43. package/dist/copilot/turn-event-log.js +31 -4
  44. package/dist/copilot/turn-event-log.test.js +24 -2
  45. package/dist/copilot/workiq-installer.test.js +2 -2
  46. package/dist/daemon-install.js +3 -2
  47. package/dist/daemon.js +9 -17
  48. package/dist/integrations/ado-client.js +90 -9
  49. package/dist/integrations/ado-client.test.js +56 -0
  50. package/dist/integrations/team-push.js +1 -0
  51. package/dist/integrations/team-push.test.js +6 -0
  52. package/dist/integrations/teams-notify.js +1 -0
  53. package/dist/integrations/teams-notify.test.js +5 -0
  54. package/dist/memory/active-scope.test.js +0 -1
  55. package/dist/memory/checkpoint.js +89 -72
  56. package/dist/memory/checkpoint.test.js +23 -3
  57. package/dist/memory/eot.js +87 -85
  58. package/dist/memory/eot.test.js +71 -3
  59. package/dist/memory/hooks.js +2 -4
  60. package/dist/memory/housekeeping-scheduler.js +1 -1
  61. package/dist/memory/housekeeping-scheduler.test.js +1 -2
  62. package/dist/memory/housekeeping.js +100 -3
  63. package/dist/memory/housekeeping.test.js +33 -2
  64. package/dist/memory/reflect.test.js +2 -0
  65. package/dist/memory/scope-lock.js +26 -0
  66. package/dist/memory/scope-lock.test.js +118 -0
  67. package/dist/memory/scopes.test.js +0 -1
  68. package/dist/mode-context.js +58 -5
  69. package/dist/mode-context.test.js +68 -0
  70. package/dist/paths.js +1 -0
  71. package/dist/setup.js +3 -2
  72. package/dist/shared/api-schemas.js +48 -5
  73. package/dist/store/connection.js +96 -0
  74. package/dist/store/db.js +5 -1498
  75. package/dist/store/db.test.js +182 -1
  76. package/dist/store/migrations.js +460 -0
  77. package/dist/store/repositories/memory.js +281 -0
  78. package/dist/store/repositories/okr.js +3 -0
  79. package/dist/store/repositories/projects.js +5 -0
  80. package/dist/store/repositories/sessions.js +284 -0
  81. package/dist/store/repositories/wiki.js +60 -0
  82. package/dist/store/schema.js +501 -0
  83. package/dist/util/logger.js +3 -2
  84. package/dist/wiki/consolidation.js +50 -9
  85. package/dist/wiki/consolidation.test.js +45 -0
  86. package/dist/wiki/frontmatter.js +43 -13
  87. package/dist/wiki/frontmatter.test.js +24 -0
  88. package/dist/wiki/fs.js +16 -4
  89. package/dist/wiki/fs.test.js +84 -0
  90. package/dist/wiki/index-manager.js +30 -2
  91. package/dist/wiki/index-manager.test.js +43 -12
  92. package/dist/wiki/ingest.js +1 -1
  93. package/dist/wiki/lock.js +11 -1
  94. package/dist/wiki/log-manager.js +2 -7
  95. package/dist/wiki/migrate.js +44 -17
  96. package/dist/wiki/project-registry.js +10 -5
  97. package/dist/wiki/project-registry.test.js +14 -0
  98. package/dist/wiki/scheduler.js +1 -1
  99. package/dist/wiki/seed-team-wiki.js +2 -1
  100. package/dist/wiki/team-sync.js +31 -6
  101. package/dist/wiki/team-sync.test.js +81 -0
  102. package/package.json +1 -1
  103. package/web/dist/assets/WikiEdit-EBVoY1Pk.js +30 -0
  104. package/web/dist/assets/WikiEdit-EBVoY1Pk.js.map +1 -0
  105. package/web/dist/assets/WikiGraph-BUbbABq-.js +2 -0
  106. package/web/dist/assets/WikiGraph-BUbbABq-.js.map +1 -0
  107. package/web/dist/assets/icon-acolyte-cream.svg +10 -0
  108. package/web/dist/assets/icon-acolyte-dark.svg +10 -0
  109. package/web/dist/assets/icon-acolyte-gold.svg +10 -0
  110. package/web/dist/assets/icon-acolyte-ibad.svg +10 -0
  111. package/web/dist/assets/icon-acolyte-lit.svg +10 -0
  112. package/web/dist/assets/icon-acolyte-mono.svg +10 -0
  113. package/web/dist/assets/icon-acolyte.png +0 -0
  114. package/web/dist/assets/icon-acolyte.svg +10 -0
  115. package/web/dist/assets/index-BGLL9pgM.css +10 -0
  116. package/web/dist/assets/index-KFX8UmOb.js +250 -0
  117. package/web/dist/assets/index-KFX8UmOb.js.map +1 -0
  118. package/web/dist/index.html +6 -4
  119. package/web/dist/assets/index-5kz9aRU9.css +0 -10
  120. package/web/dist/assets/index-iQrv3lQN.js +0 -286
  121. package/web/dist/assets/index-iQrv3lQN.js.map +0 -1
@@ -26,6 +26,58 @@ test.after(() => {
26
26
  resetSingletons();
27
27
  rmSync(sandboxRoot, { recursive: true, force: true });
28
28
  });
29
+ test("getDb returns the same singleton connection until closeDb is called", async () => {
30
+ const dbModule = await loadDbModule();
31
+ try {
32
+ const first = dbModule.getDb();
33
+ const second = dbModule.getDb();
34
+ assert.equal(second, first);
35
+ }
36
+ finally {
37
+ dbModule.closeDb();
38
+ }
39
+ });
40
+ test("closeDb allows getDb to reinitialize a new usable connection without losing persisted state", async () => {
41
+ const dbModule = await loadDbModule();
42
+ try {
43
+ const first = dbModule.getDb();
44
+ dbModule.setState("db-reinit-sentinel", "persisted");
45
+ dbModule.closeDb();
46
+ const second = dbModule.getDb();
47
+ assert.notEqual(second, first);
48
+ assert.equal(dbModule.getState("db-reinit-sentinel"), "persisted");
49
+ assert.deepEqual(second.prepare("SELECT 1 AS ok").get(), { ok: 1 });
50
+ }
51
+ finally {
52
+ dbModule.closeDb();
53
+ }
54
+ });
55
+ test("getDb records migrations once in registry order across repeated startups", async () => {
56
+ const dbModule = await loadDbModule();
57
+ try {
58
+ let db = dbModule.getDb();
59
+ const firstRows = db.prepare(`
60
+ SELECT version, name
61
+ FROM __db_migrations
62
+ ORDER BY rowid
63
+ `).all();
64
+ assert.ok(firstRows.length > 0, "expected at least one tracked migration");
65
+ assert.deepEqual(firstRows.map((row) => row.version), [...firstRows].map((row) => row.version).sort((a, b) => a - b), "migrations should be recorded in ascending registry order");
66
+ assert.equal(new Set(firstRows.map((row) => row.version)).size, firstRows.length, "migration versions should be unique");
67
+ assert.equal(new Set(firstRows.map((row) => row.name)).size, firstRows.length, "migration names should be unique");
68
+ dbModule.closeDb();
69
+ db = dbModule.getDb();
70
+ const secondRows = db.prepare(`
71
+ SELECT version, name
72
+ FROM __db_migrations
73
+ ORDER BY rowid
74
+ `).all();
75
+ assert.deepEqual(secondRows, firstRows);
76
+ }
77
+ finally {
78
+ dbModule.closeDb();
79
+ }
80
+ });
29
81
  test("getDb startup reindex populates wiki_pages when pages exist on disk", async () => {
30
82
  mkdirSync(join(chapterhouseHome, "wiki", "pages", "topics", "rust"), { recursive: true });
31
83
  writeFileSync(join(chapterhouseHome, "wiki", "pages", "topics", "rust", "index.md"), "---\ntitle: Rust\nsummary: Systems language\nupdated: 2026-05-15\n---\n\n# Rust\n\nFearless concurrency.\n", "utf-8");
@@ -68,6 +120,18 @@ test("getDb startup reindex creates the wiki log directory when it is missing",
68
120
  dbModule.closeDb();
69
121
  }
70
122
  });
123
+ test("getDb reports FTS5 readiness after deferred rebuild work finishes", async () => {
124
+ const dbModule = await loadDbModule();
125
+ try {
126
+ dbModule.getDb();
127
+ assert.equal(typeof dbModule.isFts5Ready, "function");
128
+ await new Promise((resolve) => setTimeout(resolve, 10));
129
+ assert.equal(dbModule.isFts5Ready(), true);
130
+ }
131
+ finally {
132
+ dbModule.closeDb();
133
+ }
134
+ });
71
135
  test("getDb initializes schema, state helpers, and conversation formatting", async () => {
72
136
  const dbModule = await loadDbModule();
73
137
  try {
@@ -91,13 +155,23 @@ test("getDb initializes schema, state helpers, and conversation formatting", asy
91
155
  dbModule.deleteState("router_config");
92
156
  assert.equal(dbModule.getState("router_config"), undefined);
93
157
  const longReply = "A".repeat(1_550);
158
+ const oversizedTurn = "B".repeat(10_050);
94
159
  dbModule.logConversation("system", "Started background sync", "worker");
95
160
  dbModule.logConversation("user", "Please summarize the last deploy", "web");
96
161
  dbModule.logConversation("assistant", longReply, "web");
162
+ dbModule.logConversation("assistant", oversizedTurn, "web");
163
+ const storedOversizedTurn = db.prepare(`
164
+ SELECT content
165
+ FROM conversation_log
166
+ WHERE content LIKE 'B%'
167
+ ORDER BY id DESC
168
+ LIMIT 1
169
+ `).get();
170
+ assert.equal(storedOversizedTurn.content.length, 10_000);
97
171
  assert.equal(dbModule.getRecentConversation(3), [
98
- "[worker] System: Started background sync",
99
172
  "[web] User: Please summarize the last deploy",
100
173
  `Chapterhouse: ${"A".repeat(1_500)}…`,
174
+ `Chapterhouse: ${"B".repeat(1_500)}…`,
101
175
  ].join("\n\n"));
102
176
  }
103
177
  finally {
@@ -115,6 +189,7 @@ test("getDb initializes action-item memory schema, reflect patterns schema, and
115
189
  assert.equal(tableNames.has("mem_patterns"), true, "expected mem_patterns table");
116
190
  const columns = db.prepare(`PRAGMA table_info(mem_action_items)`).all();
117
191
  const columnNames = new Set(columns.map((column) => column.name));
192
+ const columnTypes = new Map(columns.map((column) => [column.name, column.type]));
118
193
  for (const name of [
119
194
  "id",
120
195
  "scope_id",
@@ -136,6 +211,7 @@ test("getDb initializes action-item memory schema, reflect patterns schema, and
136
211
  ]) {
137
212
  assert.equal(columnNames.has(name), true, `expected mem_action_items.${name}`);
138
213
  }
214
+ assert.equal(columnTypes.get("created_at"), "DATETIME");
139
215
  const patternColumns = db.prepare(`PRAGMA table_info(mem_patterns)`).all();
140
216
  const patternColumnNames = new Set(patternColumns.map((column) => column.name));
141
217
  for (const name of [
@@ -153,6 +229,10 @@ test("getDb initializes action-item memory schema, reflect patterns schema, and
153
229
  }
154
230
  const patternIndexes = db.prepare(`PRAGMA index_list(mem_patterns)`).all();
155
231
  assert.equal(patternIndexes.some((index) => index.name === "mem_patterns_scope_tier_idx"), true, "expected mem_patterns scope/tier index");
232
+ const observationIndexes = db.prepare(`PRAGMA index_list(mem_observations)`).all();
233
+ const decisionIndexes = db.prepare(`PRAGMA index_list(mem_decisions)`).all();
234
+ assert.equal(observationIndexes.some((index) => index.name === "mem_observations_scope_superseded_by_idx"), true);
235
+ assert.equal(decisionIndexes.some((index) => index.name === "mem_decisions_scope_superseded_by_idx"), true);
156
236
  const scope = db.prepare(`SELECT id FROM mem_scopes WHERE slug = 'chapterhouse'`).get();
157
237
  const inserted = db.prepare(`
158
238
  INSERT INTO mem_action_items (scope_id, title, detail, source)
@@ -169,6 +249,107 @@ test("getDb initializes action-item memory schema, reflect patterns schema, and
169
249
  dbModule.closeDb();
170
250
  }
171
251
  });
252
+ test("getDb migrates legacy mem_action_items created_at TEXT schemas without losing rows or indexes", async () => {
253
+ const seedDb = new Database(dbPath);
254
+ seedDb.exec(`
255
+ CREATE TABLE mem_scopes (
256
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
257
+ slug TEXT NOT NULL,
258
+ title TEXT NOT NULL,
259
+ description TEXT NOT NULL,
260
+ keywords TEXT NOT NULL DEFAULT '[]',
261
+ active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)),
262
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
263
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
264
+ );
265
+ CREATE TABLE mem_action_items (
266
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
267
+ scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
268
+ entity_id INTEGER,
269
+ title TEXT NOT NULL,
270
+ detail TEXT,
271
+ status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'done', 'dropped', 'snoozed')),
272
+ due_at TEXT,
273
+ snooze_until TEXT,
274
+ source TEXT,
275
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
276
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
277
+ resolved_at TEXT,
278
+ resolution_reason TEXT,
279
+ tier TEXT NOT NULL DEFAULT 'warm' CHECK(tier IN ('hot', 'warm', 'cold')),
280
+ tier_pinned_at TEXT,
281
+ tier_reason TEXT,
282
+ last_recalled_at TEXT
283
+ );
284
+ INSERT INTO mem_scopes (slug, title, description) VALUES ('chapterhouse', 'Chapterhouse', 'Test scope');
285
+ INSERT INTO mem_action_items (
286
+ scope_id, title, detail, status, due_at, source, created_at, updated_at, tier, tier_reason
287
+ ) VALUES (
288
+ 1, 'Follow up on migration', 'Preserve this row', 'open', '2026-05-20 12:00:00', 'test',
289
+ '2026-05-14 10:11:12', '2026-05-14 10:11:13', 'hot', 'legacy'
290
+ );
291
+ `);
292
+ seedDb.close();
293
+ const dbModule = await loadDbModule();
294
+ try {
295
+ const db = dbModule.getDb();
296
+ const columns = db.prepare(`PRAGMA table_info(mem_action_items)`).all();
297
+ const columnTypes = new Map(columns.map((column) => [column.name, column.type]));
298
+ assert.equal(columnTypes.get("created_at"), "DATETIME");
299
+ assert.deepEqual(db.prepare(`SELECT title, detail, created_at, tier, tier_reason FROM mem_action_items`).get(), {
300
+ title: "Follow up on migration",
301
+ detail: "Preserve this row",
302
+ created_at: "2026-05-14 10:11:12",
303
+ tier: "hot",
304
+ tier_reason: "legacy",
305
+ });
306
+ const indexes = db.prepare(`PRAGMA index_list(mem_action_items)`).all();
307
+ assert.equal(indexes.some((index) => index.name === "idx_mem_action_items_scope_status"), true);
308
+ assert.equal(indexes.some((index) => index.name === "idx_mem_action_items_due"), true);
309
+ }
310
+ finally {
311
+ dbModule.closeDb();
312
+ }
313
+ });
314
+ test("getDb legacy entity tier rebuild promotes recent high-confidence entities to hot", async () => {
315
+ const seedDb = new Database(dbPath);
316
+ seedDb.exec(`
317
+ CREATE TABLE mem_scopes (
318
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
319
+ slug TEXT NOT NULL,
320
+ title TEXT NOT NULL,
321
+ description TEXT NOT NULL,
322
+ keywords TEXT NOT NULL DEFAULT '[]',
323
+ active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)),
324
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
325
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
326
+ );
327
+ CREATE TABLE mem_entities (
328
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
329
+ scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
330
+ kind TEXT NOT NULL,
331
+ name TEXT NOT NULL,
332
+ summary TEXT,
333
+ tier TEXT NOT NULL DEFAULT 'warm' CHECK(tier IN ('hot', 'warm', 'cold', 'glacier', 'unknown')),
334
+ confidence REAL NOT NULL DEFAULT 1.0,
335
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
336
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
337
+ );
338
+ INSERT INTO mem_scopes (slug, title, description) VALUES ('chapterhouse', 'Chapterhouse', 'Test scope');
339
+ INSERT INTO mem_entities (scope_id, kind, name, tier, confidence, updated_at)
340
+ VALUES (1, 'component', 'memory-db', 'unknown', 0.95, datetime('now'));
341
+ `);
342
+ seedDb.close();
343
+ const dbModule = await loadDbModule();
344
+ try {
345
+ const db = dbModule.getDb();
346
+ const row = db.prepare(`SELECT tier FROM mem_entities WHERE name = 'memory-db'`).get();
347
+ assert.equal(row.tier, "hot");
348
+ }
349
+ finally {
350
+ dbModule.closeDb();
351
+ }
352
+ });
172
353
  test("getDb migrates legacy conversation_log tables to allow system messages", async () => {
173
354
  const seedDb = new Database(dbPath);
174
355
  seedDb.exec(`