chapterhouse 0.4.2 → 0.5.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 (36) hide show
  1. package/agents/bellonda.agent.md +11 -0
  2. package/agents/hwi-noree.agent.md +12 -0
  3. package/dist/api/server.js +39 -2
  4. package/dist/api/server.test.js +20 -0
  5. package/dist/api/turn-sse.integration.test.js +12 -0
  6. package/dist/copilot/agents.js +16 -4
  7. package/dist/copilot/agents.test.js +43 -1
  8. package/dist/copilot/orchestrator.js +173 -32
  9. package/dist/copilot/orchestrator.test.js +236 -20
  10. package/dist/copilot/session-manager.js +11 -2
  11. package/dist/copilot/session-manager.test.js +25 -0
  12. package/dist/copilot/tools.agent.test.js +52 -4
  13. package/dist/copilot/tools.js +265 -18
  14. package/dist/copilot/tools.memory.test.js +175 -2
  15. package/dist/daemon.js +6 -0
  16. package/dist/memory/action-items.js +100 -0
  17. package/dist/memory/action-items.test.js +83 -0
  18. package/dist/memory/active-scope.js +9 -0
  19. package/dist/memory/eot.js +28 -3
  20. package/dist/memory/eot.test.js +108 -0
  21. package/dist/memory/hot-tier.js +60 -1
  22. package/dist/memory/hot-tier.test.js +38 -0
  23. package/dist/memory/housekeeping-scheduler.js +152 -0
  24. package/dist/memory/housekeeping-scheduler.test.js +187 -0
  25. package/dist/memory/index.js +2 -1
  26. package/dist/memory/recall.js +59 -0
  27. package/dist/memory/recall.test.js +27 -0
  28. package/dist/memory/tiering.js +33 -3
  29. package/dist/store/db.js +130 -17
  30. package/dist/store/db.test.js +61 -5
  31. package/package.json +1 -1
  32. package/web/dist/assets/{index-B_cCSHan.js → index-BfHqP3-C.js} +87 -87
  33. package/web/dist/assets/{index-B_cCSHan.js.map → index-BfHqP3-C.js.map} +1 -1
  34. package/web/dist/assets/index-_O6AoWOS.css +10 -0
  35. package/web/dist/index.html +2 -2
  36. package/web/dist/assets/index-DhY5yWmC.css +0 -10
package/dist/store/db.js CHANGED
@@ -141,7 +141,7 @@ function rebuildMemoryTierTables(database) {
141
141
  }
142
142
  }
143
143
  function ensureMemoryTierColumns(database) {
144
- for (const table of ["mem_entities", "mem_observations", "mem_decisions"]) {
144
+ for (const table of ["mem_entities", "mem_observations", "mem_decisions", "mem_action_items"]) {
145
145
  if (!hasColumn(database, table, "tier")) {
146
146
  database.exec(`ALTER TABLE ${table} ADD COLUMN tier TEXT DEFAULT 'warm'`);
147
147
  }
@@ -184,12 +184,24 @@ function ensureMemoryTierColumns(database) {
184
184
  ELSE 'warm'
185
185
  END
186
186
  `);
187
+ database.exec(`
188
+ UPDATE mem_action_items
189
+ SET tier = CASE
190
+ WHEN status IN ('done', 'dropped') THEN 'cold'
191
+ WHEN tier = 'glacier' THEN 'cold'
192
+ WHEN tier IN ('hot', 'warm', 'cold') THEN tier
193
+ WHEN status = 'open' AND due_at IS NOT NULL AND datetime(due_at) <= datetime('now', '+7 days') THEN 'hot'
194
+ ELSE 'warm'
195
+ END
196
+ `);
187
197
  }
188
198
  function ensureMemoryIndexes(database) {
189
199
  database.exec(`CREATE INDEX IF NOT EXISTS mem_entities_scope_kind_idx ON mem_entities(scope_id, kind)`);
190
200
  database.exec(`CREATE UNIQUE INDEX IF NOT EXISTS mem_entities_scope_kind_name_idx ON mem_entities(scope_id, kind, name)`);
191
201
  database.exec(`CREATE INDEX IF NOT EXISTS mem_observations_scope_idx ON mem_observations(scope_id)`);
192
202
  database.exec(`CREATE INDEX IF NOT EXISTS mem_decisions_scope_idx ON mem_decisions(scope_id)`);
203
+ database.exec(`CREATE INDEX IF NOT EXISTS idx_mem_action_items_scope_status ON mem_action_items(scope_id, status)`);
204
+ database.exec(`CREATE INDEX IF NOT EXISTS idx_mem_action_items_due ON mem_action_items(status, due_at)`);
193
205
  }
194
206
  const MEMORY_SCOPE_SEEDS = [
195
207
  {
@@ -561,6 +573,8 @@ export function getDb() {
561
573
  source TEXT NOT NULL DEFAULT 'unknown',
562
574
  session_key TEXT NOT NULL DEFAULT 'default',
563
575
  turn_id TEXT,
576
+ agent_slug TEXT,
577
+ agent_display_name TEXT,
564
578
  run_id TEXT,
565
579
  ts DATETIME DEFAULT CURRENT_TIMESTAMP
566
580
  )
@@ -587,6 +601,8 @@ export function getDb() {
587
601
  const oldConvColNames = new Set(oldConvCols.map((column) => column.name));
588
602
  const sessionKeySelect = oldConvColNames.has("session_key") ? "session_key" : "'default'";
589
603
  const turnIdSelect = oldConvColNames.has("turn_id") ? "turn_id" : "NULL";
604
+ const agentSlugSelect = oldConvColNames.has("agent_slug") ? "agent_slug" : "NULL";
605
+ const agentDisplayNameSelect = oldConvColNames.has("agent_display_name") ? "agent_display_name" : "NULL";
590
606
  const runIdSelect = oldConvColNames.has("run_id") ? "run_id" : "NULL";
591
607
  db.exec(`
592
608
  CREATE TABLE conversation_log (
@@ -596,13 +612,15 @@ export function getDb() {
596
612
  source TEXT NOT NULL DEFAULT 'unknown',
597
613
  session_key TEXT NOT NULL DEFAULT 'default',
598
614
  turn_id TEXT,
615
+ agent_slug TEXT,
616
+ agent_display_name TEXT,
599
617
  run_id TEXT,
600
618
  ts DATETIME DEFAULT CURRENT_TIMESTAMP
601
619
  )
602
620
  `);
603
621
  db.exec(`
604
- INSERT INTO conversation_log (role, content, source, session_key, turn_id, run_id, ts)
605
- SELECT role, content, source, ${sessionKeySelect}, ${turnIdSelect}, ${runIdSelect}, ts FROM conversation_log_old
622
+ INSERT INTO conversation_log (role, content, source, session_key, turn_id, agent_slug, agent_display_name, run_id, ts)
623
+ SELECT role, content, source, ${sessionKeySelect}, ${turnIdSelect}, ${agentSlugSelect}, ${agentDisplayNameSelect}, ${runIdSelect}, ts FROM conversation_log_old
606
624
  `);
607
625
  db.exec(`DROP TABLE conversation_log_old`);
608
626
  }
@@ -610,14 +628,40 @@ export function getDb() {
610
628
  db.exec(`
611
629
  CREATE TABLE IF NOT EXISTS copilot_sessions (
612
630
  session_key TEXT PRIMARY KEY,
613
- mode TEXT NOT NULL CHECK(mode IN ('default', 'project')),
631
+ mode TEXT NOT NULL CHECK(mode IN ('default', 'project', 'agent')),
614
632
  project_root TEXT,
615
633
  copilot_session_id TEXT NOT NULL,
616
634
  model TEXT,
617
635
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
618
636
  )
619
637
  `);
620
- // Migrate: add session_key column to conversation_log if not present
638
+ try {
639
+ db.prepare(`
640
+ INSERT INTO copilot_sessions (session_key, mode, copilot_session_id)
641
+ VALUES ('__mode_probe__', 'agent', '__probe__')
642
+ `).run();
643
+ db.prepare(`DELETE FROM copilot_sessions WHERE session_key = '__mode_probe__'`).run();
644
+ }
645
+ catch {
646
+ db.exec(`ALTER TABLE copilot_sessions RENAME TO copilot_sessions_old`);
647
+ db.exec(`
648
+ CREATE TABLE copilot_sessions (
649
+ session_key TEXT PRIMARY KEY,
650
+ mode TEXT NOT NULL CHECK(mode IN ('default', 'project', 'agent')),
651
+ project_root TEXT,
652
+ copilot_session_id TEXT NOT NULL,
653
+ model TEXT,
654
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
655
+ )
656
+ `);
657
+ db.exec(`
658
+ INSERT INTO copilot_sessions (session_key, mode, project_root, copilot_session_id, model, updated_at)
659
+ SELECT session_key, mode, project_root, copilot_session_id, model, updated_at
660
+ FROM copilot_sessions_old
661
+ `);
662
+ db.exec(`DROP TABLE copilot_sessions_old`);
663
+ }
664
+ // Migrate: add metadata columns to conversation_log if not present
621
665
  const convCols = db.prepare(`PRAGMA table_info(conversation_log)`).all();
622
666
  if (!convCols.some((c) => c.name === 'session_key')) {
623
667
  db.exec(`ALTER TABLE conversation_log ADD COLUMN session_key TEXT NOT NULL DEFAULT 'default'`);
@@ -625,6 +669,12 @@ export function getDb() {
625
669
  if (!convCols.some((c) => c.name === 'turn_id')) {
626
670
  db.exec(`ALTER TABLE conversation_log ADD COLUMN turn_id TEXT`);
627
671
  }
672
+ if (!convCols.some((c) => c.name === 'agent_slug')) {
673
+ db.exec(`ALTER TABLE conversation_log ADD COLUMN agent_slug TEXT`);
674
+ }
675
+ if (!convCols.some((c) => c.name === 'agent_display_name')) {
676
+ db.exec(`ALTER TABLE conversation_log ADD COLUMN agent_display_name TEXT`);
677
+ }
628
678
  if (!convCols.some((c) => c.name === "run_id")) {
629
679
  db.exec(`ALTER TABLE conversation_log ADD COLUMN run_id TEXT`);
630
680
  }
@@ -785,6 +835,27 @@ export function getDb() {
785
835
  last_recalled_at DATETIME,
786
836
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
787
837
  )
838
+ `);
839
+ db.exec(`
840
+ CREATE TABLE IF NOT EXISTS mem_action_items (
841
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
842
+ scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
843
+ entity_id INTEGER REFERENCES mem_entities(id),
844
+ title TEXT NOT NULL,
845
+ detail TEXT,
846
+ status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'done', 'dropped', 'snoozed')),
847
+ due_at TEXT,
848
+ snooze_until TEXT,
849
+ source TEXT,
850
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
851
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
852
+ resolved_at TEXT,
853
+ resolution_reason TEXT,
854
+ tier TEXT NOT NULL DEFAULT 'warm' CHECK(tier IN ('hot', 'warm', 'cold')),
855
+ tier_pinned_at TEXT,
856
+ tier_reason TEXT,
857
+ last_recalled_at TEXT
858
+ )
788
859
  `);
789
860
  const decisionCols = db.prepare(`PRAGMA table_info(mem_decisions)`).all();
790
861
  if (!decisionCols.some((column) => column.name === "superseded_by")) {
@@ -856,6 +927,13 @@ export function getDb() {
856
927
  rationale,
857
928
  content_rowid='id'
858
929
  )
930
+ `);
931
+ db.exec(`
932
+ CREATE VIRTUAL TABLE IF NOT EXISTS mem_action_items_fts USING fts5(
933
+ title,
934
+ detail,
935
+ content_rowid='id'
936
+ )
859
937
  `);
860
938
  // Sync triggers
861
939
  db.exec(`DROP TRIGGER IF EXISTS memories_ai`);
@@ -867,6 +945,9 @@ export function getDb() {
867
945
  db.exec(`DROP TRIGGER IF EXISTS mem_decisions_ai`);
868
946
  db.exec(`DROP TRIGGER IF EXISTS mem_decisions_ad`);
869
947
  db.exec(`DROP TRIGGER IF EXISTS mem_decisions_au`);
948
+ db.exec(`DROP TRIGGER IF EXISTS mem_action_items_ai`);
949
+ db.exec(`DROP TRIGGER IF EXISTS mem_action_items_ad`);
950
+ db.exec(`DROP TRIGGER IF EXISTS mem_action_items_au`);
870
951
  db.exec(`
871
952
  CREATE TRIGGER memories_ai AFTER INSERT ON memories BEGIN
872
953
  INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
@@ -916,6 +997,24 @@ export function getDb() {
916
997
  INSERT INTO mem_decisions_fts(rowid, title, rationale)
917
998
  VALUES (new.id, new.title, new.rationale);
918
999
  END
1000
+ `);
1001
+ db.exec(`
1002
+ CREATE TRIGGER mem_action_items_ai AFTER INSERT ON mem_action_items BEGIN
1003
+ INSERT INTO mem_action_items_fts(rowid, title, detail)
1004
+ VALUES (new.id, new.title, new.detail);
1005
+ END
1006
+ `);
1007
+ db.exec(`
1008
+ CREATE TRIGGER mem_action_items_ad AFTER DELETE ON mem_action_items BEGIN
1009
+ DELETE FROM mem_action_items_fts WHERE rowid = old.id;
1010
+ END
1011
+ `);
1012
+ db.exec(`
1013
+ CREATE TRIGGER mem_action_items_au AFTER UPDATE ON mem_action_items BEGIN
1014
+ DELETE FROM mem_action_items_fts WHERE rowid = old.id;
1015
+ INSERT INTO mem_action_items_fts(rowid, title, detail)
1016
+ VALUES (new.id, new.title, new.detail);
1017
+ END
919
1018
  `);
920
1019
  // Backfill: check if FTS is in sync by comparing row counts
921
1020
  const memCount = db.prepare(`SELECT COUNT(*) as c FROM memories`).get().c;
@@ -933,6 +1032,11 @@ export function getDb() {
933
1032
  if (decisionCount > 0 && decisionFtsCount < decisionCount) {
934
1033
  db.exec(`INSERT INTO mem_decisions_fts(mem_decisions_fts) VALUES ('rebuild')`);
935
1034
  }
1035
+ const actionItemCount = db.prepare(`SELECT COUNT(*) as c FROM mem_action_items`).get().c;
1036
+ const actionItemFtsCount = db.prepare(`SELECT COUNT(*) as c FROM mem_action_items_fts`).get().c;
1037
+ if (actionItemCount > 0 && actionItemFtsCount < actionItemCount) {
1038
+ db.exec(`INSERT INTO mem_action_items_fts(mem_action_items_fts) VALUES ('rebuild')`);
1039
+ }
936
1040
  fts5Available = true;
937
1041
  }
938
1042
  catch {
@@ -961,10 +1065,10 @@ export function deleteState(key) {
961
1065
  db.prepare(`DELETE FROM max_state WHERE key = ?`).run(key);
962
1066
  }
963
1067
  /** Log a conversation turn (user, assistant, or system). */
964
- export function logConversation(role, content, source, sessionKey = "default", turnId) {
1068
+ export function logConversation(role, content, source, sessionKey = "default", metadata) {
965
1069
  const db = getDb();
966
- db.prepare(`INSERT INTO conversation_log (role, content, source, session_key, turn_id, run_id) VALUES (?, ?, ?, ?, ?, ?)`)
967
- .run(role, content, source, sessionKey, turnId ?? null, getCurrentRunId());
1070
+ db.prepare(`INSERT INTO conversation_log (role, content, source, session_key, turn_id, agent_slug, agent_display_name, run_id)
1071
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(role, content, source, sessionKey, metadata?.turnId ?? null, metadata?.agentSlug ?? null, metadata?.agentDisplayName ?? null, getCurrentRunId());
968
1072
  // Keep last 1000 entries to support context recovery after session loss
969
1073
  logInsertCount++;
970
1074
  if (logInsertCount % 50 === 0) {
@@ -1076,24 +1180,33 @@ export function getSessionMessages(sessionKey, limit, options = {}) {
1076
1180
  const runId = options.runId ?? getCurrentRunId();
1077
1181
  const rows = includeHistorical
1078
1182
  ? db
1079
- .prepare(`SELECT id, role, content, ts, turn_id FROM conversation_log
1183
+ .prepare(`SELECT id, role, content, ts, turn_id, agent_slug, agent_display_name FROM conversation_log
1080
1184
  WHERE session_key = ? AND role IN ('user', 'assistant', 'agent_completion')
1081
1185
  ORDER BY id DESC LIMIT ?`)
1082
1186
  .all(sessionKey, effectiveLimit)
1083
1187
  : db
1084
- .prepare(`SELECT id, role, content, ts, turn_id FROM conversation_log
1188
+ .prepare(`SELECT id, role, content, ts, turn_id, agent_slug, agent_display_name FROM conversation_log
1085
1189
  WHERE session_key = ? AND run_id = ? AND role IN ('user', 'assistant', 'agent_completion')
1086
1190
  ORDER BY id DESC LIMIT ?`)
1087
1191
  .all(sessionKey, runId, effectiveLimit);
1088
1192
  // Reverse so oldest is first (chronological order for the UI)
1089
1193
  rows.reverse();
1090
- return rows.map((r) => ({
1091
- id: r.id,
1092
- role: r.role === "agent_completion" ? "assistant" : r.role,
1093
- content: r.content,
1094
- ts: normalizeSqliteTsToIso(r.ts),
1095
- turn_id: r.turn_id,
1096
- }));
1194
+ return rows.map((r) => {
1195
+ const message = {
1196
+ id: r.id,
1197
+ role: r.role === "agent_completion" ? "assistant" : r.role,
1198
+ content: r.content,
1199
+ ts: normalizeSqliteTsToIso(r.ts),
1200
+ turn_id: r.turn_id,
1201
+ };
1202
+ if (r.turn_id)
1203
+ message.turnId = r.turn_id;
1204
+ if (r.agent_slug)
1205
+ message.agentSlug = r.agent_slug;
1206
+ if (r.agent_display_name)
1207
+ message.agentDisplayName = r.agent_display_name;
1208
+ return message;
1209
+ });
1097
1210
  }
1098
1211
  /**
1099
1212
  * Append one event to agent_task_events and return the new event.
@@ -58,6 +58,53 @@ test("getDb initializes schema, state helpers, and conversation formatting", asy
58
58
  dbModule.closeDb();
59
59
  }
60
60
  });
61
+ test("getDb initializes action-item memory schema and FTS shadow", async () => {
62
+ const dbModule = await loadDbModule();
63
+ try {
64
+ const db = dbModule.getDb();
65
+ const tables = db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table'`).all();
66
+ const tableNames = new Set(tables.map((row) => row.name));
67
+ assert.equal(tableNames.has("mem_action_items"), true, "expected mem_action_items table");
68
+ assert.equal(tableNames.has("mem_action_items_fts"), true, "expected mem_action_items_fts virtual table");
69
+ const columns = db.prepare(`PRAGMA table_info(mem_action_items)`).all();
70
+ const columnNames = new Set(columns.map((column) => column.name));
71
+ for (const name of [
72
+ "id",
73
+ "scope_id",
74
+ "entity_id",
75
+ "title",
76
+ "detail",
77
+ "status",
78
+ "due_at",
79
+ "snooze_until",
80
+ "source",
81
+ "created_at",
82
+ "updated_at",
83
+ "resolved_at",
84
+ "resolution_reason",
85
+ "tier",
86
+ "tier_pinned_at",
87
+ "tier_reason",
88
+ "last_recalled_at",
89
+ ]) {
90
+ assert.equal(columnNames.has(name), true, `expected mem_action_items.${name}`);
91
+ }
92
+ const scope = db.prepare(`SELECT id FROM mem_scopes WHERE slug = 'chapterhouse'`).get();
93
+ const inserted = db.prepare(`
94
+ INSERT INTO mem_action_items (scope_id, title, detail, source)
95
+ VALUES (?, 'Action FTS sentinel', 'Searchable migration reminder', 'test')
96
+ `).run(scope.id);
97
+ const ftsHits = db.prepare(`
98
+ SELECT rowid
99
+ FROM mem_action_items_fts
100
+ WHERE mem_action_items_fts MATCH 'migration'
101
+ `).all();
102
+ assert.equal(ftsHits.some((hit) => hit.rowid === Number(inserted.lastInsertRowid)), true);
103
+ }
104
+ finally {
105
+ dbModule.closeDb();
106
+ }
107
+ });
61
108
  test("getDb migrates legacy conversation_log tables to allow system messages", async () => {
62
109
  const seedDb = new Database(dbPath);
63
110
  seedDb.exec(`
@@ -559,12 +606,15 @@ test("getSessionMessages returns empty array for unknown session", async () => {
559
606
  test("getSessionMessages returns structured messages in chronological order, includes agent completions, excludes system rows, respects limit", async () => {
560
607
  const dbModule = await loadDbModule();
561
608
  try {
562
- const db = dbModule.getDb();
609
+ dbModule.getDb();
563
610
  dbModule.logConversation("user", "hello", "web", "test-session");
564
611
  dbModule.logConversation("assistant", "hi there", "web", "test-session");
565
612
  dbModule.logConversation("system", "system noise", "worker", "test-session");
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());
613
+ dbModule.logConversation("agent_completion", "Done", "background", "test-session", {
614
+ agentSlug: "coder",
615
+ agentDisplayName: "Kaylee",
616
+ turnId: "agent-turn-1",
617
+ });
568
618
  dbModule.logConversation("user", "second message", "web", "test-session");
569
619
  dbModule.logConversation("user", "from other session", "web", "other-session");
570
620
  const all = dbModule.getSessionMessages("test-session");
@@ -574,14 +624,19 @@ test("getSessionMessages returns structured messages in chronological order, inc
574
624
  assert.equal(all[1].role, "assistant");
575
625
  assert.equal(all[1].content, "hi there");
576
626
  assert.equal(all[2].role, "assistant");
577
- assert.equal(all[2].content, "[Agent task completed] @coder finished task task-1:\n\nDone");
627
+ assert.equal(all[2].content, "Done");
628
+ assert.equal(all[2].agentSlug, "coder");
629
+ assert.equal(all[2].agentDisplayName, "Kaylee");
630
+ assert.equal(all[2].turnId, "agent-turn-1");
578
631
  assert.equal(all[3].role, "user");
579
632
  assert.equal(all[3].content, "second message");
580
633
  // Limit clamping
581
634
  const limited = dbModule.getSessionMessages("test-session", 2);
582
635
  assert.equal(limited.length, 2, "limit=2 returns 2 most recent rows");
583
636
  // After reversal, these should be the 2 most-recent renderable rows.
584
- assert.equal(limited[0].content, "[Agent task completed] @coder finished task task-1:\n\nDone");
637
+ assert.equal(limited[0].content, "Done");
638
+ assert.equal(limited[0].agentSlug, "coder");
639
+ assert.equal(limited[0].turnId, "agent-turn-1");
585
640
  assert.equal(limited[1].content, "second message");
586
641
  // Other session not leaked
587
642
  const other = dbModule.getSessionMessages("other-session");
@@ -605,6 +660,7 @@ test("getSessionMessages returns stable row id and turn_id for hydration reconci
605
660
  const message = messages[0];
606
661
  assert.equal(message.id, Number(result.lastInsertRowid));
607
662
  assert.equal(message.turn_id, "turn-stable-1");
663
+ assert.equal(message.turnId, "turn-stable-1");
608
664
  }
609
665
  finally {
610
666
  dbModule.closeDb();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chapterhouse",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
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"