chapterhouse 0.1.5 → 0.2.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.
package/README.md CHANGED
@@ -14,14 +14,16 @@ Chapterhouse is built on [Max](https://github.com/burkeholland/max) by [Burke Ho
14
14
  - **Learns any skill** — pulls from [skills.sh](https://skills.sh) or builds new skills on demand.
15
15
  - **Your Copilot subscription** — works with any model your subscription includes (Claude, GPT, Gemini, …). Auto-routing picks a tier per message.
16
16
 
17
+ See [CHANGELOG.md](CHANGELOG.md) for recent changes and feature history.
18
+
17
19
  ## Installation
18
20
 
19
- **Requires Node.js 22.5 or later.**
21
+ **Requires Node.js 24 or later** (npm ≥ 11.5.1 for Trusted Publishing support).
20
22
 
21
23
  Install globally via npm:
22
24
 
23
25
  ```bash
24
- npm install -g chapterhouse
26
+ npm install -g chapterhouse@latest
25
27
  ```
26
28
 
27
29
  After installing, run first-time setup:
@@ -57,6 +59,12 @@ ENABLE_SQUAD=1 # set to 1 to enable squad agent routi
57
59
 
58
60
  # Optional — periodic decisions→wiki sync (requires ENABLE_SQUAD=1)
59
61
  CHAPTERHOUSE_DECISIONS_SYNC_INTERVAL_MS=300000 # default 5 minutes (300 000 ms)
62
+
63
+ # Optional — logging
64
+ LOG_LEVEL=info # trace | debug | info | warn | error | fatal | silent (default: info)
65
+ # Set to "debug" to see chat message content and routing decisions.
66
+ # Logs are structured JSON (Pino). For human-readable output:
67
+ # chapterhouse start 2>&1 | npx pino-pretty
60
68
  ```
61
69
 
62
70
  Then start the daemon:
@@ -91,13 +99,33 @@ npm install && npm run build && npm link
91
99
 
92
100
  ## Upgrading
93
101
 
94
- If you already have Chapterhouse installed:
95
-
96
102
  ```bash
97
103
  chapterhouse update
98
104
  ```
99
105
 
100
- Or manually: `cd ~/.chapterhouse/src && git pull && npm install && npm run build`. Your `~/.chapterhouse/` config carries forward automatically.
106
+ `chapterhouse update` is npm-registry-aware. It detects how Chapterhouse was installed and routes accordingly:
107
+
108
+ | Install source | Update action |
109
+ | -------------- | ------------- |
110
+ | **npm global** (`npm install -g chapterhouse`) | Runs `npm install -g chapterhouse@latest` |
111
+ | **git/legacy** (`~/.chapterhouse/src`) | Git pull + rebuild (with deprecation notice) |
112
+ | **dev** (source working tree) | No-op — use `git pull` manually |
113
+
114
+ ### Update flags
115
+
116
+ | Flag | Description |
117
+ | ---- | ----------- |
118
+ | `--check-only` | Print current/latest version and exit without updating |
119
+ | `--ref <version>` | Install a specific version, e.g. `--ref 0.1.5` |
120
+ | `--force` | Bypass the Node 24 / npm 11.5.1 precondition check |
121
+
122
+ **Legacy users:** if you previously installed via git clone, switch to the registry path once:
123
+
124
+ ```bash
125
+ npm install -g chapterhouse@latest
126
+ ```
127
+
128
+ Your `~/.chapterhouse/` config carries forward automatically.
101
129
 
102
130
  ## Quick Start
103
131
 
@@ -202,13 +230,16 @@ The deployment assets for the shared instance set Entra auth and `CHAPTERHOUSE_M
202
230
 
203
231
  ## CLI Commands
204
232
 
205
- | Command | Description |
206
- | --------------------------- | ------------------------------------------------- |
207
- | `chapterhouse start` | Start the Chapterhouse daemon (web UI + HTTP API) |
208
- | `chapterhouse start --open` | Same, plus open the browser |
209
- | `chapterhouse setup` | Interactive first-run configuration |
210
- | `chapterhouse update` | Check for and install updates |
211
- | `chapterhouse help` | Show available commands |
233
+ | Command | Description |
234
+ | ------------------------------ | ------------------------------------------------- |
235
+ | `chapterhouse start` | Start the Chapterhouse daemon (web UI + HTTP API) |
236
+ | `chapterhouse start --open` | Same, plus open the browser |
237
+ | `chapterhouse setup` | Interactive first-run configuration |
238
+ | `chapterhouse update` | Check for and install updates (npm registry for global installs) |
239
+ | `chapterhouse update --check-only` | Print current/latest version without updating |
240
+ | `chapterhouse update --ref <ver>` | Install a specific version |
241
+ | `chapterhouse daemon <sub>` | Manage the persistent background service |
242
+ | `chapterhouse help` | Show available commands |
212
243
 
213
244
  ### Flags
214
245
 
@@ -217,6 +248,42 @@ The deployment assets for the shared instance set Entra auth and `CHAPTERHOUSE_M
217
248
  | `--self-edit` | Allow Chapterhouse to modify its own source code (use with `chapterhouse start`) |
218
249
  | `--open` | Open the web UI in your default browser when the daemon is ready |
219
250
 
251
+ ### Daemon Management (macOS / Linux)
252
+
253
+ Chapterhouse can run as a persistent user-level background service that starts on login and restarts automatically on crash — no root required.
254
+
255
+ #### Install
256
+
257
+ ```sh
258
+ chapterhouse daemon install
259
+ ```
260
+
261
+ This writes and loads the appropriate unit file for your OS:
262
+
263
+ | Platform | Unit file |
264
+ | -------- | ----------------------------------------------------------------------- |
265
+ | macOS | `~/Library/LaunchAgents/com.bketelsen.chapterhouse.plist` (launchd) |
266
+ | Linux | `~/.config/systemd/user/chapterhouse.service` (systemd `--user`) |
267
+ | Windows | Not supported — run `chapterhouse start` manually or use Task Scheduler |
268
+
269
+ #### Manage
270
+
271
+ ```sh
272
+ chapterhouse daemon status # is it running? what PID? where are the logs?
273
+ chapterhouse daemon stop # stop without uninstalling
274
+ chapterhouse daemon start # start without re-installing
275
+ chapterhouse daemon restart # restart in place
276
+ chapterhouse daemon logs # tail live logs (Ctrl+C to exit)
277
+ chapterhouse daemon uninstall # stop, disable, and remove the unit file
278
+ ```
279
+
280
+ #### Log locations
281
+
282
+ | Platform | Location |
283
+ | -------- | ----------------------------------------------------------- |
284
+ | macOS | `~/Library/Logs/chapterhouse.log` |
285
+ | Linux | `journalctl --user -u chapterhouse` (no extra config needed) |
286
+
220
287
  ## Web UI
221
288
 
222
289
  The browser app at `http://localhost:7788` is split into a few views:
@@ -1,4 +1,6 @@
1
1
  import { ZodError } from "zod";
2
+ import { childLogger } from "../util/logger.js";
3
+ const log = childLogger("api:errors");
2
4
  export class HttpError extends Error {
3
5
  statusCode;
4
6
  expose;
@@ -80,15 +82,15 @@ export function createApiErrorHandler() {
80
82
  }
81
83
  if (error instanceof HttpError) {
82
84
  if (error.statusCode >= 500) {
83
- console.error(`[api] ${req.method} ${req.originalUrl} failed:`, error);
85
+ log.error({ method: req.method, url: req.originalUrl, err: error instanceof Error ? error.message : error }, "API request failed");
84
86
  }
85
87
  else if (error.statusCode === 401 || error.statusCode === 403) {
86
- console.warn(`[api] ${req.method} ${req.originalUrl} denied: ${error.message}`);
88
+ log.warn({ method: req.method, url: req.originalUrl, err: error.message }, "API request denied");
87
89
  }
88
90
  res.status(error.statusCode).json({ error: error.expose ? error.message : "Internal server error" });
89
91
  return;
90
92
  }
91
- console.error(`[api] ${req.method} ${req.originalUrl} failed:`, error);
93
+ log.error({ method: req.method, url: req.originalUrl, err: error instanceof Error ? error.message : error }, "API request failed (unhandled)");
92
94
  res.status(500).json({ error: "Internal server error" });
93
95
  };
94
96
  }
@@ -64,26 +64,17 @@ test("api not-found handler returns JSON for unknown API routes", async () => {
64
64
  });
65
65
  test("centralized error handler hides internal error details", async () => {
66
66
  const app = express();
67
- const originalConsoleError = console.error;
68
- const logged = [];
69
- console.error = (...args) => {
70
- logged.push(args);
71
- };
72
- try {
73
- app.get("/api/failure", () => {
74
- throw new InternalServerError("do not leak", false);
75
- });
76
- app.use(createApiErrorHandler());
77
- await withServer(app, async (baseUrl) => {
78
- const response = await fetch(`${baseUrl}/api/failure`);
79
- assert.equal(response.status, 500);
80
- assert.deepEqual(await response.json(), { error: "Internal server error" });
81
- });
82
- assert.equal(logged.length, 1);
83
- assert.equal(logged[0]?.[0], "[api] GET /api/failure failed:");
84
- }
85
- finally {
86
- console.error = originalConsoleError;
87
- }
67
+ app.get("/api/failure", () => {
68
+ throw new InternalServerError("do not leak", false);
69
+ });
70
+ app.use(createApiErrorHandler());
71
+ await withServer(app, async (baseUrl) => {
72
+ const response = await fetch(`${baseUrl}/api/failure`);
73
+ // Internal details must NOT reach the client
74
+ assert.equal(response.status, 500);
75
+ assert.deepEqual(await response.json(), { error: "Internal server error" });
76
+ });
77
+ // Logging now goes through pino (structured JSON), not console.error.
78
+ // The HTTP response contract above is the authoritative check.
88
79
  });
89
80
  //# sourceMappingURL=errors.test.js.map
@@ -19,13 +19,15 @@ import { withWikiWrite } from "../wiki/lock.js";
19
19
  import { listSkills, removeSkill } from "../copilot/skills.js";
20
20
  import { restartDaemon } from "../daemon.js";
21
21
  import { API_TOKEN_PATH, resolveWikiRelativePath } from "../paths.js";
22
- import { getDb } from "../store/db.js";
22
+ import { getDb, getSessionMessages } from "../store/db.js";
23
23
  import { getStatus, onStatusChange } from "../status.js";
24
24
  import { formatSseData, formatSseEvent } from "./sse.js";
25
25
  import { syncDecisionsFileToWiki } from "../squad/mirror.js";
26
26
  import { resolveProjectSquad } from "../squad/discovery.js";
27
27
  import { assertAuthenticationConfigured, createHealthPayload, createPublicConfigPayload, buildHistoryEntries, getDisplayHost, resolveApiToken, shouldServeSpaPath, } from "./server-runtime.js";
28
28
  import { BadRequestError, ForbiddenError, InternalServerError, NotFoundError, apiNotFoundHandler, asBadRequest, createApiErrorHandler, parseRequest, } from "./errors.js";
29
+ import { childLogger } from "../util/logger.js";
30
+ const log = childLogger("server");
29
31
  void searchIndex; // re-exported by index-manager; reference here documents the dep
30
32
  const __dirname = dirname(fileURLToPath(import.meta.url));
31
33
  // Built SPA bundle (web/dist/), shipped alongside dist/
@@ -69,11 +71,11 @@ try {
69
71
  });
70
72
  }
71
73
  catch (err) {
72
- console.error(err instanceof Error ? err.message : String(err));
74
+ log.error({ err: err instanceof Error ? err.message : String(err) }, "Auth token error");
73
75
  process.exit(1);
74
76
  }
75
77
  if (config.standaloneMode) {
76
- console.log("[standalone] Running without authentication — team features disabled");
78
+ log.warn("Running without authentication — team features disabled");
77
79
  }
78
80
  function isLoopbackHostname(hostname) {
79
81
  return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1";
@@ -402,7 +404,7 @@ app.get("/api/models", async (_req, res) => {
402
404
  res.json({ models: models.map((m) => m.id), current: config.copilotModel });
403
405
  }
404
406
  catch (error) {
405
- console.error("[api] Failed to list models:", error);
407
+ log.error({ err: error instanceof Error ? error.message : error }, "Failed to list models");
406
408
  throw new InternalServerError();
407
409
  }
408
410
  });
@@ -418,7 +420,7 @@ app.get("/api/auto", (_req, res) => {
418
420
  app.post("/api/auto", (req, res) => {
419
421
  const body = parseRequest(autoRequestSchema, req.body ?? {});
420
422
  const updated = updateRouterConfig(body);
421
- console.log(`[chapterhouse] Auto-routing ${updated.enabled ? "enabled" : "disabled"}`);
423
+ log.info({ enabled: updated.enabled }, "Auto-routing updated");
422
424
  res.json(updated);
423
425
  });
424
426
  // ---------------------------------------------------------------------------
@@ -519,7 +521,7 @@ app.post("/api/restart", (_req, res) => {
519
521
  res.json({ status: "restarting" });
520
522
  setTimeout(() => {
521
523
  restartDaemon().catch((err) => {
522
- console.error("[chapterhouse] Restart failed:", err);
524
+ log.error({ err: err instanceof Error ? err.message : err }, "Restart failed");
523
525
  });
524
526
  }, 500);
525
527
  });
@@ -561,10 +563,10 @@ app.get("/api/projects", (_req, res) => {
561
563
  }
562
564
  const db = getDb();
563
565
  const rows = db.prepare(`
564
- SELECT project_root, squad_dir, loaded_at
566
+ SELECT project_root, squad_dir, loaded_at, last_used_at
565
567
  FROM project_squads
566
568
  WHERE registered = 1
567
- ORDER BY loaded_at DESC
569
+ ORDER BY COALESCE(last_used_at, 0) DESC
568
570
  `).all();
569
571
  res.json(rows.map((r) => ({
570
572
  projectRoot: r.project_root,
@@ -572,6 +574,7 @@ app.get("/api/projects", (_req, res) => {
572
574
  // Count from live filesystem — authoritative per Squad SDK rule: repo files win over cache.
573
575
  agentCount: countAgentsOnDisk(r.project_root),
574
576
  loadedAt: r.loaded_at,
577
+ lastUsedAt: r.last_used_at != null ? new Date(r.last_used_at).toISOString() : undefined,
575
578
  })));
576
579
  });
577
580
  app.post("/api/projects", async (req, res) => {
@@ -594,15 +597,15 @@ app.post("/api/projects", async (req, res) => {
594
597
  // Fire-and-forget: sync decisions.md to the wiki. Non-fatal if it fails.
595
598
  syncDecisionsFileToWiki(projectRoot).then(result => {
596
599
  if (result) {
597
- console.log(`[squad] Synced ${result.entriesSynced} decision entries to wiki for ${projectRoot}`);
600
+ log.info({ entriesSynced: result.entriesSynced, projectRoot }, "Synced squad decisions to wiki");
598
601
  }
599
602
  }).catch(err => {
600
- console.warn('[squad] syncDecisionsFileToWiki failed during registration (non-fatal):', err instanceof Error ? err.message : err);
603
+ log.warn({ err: err instanceof Error ? err.message : err }, "syncDecisionsFileToWiki failed during registration (non-fatal)");
601
604
  });
602
605
  // Fire-and-forget: populate squad_agents cache from disk so future queries have
603
606
  // something to work with (non-fatal — GET /api/projects counts live from disk anyway).
604
607
  resolveProjectSquad(projectRoot).catch(err => {
605
- console.warn('[squad] resolveProjectSquad failed during registration (non-fatal):', err instanceof Error ? err.message : err);
608
+ log.warn({ err: err instanceof Error ? err.message : err }, "resolveProjectSquad failed during registration (non-fatal)");
606
609
  });
607
610
  res.status(201).json({ projectRoot, message: "Project registered successfully" });
608
611
  });
@@ -622,6 +625,24 @@ app.delete("/api/projects/:projectRoot", (req, res) => {
622
625
  db.prepare(`DELETE FROM squad_agents WHERE project_root = ?`).run(projectRoot);
623
626
  res.json({ message: "Project removed" });
624
627
  });
628
+ // ---------------------------------------------------------------------------
629
+ // Session messages — frontend rehydration on reload
630
+ // ---------------------------------------------------------------------------
631
+ app.get("/api/session/:sessionKey/messages", (req, res) => {
632
+ const sessionKey = Array.isArray(req.params.sessionKey)
633
+ ? req.params.sessionKey[0]
634
+ : req.params.sessionKey;
635
+ if (!sessionKey) {
636
+ throw new BadRequestError("Missing sessionKey");
637
+ }
638
+ const rawLimit = req.query.limit;
639
+ const limit = rawLimit !== undefined ? parseInt(String(rawLimit), 10) : undefined;
640
+ if (limit !== undefined && (!Number.isFinite(limit) || limit < 1)) {
641
+ throw new BadRequestError("'limit' must be a positive integer");
642
+ }
643
+ const messages = getSessionMessages(sessionKey, limit);
644
+ res.json({ sessionKey, messages });
645
+ });
625
646
  app.use(apiNotFoundHandler);
626
647
  // ---------------------------------------------------------------------------
627
648
  // Static SPA + fallback. Mounted last so API routes win.
@@ -654,7 +675,7 @@ app.use(createApiErrorHandler());
654
675
  export function startApiServer() {
655
676
  return new Promise((resolve, reject) => {
656
677
  const server = app.listen(config.apiPort, config.apiHost, () => {
657
- console.log(`[chapterhouse] HTTP API + web UI listening on http://${getDisplayHost(config.apiHost)}:${config.apiPort}`);
678
+ log.info({ host: getDisplayHost(config.apiHost), port: config.apiPort }, "HTTP API + web UI listening");
658
679
  resolve();
659
680
  });
660
681
  server.on("error", (err) => {
package/dist/cli.js CHANGED
@@ -21,19 +21,30 @@ Usage:
21
21
  chapterhouse <command>
22
22
 
23
23
  Commands:
24
- start Start the Chapterhouse daemon (web UI at http://localhost:7788)
25
- setup Pick a default model and write ~/.chapterhouse/.env
26
- update Check for updates and install the latest version
27
- help Show this help message
24
+ start Start the Chapterhouse daemon (web UI at http://localhost:7788)
25
+ setup Pick a default model and write ~/.chapterhouse/.env
26
+ update Check for updates and install the latest version
27
+ daemon <sub> Manage the persistent background service (install/uninstall/start/stop/restart/status/logs)
28
+ help Show this help message
28
29
 
29
30
  Flags (start):
30
31
  --self-edit Allow Chapterhouse to modify his own source code (off by default)
31
32
  --open Open the web UI in your default browser once the daemon is ready
32
33
 
34
+ Flags (update):
35
+ --check-only Print version info and install-source, then exit without updating
36
+ --ref <version> Install a specific version (e.g. --ref 0.1.5)
37
+ --force Bypass the Node/npm version precondition check
38
+
33
39
  Examples:
34
40
  chapterhouse start Start the daemon, then open http://localhost:7788
35
41
  chapterhouse start --open Same, but open the browser for you
36
42
  chapterhouse start --self-edit Start with self-edit enabled
43
+ chapterhouse update Update to latest (npm registry for global installs)
44
+ chapterhouse update --check-only Show current/latest version without updating
45
+ chapterhouse update --ref 0.1.5 Install a specific version
46
+ chapterhouse daemon install Install and enable the persistent background service
47
+ chapterhouse daemon status Show whether the service is running
37
48
  `.trim());
38
49
  }
39
50
  const args = process.argv.slice(2);
@@ -54,30 +65,77 @@ switch (command) {
54
65
  await import("./setup.js");
55
66
  break;
56
67
  case "update": {
57
- const { checkForUpdate, performUpdate } = await import("./update.js");
68
+ const updateFlags = args.slice(1);
69
+ const checkOnly = updateFlags.includes("--check-only");
70
+ const force = updateFlags.includes("--force");
71
+ const refIdx = updateFlags.indexOf("--ref");
72
+ const ref = refIdx !== -1 ? (updateFlags[refIdx + 1] ?? null) : null;
73
+ const { checkForUpdate, performUpdate, detectInstallSource, checkPreconditions, buildLegacyGitInstallCommand, } = await import("./update.js");
74
+ const source = detectInstallSource();
75
+ // Precondition check (bypass with --force)
76
+ if (!force) {
77
+ const pre = checkPreconditions();
78
+ if (!pre.ok) {
79
+ console.warn(`⚠ ${pre.message}`);
80
+ console.warn(" Run with --force to proceed anyway.");
81
+ process.exit(1);
82
+ }
83
+ }
58
84
  const check = await checkForUpdate();
59
85
  if (!check.checkSucceeded) {
60
- console.error("⚠ Could not reach GitHub to check for updates. Check your network and try again.");
86
+ console.error("⚠ Could not reach npm registry or GitHub to check for updates. Check your network and try again.");
61
87
  process.exit(1);
62
88
  }
63
- if (!check.updateAvailable) {
64
- console.log(`chapterhouse v${check.current} is already the latest version.`);
65
- console.log("Pulling latest from main...");
66
- const result = await performUpdate();
67
- if (result.ok) {
68
- console.log("✅ Updated to latest main.");
89
+ console.log(`chapterhouse current: v${check.current}`);
90
+ console.log(`chapterhouse latest: v${check.latest ?? "unknown"}`);
91
+ console.log(`Install source: ${source}`);
92
+ if (checkOnly) {
93
+ if (check.updateAvailable) {
94
+ console.log(`\nUpdate available: v${check.current} v${check.latest}`);
95
+ console.log(`Run \`chapterhouse update\` to install.`);
69
96
  }
70
97
  else {
71
- console.error(`❌ Update failed: ${result.output}`);
72
- process.exit(1);
98
+ console.log("\nYou are on the latest version.");
73
99
  }
74
100
  break;
75
101
  }
76
- console.log(`Update available: v${check.current} → v${check.latest}`);
77
- console.log("Installing...");
78
- const result = await performUpdate(check.latest ? `v${check.latest}` : null);
102
+ if (source === "dev") {
103
+ console.log("ℹ Dev mode — use git pull manually.");
104
+ break;
105
+ }
106
+ if (source === "git") {
107
+ console.warn("⚠ Deprecation notice: your install uses the legacy git clone path.");
108
+ console.warn(` Switch to the npm registry path for automatic updates:`);
109
+ console.warn(` npm install -g chapterhouse@latest`);
110
+ console.warn(" Continuing with legacy git update...\n");
111
+ }
112
+ const targetRef = ref ?? (check.latest ? check.latest : null);
113
+ if (!check.updateAvailable && !ref) {
114
+ console.log(`chapterhouse v${check.current} is already the latest version.`);
115
+ if (source !== "git")
116
+ break;
117
+ // For git installs, still offer to pull latest main
118
+ console.log("Pulling latest from main...");
119
+ }
120
+ else if (check.updateAvailable) {
121
+ console.log(`\nUpdate available: v${check.current} → v${check.latest}`);
122
+ console.log("Installing...");
123
+ }
124
+ else {
125
+ console.log(`Installing specified ref: ${targetRef}`);
126
+ }
127
+ const result = await performUpdate(targetRef);
79
128
  if (result.ok) {
80
- console.log(`✅ Updated to v${check.latest}`);
129
+ if (result.source === "dev") {
130
+ console.log("ℹ Dev mode — use git pull manually.");
131
+ }
132
+ else {
133
+ const label = ref ? `${ref}` : `v${check.latest ?? "latest"}`;
134
+ console.log(`✅ Updated to ${label}`);
135
+ if (result.source === "registry" || result.source === "unknown") {
136
+ console.log(" To verify provenance: npm audit signatures (in any project that has chapterhouse as a dep)");
137
+ }
138
+ }
81
139
  }
82
140
  else {
83
141
  console.error(`❌ Update failed: ${result.output}`);
@@ -85,6 +143,41 @@ switch (command) {
85
143
  }
86
144
  break;
87
145
  }
146
+ case "daemon": {
147
+ const subcommand = args[1];
148
+ const { install, uninstall, start: daemonStart, stop: daemonStop, restart: daemonRestart, status: daemonStatus, logs: daemonLogs, printDaemonHelp, } = await import("./daemon-install.js");
149
+ switch (subcommand) {
150
+ case "install":
151
+ await install();
152
+ break;
153
+ case "uninstall":
154
+ await uninstall();
155
+ break;
156
+ case "start":
157
+ await daemonStart();
158
+ break;
159
+ case "stop":
160
+ await daemonStop();
161
+ break;
162
+ case "restart":
163
+ await daemonRestart();
164
+ break;
165
+ case "status":
166
+ await daemonStatus();
167
+ break;
168
+ case "logs":
169
+ await daemonLogs();
170
+ break;
171
+ default:
172
+ if (subcommand) {
173
+ console.error(`Unknown daemon subcommand: ${subcommand}\n`);
174
+ }
175
+ printDaemonHelp();
176
+ if (subcommand)
177
+ process.exit(1);
178
+ }
179
+ break;
180
+ }
88
181
  case "help":
89
182
  case "--help":
90
183
  case "-h":
@@ -9,6 +9,8 @@ import { getState, setState } from "../store/db.js";
9
9
  import { loadMcpConfig } from "./mcp-config.js";
10
10
  import { getSkillDirectories } from "./skills.js";
11
11
  import { findSquadAgent, renderProjectAgentRoster } from "../squad/registry.js";
12
+ import { childLogger } from "../util/logger.js";
13
+ const log = childLogger("agents");
12
14
  // Frontmatter schema
13
15
  const agentFrontmatterSchema = z.object({
14
16
  name: z.string().min(1),
@@ -62,7 +64,7 @@ export function parseAgentMd(content, slug) {
62
64
  }
63
65
  const result = agentFrontmatterSchema.safeParse(parsed);
64
66
  if (!result.success) {
65
- console.warn(`[agents] Invalid frontmatter in ${slug}.agent.md:`, result.error.format());
67
+ log.warn({ slug, errors: result.error.format() }, "Invalid frontmatter in agent file");
66
68
  return null;
67
69
  }
68
70
  const fm = result.data;
@@ -102,7 +104,7 @@ export function loadAgents() {
102
104
  configs.push(config);
103
105
  }
104
106
  catch (err) {
105
- console.warn(`[agents] Failed to read ${entry}:`, err instanceof Error ? err.message : err);
107
+ log.warn({ entry, err: err instanceof Error ? err.message : err }, "Failed to read agent entry");
106
108
  }
107
109
  }
108
110
  agentRegistry = configs;
@@ -138,7 +140,7 @@ export function ensureDefaultAgents() {
138
140
  if (!existsSync(dest)) {
139
141
  copyFileSync(src, dest);
140
142
  setState(stateKey, srcHash);
141
- console.log(`[agents] Installed bundled agent: ${file}`);
143
+ log.info({ file }, "Installed bundled agent");
142
144
  continue;
143
145
  }
144
146
  // Check if the bundled version actually changed since our last sync
@@ -150,13 +152,13 @@ export function ensureDefaultAgents() {
150
152
  const destHash = createHash("sha256").update(readFileSync(dest)).digest("hex");
151
153
  if (lastSyncedHash && destHash !== lastSyncedHash) {
152
154
  // User modified the file after our last sync — don't clobber their changes
153
- console.log(`[agents] Skipping ${file} — user has local customizations`);
155
+ log.debug({ file }, "Skipping bundled agent — user has local customizations");
154
156
  continue;
155
157
  }
156
158
  // Safe to update: either first sync (no record) or file is unmodified from our last deploy
157
159
  copyFileSync(src, dest);
158
160
  setState(stateKey, srcHash);
159
- console.log(`[agents] Updated bundled agent: ${file}`);
161
+ log.info({ file }, "Updated bundled agent");
160
162
  }
161
163
  }
162
164
  /** Create a new agent .md file. Returns error string or null on success. */
@@ -326,7 +328,7 @@ export async function createEphemeralAgentSession(slug, client, allTools, modelO
326
328
  bufferExhaustionThreshold: 0.95,
327
329
  },
328
330
  });
329
- console.log(`[agents] Created ephemeral session for @${agent.slug} (${model})`);
331
+ log.info({ agentSlug: agent.slug, model }, "Created ephemeral agent session");
330
332
  return session;
331
333
  }
332
334
  /** Create an ephemeral session for a squad virtual agent (not in CH registry). */
@@ -353,7 +355,7 @@ export async function createSquadAgentSession(slug, client, allTools, systemMess
353
355
  bufferExhaustionThreshold: 0.95,
354
356
  },
355
357
  });
356
- console.log(`[agents] Created squad virtual agent session for @${slug} (${model})`);
358
+ log.info({ agentSlug: slug, model }, "Created squad virtual agent session");
357
359
  return session;
358
360
  }
359
361
  /** Clean up active task tracking (for shutdown/restart). */
@@ -1,4 +1,6 @@
1
1
  import { approveAll } from "@github/copilot-sdk";
2
+ import { childLogger } from "../util/logger.js";
3
+ const log = childLogger("classifier");
2
4
  // ---------------------------------------------------------------------------
3
5
  // Persistent GPT-4.1 classifier session
4
6
  // ---------------------------------------------------------------------------
@@ -52,7 +54,7 @@ export async function classifyWithLLM(client, message) {
52
54
  return TIER_MAP[raw] ?? "standard";
53
55
  }
54
56
  catch (err) {
55
- console.log(`[chapterhouse] Classifier error (falling back to heuristics): ${err instanceof Error ? err.message : err}`);
57
+ log.warn({ err: err instanceof Error ? err.message : err }, "Classifier error, falling back to heuristics");
56
58
  // Destroy broken session so it's recreated next time
57
59
  if (classifierSession) {
58
60
  classifierSession.destroy().catch(() => { });