chapterhouse 0.4.3 → 0.5.1

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.
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdirSync, rmSync } from "node:fs";
2
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import test from "node:test";
5
5
  const repoRoot = process.cwd();
@@ -15,7 +15,8 @@ async function loadModules(cacheBust = `${Date.now()}-${Math.random()}`) {
15
15
  const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
16
16
  const memoryModule = await import(new URL(`./index.js?case=${cacheBust}`, import.meta.url).href);
17
17
  const eotModule = await import(new URL(`./eot.js?case=${cacheBust}`, import.meta.url).href);
18
- return { dbModule, memoryModule, eotModule };
18
+ const agentsModule = await import(new URL(`../copilot/agents.js?case=${cacheBust}`, import.meta.url).href);
19
+ return { dbModule, memoryModule, eotModule, agentsModule };
19
20
  }
20
21
  function getFunction(module, name) {
21
22
  const value = module[name];
@@ -171,7 +172,6 @@ test("runEndOfTaskMemoryHook accepts action_item proposals into mem_action_items
171
172
  payload: {
172
173
  title: "Migrate feature ideas",
173
174
  detail: "Move feature-ideas.md into mem_action_items.",
174
- source: "subagent_proposal",
175
175
  },
176
176
  confidence: 0.9,
177
177
  }));
@@ -189,11 +189,192 @@ test("runEndOfTaskMemoryHook accepts action_item proposals into mem_action_items
189
189
  }),
190
190
  });
191
191
  const actionItems = listActionItems({ scope_id: chapterhouse.id });
192
- assert.equal(actionItems.some((item) => item.title === "Migrate feature ideas"
193
- && item.detail === "Move feature-ideas.md into mem_action_items."), true);
192
+ const actionItem = actionItems.find((item) => item.title === "Migrate feature ideas");
193
+ assert.ok(actionItem);
194
+ assert.equal(actionItem.detail, "Move feature-ideas.md into mem_action_items.");
195
+ assert.equal(actionItem.status, "open");
196
+ assert.equal(actionItem.source, "subagent_proposal:coder");
194
197
  const inbox = db.prepare(`SELECT status FROM mem_inbox WHERE id = ?`).get(Number(inserted.lastInsertRowid));
195
198
  assert.equal(inbox.status, "accepted");
196
199
  });
200
+ test("runEndOfTaskMemoryHook rejects invalid action_item proposals with a clear reason", async () => {
201
+ const { dbModule, memoryModule, eotModule } = await loadModules("action-item-invalid");
202
+ const db = dbModule.getDb();
203
+ const getScope = getFunction(memoryModule, "getScope");
204
+ const createScope = getFunction(memoryModule, "createScope");
205
+ const listActionItems = getFunction(memoryModule, "listActionItems");
206
+ const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
207
+ const chapterhouse = getScope("chapterhouse");
208
+ assert.ok(chapterhouse);
209
+ const inserted = db.prepare(`
210
+ INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
211
+ VALUES (?, 'memory_proposal', ?, 'coder', 'task-eot-action-item-invalid', 'pending')
212
+ `).run(chapterhouse.id, JSON.stringify({
213
+ kind: "action_item",
214
+ payload: {
215
+ detail: "A title is required before this can become an action item.",
216
+ },
217
+ confidence: 0.9,
218
+ }));
219
+ await runEndOfTaskMemoryHook({
220
+ taskId: "task-eot-action-item-invalid",
221
+ finalResult: "Completed and proposed a malformed follow-up.",
222
+ copilotClient: {},
223
+ callLLM: async () => JSON.stringify({
224
+ decisions: [{
225
+ proposal_id: Number(inserted.lastInsertRowid),
226
+ decision: "accept",
227
+ reason: "Concrete follow-up.",
228
+ }],
229
+ implicit_memories: [],
230
+ }),
231
+ });
232
+ assert.deepEqual(listActionItems({ scope_id: chapterhouse.id }), []);
233
+ const inbox = db.prepare(`
234
+ SELECT status, resolution_reason, resolved_at
235
+ FROM mem_inbox
236
+ WHERE id = ?
237
+ `).get(Number(inserted.lastInsertRowid));
238
+ assert.equal(inbox.status, "rejected");
239
+ assert.match(inbox.resolution_reason ?? "", /title/i);
240
+ assert.ok(inbox.resolved_at);
241
+ });
242
+ test("runEndOfTaskMemoryHook rejects action_item proposals with ambiguous entity references", async () => {
243
+ const { dbModule, memoryModule, eotModule } = await loadModules("action-item-ambiguous-entity");
244
+ const db = dbModule.getDb();
245
+ const getScope = getFunction(memoryModule, "getScope");
246
+ const createScope = getFunction(memoryModule, "createScope");
247
+ const listActionItems = getFunction(memoryModule, "listActionItems");
248
+ const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
249
+ const chapterhouse = getScope("chapterhouse");
250
+ assert.ok(chapterhouse);
251
+ const inserted = db.prepare(`
252
+ INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
253
+ VALUES (?, 'memory_proposal', ?, 'coder', 'task-eot-action-item-ambiguous-entity', 'pending')
254
+ `).run(chapterhouse.id, JSON.stringify({
255
+ kind: "action_item",
256
+ payload: {
257
+ title: "Resolve ambiguous entity link",
258
+ entity_id: 1,
259
+ entity_name: "Bellonda",
260
+ entity_kind: "agent",
261
+ },
262
+ confidence: 0.9,
263
+ }));
264
+ await runEndOfTaskMemoryHook({
265
+ taskId: "task-eot-action-item-ambiguous-entity",
266
+ finalResult: "Completed and proposed an ambiguous follow-up.",
267
+ copilotClient: {},
268
+ callLLM: async () => JSON.stringify({
269
+ decisions: [{
270
+ proposal_id: Number(inserted.lastInsertRowid),
271
+ decision: "accept",
272
+ reason: "Concrete follow-up.",
273
+ }],
274
+ implicit_memories: [],
275
+ }),
276
+ });
277
+ assert.deepEqual(listActionItems({ scope_id: chapterhouse.id }), []);
278
+ const inbox = db.prepare(`
279
+ SELECT status, resolution_reason
280
+ FROM mem_inbox
281
+ WHERE id = ?
282
+ `).get(Number(inserted.lastInsertRowid));
283
+ assert.equal(inbox.status, "rejected");
284
+ assert.match(inbox.resolution_reason ?? "", /entity_id.*entity_name|entity_name.*entity_id/i);
285
+ });
286
+ test("runEndOfTaskMemoryHook falls back to the source agent bound scope for action_item proposals without scope_slug", async () => {
287
+ const home = process.env.CHAPTERHOUSE_HOME;
288
+ assert.ok(home, "test home should be set");
289
+ const { AGENTS_DIR: agentsDir } = await import("../paths.js");
290
+ mkdirSync(agentsDir, { recursive: true });
291
+ writeFileSync(join(agentsDir, "bellonda.agent.md"), [
292
+ "---",
293
+ "name: Bellonda",
294
+ "description: Mentat of the infrastructure domain",
295
+ "model: claude-sonnet-4.6",
296
+ "persistent: true",
297
+ "scope: infra",
298
+ "---",
299
+ "",
300
+ "You are Bellonda.",
301
+ ].join("\n"));
302
+ const { dbModule, memoryModule, eotModule, agentsModule } = await loadModules("action-item-bound-scope");
303
+ agentsModule.loadAgents();
304
+ const db = dbModule.getDb();
305
+ const getScope = getFunction(memoryModule, "getScope");
306
+ const createScope = getFunction(memoryModule, "createScope");
307
+ const listActionItems = getFunction(memoryModule, "listActionItems");
308
+ const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
309
+ const chapterhouse = getScope("chapterhouse");
310
+ const infra = createScope({
311
+ slug: "infra",
312
+ title: "Infra",
313
+ description: "Infra test scope",
314
+ keywords: ["infra"],
315
+ });
316
+ assert.ok(chapterhouse);
317
+ const inserted = db.prepare(`
318
+ INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
319
+ VALUES (?, 'memory_proposal', ?, 'bellonda', 'task-eot-action-item-bound-scope', 'pending')
320
+ `).run(chapterhouse.id, JSON.stringify({
321
+ kind: "action_item",
322
+ payload: {
323
+ title: "Review NAS disk alerts",
324
+ },
325
+ confidence: 0.9,
326
+ }));
327
+ await runEndOfTaskMemoryHook({
328
+ taskId: "task-eot-action-item-bound-scope",
329
+ finalResult: "Bellonda proposed an infra follow-up.",
330
+ copilotClient: {},
331
+ callLLM: async () => JSON.stringify({
332
+ decisions: [{
333
+ proposal_id: Number(inserted.lastInsertRowid),
334
+ decision: "accept",
335
+ reason: "Concrete infra follow-up.",
336
+ }],
337
+ implicit_memories: [],
338
+ }),
339
+ });
340
+ assert.equal(listActionItems({ scope_id: infra.id }).some((item) => item.title === "Review NAS disk alerts"), true);
341
+ assert.equal(listActionItems({ scope_id: chapterhouse.id }).some((item) => item.title === "Review NAS disk alerts"), false);
342
+ });
343
+ test("runEndOfTaskMemoryHook still accepts observation, decision, and entity proposals", async () => {
344
+ const { dbModule, memoryModule, eotModule } = await loadModules("existing-proposal-kinds");
345
+ const db = dbModule.getDb();
346
+ const getScope = getFunction(memoryModule, "getScope");
347
+ const listObservations = getFunction(memoryModule, "listObservations");
348
+ const listDecisions = getFunction(memoryModule, "listDecisions");
349
+ const listEntities = getFunction(memoryModule, "listEntities");
350
+ const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
351
+ const chapterhouse = getScope("chapterhouse");
352
+ assert.ok(chapterhouse);
353
+ const proposals = [
354
+ { kind: "observation", payload: { content: "Existing observation proposals still persist." } },
355
+ { kind: "decision", payload: { title: "Keep auto-accept", rationale: "It is required for EOT memory promotion." } },
356
+ { kind: "entity", payload: { name: "Bellonda", entity_kind: "agent", summary: "Infrastructure mentat." } },
357
+ ].map((envelope) => db.prepare(`
358
+ INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
359
+ VALUES (?, 'memory_proposal', ?, 'coder', 'task-eot-existing-kinds', 'pending')
360
+ `).run(chapterhouse.id, JSON.stringify(envelope)));
361
+ await runEndOfTaskMemoryHook({
362
+ taskId: "task-eot-existing-kinds",
363
+ finalResult: "Completed with several proposal kinds.",
364
+ copilotClient: {},
365
+ callLLM: async () => JSON.stringify({
366
+ decisions: proposals.map((proposal) => ({
367
+ proposal_id: Number(proposal.lastInsertRowid),
368
+ decision: "accept",
369
+ reason: "Valid durable memory.",
370
+ })),
371
+ implicit_memories: [],
372
+ }),
373
+ });
374
+ assert.equal(listObservations({ scope_id: chapterhouse.id }).some((row) => row.content === "Existing observation proposals still persist."), true);
375
+ assert.equal(listDecisions({ scope_id: chapterhouse.id }).some((row) => row.title === "Keep auto-accept" && row.rationale === "It is required for EOT memory promotion."), true);
376
+ assert.equal(listEntities({ scope_id: chapterhouse.id, kind: "agent" }).some((row) => row.name === "Bellonda"), true);
377
+ });
197
378
  test("runEndOfTaskMemoryHook accepts entity proposals with entity_kind into mem_entities", async () => {
198
379
  const { dbModule, memoryModule, eotModule } = await loadModules("entity-accept");
199
380
  const db = dbModule.getDb();
@@ -108,12 +108,17 @@ test("active-scope hot-tier queries do not leak rows from other scopes", async (
108
108
  const { dbModule, memoryModule, hotTierModule } = await loadModules();
109
109
  dbModule.getDb();
110
110
  const getScope = getFunction(memoryModule, "getScope");
111
+ const createScope = getFunction(memoryModule, "createScope");
111
112
  const setActiveScope = getFunction(memoryModule, "setActiveScope");
112
113
  const recordObservation = getFunction(memoryModule, "recordObservation");
113
114
  const chapterhouse = getScope("chapterhouse");
114
- const team = getScope("team");
115
+ const team = createScope({
116
+ slug: "team",
117
+ title: "Team",
118
+ description: "Team test scope",
119
+ keywords: ["team"],
120
+ });
115
121
  assert.ok(chapterhouse);
116
- assert.ok(team);
117
122
  recordObservation({
118
123
  scope_id: chapterhouse.id,
119
124
  content: "Chapterhouse hot entry",
@@ -135,11 +140,16 @@ test("renderHotTierXML includes open active-scope action items in a bounded acti
135
140
  const { dbModule, memoryModule, hotTierModule } = await loadModules();
136
141
  dbModule.getDb();
137
142
  const getScope = getFunction(memoryModule, "getScope");
143
+ const createScope = getFunction(memoryModule, "createScope");
138
144
  const recordActionItem = getFunction(memoryModule, "recordActionItem");
139
145
  const chapterhouse = getScope("chapterhouse");
140
- const team = getScope("team");
146
+ const team = createScope({
147
+ slug: "team",
148
+ title: "Team",
149
+ description: "Team test scope",
150
+ keywords: ["team"],
151
+ });
141
152
  assert.ok(chapterhouse);
142
- assert.ok(team);
143
153
  const urgent = recordActionItem({
144
154
  scope_id: chapterhouse.id,
145
155
  title: "Migrate <feature ideas>",
@@ -24,6 +24,15 @@ function getFunction(module, name) {
24
24
  assert.equal(typeof value, "function", `expected ${name} to be exported`);
25
25
  return value;
26
26
  }
27
+ function createTestScope(memoryModule, slug) {
28
+ const createScope = getFunction(memoryModule, "createScope");
29
+ return createScope({
30
+ slug,
31
+ title: slug.split("-").map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" "),
32
+ description: `${slug} test scope`,
33
+ keywords: [slug],
34
+ });
35
+ }
27
36
  test.beforeEach(async () => {
28
37
  const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
29
38
  dbModule.closeDb();
@@ -42,8 +51,8 @@ test("dedupObservationsPass supersedes similar observations in scope determinist
42
51
  const getScope = getFunction(memoryModule, "getScope");
43
52
  const recordObservation = getFunction(memoryModule, "recordObservation");
44
53
  const chapterhouse = getScope("chapterhouse");
45
- const team = getScope("team");
46
- assert.ok(chapterhouse && team);
54
+ const team = createTestScope(memoryModule, "team");
55
+ assert.ok(chapterhouse);
47
56
  const first = recordObservation({
48
57
  scope_id: team.id,
49
58
  content: "The worker event stream uses server sent events for live task output.",
@@ -88,9 +97,8 @@ test("dedupDecisionsPass supersedes similar active decisions within scope and ke
88
97
  const getScope = getFunction(memoryModule, "getScope");
89
98
  const upsertEntity = getFunction(memoryModule, "upsertEntity");
90
99
  const recordDecision = getFunction(memoryModule, "recordDecision");
91
- const team = getScope("team");
92
- const infra = getScope("infra");
93
- assert.ok(team && infra);
100
+ const team = createTestScope(memoryModule, "team");
101
+ const infra = createTestScope(memoryModule, "infra");
94
102
  const api = upsertEntity({ scope_id: team.id, kind: "component", name: "api" });
95
103
  const web = upsertEntity({ scope_id: team.id, kind: "component", name: "web" });
96
104
  const oldDecision = recordDecision({
@@ -139,8 +147,8 @@ test("orphanCleanupPass clears missing observation entity references without tou
139
147
  const upsertEntity = getFunction(memoryModule, "upsertEntity");
140
148
  const recordObservation = getFunction(memoryModule, "recordObservation");
141
149
  const chapterhouse = getScope("chapterhouse");
142
- const team = getScope("team");
143
- assert.ok(chapterhouse && team);
150
+ const team = createTestScope(memoryModule, "team");
151
+ assert.ok(chapterhouse);
144
152
  const entity = upsertEntity({ scope_id: chapterhouse.id, kind: "tool", name: "sqlite" });
145
153
  const valid = recordObservation({ scope_id: chapterhouse.id, entity_id: entity.id, content: "Valid entity reference", source: "test" });
146
154
  const orphan = recordObservation({ scope_id: chapterhouse.id, content: "Will become orphaned", source: "test" });
@@ -164,8 +172,8 @@ test("decayPass archives old low-confidence observations only in scope and compa
164
172
  const getScope = getFunction(memoryModule, "getScope");
165
173
  const recordObservation = getFunction(memoryModule, "recordObservation");
166
174
  const chapterhouse = getScope("chapterhouse");
167
- const team = getScope("team");
168
- assert.ok(chapterhouse && team);
175
+ const team = createTestScope(memoryModule, "team");
176
+ assert.ok(chapterhouse);
169
177
  const archiveMe = recordObservation({ scope_id: chapterhouse.id, content: "Old low confidence", source: "test", confidence: 0.2 });
170
178
  const highConfidence = recordObservation({ scope_id: chapterhouse.id, content: "Old high confidence", source: "test", confidence: 0.9 });
171
179
  const fresh = recordObservation({ scope_id: chapterhouse.id, content: "Fresh low confidence", source: "test", confidence: 0.2 });
@@ -201,8 +209,8 @@ test("runHousekeeping defaults to the active scope and can target all active sco
201
209
  const setActiveScope = getFunction(memoryModule, "setActiveScope");
202
210
  const recordObservation = getFunction(memoryModule, "recordObservation");
203
211
  const chapterhouse = getScope("chapterhouse");
204
- const team = getScope("team");
205
- assert.ok(chapterhouse && team);
212
+ const team = createTestScope(memoryModule, "team");
213
+ assert.ok(chapterhouse);
206
214
  const chapterhouseOld = recordObservation({ scope_id: chapterhouse.id, content: "Chapterhouse old low", source: "test", confidence: 0.1 });
207
215
  const teamOld = recordObservation({ scope_id: team.id, content: "Team old low", source: "test", confidence: 0.1 });
208
216
  db.prepare(`UPDATE mem_observations SET created_at = datetime('now', '-31 days') WHERE id IN (?, ?)`).run(chapterhouseOld.id, teamOld.id);
@@ -225,8 +233,7 @@ test("tieringPass promotes and demotes rows from lifecycle signals and is idempo
225
233
  const upsertEntity = getFunction(memoryModule, "upsertEntity");
226
234
  const recordObservation = getFunction(memoryModule, "recordObservation");
227
235
  const recordDecision = getFunction(memoryModule, "recordDecision");
228
- const team = getScope("team");
229
- assert.ok(team);
236
+ const team = createTestScope(memoryModule, "team");
230
237
  const entity = upsertEntity({ scope_id: team.id, kind: "component", name: "memory", tier: "warm" });
231
238
  const referencedObservation = recordObservation({
232
239
  scope_id: team.id,
@@ -1,5 +1,5 @@
1
1
  export { completeActionItem, dropActionItem, getActionItem, listActionItems, recordActionItem, snoozeActionItem, } from "./action-items.js";
2
- export { getActiveScope, inferScopeFromText, setActiveScope } from "./active-scope.js";
2
+ export { getActiveScope, inferScopeFromText, setActiveScope, withActiveScope } from "./active-scope.js";
3
3
  export { recordDecision, getDecision, listDecisions, supersedeDecision } from "./decisions.js";
4
4
  export { getEntity, findEntityByName, listEntities, upsertEntity } from "./entities.js";
5
5
  export { getHotTierEntries, renderHotTierForActiveScope, renderHotTierXML } from "./hot-tier.js";
@@ -68,13 +68,6 @@ test("getDb seeds canonical memory scopes on first run", async () => {
68
68
  ORDER BY slug
69
69
  `).all();
70
70
  assert.deepEqual(rows, [
71
- {
72
- slug: "brian",
73
- title: "Brian",
74
- description: "Brian's preferences, context, working style",
75
- keywords: JSON.stringify(["brian"]),
76
- active: 1,
77
- },
78
71
  {
79
72
  slug: "chapterhouse",
80
73
  title: "Chapterhouse",
@@ -89,20 +82,6 @@ test("getDb seeds canonical memory scopes on first run", async () => {
89
82
  keywords: JSON.stringify(["everywhere", "general"]),
90
83
  active: 1,
91
84
  },
92
- {
93
- slug: "infra",
94
- title: "Infra",
95
- description: "Infrastructure, hosting, deployment, CI/CD",
96
- keywords: JSON.stringify(["infra"]),
97
- active: 1,
98
- },
99
- {
100
- slug: "team",
101
- title: "Team",
102
- description: "Team processes, rituals, OKRs",
103
- keywords: JSON.stringify(["team"]),
104
- active: 1,
105
- },
106
85
  ]);
107
86
  }
108
87
  finally {
@@ -123,11 +102,8 @@ test("memory schema initialization is idempotent", async () => {
123
102
  ORDER BY slug
124
103
  `).all();
125
104
  assert.deepEqual(counts, [
126
- { slug: "brian", count: 1 },
127
105
  { slug: "chapterhouse", count: 1 },
128
106
  { slug: "global", count: 1 },
129
- { slug: "infra", count: 1 },
130
- { slug: "team", count: 1 },
131
107
  ]);
132
108
  reopened.dbModule.closeDb();
133
109
  }
package/dist/store/db.js CHANGED
@@ -216,24 +216,6 @@ const MEMORY_SCOPE_SEEDS = [
216
216
  description: "Chapterhouse codebase, conventions, decisions, gotchas",
217
217
  keywords: ["chapterhouse", "this repo", "this project", "the daemon"],
218
218
  },
219
- {
220
- slug: "infra",
221
- title: "Infra",
222
- description: "Infrastructure, hosting, deployment, CI/CD",
223
- keywords: ["infra"],
224
- },
225
- {
226
- slug: "team",
227
- title: "Team",
228
- description: "Team processes, rituals, OKRs",
229
- keywords: ["team"],
230
- },
231
- {
232
- slug: "brian",
233
- title: "Brian",
234
- description: "Brian's preferences, context, working style",
235
- keywords: ["brian"],
236
- },
237
219
  ];
238
220
  const CHAPTERHOUSE_WIKI_INDEX_SOURCE = "wiki:pages/projects/chapterhouse/index.md";
239
221
  const CHAPTERHOUSE_WIKI_HOT_TIER_REASON = "P6 PR1 wiki migration hot-tier candidate";
@@ -628,13 +610,39 @@ export function getDb() {
628
610
  db.exec(`
629
611
  CREATE TABLE IF NOT EXISTS copilot_sessions (
630
612
  session_key TEXT PRIMARY KEY,
631
- mode TEXT NOT NULL CHECK(mode IN ('default', 'project')),
613
+ mode TEXT NOT NULL CHECK(mode IN ('default', 'project', 'agent')),
632
614
  project_root TEXT,
633
615
  copilot_session_id TEXT NOT NULL,
634
616
  model TEXT,
635
617
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
636
618
  )
637
619
  `);
620
+ try {
621
+ db.prepare(`
622
+ INSERT INTO copilot_sessions (session_key, mode, copilot_session_id)
623
+ VALUES ('__mode_probe__', 'agent', '__probe__')
624
+ `).run();
625
+ db.prepare(`DELETE FROM copilot_sessions WHERE session_key = '__mode_probe__'`).run();
626
+ }
627
+ catch {
628
+ db.exec(`ALTER TABLE copilot_sessions RENAME TO copilot_sessions_old`);
629
+ db.exec(`
630
+ CREATE TABLE copilot_sessions (
631
+ session_key TEXT PRIMARY KEY,
632
+ mode TEXT NOT NULL CHECK(mode IN ('default', 'project', 'agent')),
633
+ project_root TEXT,
634
+ copilot_session_id TEXT NOT NULL,
635
+ model TEXT,
636
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
637
+ )
638
+ `);
639
+ db.exec(`
640
+ INSERT INTO copilot_sessions (session_key, mode, project_root, copilot_session_id, model, updated_at)
641
+ SELECT session_key, mode, project_root, copilot_session_id, model, updated_at
642
+ FROM copilot_sessions_old
643
+ `);
644
+ db.exec(`DROP TABLE copilot_sessions_old`);
645
+ }
638
646
  // Migrate: add metadata columns to conversation_log if not present
639
647
  const convCols = db.prepare(`PRAGMA table_info(conversation_log)`).all();
640
648
  if (!convCols.some((c) => c.name === 'session_key')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chapterhouse",
3
- "version": "0.4.3",
3
+ "version": "0.5.1",
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"