chapterhouse 0.4.0 → 0.4.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.
@@ -34,6 +34,7 @@ test("getDb initializes schema, state helpers, and conversation formatting", asy
34
34
  "agent_tasks",
35
35
  "projects",
36
36
  "max_state",
37
+ "daemon_runs",
37
38
  "conversation_log",
38
39
  "memories",
39
40
  ]) {
@@ -181,15 +182,23 @@ test("getDb migrates legacy memory tiers from glacier to cold and preserves expl
181
182
  const dbModule = await loadDbModule();
182
183
  try {
183
184
  const db = dbModule.getDb();
184
- assert.deepEqual(db.prepare(`SELECT id, tier FROM mem_entities ORDER BY id`).all(), [
185
+ assert.deepEqual(db.prepare(`
186
+ SELECT id, tier FROM mem_entities
187
+ WHERE name IN ('warm kept', 'cold mapped')
188
+ ORDER BY id
189
+ `).all(), [
185
190
  { id: 1, tier: "warm" },
186
191
  { id: 2, tier: "cold" },
187
192
  ]);
188
- assert.deepEqual(db.prepare(`SELECT id, tier FROM mem_observations ORDER BY id`).all(), [
193
+ assert.deepEqual(db.prepare(`SELECT id, tier FROM mem_observations WHERE source = 'test' ORDER BY id`).all(), [
189
194
  { id: 1, tier: "hot" },
190
195
  { id: 2, tier: "cold" },
191
196
  ]);
192
- assert.deepEqual(db.prepare(`SELECT id, tier FROM mem_decisions ORDER BY id`).all(), [
197
+ assert.deepEqual(db.prepare(`
198
+ SELECT id, tier FROM mem_decisions
199
+ WHERE title IN ('warm kept', 'cold mapped')
200
+ ORDER BY id
201
+ `).all(), [
193
202
  { id: 1, tier: "warm" },
194
203
  { id: 2, tier: "cold" },
195
204
  ]);
@@ -200,6 +209,305 @@ test("getDb migrates legacy memory tiers from glacier to cold and preserves expl
200
209
  dbModule.closeDb();
201
210
  }
202
211
  });
212
+ test("getDb seeds the Chapterhouse entity and PR1 wiki observations idempotently", async () => {
213
+ const dbModule = await loadDbModule();
214
+ try {
215
+ const expectedObservations = [
216
+ {
217
+ content: "Chapterhouse architecture: Node.js daemon, web UI, per-conversation sessions, specialist agents, modular skills, MCP servers, local markdown wiki, project-rules prompt injection, and background workers with SSE event streaming.",
218
+ tier: "warm",
219
+ },
220
+ {
221
+ content: "Chapterhouse source lives at bketelsen/chapterhouse with local checkout ~/projects/chapterhouse; engineering plans are in docs/plans/ and product specs are in docs/prd/.",
222
+ tier: "warm",
223
+ },
224
+ {
225
+ content: "Background agent completions arrive as automatic system notifications; after spawning a background agent, end the turn and wait instead of polling read_agent, sleeping, or re-checking list_agents.",
226
+ tier: "hot",
227
+ },
228
+ {
229
+ content: "Use the default Chapterhouse chat for research and general questions; move code changes against a specific project into that project's chat session.",
230
+ tier: "warm",
231
+ },
232
+ {
233
+ content: "Squad manages Chapterhouse's own development workflow.",
234
+ tier: "warm",
235
+ },
236
+ {
237
+ content: "Chapterhouse uses frequent semver patch releases published to npm via the Publish to npm GitHub Actions workflow on tag push; never publish manually.",
238
+ tier: "hot",
239
+ },
240
+ {
241
+ content: "Agent-memory P1 shipped on 2026-05-13 as Chapterhouse v0.4.0 across 7 PRs: #215 through #225.",
242
+ tier: "warm",
243
+ },
244
+ {
245
+ content: "The v0.3.2 per-session orchestrator was confirmed under real concurrent load on 2026-05-08, enabling concurrent sessions without cross-blocking.",
246
+ tier: "hot",
247
+ },
248
+ ];
249
+ let db = dbModule.getDb();
250
+ dbModule.closeDb();
251
+ db = dbModule.getDb();
252
+ const chapterhouseScope = db.prepare(`
253
+ SELECT id, slug
254
+ FROM mem_scopes
255
+ WHERE slug = 'chapterhouse'
256
+ `).get();
257
+ assert.ok(chapterhouseScope, "chapterhouse scope should be seeded");
258
+ const entity = db.prepare(`
259
+ SELECT id, scope_id, kind, name, summary, tier
260
+ FROM mem_entities
261
+ WHERE scope_id = ? AND kind = 'project' AND name = 'Chapterhouse'
262
+ `).get(chapterhouseScope.id);
263
+ assert.ok(entity, "Chapterhouse project entity should be seeded");
264
+ assert.equal(entity.scope_id, chapterhouseScope.id);
265
+ assert.equal(entity.summary, "Always-on team-level AI assistant daemon that orchestrates specialist subagents and maintains a persistent knowledge wiki.");
266
+ const rows = db.prepare(`
267
+ SELECT scope_id, entity_id, content, source, tier, tier_pinned_at, tier_reason
268
+ FROM mem_observations
269
+ WHERE source = 'wiki:pages/projects/chapterhouse/index.md'
270
+ ORDER BY content
271
+ `).all();
272
+ assert.equal(rows.length, expectedObservations.length, "PR1 should seed exactly 8 index.md observations");
273
+ for (const expected of expectedObservations) {
274
+ const row = rows.find((entry) => entry.content === expected.content);
275
+ assert.ok(row, `missing seeded observation: ${expected.content}`);
276
+ assert.equal(row.scope_id, chapterhouseScope.id, "all PR1 observations must be scoped to chapterhouse");
277
+ assert.equal(row.entity_id, entity.id, "all PR1 observations should attach to the Chapterhouse entity");
278
+ assert.equal(row.tier, expected.tier);
279
+ if (expected.tier === "hot") {
280
+ assert.ok(row.tier_pinned_at, "hot PR1 observations should be manually pinned");
281
+ assert.equal(row.tier_reason, "P6 PR1 wiki migration hot-tier candidate");
282
+ }
283
+ }
284
+ const duplicates = db.prepare(`
285
+ SELECT content, COUNT(*) AS count
286
+ FROM mem_observations
287
+ WHERE source = 'wiki:pages/projects/chapterhouse/index.md'
288
+ GROUP BY content
289
+ HAVING COUNT(*) > 1
290
+ `).all();
291
+ assert.deepEqual(duplicates, [], "re-running getDb must not duplicate seeded observations");
292
+ const entityCount = db.prepare(`
293
+ SELECT COUNT(*) AS count
294
+ FROM mem_entities
295
+ WHERE scope_id = ? AND kind = 'project' AND name = 'Chapterhouse'
296
+ `).get(chapterhouseScope.id).count;
297
+ assert.equal(entityCount, 1, "re-running getDb must not duplicate the Chapterhouse entity");
298
+ }
299
+ finally {
300
+ dbModule.closeDb();
301
+ }
302
+ });
303
+ test("getDb seeds the Chapterhouse PR2 wiki decisions idempotently", async () => {
304
+ const dbModule = await loadDbModule();
305
+ try {
306
+ const expectedDecisions = [
307
+ {
308
+ title: "Standardize API validation and JSON errors around `src/api/errors.ts`",
309
+ rationale: "Keep one 400/403/404/500 contract across routes instead of ad hoc parsing and responses; `src/api/errors.ts`, `src/api/server.ts`.",
310
+ decided_at: "2026-05-06",
311
+ tier: "warm",
312
+ },
313
+ {
314
+ title: "Run in standalone mode when neither Entra nor an API token is configured",
315
+ rationale: "Disable auth checks and team sync together so local single-user startup still works; `src/config.ts`, `src/api/auth.ts`.",
316
+ decided_at: "2026-05-06",
317
+ tier: "warm",
318
+ },
319
+ {
320
+ title: "Use bearer auth for SSE and serialize wiki writes",
321
+ rationale: "`/stream` no longer accepts query tokens, and wiki mutations go through `withWikiWrite()`; `web/src/stream.ts`, `src/wiki/lock.ts`.",
322
+ decided_at: "2026-05-06",
323
+ tier: "warm",
324
+ },
325
+ {
326
+ title: "Default production to least privilege",
327
+ rationale: "Run as a non-root container, validate env early, and keep CORS and security headers tight in production; `Dockerfile`, `deploy/deploy.sh`, `src/config.ts`.",
328
+ decided_at: "2026-05-06",
329
+ tier: "warm",
330
+ },
331
+ {
332
+ title: "Keep chat state session-scoped by `sessionKey`",
333
+ rationale: "Conversation history, SSE routing, and frontend buffers stay isolated per session instead of bleeding across chats; `src/store/db.ts`, `web/src/store.ts`.",
334
+ decided_at: "2026-05-07",
335
+ tier: "warm",
336
+ },
337
+ {
338
+ title: "Preserve the 3-layer daemon timing contract",
339
+ rationale: "Orchestrator timeout must stay below daemon shutdown grace, which must stay below systemd `TimeoutStopSec`; `README.md`, `src/daemon.ts`, `src/daemon-install.ts`.",
340
+ decided_at: "2026-05-08",
341
+ tier: "hot",
342
+ },
343
+ {
344
+ title: "Start WorkIQ MCP auto-install at daemon startup",
345
+ rationale: "Write the MCP entry before the SDK client starts so new sessions see it immediately; issue #33, PR #78.",
346
+ decided_at: "2026-05-08",
347
+ tier: "warm",
348
+ },
349
+ {
350
+ title: "Use per-session orchestrators instead of a global queue",
351
+ rationale: "`SessionManager` and `SessionRegistry` keep independent queues so one chat cannot block another; issue #74, `src/copilot/session-manager.ts`.",
352
+ decided_at: "2026-05-08",
353
+ tier: "hot",
354
+ },
355
+ {
356
+ title: "Make wiki path hierarchy the primary navigation model",
357
+ rationale: "`path` drives the tree and breadcrumbs, while `section` remains secondary metadata; `web/src/wiki/index.ts`, `web/src/routes/Wiki.tsx`.",
358
+ decided_at: "2026-05-06",
359
+ tier: "warm",
360
+ },
361
+ {
362
+ title: "Route wiki breadcrumbs through `selected` state",
363
+ rationale: "Breadcrumb clicks use the same route-state flow as the sidebar so navigation stays predictable; `web/src/components/wiki/WikiBreadcrumbs.tsx`.",
364
+ decided_at: "2026-05-06",
365
+ tier: "warm",
366
+ },
367
+ {
368
+ title: "Derive wiki page scope from configured team paths",
369
+ rationale: "The API returns `scope` and the UI shows personal/team indicators without adding new persisted metadata; `src/api/server.ts`, `web/src/components/wiki/WikiScopeIndicator.tsx`.",
370
+ decided_at: "2026-05-06",
371
+ tier: "warm",
372
+ },
373
+ {
374
+ title: "Sort sidebar recents by actual project activity",
375
+ rationale: "`last_used_at` replaces registration time so recent projects reflect chat usage rather than signup order; issue #26, PR #30.",
376
+ decided_at: "2026-05-08",
377
+ tier: "warm",
378
+ },
379
+ {
380
+ title: "Preload `CHAPTERHOUSE_DISABLE_DOTENV=1` for Node tests",
381
+ rationale: "Tests must not inherit developer-local env files before config singletons initialize; `src/test/setup-env.ts`, `src/config.ts`.",
382
+ decided_at: "2026-05-06",
383
+ tier: "warm",
384
+ },
385
+ {
386
+ title: "Test API routes through a spawned server process",
387
+ rationale: "Real HTTP coverage catches auth, config preload, error middleware, and wiki I/O behavior that an unexported in-process app would miss; `src/api/server.test.ts`.",
388
+ decided_at: "2026-05-06",
389
+ tier: "warm",
390
+ },
391
+ {
392
+ title: "Centralize frontend API Zod schemas in `web/src/api-schemas.ts`",
393
+ rationale: "Keep one audit surface for the browser-to-daemon contract and require validated JSON reads; issue #40, PR #63.",
394
+ decided_at: "2026-05-08",
395
+ tier: "warm",
396
+ },
397
+ {
398
+ title: "Make frontend test isolation an infrastructure concern",
399
+ rationale: "Vitest globals, `test-setup.ts`, and MSW own cleanup so tests do not hand-roll it; issue #43, PR #62.",
400
+ decided_at: "2026-05-08",
401
+ tier: "warm",
402
+ },
403
+ {
404
+ title: "Clean `dist/` before `npm test`",
405
+ rationale: "Remove stale compiled artifacts so deleted tests cannot linger in the Node test run; `package.json`.",
406
+ decided_at: "2026-05-08",
407
+ tier: "warm",
408
+ },
409
+ {
410
+ title: "Adopt `pino` as the backend logger",
411
+ rationale: "Use `childLogger()` everywhere and keep chat content at `debug`, not `info`; issue #13, PR #28.",
412
+ decided_at: "2026-05-08",
413
+ tier: "warm",
414
+ },
415
+ {
416
+ title: "Keep daemon control as a thin CLI over `src/daemon-install.ts`",
417
+ rationale: "Generate service artifacts in one module and support launchd and systemd user services without a separate control layer; issue #14, PR #24.",
418
+ decided_at: "2026-05-08",
419
+ tier: "warm",
420
+ },
421
+ {
422
+ title: "Default publish workflows to Node 24+",
423
+ rationale: "npm Trusted Publishing depends on npm 11.5.1+, which Node 22 does not provide; `.github/workflows/npm-publish.yml`.",
424
+ decided_at: "2026-05-08",
425
+ tier: "warm",
426
+ },
427
+ {
428
+ title: "Make `chapterhouse update` registry-aware",
429
+ rationale: "Registry installs update via npm, while legacy git installs keep the older pull-and-rebuild path; issue #31, PR #32.",
430
+ decided_at: "2026-05-08",
431
+ tier: "warm",
432
+ },
433
+ {
434
+ title: "Keep markdownlint pragmatic rather than maximalist",
435
+ rationale: "Disable MD060, ignore `.github/agents/**`, and suppress MD041 only where template UX requires it; `.markdownlint-cli2.jsonc`.",
436
+ decided_at: "2026-05-08",
437
+ tier: "warm",
438
+ },
439
+ {
440
+ title: "Enforce issue-closing references in feature and fix PRs",
441
+ rationale: "PR bodies must include `Closes #N`, `Fixes #N`, or `Resolves #N`, with a documented `no-issue` bypass; PR #60, `.github/workflows/lint-pr-closes.yml`.",
442
+ decided_at: "2026-05-08",
443
+ tier: "warm",
444
+ },
445
+ {
446
+ title: "Use Conventional Commits for commits and PR titles",
447
+ rationale: "Keep repo history machine-readable and enforce it with commitlint, husky, and PR-title linting; `commitlint.config.js`, `.github/workflows/lint-pr-title.yml`.",
448
+ decided_at: "2026-05-08",
449
+ tier: "warm",
450
+ },
451
+ ];
452
+ let db = dbModule.getDb();
453
+ dbModule.closeDb();
454
+ db = dbModule.getDb();
455
+ const chapterhouseScope = db.prepare(`
456
+ SELECT id
457
+ FROM mem_scopes
458
+ WHERE slug = 'chapterhouse'
459
+ `).get();
460
+ assert.ok(chapterhouseScope, "chapterhouse scope should be seeded");
461
+ const chapterhouseEntity = db.prepare(`
462
+ SELECT id
463
+ FROM mem_entities
464
+ WHERE scope_id = ? AND kind = 'project' AND name = 'Chapterhouse'
465
+ `).get(chapterhouseScope.id);
466
+ assert.ok(chapterhouseEntity, "Chapterhouse project entity should be seeded");
467
+ const rows = db.prepare(`
468
+ SELECT title, rationale, decided_at, tier, entity_id, tier_pinned_at, tier_reason, superseded_by
469
+ FROM mem_decisions
470
+ WHERE scope_id = ?
471
+ ORDER BY decided_at, title
472
+ `).all(chapterhouseScope.id);
473
+ assert.equal(rows.length, 24, "PR2 should seed exactly 24 decisions from decisions.md");
474
+ assert.equal(expectedDecisions.length, 24, "test fixture should reflect the wiki source decision count");
475
+ for (const expected of expectedDecisions) {
476
+ const row = rows.find((entry) => entry.title === expected.title);
477
+ assert.ok(row, `missing seeded decision: ${expected.title}`);
478
+ assert.equal(row.rationale, expected.rationale);
479
+ assert.equal(row.decided_at, expected.decided_at);
480
+ assert.equal(row.tier, expected.tier);
481
+ assert.equal(row.entity_id, chapterhouseEntity.id, "all PR2 decisions should attach to the Chapterhouse entity");
482
+ assert.equal(row.superseded_by, null, "source has no explicit superseded-by markers");
483
+ if (expected.tier === "hot") {
484
+ assert.ok(row.tier_pinned_at, "hot PR2 decisions should be manually pinned");
485
+ assert.equal(row.tier_reason, "P6 PR2 wiki migration hot-tier candidate");
486
+ }
487
+ else {
488
+ assert.equal(row.tier_pinned_at, null);
489
+ assert.equal(row.tier_reason, null);
490
+ }
491
+ }
492
+ const duplicates = db.prepare(`
493
+ SELECT title, COUNT(*) AS count
494
+ FROM mem_decisions
495
+ WHERE scope_id = ?
496
+ GROUP BY title
497
+ HAVING COUNT(*) > 1
498
+ `).all(chapterhouseScope.id);
499
+ assert.deepEqual(duplicates, [], "re-running getDb must not duplicate seeded decisions");
500
+ const supersededCount = db.prepare(`
501
+ SELECT COUNT(*) AS count
502
+ FROM mem_decisions
503
+ WHERE scope_id = ? AND superseded_by IS NOT NULL
504
+ `).get(chapterhouseScope.id).count;
505
+ assert.equal(supersededCount, 0, "source has no explicit superseded-by markers to port");
506
+ }
507
+ finally {
508
+ dbModule.closeDb();
509
+ }
510
+ });
203
511
  test("getDb prunes oversized conversation logs on startup and during inserts", async () => {
204
512
  const seedDb = new Database(dbPath);
205
513
  seedDb.exec(`
@@ -255,8 +563,8 @@ test("getSessionMessages returns structured messages in chronological order, inc
255
563
  dbModule.logConversation("user", "hello", "web", "test-session");
256
564
  dbModule.logConversation("assistant", "hi there", "web", "test-session");
257
565
  dbModule.logConversation("system", "system noise", "worker", "test-session");
258
- db.prepare(`INSERT INTO conversation_log (role, content, source, session_key)
259
- VALUES ('agent_completion', ?, 'background', 'test-session')`).run("[Agent task completed] @coder finished task task-1:\n\nDone");
566
+ db.prepare(`INSERT INTO conversation_log (role, content, source, session_key, run_id)
567
+ VALUES ('agent_completion', ?, 'background', 'test-session', ?)`).run("[Agent task completed] @coder finished task task-1:\n\nDone", dbModule.getCurrentRunId());
260
568
  dbModule.logConversation("user", "second message", "web", "test-session");
261
569
  dbModule.logConversation("user", "from other session", "web", "other-session");
262
570
  const all = dbModule.getSessionMessages("test-session");
@@ -284,6 +592,84 @@ test("getSessionMessages returns structured messages in chronological order, inc
284
592
  dbModule.closeDb();
285
593
  }
286
594
  });
595
+ test("getSessionMessages returns stable row id and turn_id for hydration reconciliation", async () => {
596
+ const dbModule = await loadDbModule();
597
+ try {
598
+ const db = dbModule.getDb();
599
+ const columns = db.prepare(`PRAGMA table_info(conversation_log)`).all();
600
+ assert.equal(columns.some((column) => column.name === "turn_id"), true, "conversation_log should persist turn_id");
601
+ const result = db.prepare(`INSERT INTO conversation_log (role, content, source, session_key, turn_id, run_id)
602
+ VALUES ('assistant', 'hydrated answer', 'web', 'stable-session', 'turn-stable-1', ?)`).run(dbModule.getCurrentRunId());
603
+ const messages = dbModule.getSessionMessages("stable-session");
604
+ assert.equal(messages.length, 1);
605
+ const message = messages[0];
606
+ assert.equal(message.id, Number(result.lastInsertRowid));
607
+ assert.equal(message.turn_id, "turn-stable-1");
608
+ }
609
+ finally {
610
+ dbModule.closeDb();
611
+ }
612
+ });
613
+ test("getSessionMessages defaults to the current daemon run and can include historical rows", async () => {
614
+ const dbModule = await loadDbModule();
615
+ try {
616
+ const db = dbModule.getDb();
617
+ const columns = db.prepare(`PRAGMA table_info(conversation_log)`).all();
618
+ if (!columns.some((column) => column.name === "run_id")) {
619
+ db.exec(`ALTER TABLE conversation_log ADD COLUMN run_id TEXT`);
620
+ }
621
+ db.prepare(`INSERT INTO conversation_log (role, content, source, session_key, run_id)
622
+ VALUES ('user', ?, 'web', 'run-session', ?)`).run("previous run", "previous-run");
623
+ db.prepare(`INSERT INTO conversation_log (role, content, source, session_key, run_id)
624
+ VALUES ('assistant', ?, 'web', 'run-session', ?)`).run("current run", "current-run");
625
+ const getSessionMessages = dbModule.getSessionMessages;
626
+ assert.deepEqual(getSessionMessages("run-session", undefined, { runId: "current-run" }).map((message) => message.content), ["current run"]);
627
+ assert.deepEqual(getSessionMessages("run-session", undefined, { runId: "current-run", includeHistorical: true }).map((message) => message.content), ["previous run", "current run"]);
628
+ }
629
+ finally {
630
+ dbModule.closeDb();
631
+ }
632
+ });
633
+ test("getDb migrates legacy hydration rows with NULL run_id and excludes them by default", async () => {
634
+ const seedDb = new Database(dbPath);
635
+ seedDb.exec(`
636
+ CREATE TABLE conversation_log (
637
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
638
+ role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'agent_completion')),
639
+ content TEXT NOT NULL,
640
+ source TEXT NOT NULL DEFAULT 'unknown',
641
+ session_key TEXT NOT NULL DEFAULT 'default',
642
+ ts DATETIME DEFAULT CURRENT_TIMESTAMP
643
+ );
644
+ INSERT INTO conversation_log (role, content, source, session_key)
645
+ VALUES ('user', 'legacy row', 'web', 'legacy-session');
646
+ CREATE TABLE turn_events (
647
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
648
+ turn_id TEXT NOT NULL,
649
+ session_key TEXT NOT NULL DEFAULT 'default',
650
+ seq INTEGER NOT NULL,
651
+ ts INTEGER NOT NULL,
652
+ event_type TEXT NOT NULL,
653
+ payload TEXT NOT NULL
654
+ );
655
+ INSERT INTO turn_events (turn_id, session_key, seq, ts, event_type, payload)
656
+ VALUES ('legacy-turn', 'legacy-session', 1, 1, 'turn:complete', '{"type":"turn:complete","turnId":"legacy-turn","sessionKey":"legacy-session","finalMessage":"legacy","_seq":1,"_ts":1}');
657
+ `);
658
+ seedDb.close();
659
+ const dbModule = await loadDbModule();
660
+ try {
661
+ const db = dbModule.getDb();
662
+ const columns = db.prepare(`PRAGMA table_info(conversation_log)`).all();
663
+ assert.ok(columns.some((column) => column.name === "run_id"), "conversation_log.run_id column must be added");
664
+ const turnColumns = db.prepare(`PRAGMA table_info(turn_events)`).all();
665
+ assert.ok(turnColumns.some((column) => column.name === "run_id"), "turn_events.run_id column must be added");
666
+ dbModule.logConversation("assistant", "current row", "web", "legacy-session");
667
+ assert.deepEqual(dbModule.getSessionMessages("legacy-session").map((message) => message.content), ["current row"]);
668
+ }
669
+ finally {
670
+ dbModule.closeDb();
671
+ }
672
+ });
287
673
  // ---------------------------------------------------------------------------
288
674
  // #86: agent_task_events — appendTaskEvent and getTaskEvents
289
675
  // ---------------------------------------------------------------------------
@@ -441,8 +827,8 @@ test("getSessionMessages returns ts values normalized to ISO-8601 UTC (ends with
441
827
  try {
442
828
  const db = dbModule.getDb();
443
829
  // Insert a row with a bare SQLite-format timestamp (no T, no Z)
444
- db.prepare(`INSERT INTO conversation_log (role, content, source, session_key, ts)
445
- VALUES ('user', 'timezone test', 'web', 'tz-session', '2026-05-09 01:23:45')`).run();
830
+ db.prepare(`INSERT INTO conversation_log (role, content, source, session_key, run_id, ts)
831
+ VALUES ('user', 'timezone test', 'web', 'tz-session', ?, '2026-05-09 01:23:45')`).run(dbModule.getCurrentRunId());
446
832
  const messages = dbModule.getSessionMessages("tz-session");
447
833
  assert.equal(messages.length, 1, "should return the inserted row");
448
834
  assert.equal(messages[0].ts, "2026-05-09T01:23:45Z", "ts must be normalized to ISO-8601 UTC");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chapterhouse",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Chapterhouse — a team-level AI assistant for engineering teams, built on the GitHub Copilot SDK. Web UI only.",
5
5
  "bin": {
6
6
  "chapterhouse": "dist/cli.js"