chapterhouse 0.6.0 → 0.8.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 (80) hide show
  1. package/agents/korg.agent.md +65 -0
  2. package/dist/api/agent-edit-access.js +11 -0
  3. package/dist/api/agents.api.test.js +48 -0
  4. package/dist/api/korg.js +34 -0
  5. package/dist/api/korg.test.js +42 -0
  6. package/dist/api/server.js +420 -13
  7. package/dist/api/server.test.js +533 -3
  8. package/dist/config.js +28 -0
  9. package/dist/config.test.js +20 -0
  10. package/dist/copilot/agent-event-bus.js +1 -0
  11. package/dist/copilot/agents.js +117 -50
  12. package/dist/copilot/agents.mcp-servers.test.js +87 -0
  13. package/dist/copilot/agents.parse.test.js +69 -0
  14. package/dist/copilot/agents.test.js +137 -2
  15. package/dist/copilot/orchestrator.js +62 -13
  16. package/dist/copilot/orchestrator.test.js +130 -8
  17. package/dist/copilot/session-manager.js +34 -0
  18. package/dist/copilot/system-message.js +11 -10
  19. package/dist/copilot/system-message.test.js +6 -1
  20. package/dist/copilot/tools.js +184 -376
  21. package/dist/copilot/tools.memory.test.js +32 -0
  22. package/dist/copilot/tools.wiki.test.js +53 -59
  23. package/dist/daemon.js +9 -0
  24. package/dist/memory/decisions.js +6 -5
  25. package/dist/memory/entities.js +20 -9
  26. package/dist/memory/hooks.js +151 -0
  27. package/dist/memory/hooks.test.js +325 -0
  28. package/dist/memory/hot-tier.js +37 -0
  29. package/dist/memory/hot-tier.test.js +30 -0
  30. package/dist/memory/housekeeping-scheduler.js +35 -0
  31. package/dist/memory/housekeeping-scheduler.test.js +50 -0
  32. package/dist/memory/inbox.js +10 -0
  33. package/dist/memory/index.js +3 -1
  34. package/dist/memory/migration.js +244 -0
  35. package/dist/memory/migration.test.js +100 -0
  36. package/dist/memory/reflect.js +273 -0
  37. package/dist/memory/reflect.test.js +254 -0
  38. package/dist/store/db.js +119 -4
  39. package/dist/store/db.test.js +19 -1
  40. package/dist/test/setup-env.js +3 -1
  41. package/dist/test/setup-env.test.js +8 -1
  42. package/dist/wiki/consolidation.js +641 -0
  43. package/dist/wiki/consolidation.test.js +140 -0
  44. package/dist/wiki/frontmatter.js +48 -0
  45. package/dist/wiki/frontmatter.test.js +42 -0
  46. package/dist/wiki/index-manager.js +246 -330
  47. package/dist/wiki/index-manager.test.js +138 -145
  48. package/dist/wiki/ingest.js +347 -0
  49. package/dist/wiki/ingest.test.js +111 -0
  50. package/dist/wiki/links.js +151 -0
  51. package/dist/wiki/links.test.js +176 -0
  52. package/dist/wiki/migrate-topics.test.js +16 -6
  53. package/dist/wiki/scheduler.js +118 -0
  54. package/dist/wiki/scheduler.test.js +64 -0
  55. package/dist/wiki/timeline.js +51 -0
  56. package/dist/wiki/timeline.test.js +65 -0
  57. package/dist/wiki/topic-structure.js +1 -1
  58. package/package.json +3 -1
  59. package/skills/pkb-ideas/SKILL.md +78 -0
  60. package/skills/pkb-ideas/_meta.json +4 -0
  61. package/skills/pkb-org/SKILL.md +82 -0
  62. package/skills/pkb-org/_meta.json +4 -0
  63. package/skills/pkb-people/SKILL.md +74 -0
  64. package/skills/pkb-people/_meta.json +4 -0
  65. package/skills/pkb-research/SKILL.md +83 -0
  66. package/skills/pkb-research/_meta.json +4 -0
  67. package/skills/pkb-source/SKILL.md +38 -0
  68. package/skills/pkb-source/_meta.json +4 -0
  69. package/skills/wiki-conventions/SKILL.md +5 -5
  70. package/web/dist/assets/index-5kz9aRU9.css +10 -0
  71. package/web/dist/assets/{index-B5oDsQ5y.js → index-BbX9RKf3.js} +101 -99
  72. package/web/dist/assets/index-BbX9RKf3.js.map +1 -0
  73. package/web/dist/index.html +2 -2
  74. package/dist/wiki/context.js +0 -138
  75. package/dist/wiki/fix.js +0 -335
  76. package/dist/wiki/fix.test.js +0 -350
  77. package/dist/wiki/lint.js +0 -451
  78. package/dist/wiki/lint.test.js +0 -329
  79. package/web/dist/assets/index-B5oDsQ5y.js.map +0 -1
  80. package/web/dist/assets/index-DknKAtDS.css +0 -10
@@ -13,10 +13,9 @@ import { getSkillDirectories } from "./skills.js";
13
13
  import { resetClient } from "./client.js";
14
14
  import { logConversation, getState, setState, deleteState, getCopilotSession, upsertCopilotSession, getTaskSessionKey, getDb, appendTaskEvent } from "../store/db.js";
15
15
  import { maybeWriteEpisode } from "./episode-writer.js";
16
- import { getWikiSummary } from "../wiki/context.js";
17
16
  import { SESSIONS_DIR } from "../paths.js";
18
17
  import { resolveModel } from "./router.js";
19
- import { loadAgents, ensureDefaultAgents, clearActiveTasks, getAgentRegistry, setActiveAgent, parseAtMention, buildAgentRoster, getActiveTasks, getAgent, composeAgentSystemMessage, filterToolsForAgent, withToolTaskContext, } from "./agents.js";
18
+ import { loadAgents, ensureDefaultAgents, clearActiveTasks, getAgentRegistry, setActiveAgent, parseAtMention, buildAgentRoster, getActiveTasks, getAgent, composeAgentSystemMessage, filterMcpServersForAgent, filterToolsForAgent, withToolTaskContext, } from "./agents.js";
20
19
  import * as agentsModule from "./agents.js";
21
20
  import { childLogger } from "../util/logger.js";
22
21
  import { agentEventBus } from "./agent-event-bus.js";
@@ -36,6 +35,18 @@ const AGENT_REPLY_CHUNK_SIZE = 500;
36
35
  const AGENT_REPLY_CHUNK_THRESHOLD = 8 * 1024;
37
36
  const ORCHESTRATOR_SESSION_KEY = "orchestrator_session_id";
38
37
  const LAST_AUTHENTICATED_USER_KEY = "last_authenticated_user";
38
+ function getWikiSummary() {
39
+ try {
40
+ const db = getDb();
41
+ const pages = db.prepare(`SELECT title, summary, last_updated FROM wiki_pages ORDER BY last_updated DESC LIMIT 50`).all();
42
+ if (pages.length === 0)
43
+ return "";
44
+ return pages.map((page) => `${page.title}: ${page.summary || ""}`.trim()).join("\n");
45
+ }
46
+ catch {
47
+ return "";
48
+ }
49
+ }
39
50
  let logMessage = () => { };
40
51
  export function setMessageLogger(fn) {
41
52
  logMessage = fn;
@@ -179,6 +190,13 @@ function getSessionConfig() {
179
190
  function agentSlugFromSessionKey(sessionKey) {
180
191
  return sessionKey.startsWith("agent:") ? sessionKey.slice("agent:".length) : undefined;
181
192
  }
193
+ function persistentAgentSessionKey(slug) {
194
+ const agent = getAgent(slug);
195
+ if (!agent?.persistent) {
196
+ return undefined;
197
+ }
198
+ return `agent:${agent.slug}`;
199
+ }
182
200
  function getPersistentAgentForSessionKey(sessionKey) {
183
201
  const slug = agentSlugFromSessionKey(sessionKey);
184
202
  if (!slug)
@@ -344,6 +362,7 @@ async function createOrResumeSession(sessionKey, projectRoot) {
344
362
  client: copilotClient,
345
363
  onAgentTaskComplete: feedAgentResult,
346
364
  })));
365
+ mcpServers = filterMcpServersForAgent(persistentAgent, mcpServers);
347
366
  const scopedHotTier = (await memoryCoordinator?.buildHotTierContext(sessionKey)) || undefined;
348
367
  const channelNote = `You are in your persistent Chapterhouse channel (${sessionKey}). Your memory scope is ${persistentAgent.scope}.`;
349
368
  systemMessageContent = [
@@ -1209,19 +1228,18 @@ export async function interruptCurrentTurn(sessionKey, newPrompt, source, callba
1209
1228
  await manager.abortCurrentTurn();
1210
1229
  log.info({ sessionKey, replacementTurnId: turnId }, "turn.interrupted");
1211
1230
  }
1212
- /**
1213
- * Enqueue a turn for the new POST→SSE chat path (#130).
1214
- *
1215
- * Unlike `sendToOrchestrator`, this function:
1216
- * - Returns the `turnId` immediately without waiting for the turn to complete.
1217
- * - Routes through the shared lifecycle emitter in sendToOrchestrator.
1218
- * - Does NOT write to sseClients — the SSE channel delivers events via subscribeSession().
1219
- * - Supports interrupt: true which calls interruptCurrentTurn under the hood.
1220
- *
1221
- * @returns turnId (UUID)
1222
- */
1223
1231
  export function enqueueForSse(opts) {
1232
+ if (opts.type === "agent_saved") {
1233
+ agentEventBus.emit({
1234
+ type: "agent_saved",
1235
+ payload: { slug: opts.slug ?? "" },
1236
+ });
1237
+ return;
1238
+ }
1224
1239
  const { sessionKey, prompt, attachments, authUser, authHeader, interrupt } = opts;
1240
+ if (!sessionKey || !prompt) {
1241
+ throw new Error("Missing sessionKey or prompt for SSE turn enqueue");
1242
+ }
1225
1243
  const turnId = randomUUID();
1226
1244
  // sse-web carries auth and enables onQueued — unlike "background" (Fixes 2 & 3).
1227
1245
  const source = { type: "sse-web", sessionKey, user: authUser, authorizationHeader: authHeader };
@@ -1288,6 +1306,37 @@ export async function interruptSessionTurn(sessionKey) {
1288
1306
  }
1289
1307
  return aborted;
1290
1308
  }
1309
+ export function getPersistentAgentSessionState(slug) {
1310
+ const sessionKey = persistentAgentSessionKey(slug);
1311
+ if (!sessionKey) {
1312
+ return "none";
1313
+ }
1314
+ const manager = registry?.get(sessionKey);
1315
+ if (!manager) {
1316
+ return "none";
1317
+ }
1318
+ return manager.isProcessing ? "in_flight" : "idle";
1319
+ }
1320
+ export async function reloadPersistentAgent(slug, onReloaded) {
1321
+ const sessionKey = persistentAgentSessionKey(slug);
1322
+ if (!sessionKey || !registry) {
1323
+ return "none";
1324
+ }
1325
+ let manager = registry.get(sessionKey);
1326
+ if (!manager) {
1327
+ manager = registry.getOrCreate(sessionKey);
1328
+ await manager.ensureSession();
1329
+ onReloaded?.();
1330
+ return "reloaded";
1331
+ }
1332
+ if (manager.isProcessing) {
1333
+ manager.requestSessionReload(onReloaded);
1334
+ return "scheduled";
1335
+ }
1336
+ await manager.restartSession();
1337
+ onReloaded?.();
1338
+ return "reloaded";
1339
+ }
1291
1340
  /** Switch the model on the live default orchestrator session without destroying it. */
1292
1341
  export function switchSessionModel(newModel) {
1293
1342
  const manager = registry?.get("default");
@@ -130,6 +130,7 @@ async function loadOrchestratorModule(t, overrides = {}) {
130
130
  housekeepingRuns: [],
131
131
  housekeepingInFlight: false,
132
132
  activeScope: makeScope(1, "chapterhouse", "Chapterhouse", "Core Chapterhouse work."),
133
+ wikiPages: [{ title: "Chapterhouse", summary: "wiki summary", last_updated: "2026-05-13" }],
133
134
  ...overrides,
134
135
  };
135
136
  const client = createFakeClient(state);
@@ -382,7 +383,10 @@ async function loadOrchestratorModule(t, overrides = {}) {
382
383
  });
383
384
  t.mock.module("./mcp-config.js", {
384
385
  namedExports: {
385
- loadMcpConfig: () => ({ filesystem: { command: "filesystem" } }),
386
+ loadMcpConfig: () => ({
387
+ filesystem: { command: "filesystem" },
388
+ truenas: { command: "truenas" },
389
+ }),
386
390
  },
387
391
  });
388
392
  t.mock.module("./skills.js", {
@@ -423,7 +427,7 @@ async function loadOrchestratorModule(t, overrides = {}) {
423
427
  return {};
424
428
  },
425
429
  get: () => undefined,
426
- all: () => [],
430
+ all: () => (sql.includes("FROM wiki_pages") ? (state.wikiPages ?? []) : []),
427
431
  }),
428
432
  transaction: (fn) => fn,
429
433
  }),
@@ -444,11 +448,6 @@ async function loadOrchestratorModule(t, overrides = {}) {
444
448
  },
445
449
  },
446
450
  });
447
- t.mock.module("../wiki/context.js", {
448
- namedExports: {
449
- getWikiSummary: () => "wiki summary",
450
- },
451
- });
452
451
  t.mock.module("../wiki/project-registry.js", {
453
452
  namedExports: {
454
453
  loadRegistry: () => state.projectRegistry,
@@ -513,6 +512,13 @@ async function loadOrchestratorModule(t, overrides = {}) {
513
512
  return "@coder @designer";
514
513
  },
515
514
  composeAgentSystemMessage: (agent) => agent.systemMessage ?? `You are ${agent.slug}.`,
515
+ filterMcpServersForAgent: (agent, mcpServers) => {
516
+ if (agent.mcpServers && agent.mcpServers.length > 0) {
517
+ const allowed = new Set(agent.mcpServers);
518
+ return Object.fromEntries(Object.entries(mcpServers).filter(([name]) => allowed.has(name)));
519
+ }
520
+ return mcpServers;
521
+ },
516
522
  filterToolsForAgent: (_agent, tools) => tools,
517
523
  bindToolsToAgent: (_agentSlug, tools) => tools,
518
524
  withToolTaskContext: (_taskId, fn) => fn(),
@@ -627,7 +633,7 @@ test("initOrchestrator falls back to an available model and eagerly creates a se
627
633
  assert.equal(state.loadAgentsCalls, 1);
628
634
  assert.equal(state.createSessionCalls.length, 1);
629
635
  assert.equal(state.healthCheckIntervalMs, 30_000);
630
- assert.equal(state.systemOptions?.memorySummary, "wiki summary");
636
+ assert.equal(state.systemOptions?.memorySummary, "Chapterhouse: wiki summary");
631
637
  assert.equal(state.store.get("orchestrator_session_id"), "session-123");
632
638
  });
633
639
  test("initOrchestrator passes hot-tier XML into the orchestrator system prompt when injection is enabled", async (t) => {
@@ -728,6 +734,28 @@ test("initOrchestrator prewarms persistent agent sessions with scoped hot-tier c
728
734
  assert.equal(persistentCall.systemMessage.content.includes("scope=\"infra\""), true);
729
735
  assert.deepEqual(state.dbWrites.filter((write) => write.sql.includes("copilot_sessions")), []);
730
736
  });
737
+ test("initOrchestrator prewarms persistent agent sessions with only allowed MCP servers", async (t) => {
738
+ const { orchestrator, state, client } = await loadOrchestratorModule(t, {
739
+ registry: [
740
+ { slug: "coder", name: "Kaylee", model: "claude-sonnet-4.6", systemMessage: "You are Kaylee." },
741
+ {
742
+ slug: "bellonda",
743
+ name: "Bellonda",
744
+ model: "claude-sonnet-4.6",
745
+ systemMessage: "You are Bellonda.",
746
+ persistent: true,
747
+ scope: "infra",
748
+ mcpServers: ["truenas"],
749
+ },
750
+ ],
751
+ });
752
+ await orchestrator.initOrchestrator(client);
753
+ const persistentCall = state.createSessionCalls.find((call) => String(call.systemMessage?.content ?? "").includes("Bellonda"));
754
+ assert.ok(persistentCall, "expected a prewarmed Bellonda session");
755
+ assert.deepEqual(persistentCall.mcpServers, {
756
+ truenas: { command: "truenas" },
757
+ });
758
+ });
731
759
  test("sendToOrchestrator routes agent session keys directly to persistent agent sessions", async (t) => {
732
760
  const { orchestrator, state, client } = await loadOrchestratorModule(t, {
733
761
  config: {
@@ -1517,6 +1545,100 @@ test("cancelCurrentMessage aborts the active request and agent helpers expose ru
1517
1545
  await orchestrator.shutdownAgents();
1518
1546
  assert.equal(state.clearActiveTasksCalls, 1);
1519
1547
  });
1548
+ test("persistent agent helpers report session state and reload idle sessions", async (t) => {
1549
+ const { orchestrator, state, client } = await loadOrchestratorModule(t, {
1550
+ config: {
1551
+ copilotModel: "claude-sonnet-4.6",
1552
+ selfEditEnabled: true,
1553
+ },
1554
+ registry: [
1555
+ {
1556
+ slug: "bellonda",
1557
+ name: "Bellonda",
1558
+ model: "claude-sonnet-4.6",
1559
+ systemMessage: "You are Bellonda.",
1560
+ persistent: true,
1561
+ scope: "infra",
1562
+ },
1563
+ ],
1564
+ });
1565
+ await orchestrator.initOrchestrator(client);
1566
+ const createCallsBeforeReload = state.createSessionCalls.length;
1567
+ assert.equal(orchestrator.getPersistentAgentSessionState("bellonda"), "idle");
1568
+ const reloaded = await orchestrator.reloadPersistentAgent("bellonda");
1569
+ assert.equal(reloaded, "reloaded");
1570
+ assert.equal(orchestrator.getPersistentAgentSessionState("bellonda"), "idle");
1571
+ assert.equal(state.disconnectCalls, 1);
1572
+ assert.equal(state.createSessionCalls.length, createCallsBeforeReload + 1);
1573
+ });
1574
+ test("persistent agent helpers detect in-flight turns", async (t) => {
1575
+ const { orchestrator, state, client } = await loadOrchestratorModule(t, {
1576
+ config: {
1577
+ copilotModel: "claude-sonnet-4.6",
1578
+ selfEditEnabled: true,
1579
+ },
1580
+ registry: [
1581
+ {
1582
+ slug: "bellonda",
1583
+ name: "Bellonda",
1584
+ model: "claude-sonnet-4.6",
1585
+ systemMessage: "You are Bellonda.",
1586
+ persistent: true,
1587
+ scope: "infra",
1588
+ },
1589
+ ],
1590
+ sendResult: "__PENDING__",
1591
+ });
1592
+ await orchestrator.initOrchestrator(client);
1593
+ orchestrator.sendToOrchestrator("check deploy health", { type: "background", sessionKey: "agent:bellonda" }, () => { });
1594
+ await new Promise((resolve) => setTimeout(resolve, 10));
1595
+ assert.equal(orchestrator.getPersistentAgentSessionState("bellonda"), "in_flight");
1596
+ state.pendingReject?.(new Error("test teardown"));
1597
+ await new Promise((resolve) => setTimeout(resolve, 0));
1598
+ });
1599
+ test("reloadPersistentAgent defers reloads until the in-flight turn finishes and preserves queued turns", async (t) => {
1600
+ const { orchestrator, state, client } = await loadOrchestratorModule(t, {
1601
+ config: {
1602
+ copilotModel: "claude-sonnet-4.6",
1603
+ selfEditEnabled: true,
1604
+ },
1605
+ registry: [
1606
+ {
1607
+ slug: "bellonda",
1608
+ name: "Bellonda",
1609
+ model: "claude-sonnet-4.6",
1610
+ systemMessage: "You are Bellonda.",
1611
+ persistent: true,
1612
+ scope: "infra",
1613
+ },
1614
+ ],
1615
+ sendResult: "__PENDING__",
1616
+ });
1617
+ await orchestrator.initOrchestrator(client);
1618
+ const createCallsBeforeReload = state.createSessionCalls.length;
1619
+ const reloadEvents = [];
1620
+ orchestrator.sendToOrchestrator("check deploy health", { type: "background", sessionKey: "agent:bellonda" }, () => { });
1621
+ await new Promise((resolve) => setTimeout(resolve, 10));
1622
+ const queuedTurn = new Promise((resolve) => {
1623
+ orchestrator.sendToOrchestrator("summarize the queued follow-up", { type: "background", sessionKey: "agent:bellonda" }, (text, done) => {
1624
+ if (done)
1625
+ resolve(text);
1626
+ });
1627
+ });
1628
+ await new Promise((resolve) => setTimeout(resolve, 10));
1629
+ const reloadResult = await orchestrator.reloadPersistentAgent("bellonda", () => {
1630
+ reloadEvents.push("reloaded");
1631
+ });
1632
+ assert.equal(reloadResult, "scheduled");
1633
+ assert.equal(state.disconnectCalls, 0, "deferred reload should wait for the active turn to finish");
1634
+ state.sendResult = "Queued follow-up complete";
1635
+ state.pendingReject?.(new Error("forced restart"));
1636
+ assert.equal(await queuedTurn, "Queued follow-up complete");
1637
+ await new Promise((resolve) => setTimeout(resolve, 0));
1638
+ assert.deepEqual(reloadEvents, ["reloaded"]);
1639
+ assert.equal(state.disconnectCalls, 1, "deferred reload should restart the session once the turn finishes");
1640
+ assert.equal(state.createSessionCalls.length, createCallsBeforeReload + 1);
1641
+ });
1520
1642
  // ---------------------------------------------------------------------------
1521
1643
  // REGRESSION: #35 — per-session isolation
1522
1644
  // This test would have caught the original bug. With a global shared queue,
@@ -60,6 +60,9 @@ export class SessionManager {
60
60
  * never-evict-mid-turn invariant. */
61
61
  _pendingClose = false;
62
62
  _onPendingCloseEvict;
63
+ /** Set when a persistent session should be recreated before the next queued turn runs. */
64
+ _pendingReload = false;
65
+ _onPendingReload = [];
63
66
  constructor(sessionKey, worker, sessionFactory) {
64
67
  this.worker = worker;
65
68
  this.sessionFactory = sessionFactory;
@@ -178,6 +181,19 @@ export class SessionManager {
178
181
  item.reject(err);
179
182
  }
180
183
  this._lastActivityAt = Date.now();
184
+ if (this._pendingReload) {
185
+ await this.restartSession();
186
+ const callbacks = this._onPendingReload.splice(0);
187
+ this._pendingReload = false;
188
+ for (const callback of callbacks) {
189
+ try {
190
+ callback();
191
+ }
192
+ catch (err) {
193
+ log.warn({ sessionKey: this.sessionKey, err: err instanceof Error ? err.message : String(err) }, "session.reload.callback.failed");
194
+ }
195
+ }
196
+ }
181
197
  }
182
198
  this._currentTurnId = undefined;
183
199
  this._processing = false;
@@ -213,6 +229,24 @@ export class SessionManager {
213
229
  this._session = undefined;
214
230
  this._sessionCreatePromise = undefined;
215
231
  }
232
+ async restartSession() {
233
+ if (this._session) {
234
+ try {
235
+ await this._session.disconnect();
236
+ }
237
+ catch {
238
+ // best effort
239
+ }
240
+ }
241
+ this.invalidateSession();
242
+ await this.ensureSession();
243
+ }
244
+ requestSessionReload(onReloaded) {
245
+ this._pendingReload = true;
246
+ if (onReloaded) {
247
+ this._onPendingReload.push(onReloaded);
248
+ }
249
+ }
216
250
  /** Reject all queued messages without evicting the session. Returns count drained. */
217
251
  cancelQueued() {
218
252
  const count = this._queue.length;
@@ -4,7 +4,7 @@ export function getOrchestratorSystemMessage(opts) {
4
4
  const versionBanner = opts?.version ? `\nYou are running inside chapterhouse v${opts.version}.\n` : "";
5
5
  const memoryBlock = opts?.memorySummary
6
6
  ? `\n## Memory\nYou have a persistent memory store. Here's what you currently remember:\n\n${opts.memorySummary}\n`
7
- : "\n## Memory\nYou have a persistent memory store. It's currently empty — use `remember` to start building it!\n";
7
+ : "\n## Memory\nYou have a persistent memory store. It's currently empty — use `memory_remember` for agent memory or `wiki_update` for wiki knowledge.\n";
8
8
  const selfEditBlock = opts?.selfEditEnabled
9
9
  ? ""
10
10
  : `\n## Self-Edit Protection
@@ -115,22 +115,23 @@ You can delegate **multiple tasks simultaneously**. Different agents can work in
115
115
  - \`restart_chapterhouse\`: Restart the Chapterhouse daemon.
116
116
 
117
117
  ### Memory
118
- - \`remember\`: Save something to memory.
119
- - \`recall\`: Search your memory for stored facts, preferences, or information.
120
- - \`forget\`: Remove content from the wiki.
118
+ - \`memory_remember\`: Save something durable to scoped agent memory.
119
+ - \`memory_recall\`: Search scoped agent memory for stored facts, decisions, and observations.
120
+ - \`memory_reflect\`: Synthesize durable patterns from repeated observations in the scoped memory store.
121
+ - \`wiki_update\`: Create or update wiki pages when knowledge belongs in the shared wiki.
121
122
 
122
123
  Subagent proposals from \`memory_propose\` are processed automatically at end-of-task, so you do not need to manually review them mid-conversation.
123
124
 
124
- **Past conversations**: Daily conversation summaries are auto-written to \`pages/conversations/YYYY-MM-DD.md\`. When the user references something from earlier ("what did we decide about X", "remember when we…", "the thing we discussed yesterday"), call \`wiki_search\` (or \`recall\`) — don't guess from your own context, since older turns may have been compacted out.
125
+ **Past conversations**: Daily conversation summaries are auto-written to \`pages/conversations/YYYY-MM-DD.md\`. When the user references something from earlier ("what did we decide about X", "remember when we…", "the thing we discussed yesterday"), call \`wiki_search\` or \`memory_recall\` — don't guess from your own context, since older turns may have been compacted out.
125
126
 
126
127
  **Wiki structure** — the wiki enforces a topic layout, so put things in the right place:
127
128
  - **Entity categories** (\`projects\`, \`people\`, \`orgs\`, \`tools\`, \`topics\`, \`areas\`): every named thing gets its own directory \`pages/<category>/<topic-slug>/\`. The topic's overview lives at \`pages/<category>/<topic-slug>/index.md\`; related sub-pages ("facets") go alongside it, e.g. \`pages/projects/chapterhouse/decisions.md\`, \`pages/projects/chapterhouse/feature-ideas.md\`. Exactly one topic level deep; lowercase-slug names only.
128
129
  - **Flat categories** (\`preferences\`, \`facts\`, \`routines\`): a single file each, e.g. \`pages/preferences.md\`.
129
- - **Decisions** are always recorded against an entity, never as a standalone page: a decision about a project goes to \`pages/projects/<topic>/decisions.md\`. With \`remember\`, pass \`category: "decision"\` plus \`about\` (which entity category) and \`entity\`.
130
- - With \`remember\`, pass \`entity\` for entity categories (and \`facet\` to target a sub-page). With \`wiki_update\`, give the full canonical path — bad paths are rejected with a suggested correction; just retry with the suggestion.
131
- - If the structure ever looks wrong, call \`wiki_rebuild_index\` to regenerate the index from disk.
130
+ - **Decisions** are always recorded against an entity, never as a standalone page: a decision about a project goes to \`pages/projects/<topic>/decisions.md\`. Use \`wiki_update\` for shared wiki records or \`memory_remember\` for scoped agent memory.
131
+ - For entity pages and facet pages, use \`wiki_update\` with the full canonical path — bad paths are rejected with a suggested correction; just retry with the suggestion.
132
+ - Source material that should be preserved before synthesis belongs in \`wiki_ingest_source\`.
132
133
 
133
- **Wiki writes and restructuring**: Before writing or restructuring wiki content, invoke the \`wiki-conventions\` skill. Treat \`wiki_update\`, \`remember\`, \`forget\`, \`wiki_ingest\`, \`wiki_lint\`, and \`wiki_rebuild_index\` as write-sensitive workflows. Before using them, read \`pages/index.md\`, scan the last 20-30 entries of \`pages/_meta/log.md\`, and run \`wiki_search\` for the topic when the wiki is large or the topic is ambiguous.
134
+ **Wiki writes and restructuring**: Before writing or restructuring wiki content, invoke the \`wiki-conventions\` skill. Treat \`wiki_update\` and \`wiki_ingest_source\` as write-sensitive workflows, and use \`memory_remember\` / \`memory_recall\` for scoped agent memory. Before using wiki write tools, read \`pages/index.md\`, scan the last 20-30 entries of \`pages/_meta/log.md\`, and run \`wiki_search\` for the topic when the wiki is large or the topic is ambiguous.
134
135
 
135
136
  **Learning workflow**: When the user asks you to do something you don't have a skill for:
136
137
  1. **Search skills.sh first**: Use the find-skills skill to search for existing community skills.
@@ -154,7 +155,7 @@ Subagent proposals from \`memory_propose\` are processed automatically at end-of
154
155
  10. Expand shorthand paths: "${getExampleProjectPath()}" is the expanded form of the user's home-directory project path.
155
156
  11. Be conversational and human. You're Chapterhouse.
156
157
  12. When using skills, follow the skill's instructions precisely.
157
- 13. **Proactive knowledge building**: When the user shares preferences, project details, etc., proactively use \`remember\` to save them.
158
+ 13. **Proactive knowledge building**: When the user shares preferences, project details, etc., proactively use \`memory_remember\` for scoped agent memory or \`wiki_update\` for shared wiki knowledge.
158
159
  14. When a user mentions completing work or shipping something, proactively suggest logging it as OKR progress with \`log_okr_progress\`.
159
160
  ${selfEditBlock}${memoryBlock}`;
160
161
  }
@@ -86,7 +86,8 @@ test("orchestrator prompt omits memory_context when hot-tier XML is not provided
86
86
  });
87
87
  test("orchestrator prompt requires wiki-conventions before write-sensitive wiki work", () => {
88
88
  const message = getOrchestratorSystemMessage();
89
- assert.match(message, /wiki-conventions[\s\S]{0,500}wiki_update[\s\S]{0,200}remember[\s\S]{0,200}forget[\s\S]{0,200}wiki_ingest[\s\S]{0,200}wiki_lint[\s\S]{0,200}wiki_rebuild_index/i);
89
+ assert.match(message, /wiki-conventions[\s\S]{0,500}wiki_update[\s\S]{0,200}wiki_ingest_source[\s\S]{0,200}memory_remember[\s\S]{0,200}memory_recall/i);
90
+ assert.doesNotMatch(message, /`remember`|`recall`|`forget`|`wiki_ingest`(?!_source)|`wiki_lint`|`wiki_rebuild_index`|`wiki_fix`/);
90
91
  assert.match(message, /before writing or restructuring wiki content/i);
91
92
  });
92
93
  test("orchestrator prompt describes the wiki orientation ritual", () => {
@@ -101,4 +102,8 @@ test("orchestrator prompt explains that subagent memory proposals are processed
101
102
  assert.match(message, /processed automatically at end-of-task|processed automatically at the end of the task/i);
102
103
  assert.match(message, /do not need to manually review them mid-conversation|don't need to manually review them mid-conversation/i);
103
104
  });
105
+ test("orchestrator prompt includes the memory_reflect tool in memory guidance", () => {
106
+ const message = getOrchestratorSystemMessage();
107
+ assert.match(message, /memory_reflect/);
108
+ });
104
109
  //# sourceMappingURL=system-message.test.js.map