chapterhouse 0.3.13 → 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 (52) hide show
  1. package/README.md +2 -69
  2. package/dist/api/server.js +8 -155
  3. package/dist/api/server.test.js +1 -1
  4. package/dist/cli.js +0 -30
  5. package/dist/config.js +0 -3
  6. package/dist/copilot/agent-event-bus.js +41 -0
  7. package/dist/copilot/agent-event-bus.test.js +23 -0
  8. package/dist/copilot/agents.js +4 -59
  9. package/dist/copilot/orchestrator.js +20 -39
  10. package/dist/copilot/orchestrator.test.js +73 -158
  11. package/dist/copilot/task-event-log.js +5 -5
  12. package/dist/copilot/task-event-log.test.js +68 -142
  13. package/dist/copilot/tools.js +9 -85
  14. package/dist/daemon.js +0 -22
  15. package/dist/store/db.js +2 -50
  16. package/dist/store/db.test.js +0 -45
  17. package/package.json +1 -3
  18. package/web/dist/assets/index-BlIWCM11.js +217 -0
  19. package/web/dist/assets/index-BlIWCM11.js.map +1 -0
  20. package/web/dist/assets/{index-BtAcw3EP.css → index-lvHFM_ut.css} +1 -1
  21. package/web/dist/index.html +2 -2
  22. package/dist/api/ralph.js +0 -153
  23. package/dist/api/ralph.test.js +0 -101
  24. package/dist/copilot/agents.squad.test.js +0 -72
  25. package/dist/copilot/hooks.js +0 -157
  26. package/dist/copilot/hooks.test.js +0 -315
  27. package/dist/copilot/squad-event-bus.js +0 -27
  28. package/dist/copilot/tools.squad.test.js +0 -168
  29. package/dist/squad/charter.js +0 -125
  30. package/dist/squad/charter.test.js +0 -89
  31. package/dist/squad/context.js +0 -48
  32. package/dist/squad/context.test.js +0 -59
  33. package/dist/squad/discovery.js +0 -268
  34. package/dist/squad/discovery.test.js +0 -154
  35. package/dist/squad/index.js +0 -9
  36. package/dist/squad/init-cli.js +0 -109
  37. package/dist/squad/init.js +0 -395
  38. package/dist/squad/init.test.js +0 -351
  39. package/dist/squad/mirror.js +0 -83
  40. package/dist/squad/mirror.scheduler.js +0 -80
  41. package/dist/squad/mirror.scheduler.test.js +0 -197
  42. package/dist/squad/mirror.test.js +0 -172
  43. package/dist/squad/registry.js +0 -162
  44. package/dist/squad/registry.test.js +0 -31
  45. package/dist/squad/squad-coordinator-system-message.test.js +0 -190
  46. package/dist/squad/squad-session-routing.test.js +0 -260
  47. package/dist/squad/types.js +0 -4
  48. package/dist/squad/worktree.js +0 -295
  49. package/dist/squad/worktree.test.js +0 -189
  50. package/dist/store/squad-sessions.test.js +0 -341
  51. package/web/dist/assets/index-IgSOXx_a.js +0 -219
  52. package/web/dist/assets/index-IgSOXx_a.js.map +0 -1
package/README.md CHANGED
@@ -54,12 +54,6 @@ ADO_ORG=https://dev.azure.com/your-org
54
54
  ADO_PROJECT=your-project
55
55
  ADO_PAT=your-ado-pat-here
56
56
 
57
- # Optional — Squad (multi-agent team) integration
58
- ENABLE_SQUAD=1 # set to 1 to enable squad agent routing
59
-
60
- # Optional — periodic decisions→wiki sync (requires ENABLE_SQUAD=1)
61
- CHAPTERHOUSE_DECISIONS_SYNC_INTERVAL_MS=300000 # default 5 minutes (300 000 ms)
62
-
63
57
  # Optional — logging
64
58
  LOG_LEVEL=info # trace | debug | info | warn | error | fatal | silent (default: info)
65
59
  # Set to "debug" to see chat message content and routing decisions.
@@ -254,69 +248,8 @@ The deployment assets for the shared instance set Entra auth and `CHAPTERHOUSE_M
254
248
  | `chapterhouse update --check-only` | Print current/latest version without updating |
255
249
  | `chapterhouse update --ref <ver>` | Install a specific version |
256
250
  | `chapterhouse daemon <sub>` | Manage the persistent background service |
257
- | `chapterhouse squad init` | Initialize a new Squad team for this project (guided dialog) |
258
- | `chapterhouse squad worktree <sub>` | Manage per-agent git worktrees for concurrent squad work |
259
251
  | `chapterhouse help` | Show available commands |
260
252
 
261
- ### Squad Init Command
262
-
263
- Bootstrap a Squad team for a project that has no `.squad/` directory yet. The command runs an interactive dialog and writes the full `.squad/` scaffold:
264
-
265
- ```sh
266
- chapterhouse squad init
267
- # Runs the guided dialog: project name, stack, goal, team size, universe.
268
- # Proposes a named roster, then writes .squad/ on confirmation.
269
-
270
- chapterhouse squad init --force
271
- # Re-scaffolds even if .squad/ already exists (overwrites existing files).
272
-
273
- chapterhouse squad init --yes
274
- # Skip the confirmation prompt (non-interactive / CI mode).
275
-
276
- chapterhouse squad init --universe Firefly
277
- # Use a specific naming universe instead of the default (Dune).
278
- # Available: Dune (default), Firefly, Star Wars, The Matrix, Breaking Bad
279
- ```
280
-
281
- **What gets written:**
282
-
283
- | Path | Purpose |
284
- |------|---------|
285
- | `.squad/team.md` | Roster with `## Members` table (required by GitHub workflows) |
286
- | `.squad/routing.md` | Task routing rules by role |
287
- | `.squad/ceremonies.md` | Sprint rhythm and review gates |
288
- | `.squad/decisions.md` | Append-only decision log |
289
- | `.squad/agents/{name}/charter.md` | Per-agent charter with project context |
290
- | `.squad/agents/{name}/history.md` | Per-agent day-1 seed history |
291
- | `.squad/casting/policy.json` | Universe allowlist and capacity |
292
- | `.squad/casting/registry.json` | Persistent name registry (cast names survive re-runs) |
293
- | `.squad/casting/history.json` | Universe assignment audit trail |
294
- | `.gitattributes` | Union merge rules for append-only Squad files |
295
-
296
- **Naming universes:** By default, Squad uses the **Dune** universe (Leto, Jessica, Stilgar, Chani, Gurney, Duncan, …). This is configurable with `--universe` or via the dialog. Scribe and Ralph are always exempt from casting — they keep their names regardless of universe.
297
-
298
- ### Squad Worktree Commands
299
-
300
- Squad agents that work on GitHub issues each get a dedicated git worktree so they never step on each other. The `chapterhouse squad worktree` subcommands manage these worktrees:
301
-
302
- ```sh
303
- chapterhouse squad worktree create <agent> <issue> [--base main] [--slug <slug>]
304
- # Creates .worktrees/{agent}-{issue}/ and branch squad/{issue}-{slug}
305
- # Prints the worktree path to stdout. Reuses existing worktree if present.
306
-
307
- chapterhouse squad worktree list
308
- # Shows all active squad worktrees: agent, issue, branch, status, path
309
-
310
- chapterhouse squad worktree remove <agent> <issue> [--force] [--delete-branch]
311
- # Removes a worktree. Refuses if dirty unless --force is passed.
312
-
313
- chapterhouse squad worktree prune [--base main] [--dry-run]
314
- # Removes all worktrees whose branch has been merged into main.
315
- # Skips dirty worktrees with a warning.
316
- ```
317
-
318
- **How it works:** Worktrees live at `.worktrees/{agent}-{issue}/` inside the repo (gitignored). The Squad coordinator creates each worktree *before* spawning an agent and passes the path as `WORKTREE_PATH` in the spawn prompt. Agents do all their work—reads, edits, commits—inside that path. No agent ever runs `git checkout` in a working tree it doesn't own.
319
-
320
253
  ### Flags
321
254
 
322
255
  | Flag | Description |
@@ -374,7 +307,7 @@ Chapterhouse enforces a **3-layer timing contract** so in-flight LLM streams can
374
307
 
375
308
  #### Session lifecycle
376
309
 
377
- Each conversation window (browser tab, terminal session, squad worktree) maps to a separate `SessionManager` with its own queue and SDK session. Two env vars control how long sessions live:
310
+ Each conversation window (browser tab, terminal session) maps to a separate `SessionManager` with its own queue and SDK session. Two env vars control how long sessions live:
378
311
 
379
312
  | Config | What it controls | Default |
380
313
  |--------|-----------------|---------|
@@ -546,7 +479,7 @@ git push origin main --follow-tags
546
479
 
547
480
  `npm version` handles the commit and tag automatically. `prepublishOnly` runs `npm run build` before publish so the tarball always contains a fresh build. If you don't have CI set up, publish manually with `npm publish` after the tag push.
548
481
 
549
- > **Pre-release gate:** `preversion` runs `npm run release:check` automatically before any `npm version` call. The script aborts with a clear error if the git working tree is dirty. Stash or commit all changes (including `.squad/` metadata edits) before bumping the version.
482
+ > **Pre-release gate:** `preversion` runs `npm run release:check` automatically before any `npm version` call. The script aborts with a clear error if the git working tree is dirty. Stash or commit all changes before bumping the version.
550
483
 
551
484
  ### Commit message convention
552
485
 
@@ -1,12 +1,12 @@
1
1
  import cors from "cors";
2
2
  import express from "express";
3
3
  import helmet from "helmet";
4
- import { existsSync, statSync, readdirSync } from "fs";
4
+ import { existsSync, statSync } from "fs";
5
5
  import { join, dirname } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { z } from "zod";
8
8
  import { sendToOrchestrator, interruptCurrentTurn, enqueueForSse, getAgentInfo, cancelCurrentMessage, getLastRouteResult, getCurrentSessionKey } from "../copilot/orchestrator.js";
9
- import { squadEventBus } from "../copilot/squad-event-bus.js";
9
+ import { agentEventBus } from "../copilot/agent-event-bus.js";
10
10
  import { getAgentRegistry } from "../copilot/agents.js";
11
11
  import { config, persistModel } from "../config.js";
12
12
  import { getRouterConfig, updateRouterConfig } from "../copilot/router.js";
@@ -20,17 +20,14 @@ import { withWikiWrite } from "../wiki/lock.js";
20
20
  import { listSkills, removeSkill } from "../copilot/skills.js";
21
21
  import { restartDaemon } from "../daemon.js";
22
22
  import { API_TOKEN_PATH, resolveWikiRelativePath } from "../paths.js";
23
- import { getDb, getSessionMessages, getTaskEvents, normalizeSqliteTsToIso } from "../store/db.js";
23
+ import { getDb, getSessionMessages, getTaskEvents } from "../store/db.js";
24
24
  import { getTaskLogEvents, subscribeTaskLog } from "../copilot/task-event-log.js";
25
25
  import { subscribeSession, getSessionEventsFromDb, oldestSessionSeq, } from "../copilot/turn-event-log.js";
26
26
  import { getStatus, onStatusChange } from "../status.js";
27
27
  import { formatSseData, formatSseEvent } from "./sse.js";
28
- import { syncDecisionsFileToWiki } from "../squad/mirror.js";
29
- import { resolveProjectSquad } from "../squad/discovery.js";
30
28
  import { assertAuthenticationConfigured, createHealthPayload, createPublicConfigPayload, buildHistoryEntries, getDisplayHost, resolveApiToken, shouldServeSpaPath, } from "./server-runtime.js";
31
29
  import { BadRequestError, ForbiddenError, InternalServerError, NotFoundError, apiNotFoundHandler, asBadRequest, createApiErrorHandler, parseRequest, } from "./errors.js";
32
30
  import { childLogger } from "../util/logger.js";
33
- import { getRalphStatus, startRalph, stopRalph, getRalphQueue } from "./ralph.js";
34
31
  const log = childLogger("server");
35
32
  void searchIndex; // re-exported by index-manager; reference here documents the dep
36
33
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -249,7 +246,7 @@ app.get("/api/agents", (_req, res) => {
249
246
  res.json(getAgentInfo());
250
247
  });
251
248
  // List all workers: reads from SQLite agent_tasks (last 24 hours) so completed
252
- // squad-dispatched tasks remain visible after they finish, not just in-flight ones.
249
+ // dispatched subagents remain visible after they finish, not just in-flight ones.
253
250
  app.get("/api/workers", (_req, res) => {
254
251
  const rows = getDb()
255
252
  .prepare(`SELECT task_id, agent_slug, description, status, started_at, completed_at
@@ -341,13 +338,13 @@ app.get("/api/workers/:taskId/events/stream", (req, res) => {
341
338
  });
342
339
  });
343
340
  // ---------------------------------------------------------------------------
344
- // Global squad EventBus SSE stream — thin pass-through of SDK SquadEvents.
341
+ // Global agent EventBus SSE stream — thin pass-through of AgentEvents.
345
342
  // Replaces the 4-second poll in the Workers frontend: clients subscribe once
346
343
  // and receive push notifications on session:created / session:destroyed /
347
344
  // session:error so the worker list updates in real time.
348
345
  // Chat-specific events (delta, message, queued) are NOT emitted here.
349
346
  // ---------------------------------------------------------------------------
350
- app.get("/api/squad/stream", (req, res) => {
347
+ app.get("/api/agents/stream", (req, res) => {
351
348
  res.writeHead(200, {
352
349
  "Content-Type": "text/event-stream",
353
350
  "Cache-Control": "no-cache",
@@ -355,58 +352,14 @@ app.get("/api/squad/stream", (req, res) => {
355
352
  });
356
353
  res.write(formatSseData({ type: "connected" }));
357
354
  const heartbeat = setInterval(() => { res.write(`:ping\n\n`); }, 20_000);
358
- const unsub = squadEventBus.subscribeAll((event) => {
359
- res.write(formatSseData({ type: "squad_event", squadEvent: event }));
355
+ const unsub = agentEventBus.subscribeAll((event) => {
356
+ res.write(formatSseData({ type: "agent_event", agentEvent: event }));
360
357
  });
361
358
  req.on("close", () => {
362
359
  clearInterval(heartbeat);
363
360
  unsub();
364
361
  });
365
362
  });
366
- // ---------------------------------------------------------------------------
367
- // Ralph watch panel endpoints — /api/squad/ralph/*
368
- // ---------------------------------------------------------------------------
369
- const ralphStartSchema = z.object({
370
- projectRoot: z.string().min(1, "projectRoot is required"),
371
- interval: z.number().int().min(1).max(120).default(10),
372
- }).strict();
373
- app.get("/api/squad/ralph/status", (_req, res) => {
374
- res.json(getRalphStatus());
375
- });
376
- app.post("/api/squad/ralph/start", (req, res) => {
377
- const { projectRoot, interval } = parseRequest(ralphStartSchema, req.body);
378
- const result = startRalph(projectRoot, interval);
379
- if (!result.started) {
380
- res.status(409).json({ error: result.error });
381
- return;
382
- }
383
- res.status(201).json({ pid: result.pid, projectRoot, interval });
384
- });
385
- app.post("/api/squad/ralph/stop", (_req, res) => {
386
- const result = stopRalph();
387
- if (!result.stopped) {
388
- res.status(409).json({ error: result.error });
389
- return;
390
- }
391
- res.json({ stopped: true });
392
- });
393
- app.get("/api/squad/ralph/queue", async (req, res) => {
394
- const projectRoot = Array.isArray(req.query.projectRoot)
395
- ? req.query.projectRoot[0]
396
- : req.query.projectRoot;
397
- if (!projectRoot || typeof projectRoot !== "string") {
398
- res.status(400).json({ error: "Missing required query param: projectRoot" });
399
- return;
400
- }
401
- try {
402
- const issues = await getRalphQueue(projectRoot);
403
- res.json({ projectRoot, issues });
404
- }
405
- catch (err) {
406
- log.warn({ err: err instanceof Error ? err.message : err }, "getRalphQueue failed");
407
- res.status(502).json({ error: err instanceof Error ? err.message : "gh issue list failed" });
408
- }
409
- });
410
363
  app.get("/stream", (req, res) => {
411
364
  const connectionId = `web-${++connectionCounter}`;
412
365
  res.writeHead(200, {
@@ -847,106 +800,6 @@ app.post("/api/restart", (_req, res) => {
847
800
  }, 500);
848
801
  });
849
802
  // ---------------------------------------------------------------------------
850
- // Projects (Squad integration)
851
- // ---------------------------------------------------------------------------
852
- const projectRegisterSchema = z.object({
853
- projectRoot: requiredString("projectRoot must be a non-empty string"),
854
- }).strict();
855
- /**
856
- * Count squad agents on disk for a project.
857
- * Authoritative source: each subdirectory of <projectRoot>/.squad/agents/ that
858
- * contains a charter.md is one agent. Never relies on the SQLite cache so the
859
- * badge is always accurate even before the cache is warm.
860
- */
861
- function countAgentsOnDisk(projectRoot) {
862
- const agentsDir = join(projectRoot, ".squad", "agents");
863
- if (!existsSync(agentsDir))
864
- return 0;
865
- try {
866
- return readdirSync(agentsDir).filter((entry) => {
867
- try {
868
- return statSync(join(agentsDir, entry)).isDirectory() &&
869
- existsSync(join(agentsDir, entry, "charter.md"));
870
- }
871
- catch {
872
- return false;
873
- }
874
- }).length;
875
- }
876
- catch {
877
- return 0;
878
- }
879
- }
880
- app.get("/api/projects", (_req, res) => {
881
- if (!config.squadEnabled) {
882
- res.status(503).json({ error: "Squad integration is disabled. Set ENABLE_SQUAD=1 to enable." });
883
- return;
884
- }
885
- const db = getDb();
886
- const rows = db.prepare(`
887
- SELECT project_root, squad_dir, loaded_at, last_used_at
888
- FROM project_squads
889
- WHERE registered = 1
890
- ORDER BY COALESCE(last_used_at, 0) DESC
891
- `).all();
892
- res.json(rows.map((r) => ({
893
- projectRoot: r.project_root,
894
- squadDir: r.squad_dir,
895
- // Count from live filesystem — authoritative per Squad SDK rule: repo files win over cache.
896
- agentCount: countAgentsOnDisk(r.project_root),
897
- loadedAt: normalizeSqliteTsToIso(r.loaded_at),
898
- lastUsedAt: r.last_used_at != null ? new Date(r.last_used_at).toISOString() : undefined,
899
- })));
900
- });
901
- app.post("/api/projects", async (req, res) => {
902
- if (!config.squadEnabled) {
903
- res.status(503).json({ error: "Squad integration is disabled. Set ENABLE_SQUAD=1 to enable." });
904
- return;
905
- }
906
- const { projectRoot } = parseRequest(projectRegisterSchema, req.body);
907
- if (!existsSync(projectRoot)) {
908
- throw new BadRequestError(`Directory not found: ${projectRoot}`);
909
- }
910
- const squadDir = join(projectRoot, ".squad");
911
- if (!existsSync(squadDir)) {
912
- res.status(400).json({ error: "No .squad directory found at this path" });
913
- return;
914
- }
915
- const db = getDb();
916
- db.prepare(`INSERT OR REPLACE INTO project_squads (project_root, squad_dir, team_dir, mode, registered) VALUES (?, ?, ?, 'local', 1)`)
917
- .run(projectRoot, squadDir, squadDir);
918
- // Fire-and-forget: sync decisions.md to the wiki. Non-fatal if it fails.
919
- syncDecisionsFileToWiki(projectRoot).then(result => {
920
- if (result) {
921
- log.info({ entriesSynced: result.entriesSynced, projectRoot }, "Synced squad decisions to wiki");
922
- }
923
- }).catch(err => {
924
- log.warn({ err: err instanceof Error ? err.message : err }, "syncDecisionsFileToWiki failed during registration (non-fatal)");
925
- });
926
- // Fire-and-forget: populate squad_agents cache from disk so future queries have
927
- // something to work with (non-fatal — GET /api/projects counts live from disk anyway).
928
- resolveProjectSquad(projectRoot).catch(err => {
929
- log.warn({ err: err instanceof Error ? err.message : err }, "resolveProjectSquad failed during registration (non-fatal)");
930
- });
931
- res.status(201).json({ projectRoot, message: "Project registered successfully" });
932
- });
933
- app.delete("/api/projects/:projectRoot", (req, res) => {
934
- if (!config.squadEnabled) {
935
- res.status(503).json({ error: "Squad integration is disabled. Set ENABLE_SQUAD=1 to enable." });
936
- return;
937
- }
938
- const raw = Array.isArray(req.params.projectRoot) ? req.params.projectRoot[0] : req.params.projectRoot;
939
- const projectRoot = decodeURIComponent(raw);
940
- const db = getDb();
941
- const existing = db.prepare(`SELECT project_root FROM project_squads WHERE project_root = ?`).get(projectRoot);
942
- if (!existing) {
943
- throw new NotFoundError("Project not found");
944
- }
945
- db.prepare(`DELETE FROM project_squads WHERE project_root = ?`).run(projectRoot);
946
- db.prepare(`DELETE FROM squad_agents WHERE project_root = ?`).run(projectRoot);
947
- res.json({ message: "Project removed" });
948
- });
949
- // ---------------------------------------------------------------------------
950
803
  // Session messages — frontend rehydration on reload
951
804
  // ---------------------------------------------------------------------------
952
805
  app.get("/api/session/:sessionKey/messages", (req, res) => {
@@ -124,7 +124,7 @@ async function stopChild(child) {
124
124
  // The Copilot SDK's client.start() then blocks the event loop while authenticating,
125
125
  // so the HTTP server cannot respond until SDK init completes (~10-20 s depending on
126
126
  // session state). Other modes complete in ~330 ms because the SDK yields quickly.
127
- // Root cause tracked in .squad/decisions/ for Kaylee (backend fix: lazy-import or
127
+ // Root cause tracked in decisions notes (backend fix: lazy-import or
128
128
  // guard env var in daemon.ts). Interim fix: 30 s timeout for standalone, 10 s elsewhere.
129
129
  // Additionally, strip COPILOT_* env vars from the child so it doesn't accidentally
130
130
  // piggy-back on the running agent session, which worsens the blocking behaviour.
package/dist/cli.js CHANGED
@@ -25,7 +25,6 @@ Commands:
25
25
  setup Pick a default model and write ~/.chapterhouse/.env
26
26
  update Check for updates and install the latest version
27
27
  daemon <sub> Manage the persistent background service (install/uninstall/start/stop/restart/status/logs)
28
- squad <sub> Squad agent tools (init, worktree management)
29
28
  help Show this help message
30
29
 
31
30
  Flags (start):
@@ -179,35 +178,6 @@ switch (command) {
179
178
  }
180
179
  break;
181
180
  }
182
- case "squad": {
183
- const squadSub = args[1];
184
- if (squadSub === 'worktree') {
185
- const { runWorktreeCli } = await import("./squad/worktree.js");
186
- await runWorktreeCli(args.slice(2));
187
- }
188
- else if (squadSub === 'init') {
189
- const { runInitCli } = await import("./squad/init-cli.js");
190
- await runInitCli(args.slice(2));
191
- }
192
- else {
193
- if (squadSub) {
194
- console.error(`Unknown squad subcommand: ${squadSub}\n`);
195
- }
196
- console.log(`
197
- chapterhouse squad — Squad agent tools
198
-
199
- Subcommands:
200
- init Initialize a new Squad team for this project (guided dialog)
201
- worktree Manage per-agent git worktrees (create / list / remove / prune)
202
-
203
- Run \`chapterhouse squad init\` to scaffold a .squad/ directory with a named team.
204
- Run \`chapterhouse squad worktree\` for worktree subcommand help.
205
- `.trim());
206
- if (squadSub)
207
- process.exit(1);
208
- }
209
- break;
210
- }
211
181
  case "help":
212
182
  case "--help":
213
183
  case "-h":
package/dist/config.js CHANGED
@@ -45,7 +45,6 @@ const configSchema = z.object({
45
45
  API_RATE_LIMIT_GENERAL_MAX: z.string().optional(),
46
46
  API_RATE_LIMIT_AUTH_MAX: z.string().optional(),
47
47
  API_RATE_LIMIT_SSE_MAX_CONNECTIONS: z.string().optional(),
48
- ENABLE_SQUAD: z.string().optional(),
49
48
  CHAPTERHOUSE_WORKIQ_AUTO_INSTALL: z.string().optional(),
50
49
  CHAPTERHOUSE_CHAT_SSE: z.string().optional(),
51
50
  });
@@ -220,7 +219,6 @@ export function parseRuntimeConfig(env, options = {}) {
220
219
  apiRateLimitGeneralMax,
221
220
  apiRateLimitAuthMax,
222
221
  apiRateLimitSseMaxConnections,
223
- squadEnabled: raw.ENABLE_SQUAD === "1",
224
222
  workiqAutoInstall: parseBooleanEnv("CHAPTERHOUSE_WORKIQ_AUTO_INSTALL", raw.CHAPTERHOUSE_WORKIQ_AUTO_INSTALL, true),
225
223
  chatSseEnabled: raw.CHAPTERHOUSE_CHAT_SSE === "1",
226
224
  };
@@ -261,7 +259,6 @@ export const config = {
261
259
  apiRateLimitGeneralMax: runtimeConfig.apiRateLimitGeneralMax,
262
260
  apiRateLimitAuthMax: runtimeConfig.apiRateLimitAuthMax,
263
261
  apiRateLimitSseMaxConnections: runtimeConfig.apiRateLimitSseMaxConnections,
264
- squadEnabled: runtimeConfig.squadEnabled,
265
262
  workiqAutoInstall: runtimeConfig.workiqAutoInstall,
266
263
  chatSseEnabled: runtimeConfig.chatSseEnabled,
267
264
  copilotAuthToken: runtimeConfig.copilotAuthToken,
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Process-scoped singleton event bus for agent (subagent) lifecycle events.
3
+ *
4
+ * Backed by Node's EventEmitter. The entire daemon process shares one bus.
5
+ * Keeps the same surface used by
6
+ * task-event-log.ts (subscribe by type), orchestrator.ts (emit), and the
7
+ * /api/agents/stream SSE endpoint (subscribeAll).
8
+ *
9
+ * Event types:
10
+ * session:created — subagent spawned payload: { agentName, priority }
11
+ * session:tool_call — tool invoked payload: { toolName, toolArgs, resultType?, _kind?, _seq?, _ts?, _summary? }
12
+ * session:destroyed — subagent finished payload: { agentName, reason: "complete" | "error" | "abort" }
13
+ * session:error — subagent failed payload: { agentName, error }
14
+ *
15
+ * @module copilot/agent-event-bus
16
+ */
17
+ import { EventEmitter } from "node:events";
18
+ const ALL = "*";
19
+ class AgentEventBus {
20
+ emitter = new EventEmitter();
21
+ constructor() {
22
+ // Subagent fan-out can exceed the default 10 listeners; lift the cap.
23
+ this.emitter.setMaxListeners(0);
24
+ }
25
+ emit(event) {
26
+ const e = { timestamp: new Date(), ...event };
27
+ this.emitter.emit(e.type, e);
28
+ this.emitter.emit(ALL, e);
29
+ }
30
+ subscribe(type, handler) {
31
+ this.emitter.on(type, handler);
32
+ return () => this.emitter.off(type, handler);
33
+ }
34
+ subscribeAll(handler) {
35
+ this.emitter.on(ALL, handler);
36
+ return () => this.emitter.off(ALL, handler);
37
+ }
38
+ }
39
+ /** Process-level singleton. Import this everywhere instead of newing a bus. */
40
+ export const agentEventBus = new AgentEventBus();
41
+ //# sourceMappingURL=agent-event-bus.js.map
@@ -0,0 +1,23 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { agentEventBus } from "./agent-event-bus.js";
4
+ test("subscribe receives only matching event type", () => {
5
+ const got = [];
6
+ const unsub = agentEventBus.subscribe("session:created", (e) => got.push(e));
7
+ agentEventBus.emit({ type: "session:created", sessionId: "a", payload: { agentName: "x", priority: "normal" } });
8
+ agentEventBus.emit({ type: "session:destroyed", sessionId: "a", payload: { agentName: "x", reason: "complete" } });
9
+ unsub();
10
+ assert.equal(got.length, 1);
11
+ assert.equal(got[0].sessionId, "a");
12
+ assert.ok(got[0].timestamp instanceof Date);
13
+ });
14
+ test("subscribeAll receives every event; unsub stops delivery", () => {
15
+ const got = [];
16
+ const unsub = agentEventBus.subscribeAll((e) => got.push(e));
17
+ agentEventBus.emit({ type: "session:created", sessionId: "b", payload: {} });
18
+ agentEventBus.emit({ type: "session:tool_call", sessionId: "b", payload: { toolName: "Read" } });
19
+ unsub();
20
+ agentEventBus.emit({ type: "session:error", sessionId: "b", payload: { agentName: "x", error: "boom" } });
21
+ assert.equal(got.length, 2);
22
+ });
23
+ //# sourceMappingURL=agent-event-bus.test.js.map
@@ -8,8 +8,6 @@ import { AGENTS_DIR, SESSIONS_DIR } from "../paths.js";
8
8
  import { getState, setState } from "../store/db.js";
9
9
  import { loadMcpConfig } from "./mcp-config.js";
10
10
  import { getSkillDirectories } from "./skills.js";
11
- import { createSessionHooks } from "./hooks.js";
12
- import { findSquadAgent, renderProjectAgentRoster } from "../squad/registry.js";
13
11
  import { childLogger } from "../util/logger.js";
14
12
  const log = childLogger("agents");
15
13
  // Frontmatter schema
@@ -246,7 +244,7 @@ export function composeAgentSystemMessage(agent, rosterInfo) {
246
244
  return `${agentPrompt}\n\n${base}`;
247
245
  }
248
246
  /** Build a roster description of all agents for @chapterhouse's system prompt. */
249
- export function buildAgentRoster(projectRoot) {
247
+ export function buildAgentRoster() {
250
248
  const agents = getAgentRegistry();
251
249
  const chLines = agents
252
250
  .filter((a) => a.slug !== "chapterhouse")
@@ -255,22 +253,9 @@ export function buildAgentRoster(projectRoot) {
255
253
  const skills = a.skills?.length ? ` | skills: ${a.skills.join(", ")}` : "";
256
254
  return `- **@${a.slug}** — ${a.description} (model: ${model}${skills})`;
257
255
  });
258
- let squadLines = [];
259
- if (projectRoot) {
260
- try {
261
- const squadRoster = renderProjectAgentRoster(projectRoot);
262
- if (squadRoster) {
263
- squadLines = squadRoster.split("\n").filter(Boolean);
264
- }
265
- }
266
- catch {
267
- // squad roster unavailable — skip
268
- }
269
- }
270
- const allLines = [...chLines, ...squadLines];
271
- if (allLines.length === 0)
256
+ if (chLines.length === 0)
272
257
  return "No agents registered.";
273
- return allLines.join("\n");
258
+ return chLines.join("\n");
274
259
  }
275
260
  // The wiki tools that every agent gets regardless of tool config
276
261
  const WIKI_TOOL_NAMES = new Set([
@@ -325,7 +310,6 @@ export async function createEphemeralAgentSession(slug, client, allTools, modelO
325
310
  mcpServers,
326
311
  skillDirectories,
327
312
  onPermissionRequest: approveAll,
328
- hooks: createSessionHooks(slug, agent.allowedPaths),
329
313
  infiniteSessions: {
330
314
  enabled: true,
331
315
  backgroundCompactionThreshold: 0.80,
@@ -335,34 +319,6 @@ export async function createEphemeralAgentSession(slug, client, allTools, modelO
335
319
  log.info({ agentSlug: agent.slug, model }, "Created ephemeral agent session");
336
320
  return session;
337
321
  }
338
- /** Create an ephemeral session for a squad virtual agent (not in CH registry). */
339
- export async function createSquadAgentSession(slug, client, allTools, systemMessagePrefix, modelOverride) {
340
- const model = (modelOverride && modelOverride.length > 0)
341
- ? modelOverride
342
- : "claude-sonnet-4.6";
343
- const squadTools = allTools.filter((t) => !MANAGEMENT_TOOL_NAMES.has(t.name));
344
- const mcpServers = loadMcpConfig();
345
- const skillDirectories = getSkillDirectories();
346
- const session = await client.createSession({
347
- model,
348
- configDir: SESSIONS_DIR,
349
- workingDirectory: process.cwd(),
350
- streaming: true,
351
- systemMessage: { content: systemMessagePrefix },
352
- tools: squadTools,
353
- mcpServers,
354
- skillDirectories,
355
- onPermissionRequest: approveAll,
356
- hooks: createSessionHooks(slug),
357
- infiniteSessions: {
358
- enabled: true,
359
- backgroundCompactionThreshold: 0.80,
360
- bufferExhaustionThreshold: 0.95,
361
- },
362
- });
363
- log.info({ agentSlug: slug, model }, "Created squad virtual agent session");
364
- return session;
365
- }
366
322
  /** Clean up active task tracking (for shutdown/restart). */
367
323
  export async function clearActiveTasks() {
368
324
  activeTasks.clear();
@@ -428,7 +384,7 @@ export function setActiveAgent(channel, slug) {
428
384
  activeAgentByChannel.set(channel, slug);
429
385
  }
430
386
  /** Parse @mention from message text. Returns agent slug and remaining message, or null. */
431
- export function parseAtMention(text, projectRoot) {
387
+ export function parseAtMention(text) {
432
388
  const match = text.match(/^@([a-zA-Z0-9-]+)\s*([\s\S]*)$/);
433
389
  if (!match)
434
390
  return null;
@@ -438,17 +394,6 @@ export function parseAtMention(text, projectRoot) {
438
394
  if (agent) {
439
395
  return { agentSlug: agent.slug, message: message || "" };
440
396
  }
441
- if (projectRoot) {
442
- try {
443
- const squadAgent = findSquadAgent(projectRoot, mentionedName);
444
- if (squadAgent) {
445
- return { agentSlug: mentionedName, message: message || "" };
446
- }
447
- }
448
- catch {
449
- // squad lookup failed — fall through
450
- }
451
- }
452
397
  return null;
453
398
  }
454
399
  //# sourceMappingURL=agents.js.map