chapterhouse 0.3.12 → 0.3.14

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 (53) hide show
  1. package/README.md +2 -69
  2. package/dist/api/server.js +15 -157
  3. package/dist/api/server.test.js +1 -1
  4. package/dist/api/turn-sse.integration.test.js +36 -0
  5. package/dist/cli.js +0 -30
  6. package/dist/config.js +0 -3
  7. package/dist/copilot/agent-event-bus.js +41 -0
  8. package/dist/copilot/agent-event-bus.test.js +23 -0
  9. package/dist/copilot/agents.js +4 -59
  10. package/dist/copilot/orchestrator.js +60 -65
  11. package/dist/copilot/orchestrator.test.js +73 -158
  12. package/dist/copilot/task-event-log.js +5 -5
  13. package/dist/copilot/task-event-log.test.js +68 -142
  14. package/dist/copilot/tools.js +9 -85
  15. package/dist/daemon.js +0 -22
  16. package/dist/store/db.js +2 -50
  17. package/dist/store/db.test.js +0 -45
  18. package/package.json +1 -3
  19. package/web/dist/assets/index-BlIWCM11.js +217 -0
  20. package/web/dist/assets/index-BlIWCM11.js.map +1 -0
  21. package/web/dist/assets/{index-BtAcw3EP.css → index-lvHFM_ut.css} +1 -1
  22. package/web/dist/index.html +2 -2
  23. package/dist/api/ralph.js +0 -153
  24. package/dist/api/ralph.test.js +0 -101
  25. package/dist/copilot/agents.squad.test.js +0 -72
  26. package/dist/copilot/hooks.js +0 -157
  27. package/dist/copilot/hooks.test.js +0 -315
  28. package/dist/copilot/squad-event-bus.js +0 -27
  29. package/dist/copilot/tools.squad.test.js +0 -168
  30. package/dist/squad/charter.js +0 -125
  31. package/dist/squad/charter.test.js +0 -89
  32. package/dist/squad/context.js +0 -48
  33. package/dist/squad/context.test.js +0 -59
  34. package/dist/squad/discovery.js +0 -268
  35. package/dist/squad/discovery.test.js +0 -154
  36. package/dist/squad/index.js +0 -9
  37. package/dist/squad/init-cli.js +0 -109
  38. package/dist/squad/init.js +0 -395
  39. package/dist/squad/init.test.js +0 -351
  40. package/dist/squad/mirror.js +0 -83
  41. package/dist/squad/mirror.scheduler.js +0 -80
  42. package/dist/squad/mirror.scheduler.test.js +0 -197
  43. package/dist/squad/mirror.test.js +0 -172
  44. package/dist/squad/registry.js +0 -162
  45. package/dist/squad/registry.test.js +0 -31
  46. package/dist/squad/squad-coordinator-system-message.test.js +0 -190
  47. package/dist/squad/squad-session-routing.test.js +0 -260
  48. package/dist/squad/types.js +0 -4
  49. package/dist/squad/worktree.js +0 -295
  50. package/dist/squad/worktree.test.js +0 -189
  51. package/dist/store/squad-sessions.test.js +0 -341
  52. package/web/dist/assets/index-BR2cks94.js +0 -219
  53. package/web/dist/assets/index-BR2cks94.js.map +0 -1
@@ -14,13 +14,13 @@
14
14
  * 10. Multiple tasks tracked independently
15
15
  * 11. RING_BUFFER_CAPACITY constant: 500 items, evicts oldest
16
16
  *
17
- * Uses node:test (same pattern as hooks.test.ts, agents.squad.test.ts) so the
17
+ * Uses node:test (same pattern as hooks.test.ts) so the
18
18
  * daemon's `npm test` runner (node --test) picks these up from dist/.
19
19
  */
20
20
  import { describe, it, beforeEach, afterEach } from "node:test";
21
21
  import assert from "node:assert/strict";
22
- import { EventBus } from "@bradygaster/squad-sdk/runtime/event-bus";
23
- import { RingBuffer, RING_BUFFER_CAPACITY, } from "./task-event-log.js";
22
+ import { agentEventBus } from "./agent-event-bus.js";
23
+ import { RingBuffer, RING_BUFFER_CAPACITY, initTaskEventLog, getTaskLogEvents, subscribeTaskLog, clearTaskLog, taskLogSize, } from "./task-event-log.js";
24
24
  // ---------------------------------------------------------------------------
25
25
  // Helpers
26
26
  // ---------------------------------------------------------------------------
@@ -48,87 +48,6 @@ function makeDestroyedEvent(sessionId) {
48
48
  timestamp: new Date(),
49
49
  };
50
50
  }
51
- /**
52
- * Builds an isolated task-event-log wired to a fresh EventBus so tests don't
53
- * share state through the process-level squadEventBus singleton.
54
- */
55
- function buildIsolatedLog(bus) {
56
- const CAPACITY = RING_BUFFER_CAPACITY;
57
- const buffers = new Map();
58
- const listeners = new Map();
59
- function getOrCreate(taskId) {
60
- let b = buffers.get(taskId);
61
- if (!b) {
62
- b = new RingBuffer(CAPACITY);
63
- buffers.set(taskId, b);
64
- }
65
- return b;
66
- }
67
- function notify(taskId, event) {
68
- const ls = listeners.get(taskId);
69
- if (!ls)
70
- return;
71
- for (const fn of ls) {
72
- try {
73
- fn(event);
74
- }
75
- catch { /* non-fatal */ }
76
- }
77
- }
78
- const unsubToolCall = bus.subscribe("session:tool_call", (event) => {
79
- const taskId = event.sessionId;
80
- if (!taskId)
81
- return;
82
- const p = event.payload;
83
- const ev = {
84
- id: 0, taskId,
85
- seq: p._seq ?? 0,
86
- ts: p._ts ?? Date.now(),
87
- kind: p._kind === "tool_complete" ? "tool_complete" : "tool_start",
88
- toolName: p.toolName ?? null,
89
- summary: p._summary ?? null,
90
- };
91
- getOrCreate(taskId).push(ev);
92
- notify(taskId, ev);
93
- });
94
- const unsubDestroyed = bus.subscribe("session:destroyed", (event) => {
95
- if (event.sessionId) {
96
- buffers.delete(event.sessionId);
97
- listeners.delete(event.sessionId);
98
- }
99
- });
100
- return {
101
- getEvents: (taskId, afterSeq = 0) => {
102
- const buf = buffers.get(taskId);
103
- if (!buf)
104
- return [];
105
- const all = buf.getAll();
106
- return afterSeq === 0 ? all : all.filter((e) => e.seq > afterSeq);
107
- },
108
- subscribe: (taskId, fn) => {
109
- let ls = listeners.get(taskId);
110
- if (!ls) {
111
- ls = new Set();
112
- listeners.set(taskId, ls);
113
- }
114
- ls.add(fn);
115
- return () => {
116
- const lss = listeners.get(taskId);
117
- if (lss) {
118
- lss.delete(fn);
119
- if (lss.size === 0)
120
- listeners.delete(taskId);
121
- }
122
- };
123
- },
124
- clear: (taskId) => {
125
- buffers.delete(taskId);
126
- listeners.delete(taskId);
127
- },
128
- size: () => buffers.size,
129
- shutdown: () => { unsubToolCall(); unsubDestroyed(); },
130
- };
131
- }
132
51
  // ---------------------------------------------------------------------------
133
52
  // RingBuffer — pure class tests
134
53
  // ---------------------------------------------------------------------------
@@ -189,87 +108,94 @@ describe("RingBuffer", () => {
189
108
  // Task event log (bus-wired) tests
190
109
  // ---------------------------------------------------------------------------
191
110
  describe("task event log — bus-wired", () => {
192
- let bus;
193
- let log;
111
+ let shutdown;
194
112
  beforeEach(() => {
195
- bus = new EventBus();
196
- log = buildIsolatedLog(bus);
113
+ shutdown = initTaskEventLog();
197
114
  });
198
115
  afterEach(() => {
199
- log.shutdown();
116
+ shutdown();
200
117
  });
201
118
  it("returns [] for an unknown taskId", () => {
202
- assert.deepEqual(log.getEvents("no-such-task"), []);
119
+ assert.deepEqual(getTaskLogEvents("no-such-task"), []);
203
120
  });
204
- it("populates ring buffer on session:tool_call bus event", async () => {
205
- await bus.emit(makeToolCallEvent({ sessionId: "task-001", kind: "tool_start", seq: 1, toolName: "view" }));
206
- const events = log.getEvents("task-001");
121
+ it("populates ring buffer on session:tool_call bus event", () => {
122
+ agentEventBus.emit(makeToolCallEvent({ sessionId: "task-001", kind: "tool_start", seq: 1, toolName: "view" }));
123
+ const events = getTaskLogEvents("task-001");
207
124
  assert.equal(events.length, 1);
208
125
  assert.equal(events[0].kind, "tool_start");
209
126
  assert.equal(events[0].toolName, "view");
210
127
  assert.equal(events[0].taskId, "task-001");
211
128
  assert.equal(events[0].seq, 1);
129
+ clearTaskLog("task-001");
212
130
  });
213
- it("accumulates multiple events in order", async () => {
214
- await bus.emit(makeToolCallEvent({ sessionId: "task-002", kind: "tool_start", seq: 1, toolName: "bash" }));
215
- await bus.emit(makeToolCallEvent({ sessionId: "task-002", kind: "tool_complete", seq: 2, summary: "ok" }));
216
- await bus.emit(makeToolCallEvent({ sessionId: "task-002", kind: "tool_start", seq: 3, toolName: "view" }));
217
- const events = log.getEvents("task-002");
131
+ it("accumulates multiple events in order", () => {
132
+ agentEventBus.emit(makeToolCallEvent({ sessionId: "task-002", kind: "tool_start", seq: 1, toolName: "bash" }));
133
+ agentEventBus.emit(makeToolCallEvent({ sessionId: "task-002", kind: "tool_complete", seq: 2, summary: "ok" }));
134
+ agentEventBus.emit(makeToolCallEvent({ sessionId: "task-002", kind: "tool_start", seq: 3, toolName: "view" }));
135
+ const events = getTaskLogEvents("task-002");
218
136
  assert.equal(events.length, 3);
219
137
  assert.equal(events[0].seq, 1);
220
138
  assert.equal(events[2].seq, 3);
221
139
  assert.equal(events[1].kind, "tool_complete");
222
- });
223
- it("getEvents filters by afterSeq", async () => {
224
- await bus.emit(makeToolCallEvent({ sessionId: "task-003", seq: 1 }));
225
- await bus.emit(makeToolCallEvent({ sessionId: "task-003", seq: 2 }));
226
- await bus.emit(makeToolCallEvent({ sessionId: "task-003", seq: 3 }));
227
- assert.equal(log.getEvents("task-003", 1).length, 2);
228
- assert.equal(log.getEvents("task-003", 2).length, 1);
229
- assert.equal(log.getEvents("task-003", 3).length, 0);
230
- });
231
- it("subscribe delivers events as they arrive", async () => {
140
+ clearTaskLog("task-002");
141
+ });
142
+ it("getEvents filters by afterSeq", () => {
143
+ agentEventBus.emit(makeToolCallEvent({ sessionId: "task-003", seq: 1 }));
144
+ agentEventBus.emit(makeToolCallEvent({ sessionId: "task-003", seq: 2 }));
145
+ agentEventBus.emit(makeToolCallEvent({ sessionId: "task-003", seq: 3 }));
146
+ assert.equal(getTaskLogEvents("task-003", 1).length, 2);
147
+ assert.equal(getTaskLogEvents("task-003", 2).length, 1);
148
+ assert.equal(getTaskLogEvents("task-003", 3).length, 0);
149
+ clearTaskLog("task-003");
150
+ });
151
+ it("subscribe delivers events as they arrive", () => {
232
152
  const received = [];
233
- log.subscribe("task-004", (e) => received.push(e.toolName ?? "?"));
234
- await bus.emit(makeToolCallEvent({ sessionId: "task-004", toolName: "bash" }));
235
- await bus.emit(makeToolCallEvent({ sessionId: "task-004", toolName: "view" }));
153
+ const unsub = subscribeTaskLog("task-004", (e) => received.push(e.toolName ?? "?"));
154
+ agentEventBus.emit(makeToolCallEvent({ sessionId: "task-004", toolName: "bash" }));
155
+ agentEventBus.emit(makeToolCallEvent({ sessionId: "task-004", toolName: "view" }));
156
+ unsub();
236
157
  assert.deepEqual(received, ["bash", "view"]);
158
+ clearTaskLog("task-004");
237
159
  });
238
- it("subscribe unsub stops delivery", async () => {
160
+ it("subscribe unsub stops delivery", () => {
239
161
  const received = [];
240
- const unsub = log.subscribe("task-005", (e) => received.push(e.toolName ?? "?"));
241
- await bus.emit(makeToolCallEvent({ sessionId: "task-005", toolName: "bash" }));
162
+ const unsub = subscribeTaskLog("task-005", (e) => received.push(e.toolName ?? "?"));
163
+ agentEventBus.emit(makeToolCallEvent({ sessionId: "task-005", toolName: "bash" }));
242
164
  unsub();
243
- await bus.emit(makeToolCallEvent({ sessionId: "task-005", toolName: "view" }));
165
+ agentEventBus.emit(makeToolCallEvent({ sessionId: "task-005", toolName: "view" }));
244
166
  assert.equal(received.length, 1);
245
167
  assert.equal(received[0], "bash");
246
- });
247
- it("clearTaskLog removes buffer and listeners", async () => {
248
- await bus.emit(makeToolCallEvent({ sessionId: "task-006", seq: 1 }));
249
- assert.equal(log.getEvents("task-006").length, 1);
250
- log.clear("task-006");
251
- assert.equal(log.getEvents("task-006").length, 0);
252
- });
253
- it("session:destroyed auto-clears ring buffer", async () => {
254
- await bus.emit(makeToolCallEvent({ sessionId: "task-007", seq: 1 }));
255
- await bus.emit(makeToolCallEvent({ sessionId: "task-007", seq: 2 }));
256
- assert.equal(log.getEvents("task-007").length, 2);
257
- await bus.emit(makeDestroyedEvent("task-007"));
258
- assert.equal(log.getEvents("task-007").length, 0);
259
- });
260
- it("tracks multiple tasks independently", async () => {
261
- await bus.emit(makeToolCallEvent({ sessionId: "task-A", seq: 1, toolName: "bash" }));
262
- await bus.emit(makeToolCallEvent({ sessionId: "task-B", seq: 1, toolName: "view" }));
263
- await bus.emit(makeToolCallEvent({ sessionId: "task-A", seq: 2, toolName: "edit" }));
264
- assert.equal(log.getEvents("task-A").length, 2);
265
- assert.equal(log.getEvents("task-B").length, 1);
266
- assert.equal(log.getEvents("task-A")[0].toolName, "bash");
267
- assert.equal(log.getEvents("task-B")[0].toolName, "view");
268
- assert.equal(log.size(), 2);
269
- });
270
- it("events for wrong taskId are not visible to other tasks", async () => {
271
- await bus.emit(makeToolCallEvent({ sessionId: "task-X", seq: 1 }));
272
- assert.equal(log.getEvents("task-Y").length, 0);
168
+ clearTaskLog("task-005");
169
+ });
170
+ it("clearTaskLog removes buffer and listeners", () => {
171
+ agentEventBus.emit(makeToolCallEvent({ sessionId: "task-006", seq: 1 }));
172
+ assert.equal(getTaskLogEvents("task-006").length, 1);
173
+ clearTaskLog("task-006");
174
+ assert.equal(getTaskLogEvents("task-006").length, 0);
175
+ });
176
+ it("session:destroyed auto-clears ring buffer", () => {
177
+ agentEventBus.emit(makeToolCallEvent({ sessionId: "task-007", seq: 1 }));
178
+ agentEventBus.emit(makeToolCallEvent({ sessionId: "task-007", seq: 2 }));
179
+ assert.equal(getTaskLogEvents("task-007").length, 2);
180
+ agentEventBus.emit(makeDestroyedEvent("task-007"));
181
+ assert.equal(getTaskLogEvents("task-007").length, 0);
182
+ });
183
+ it("tracks multiple tasks independently", () => {
184
+ agentEventBus.emit(makeToolCallEvent({ sessionId: "task-A", seq: 1, toolName: "bash" }));
185
+ agentEventBus.emit(makeToolCallEvent({ sessionId: "task-B", seq: 1, toolName: "view" }));
186
+ agentEventBus.emit(makeToolCallEvent({ sessionId: "task-A", seq: 2, toolName: "edit" }));
187
+ assert.equal(getTaskLogEvents("task-A").length, 2);
188
+ assert.equal(getTaskLogEvents("task-B").length, 1);
189
+ assert.equal(getTaskLogEvents("task-A")[0].toolName, "bash");
190
+ assert.equal(getTaskLogEvents("task-B")[0].toolName, "view");
191
+ assert.equal(taskLogSize(), 2);
192
+ clearTaskLog("task-A");
193
+ clearTaskLog("task-B");
194
+ });
195
+ it("events for wrong taskId are not visible to other tasks", () => {
196
+ agentEventBus.emit(makeToolCallEvent({ sessionId: "task-X", seq: 1 }));
197
+ assert.equal(getTaskLogEvents("task-Y").length, 0);
198
+ clearTaskLog("task-X");
273
199
  });
274
200
  });
275
201
  //# sourceMappingURL=task-event-log.test.js.map
@@ -6,23 +6,18 @@ import { join } from "path";
6
6
  import { homedir } from "os";
7
7
  import { listSkills, createSkill, removeSkill } from "./skills.js";
8
8
  import { config, persistModel } from "../config.js";
9
- import { createSessionHooks } from "./hooks.js";
10
- import { getCurrentSourceChannel, getCurrentActivityCallback, getCurrentAuthenticatedUser, getLastAuthenticatedUser, getCurrentAuthorizationHeader, getCurrentChannelKey, getCurrentSessionKey, switchSessionModel, } from "./orchestrator.js";
9
+ import { getCurrentSourceChannel, getCurrentActivityCallback, getCurrentAuthenticatedUser, getLastAuthenticatedUser, getCurrentAuthorizationHeader, getCurrentSessionKey, switchSessionModel, } from "./orchestrator.js";
11
10
  import { getRouterConfig, updateRouterConfig } from "./router.js";
12
11
  import { ensureWikiStructure, readPage, writePage, deletePage, listPages, writeRawSource, listSources, assertPagePath } from "../wiki/fs.js";
13
12
  import { searchIndex, addToIndex, removeFromIndex, parseIndex, buildIndexEntryForPage } from "../wiki/index-manager.js";
14
13
  import { appendLog } from "../wiki/log-manager.js";
15
14
  import { withWikiWrite } from "../wiki/lock.js";
16
15
  import { readWikiPage, teamWikiSync } from "../wiki/team-sync.js";
17
- import { getAgentRegistry, getAgent, createEphemeralAgentSession, createSquadAgentSession, getAgentSessionStatus, getTask, registerTask, completeTask, failTask, createAgentFile, removeAgentFile, loadAgents, } from "./agents.js";
16
+ import { getAgentRegistry, getAgent, createEphemeralAgentSession, getAgentSessionStatus, getTask, registerTask, completeTask, failTask, createAgentFile, removeAgentFile, loadAgents, } from "./agents.js";
18
17
  import { adoGetOkrs, adoOkrSummary, adoUpdateKr } from "../integrations/ado-skill.js";
19
18
  import { TeamsNotifier } from "../integrations/teams-notify.js";
20
19
  import { TeamPushClient } from "../integrations/team-push.js";
21
20
  import { OKRMapper, parseOKRPageContent } from "./okr-mapper.js";
22
- import { getChannelProject } from "../squad/context.js";
23
- import { findSquadAgent } from "../squad/registry.js";
24
- import { buildSquadSystemPrefix } from "../squad/charter.js";
25
- import { mirrorDecisionToWiki, syncDecisionsFileToWiki } from "../squad/mirror.js";
26
21
  import { childLogger } from "../util/logger.js";
27
22
  const log = childLogger("tools");
28
23
  function getCategoryDir(category) {
@@ -112,45 +107,15 @@ export function createTools(deps) {
112
107
  if (agent?.slug === "chapterhouse") {
113
108
  return "Cannot delegate to yourself. Handle this directly or pick a specialist agent.";
114
109
  }
115
- const channelKey = getCurrentChannelKey() ?? "default";
116
- const projectRoot = config.squadEnabled ? (getChannelProject(channelKey) ?? undefined) : undefined;
117
- const squadDescriptor = (!agent && projectRoot)
118
- ? (() => { try {
119
- return findSquadAgent(projectRoot, args.agent_name);
120
- }
121
- catch {
122
- return null;
123
- } })()
124
- : null;
125
- if (!agent && !squadDescriptor) {
110
+ if (!agent) {
126
111
  const available = getAgentRegistry().map((a) => a.slug).join(", ");
127
112
  return `Agent '${args.agent_name}' not found. Available agents: ${available}`;
128
113
  }
129
- const delegatedSlug = agent?.slug ?? squadDescriptor?.slug ?? args.agent_name;
114
+ const delegatedSlug = agent.slug;
130
115
  let session;
131
- let isSquadAgent = false;
132
116
  try {
133
117
  const allTools = createTools(deps);
134
- if (squadDescriptor && projectRoot) {
135
- const squadContext = {
136
- projectRoot,
137
- squadDir: join(projectRoot, ".squad"),
138
- teamDir: join(projectRoot, ".squad"),
139
- personalDir: null,
140
- mode: "local",
141
- projectKey: null,
142
- config: {},
143
- agents: [squadDescriptor],
144
- decisionsPath: join(projectRoot, ".squad", "decisions.md"),
145
- loadedAt: new Date().toISOString(),
146
- };
147
- const charterPrefix = await buildSquadSystemPrefix(squadContext, squadDescriptor);
148
- session = await createSquadAgentSession(delegatedSlug, deps.client, allTools, charterPrefix, args.model_override || squadDescriptor.modelPreference);
149
- isSquadAgent = true;
150
- }
151
- else {
152
- session = await createEphemeralAgentSession(agent.slug, deps.client, allTools, args.model_override);
153
- }
118
+ session = await createEphemeralAgentSession(agent.slug, deps.client, allTools, args.model_override);
154
119
  }
155
120
  catch (err) {
156
121
  const msg = err instanceof Error ? err.message : String(err);
@@ -212,28 +177,6 @@ export function createTools(deps) {
212
177
  completeTask(task.taskId, output);
213
178
  db.prepare(`UPDATE agent_tasks SET status = 'completed', result = ?, completed_at = CURRENT_TIMESTAMP WHERE task_id = ?`).run(output.slice(0, 10000), task.taskId);
214
179
  deps.onAgentTaskComplete(task.taskId, delegatedSlug, output);
215
- if (isSquadAgent && projectRoot && squadDescriptor) {
216
- try {
217
- const wikiDecisionPath = await mirrorDecisionToWiki({
218
- taskId: task.taskId,
219
- projectRoot,
220
- squadAgentSlug: squadDescriptor.slug,
221
- wikiDecisionPath: "",
222
- }, args.summary, output);
223
- db.prepare(`INSERT OR REPLACE INTO squad_task_links (task_id, project_root, squad_agent_slug, wiki_decision_path)
224
- VALUES (?, ?, ?, ?)`).run(task.taskId, projectRoot, squadDescriptor.slug, wikiDecisionPath);
225
- // Sync the full decisions.md file to the wiki so the page reflects
226
- // all 89+ entries, not just this task-completion entry.
227
- syncDecisionsFileToWiki(projectRoot).then(syncResult => {
228
- if (syncResult) {
229
- log.info({ entriesSynced: syncResult.entriesSynced, wikiPath: syncResult.wikiPath }, "Post-task squad decisions synced to wiki");
230
- }
231
- }).catch(() => { });
232
- }
233
- catch (mirrorErr) {
234
- log.error({ err: mirrorErr instanceof Error ? mirrorErr.message : mirrorErr }, "Failed to mirror squad decision to wiki (non-fatal)");
235
- }
236
- }
237
180
  }
238
181
  catch (err) {
239
182
  const msg = err instanceof Error ? err.message : String(err);
@@ -253,7 +196,7 @@ export function createTools(deps) {
253
196
  })();
254
197
  const model = (args.model_override && args.model_override.length > 0)
255
198
  ? args.model_override
256
- : squadDescriptor?.modelPreference || (agent?.model === "auto" ? "claude-sonnet-4.6" : agent?.model || "claude-sonnet-4.6");
199
+ : (agent.model === "auto" ? "claude-sonnet-4.6" : agent.model || "claude-sonnet-4.6");
257
200
  return `Task delegated to @${delegatedSlug} (${model}). Task ID: ${task.taskId}. I'll notify you when it's done.`;
258
201
  },
259
202
  }),
@@ -323,12 +266,10 @@ export function createTools(deps) {
323
266
  },
324
267
  }),
325
268
  defineTool("show_agent_roster", {
326
- description: "List all registered agents with their name, model, status, and current tasks. When a squad project is active, also shows squad team members available via @mention.",
269
+ description: "List all registered agents with their name, model, status, and current tasks.",
327
270
  parameters: z.object({}),
328
271
  handler: async () => {
329
272
  const agents = getAgentRegistry();
330
- const channelKey = getCurrentChannelKey() ?? "default";
331
- const projectRoot = config.squadEnabled ? (getChannelProject(channelKey) ?? undefined) : undefined;
332
273
  const chLines = agents.map((a) => {
333
274
  const status = getAgentSessionStatus(a.slug);
334
275
  const runningTasks = status.tasks.filter((t) => t.status === "running");
@@ -338,25 +279,9 @@ export function createTools(deps) {
338
279
  : "";
339
280
  return `• @${a.slug} (${a.name}) — ${a.model} — ${badge}${taskInfo}\n ${a.description}`;
340
281
  });
341
- let squadSection = "";
342
- if (projectRoot) {
343
- try {
344
- const { renderProjectAgentRoster } = await import("../squad/registry.js");
345
- const squadRoster = renderProjectAgentRoster(projectRoot);
346
- if (squadRoster) {
347
- squadSection = `\n\nSquad agents (project: ${projectRoot}):\n${squadRoster}`;
348
- }
349
- }
350
- catch {
351
- // squad unavailable — skip
352
- }
353
- }
354
- else if (config.squadEnabled) {
355
- squadSection = "\n\n(Squad is enabled — select a project context to see squad agents)";
356
- }
357
- if (chLines.length === 0 && !squadSection)
282
+ if (chLines.length === 0)
358
283
  return "No agents registered.";
359
- return `Registered agents (${chLines.length}):\n${chLines.join("\n")}${squadSection}`;
284
+ return `Registered agents (${chLines.length}):\n${chLines.join("\n")}`;
360
285
  },
361
286
  }),
362
287
  defineTool("teams_notify", {
@@ -584,7 +509,6 @@ export function createTools(deps) {
584
509
  const session = await deps.client.resumeSession(args.session_id, {
585
510
  model: config.copilotModel,
586
511
  onPermissionRequest: approveAll,
587
- hooks: createSessionHooks(args.name),
588
512
  });
589
513
  const db = getDb();
590
514
  db.prepare(`INSERT OR REPLACE INTO agent_sessions (slug, copilot_session_id, model, status)
package/dist/daemon.js CHANGED
@@ -2,7 +2,6 @@ import { getClient, stopClient } from "./copilot/client.js";
2
2
  import { initOrchestrator, setMessageLogger, setProactiveNotify, getAgentInfo, shutdownAgents } from "./copilot/orchestrator.js";
3
3
  import { stopEpisodeWriter } from "./copilot/episode-writer.js";
4
4
  import { startApiServer, broadcastToSSE } from "./api/server.js";
5
- import { killRalphOnShutdown } from "./api/ralph.js";
6
5
  import { getDb, closeDb, getState } from "./store/db.js";
7
6
  import { config } from "./config.js";
8
7
  import { spawn } from "child_process";
@@ -15,7 +14,6 @@ import { shouldMigrate, migrateMemoriesToWiki, shouldReorganize, reorganizeWiki
15
14
  import { SESSIONS_DIR } from "./paths.js";
16
15
  import { getDisplayHost } from "./api/server-runtime.js";
17
16
  import { StandupScheduler } from "./copilot/standup.js";
18
- import { DecisionsSyncScheduler } from "./squad/mirror.scheduler.js";
19
17
  import { registerShutdownSignals } from "./shutdown-signals.js";
20
18
  import { logger } from "./util/logger.js";
21
19
  import { CHAPTERHOUSE_VERSION } from "./version.js";
@@ -148,9 +146,6 @@ async function main() {
148
146
  if (config.chapterhouseMode === "personal" && (config.adoPat || config.teamChapterhouseUrl)) {
149
147
  new StandupScheduler().schedule();
150
148
  }
151
- // Start periodic decisions→wiki sync for all registered projects
152
- decisionsSyncScheduler = new DecisionsSyncScheduler();
153
- decisionsSyncScheduler.start();
154
149
  const url = `http://${getDisplayHost(config.apiHost)}:${config.apiPort}`;
155
150
  log.info({ url }, "Chapterhouse is fully operational");
156
151
  if (process.env.CHAPTERHOUSE_OPEN_BROWSER === "1") {
@@ -177,7 +172,6 @@ async function main() {
177
172
  }
178
173
  // Graceful shutdown
179
174
  let shutdownState = "idle";
180
- let decisionsSyncScheduler;
181
175
  async function shutdown() {
182
176
  if (shutdownState === "shutting_down") {
183
177
  log.warn("Forced exit");
@@ -202,10 +196,6 @@ async function shutdown() {
202
196
  forceTimer.unref();
203
197
  // Destroy all active agent sessions
204
198
  await shutdownAgents();
205
- try {
206
- decisionsSyncScheduler?.stop();
207
- }
208
- catch { /* best effort */ }
209
199
  try {
210
200
  stopEpisodeWriter();
211
201
  }
@@ -214,10 +204,6 @@ async function shutdown() {
214
204
  await stopClient();
215
205
  }
216
206
  catch { /* best effort */ }
217
- try {
218
- killRalphOnShutdown();
219
- }
220
- catch { /* best effort */ }
221
207
  closeDb();
222
208
  log.info("Goodbye");
223
209
  process.exit(0);
@@ -231,10 +217,6 @@ export async function restartDaemon() {
231
217
  }
232
218
  // Destroy all active agent sessions
233
219
  await shutdownAgents();
234
- try {
235
- decisionsSyncScheduler?.stop();
236
- }
237
- catch { /* best effort */ }
238
220
  try {
239
221
  stopEpisodeWriter();
240
222
  }
@@ -243,10 +225,6 @@ export async function restartDaemon() {
243
225
  await stopClient();
244
226
  }
245
227
  catch { /* best effort */ }
246
- try {
247
- killRalphOnShutdown();
248
- }
249
- catch { /* best effort */ }
250
228
  closeDb();
251
229
  // Spawn a detached replacement process with the same args (include execArgv for tsx/loaders)
252
230
  const child = spawn(process.execPath, [...process.execArgv, ...process.argv.slice(1)], {
package/dist/store/db.js CHANGED
@@ -68,42 +68,6 @@ export function getDb() {
68
68
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
69
69
  last_accessed DATETIME DEFAULT CURRENT_TIMESTAMP
70
70
  )
71
- `);
72
- db.exec(`
73
- CREATE TABLE IF NOT EXISTS project_squads (
74
- project_root TEXT PRIMARY KEY,
75
- squad_dir TEXT NOT NULL,
76
- team_dir TEXT NOT NULL,
77
- personal_dir TEXT,
78
- mode TEXT NOT NULL CHECK(mode IN ('local', 'remote')),
79
- project_key TEXT,
80
- config_source TEXT,
81
- registered INTEGER NOT NULL DEFAULT 0,
82
- loaded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
83
- )
84
- `);
85
- db.exec(`
86
- CREATE TABLE IF NOT EXISTS squad_agents (
87
- project_root TEXT NOT NULL,
88
- slug TEXT NOT NULL,
89
- role TEXT NOT NULL,
90
- description TEXT,
91
- charter_path TEXT NOT NULL,
92
- model_preference TEXT,
93
- tools_json TEXT,
94
- capabilities_json TEXT,
95
- stale INTEGER NOT NULL DEFAULT 0,
96
- loaded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
97
- PRIMARY KEY (project_root, slug)
98
- )
99
- `);
100
- db.exec(`
101
- CREATE TABLE IF NOT EXISTS squad_task_links (
102
- task_id TEXT PRIMARY KEY,
103
- project_root TEXT NOT NULL,
104
- squad_agent_slug TEXT NOT NULL,
105
- wiki_decision_path TEXT NOT NULL
106
- )
107
71
  `);
108
72
  // Migrate: if the table already existed with a stricter CHECK, recreate it
109
73
  try {
@@ -146,7 +110,7 @@ export function getDb() {
146
110
  if (!taskCols.some((c) => c.name === 'session_key')) {
147
111
  db.exec(`ALTER TABLE agent_tasks ADD COLUMN session_key TEXT NOT NULL DEFAULT 'default'`);
148
112
  }
149
- // Migrate: add source column to agent_tasks ('adhoc' | 'squad') if not present
113
+ // Migrate: add source column to agent_tasks ('adhoc') if not present
150
114
  if (!taskCols.some((c) => c.name === 'source')) {
151
115
  db.exec(`ALTER TABLE agent_tasks ADD COLUMN source TEXT NOT NULL DEFAULT 'adhoc'`);
152
116
  }
@@ -168,13 +132,6 @@ export function getDb() {
168
132
  if (!taskColsNow.some((c) => c.name === 'event_seq')) {
169
133
  db.exec(`ALTER TABLE agent_tasks ADD COLUMN event_seq INTEGER NOT NULL DEFAULT 0`);
170
134
  }
171
- // Migrate: add last_used_at column to project_squads (epoch ms, nullable)
172
- const projectCols = db.prepare(`PRAGMA table_info(project_squads)`).all();
173
- if (!projectCols.some((c) => c.name === 'last_used_at')) {
174
- db.exec(`ALTER TABLE project_squads ADD COLUMN last_used_at INTEGER`);
175
- // Backfill from loaded_at so sidebar is not empty for existing projects
176
- db.exec(`UPDATE project_squads SET last_used_at = CAST((julianday(loaded_at) - 2440587.5) * 86400000 AS INTEGER) WHERE last_used_at IS NULL`);
177
- }
178
135
  // turn_events: append-only per-turn event log for the SSE chat channel (#130).
179
136
  // Events are written on turn completion; ring buffer serves live/recent replay.
180
137
  db.exec(`
@@ -297,7 +254,7 @@ export function getTaskSessionKey(taskId) {
297
254
  * Get recent conversation history formatted for injection into system message.
298
255
  *
299
256
  * When `sessionKey` is provided, only rows belonging to that session are
300
- * returned — useful for project/squad sessions that must not bleed context
257
+ * returned — useful for per-session isolation that must not bleed context
301
258
  * from other sessions. When omitted, rows from all sessions are returned
302
259
  * (legacy behavior for callers that don't care about session isolation).
303
260
  */
@@ -408,11 +365,6 @@ export function getTaskEvents(taskId, afterSeq = 0) {
408
365
  summary: r.summary,
409
366
  }));
410
367
  }
411
- export function bumpProjectLastUsed(projectRoot) {
412
- getDb()
413
- .prepare(`UPDATE project_squads SET last_used_at = ? WHERE project_root = ?`)
414
- .run(Date.now(), projectRoot);
415
- }
416
368
  export function closeDb() {
417
369
  if (db) {
418
370
  db.close();
@@ -167,51 +167,6 @@ test("getSessionMessages returns structured messages in chronological order, exc
167
167
  }
168
168
  });
169
169
  // ---------------------------------------------------------------------------
170
- // #26 — bumpProjectLastUsed
171
- // ---------------------------------------------------------------------------
172
- test("migration adds last_used_at column to project_squads when absent", async () => {
173
- const dbModule = await loadDbModule();
174
- try {
175
- const db = dbModule.getDb();
176
- const cols = db.prepare(`PRAGMA table_info(project_squads)`).all();
177
- assert.equal(cols.some((c) => c.name === "last_used_at"), true, "last_used_at column should exist after migration");
178
- }
179
- finally {
180
- dbModule.closeDb();
181
- }
182
- });
183
- test("bumpProjectLastUsed updates last_used_at for the given project_root", async () => {
184
- const dbModule = await loadDbModule();
185
- try {
186
- const db = dbModule.getDb();
187
- db.prepare(`INSERT INTO project_squads (project_root, squad_dir, team_dir, mode, registered) VALUES (?, ?, ?, 'local', 1)`).run("/home/user/test-proj", "/home/user/test-proj/.squad", "/home/user/test-proj/.squad");
188
- const before = db
189
- .prepare(`SELECT last_used_at FROM project_squads WHERE project_root = ?`)
190
- .get("/home/user/test-proj");
191
- const beforeTs = before.last_used_at ?? 0;
192
- await new Promise((r) => setTimeout(r, 5));
193
- dbModule.bumpProjectLastUsed("/home/user/test-proj");
194
- const after = db
195
- .prepare(`SELECT last_used_at FROM project_squads WHERE project_root = ?`)
196
- .get("/home/user/test-proj");
197
- assert.ok(after.last_used_at !== null, "last_used_at should not be null after bump");
198
- assert.ok(after.last_used_at > beforeTs, "last_used_at should advance after bump");
199
- }
200
- finally {
201
- dbModule.closeDb();
202
- }
203
- });
204
- test("bumpProjectLastUsed is a no-op for unknown project_root (no throw)", async () => {
205
- const dbModule = await loadDbModule();
206
- try {
207
- dbModule.getDb();
208
- dbModule.bumpProjectLastUsed("/does/not/exist");
209
- }
210
- finally {
211
- dbModule.closeDb();
212
- }
213
- });
214
- // ---------------------------------------------------------------------------
215
170
  // #86: agent_task_events — appendTaskEvent and getTaskEvents
216
171
  // ---------------------------------------------------------------------------
217
172
  test("#86: appendTaskEvent inserts a row and getTaskEvents returns it ordered by seq", async () => {