chapterhouse 0.1.5 → 0.3.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 +112 -12
- package/dist/api/errors.js +5 -3
- package/dist/api/errors.test.js +12 -21
- package/dist/api/server.js +33 -12
- package/dist/cli.js +135 -18
- package/dist/copilot/agents.js +9 -7
- package/dist/copilot/classifier.js +3 -1
- package/dist/copilot/orchestrator.js +35 -31
- package/dist/copilot/orchestrator.test.js +1 -0
- package/dist/copilot/router.js +4 -2
- package/dist/copilot/tools.js +6 -4
- package/dist/daemon-install.js +368 -0
- package/dist/daemon-install.test.js +98 -0
- package/dist/daemon.js +35 -33
- package/dist/squad/index.js +1 -0
- package/dist/squad/worktree.js +295 -0
- package/dist/squad/worktree.test.js +189 -0
- package/dist/store/db.js +38 -0
- package/dist/store/db.test.js +88 -0
- package/dist/update.js +162 -28
- package/dist/update.test.js +84 -5
- package/dist/util/logger.js +41 -0
- package/dist/util/logger.test.js +53 -0
- package/dist/wiki/migrate.js +4 -2
- package/dist/wiki/seed-team-wiki.js +4 -2
- package/package.json +10 -2
- package/web/dist/assets/index-CxT9905O.css +10 -0
- package/web/dist/assets/{index-DAg9IrpO.js → index-DI3rnGm-.js} +59 -59
- package/web/dist/assets/index-DI3rnGm-.js.map +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-D-e7K-fT.css +0 -10
- package/web/dist/assets/index-DAg9IrpO.js.map +0 -1
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
|
|
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
|
-
|
|
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,39 @@ The deployment assets for the shared instance set Entra auth and `CHAPTERHOUSE_M
|
|
|
202
230
|
|
|
203
231
|
## CLI Commands
|
|
204
232
|
|
|
205
|
-
| Command
|
|
206
|
-
|
|
|
207
|
-
| `chapterhouse start`
|
|
208
|
-
| `chapterhouse start --open`
|
|
209
|
-
| `chapterhouse setup`
|
|
210
|
-
| `chapterhouse update`
|
|
211
|
-
| `chapterhouse
|
|
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 squad worktree <sub>` | Manage per-agent git worktrees for concurrent squad work |
|
|
243
|
+
| `chapterhouse help` | Show available commands |
|
|
244
|
+
|
|
245
|
+
### Squad Worktree Commands
|
|
246
|
+
|
|
247
|
+
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:
|
|
248
|
+
|
|
249
|
+
```sh
|
|
250
|
+
chapterhouse squad worktree create <agent> <issue> [--base main] [--slug <slug>]
|
|
251
|
+
# Creates .worktrees/{agent}-{issue}/ and branch squad/{issue}-{slug}
|
|
252
|
+
# Prints the worktree path to stdout. Reuses existing worktree if present.
|
|
253
|
+
|
|
254
|
+
chapterhouse squad worktree list
|
|
255
|
+
# Shows all active squad worktrees: agent, issue, branch, status, path
|
|
256
|
+
|
|
257
|
+
chapterhouse squad worktree remove <agent> <issue> [--force] [--delete-branch]
|
|
258
|
+
# Removes a worktree. Refuses if dirty unless --force is passed.
|
|
259
|
+
|
|
260
|
+
chapterhouse squad worktree prune [--base main] [--dry-run]
|
|
261
|
+
# Removes all worktrees whose branch has been merged into main.
|
|
262
|
+
# Skips dirty worktrees with a warning.
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**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.
|
|
212
266
|
|
|
213
267
|
### Flags
|
|
214
268
|
|
|
@@ -217,6 +271,42 @@ The deployment assets for the shared instance set Entra auth and `CHAPTERHOUSE_M
|
|
|
217
271
|
| `--self-edit` | Allow Chapterhouse to modify its own source code (use with `chapterhouse start`) |
|
|
218
272
|
| `--open` | Open the web UI in your default browser when the daemon is ready |
|
|
219
273
|
|
|
274
|
+
### Daemon Management (macOS / Linux)
|
|
275
|
+
|
|
276
|
+
Chapterhouse can run as a persistent user-level background service that starts on login and restarts automatically on crash — no root required.
|
|
277
|
+
|
|
278
|
+
#### Install
|
|
279
|
+
|
|
280
|
+
```sh
|
|
281
|
+
chapterhouse daemon install
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
This writes and loads the appropriate unit file for your OS:
|
|
285
|
+
|
|
286
|
+
| Platform | Unit file |
|
|
287
|
+
| -------- | ----------------------------------------------------------------------- |
|
|
288
|
+
| macOS | `~/Library/LaunchAgents/com.bketelsen.chapterhouse.plist` (launchd) |
|
|
289
|
+
| Linux | `~/.config/systemd/user/chapterhouse.service` (systemd `--user`) |
|
|
290
|
+
| Windows | Not supported — run `chapterhouse start` manually or use Task Scheduler |
|
|
291
|
+
|
|
292
|
+
#### Manage
|
|
293
|
+
|
|
294
|
+
```sh
|
|
295
|
+
chapterhouse daemon status # is it running? what PID? where are the logs?
|
|
296
|
+
chapterhouse daemon stop # stop without uninstalling
|
|
297
|
+
chapterhouse daemon start # start without re-installing
|
|
298
|
+
chapterhouse daemon restart # restart in place
|
|
299
|
+
chapterhouse daemon logs # tail live logs (Ctrl+C to exit)
|
|
300
|
+
chapterhouse daemon uninstall # stop, disable, and remove the unit file
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
#### Log locations
|
|
304
|
+
|
|
305
|
+
| Platform | Location |
|
|
306
|
+
| -------- | ----------------------------------------------------------- |
|
|
307
|
+
| macOS | `~/Library/Logs/chapterhouse.log` |
|
|
308
|
+
| Linux | `journalctl --user -u chapterhouse` (no extra config needed) |
|
|
309
|
+
|
|
220
310
|
## Web UI
|
|
221
311
|
|
|
222
312
|
The browser app at `http://localhost:7788` is split into a few views:
|
|
@@ -361,3 +451,13 @@ git push origin main --follow-tags
|
|
|
361
451
|
```
|
|
362
452
|
|
|
363
453
|
`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.
|
|
454
|
+
|
|
455
|
+
> **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.
|
|
456
|
+
|
|
457
|
+
### Commit message convention
|
|
458
|
+
|
|
459
|
+
All commits on this repository follow **[Conventional Commits v1.0.0](https://www.conventionalcommits.org/en/v1.0.0/)**. The format is `<type>(<scope>): <subject>` (e.g. `feat(api): add session export endpoint`). Allowed types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `build`, `ci`, `revert`, `release`.
|
|
460
|
+
|
|
461
|
+
This is automatically enforced:
|
|
462
|
+
- **Locally:** `husky` installs a `commit-msg` git hook on `npm install` that runs `commitlint` against every commit message. Bad messages are rejected before the commit lands.
|
|
463
|
+
- **On PRs:** A GitHub Action (`lint-pr-title.yml`) validates the PR title on every open/edit. This matters because squash-merges use the PR title as the commit message on `main`.
|
package/dist/api/errors.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/api/errors.test.js
CHANGED
|
@@ -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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
package/dist/api/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
600
|
+
log.info({ entriesSynced: result.entriesSynced, projectRoot }, "Synced squad decisions to wiki");
|
|
598
601
|
}
|
|
599
602
|
}).catch(err => {
|
|
600
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,31 @@ Usage:
|
|
|
21
21
|
chapterhouse <command>
|
|
22
22
|
|
|
23
23
|
Commands:
|
|
24
|
-
start
|
|
25
|
-
setup
|
|
26
|
-
update
|
|
27
|
-
|
|
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
|
+
squad <sub> Squad agent tools (worktree management)
|
|
29
|
+
help Show this help message
|
|
28
30
|
|
|
29
31
|
Flags (start):
|
|
30
32
|
--self-edit Allow Chapterhouse to modify his own source code (off by default)
|
|
31
33
|
--open Open the web UI in your default browser once the daemon is ready
|
|
32
34
|
|
|
35
|
+
Flags (update):
|
|
36
|
+
--check-only Print version info and install-source, then exit without updating
|
|
37
|
+
--ref <version> Install a specific version (e.g. --ref 0.1.5)
|
|
38
|
+
--force Bypass the Node/npm version precondition check
|
|
39
|
+
|
|
33
40
|
Examples:
|
|
34
41
|
chapterhouse start Start the daemon, then open http://localhost:7788
|
|
35
42
|
chapterhouse start --open Same, but open the browser for you
|
|
36
43
|
chapterhouse start --self-edit Start with self-edit enabled
|
|
44
|
+
chapterhouse update Update to latest (npm registry for global installs)
|
|
45
|
+
chapterhouse update --check-only Show current/latest version without updating
|
|
46
|
+
chapterhouse update --ref 0.1.5 Install a specific version
|
|
47
|
+
chapterhouse daemon install Install and enable the persistent background service
|
|
48
|
+
chapterhouse daemon status Show whether the service is running
|
|
37
49
|
`.trim());
|
|
38
50
|
}
|
|
39
51
|
const args = process.argv.slice(2);
|
|
@@ -54,30 +66,77 @@ switch (command) {
|
|
|
54
66
|
await import("./setup.js");
|
|
55
67
|
break;
|
|
56
68
|
case "update": {
|
|
57
|
-
const
|
|
69
|
+
const updateFlags = args.slice(1);
|
|
70
|
+
const checkOnly = updateFlags.includes("--check-only");
|
|
71
|
+
const force = updateFlags.includes("--force");
|
|
72
|
+
const refIdx = updateFlags.indexOf("--ref");
|
|
73
|
+
const ref = refIdx !== -1 ? (updateFlags[refIdx + 1] ?? null) : null;
|
|
74
|
+
const { checkForUpdate, performUpdate, detectInstallSource, checkPreconditions, buildLegacyGitInstallCommand, } = await import("./update.js");
|
|
75
|
+
const source = detectInstallSource();
|
|
76
|
+
// Precondition check (bypass with --force)
|
|
77
|
+
if (!force) {
|
|
78
|
+
const pre = checkPreconditions();
|
|
79
|
+
if (!pre.ok) {
|
|
80
|
+
console.warn(`⚠ ${pre.message}`);
|
|
81
|
+
console.warn(" Run with --force to proceed anyway.");
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
58
85
|
const check = await checkForUpdate();
|
|
59
86
|
if (!check.checkSucceeded) {
|
|
60
|
-
console.error("⚠ Could not reach GitHub to check for updates. Check your network and try again.");
|
|
87
|
+
console.error("⚠ Could not reach npm registry or GitHub to check for updates. Check your network and try again.");
|
|
61
88
|
process.exit(1);
|
|
62
89
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
console.log(
|
|
90
|
+
console.log(`chapterhouse current: v${check.current}`);
|
|
91
|
+
console.log(`chapterhouse latest: v${check.latest ?? "unknown"}`);
|
|
92
|
+
console.log(`Install source: ${source}`);
|
|
93
|
+
if (checkOnly) {
|
|
94
|
+
if (check.updateAvailable) {
|
|
95
|
+
console.log(`\nUpdate available: v${check.current} → v${check.latest}`);
|
|
96
|
+
console.log(`Run \`chapterhouse update\` to install.`);
|
|
69
97
|
}
|
|
70
98
|
else {
|
|
71
|
-
console.
|
|
72
|
-
process.exit(1);
|
|
99
|
+
console.log("\nYou are on the latest version.");
|
|
73
100
|
}
|
|
74
101
|
break;
|
|
75
102
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
103
|
+
if (source === "dev") {
|
|
104
|
+
console.log("ℹ Dev mode — use git pull manually.");
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
if (source === "git") {
|
|
108
|
+
console.warn("⚠ Deprecation notice: your install uses the legacy git clone path.");
|
|
109
|
+
console.warn(` Switch to the npm registry path for automatic updates:`);
|
|
110
|
+
console.warn(` npm install -g chapterhouse@latest`);
|
|
111
|
+
console.warn(" Continuing with legacy git update...\n");
|
|
112
|
+
}
|
|
113
|
+
const targetRef = ref ?? (check.latest ? check.latest : null);
|
|
114
|
+
if (!check.updateAvailable && !ref) {
|
|
115
|
+
console.log(`chapterhouse v${check.current} is already the latest version.`);
|
|
116
|
+
if (source !== "git")
|
|
117
|
+
break;
|
|
118
|
+
// For git installs, still offer to pull latest main
|
|
119
|
+
console.log("Pulling latest from main...");
|
|
120
|
+
}
|
|
121
|
+
else if (check.updateAvailable) {
|
|
122
|
+
console.log(`\nUpdate available: v${check.current} → v${check.latest}`);
|
|
123
|
+
console.log("Installing...");
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
console.log(`Installing specified ref: ${targetRef}`);
|
|
127
|
+
}
|
|
128
|
+
const result = await performUpdate(targetRef);
|
|
79
129
|
if (result.ok) {
|
|
80
|
-
|
|
130
|
+
if (result.source === "dev") {
|
|
131
|
+
console.log("ℹ Dev mode — use git pull manually.");
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const label = ref ? `${ref}` : `v${check.latest ?? "latest"}`;
|
|
135
|
+
console.log(`✅ Updated to ${label}`);
|
|
136
|
+
if (result.source === "registry" || result.source === "unknown") {
|
|
137
|
+
console.log(" To verify provenance: npm audit signatures (in any project that has chapterhouse as a dep)");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
81
140
|
}
|
|
82
141
|
else {
|
|
83
142
|
console.error(`❌ Update failed: ${result.output}`);
|
|
@@ -85,6 +144,64 @@ switch (command) {
|
|
|
85
144
|
}
|
|
86
145
|
break;
|
|
87
146
|
}
|
|
147
|
+
case "daemon": {
|
|
148
|
+
const subcommand = args[1];
|
|
149
|
+
const { install, uninstall, start: daemonStart, stop: daemonStop, restart: daemonRestart, status: daemonStatus, logs: daemonLogs, printDaemonHelp, } = await import("./daemon-install.js");
|
|
150
|
+
switch (subcommand) {
|
|
151
|
+
case "install":
|
|
152
|
+
await install();
|
|
153
|
+
break;
|
|
154
|
+
case "uninstall":
|
|
155
|
+
await uninstall();
|
|
156
|
+
break;
|
|
157
|
+
case "start":
|
|
158
|
+
await daemonStart();
|
|
159
|
+
break;
|
|
160
|
+
case "stop":
|
|
161
|
+
await daemonStop();
|
|
162
|
+
break;
|
|
163
|
+
case "restart":
|
|
164
|
+
await daemonRestart();
|
|
165
|
+
break;
|
|
166
|
+
case "status":
|
|
167
|
+
await daemonStatus();
|
|
168
|
+
break;
|
|
169
|
+
case "logs":
|
|
170
|
+
await daemonLogs();
|
|
171
|
+
break;
|
|
172
|
+
default:
|
|
173
|
+
if (subcommand) {
|
|
174
|
+
console.error(`Unknown daemon subcommand: ${subcommand}\n`);
|
|
175
|
+
}
|
|
176
|
+
printDaemonHelp();
|
|
177
|
+
if (subcommand)
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case "squad": {
|
|
183
|
+
const squadSub = args[1];
|
|
184
|
+
if (squadSub === 'worktree') {
|
|
185
|
+
const { runWorktreeCli, printWorktreeHelp } = await import("./squad/worktree.js");
|
|
186
|
+
await runWorktreeCli(args.slice(2));
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
if (squadSub) {
|
|
190
|
+
console.error(`Unknown squad subcommand: ${squadSub}\n`);
|
|
191
|
+
}
|
|
192
|
+
console.log(`
|
|
193
|
+
chapterhouse squad — Squad agent tools
|
|
194
|
+
|
|
195
|
+
Subcommands:
|
|
196
|
+
worktree Manage per-agent git worktrees (create / list / remove / prune)
|
|
197
|
+
|
|
198
|
+
Run \`chapterhouse squad worktree\` for worktree subcommand help.
|
|
199
|
+
`.trim());
|
|
200
|
+
if (squadSub)
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
88
205
|
case "help":
|
|
89
206
|
case "--help":
|
|
90
207
|
case "-h":
|
package/dist/copilot/agents.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(() => { });
|