chapterhouse 0.3.1 → 0.3.3
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 +73 -1
- package/dist/api/auth.js +4 -3
- package/dist/api/auth.test.js +27 -39
- package/dist/api/server.js +43 -2
- package/dist/api/team.js +4 -2
- package/dist/cli.js +8 -2
- package/dist/config.js +3 -0
- package/dist/copilot/episode-writer.js +4 -2
- package/dist/copilot/orchestrator.js +410 -356
- package/dist/copilot/orchestrator.test.js +244 -0
- package/dist/copilot/session-manager.js +337 -0
- package/dist/copilot/session-manager.test.js +358 -0
- package/dist/copilot/system-message.js +2 -1
- package/dist/copilot/system-message.test.js +8 -0
- package/dist/copilot/workiq-installer.js +91 -0
- package/dist/copilot/workiq-installer.test.js +148 -0
- package/dist/daemon.js +12 -1
- package/dist/integrations/teams-notify.js +3 -1
- package/dist/squad/index.js +1 -0
- package/dist/squad/init-cli.js +109 -0
- package/dist/squad/init.js +395 -0
- package/dist/squad/init.test.js +351 -0
- package/dist/squad/mirror.js +4 -2
- package/dist/squad/mirror.scheduler.js +9 -7
- package/dist/store/db.js +58 -5
- package/dist/store/db.test.js +69 -0
- package/dist/version.js +7 -0
- package/dist/wiki/team-sync.js +3 -1
- package/package.json +4 -3
- package/web/dist/assets/index-BkB7gY18.css +10 -0
- package/web/dist/assets/index-DSqc46G_.js +208 -0
- package/web/dist/assets/index-DSqc46G_.js.map +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-CxT9905O.css +0 -10
- package/web/dist/assets/index-DI3rnGm-.js +0 -142
- package/web/dist/assets/index-DI3rnGm-.js.map +0 -1
package/README.md
CHANGED
|
@@ -182,6 +182,21 @@ Optional Entra settings:
|
|
|
182
182
|
- `ENTRA_REQUIRED_ROLE` — if set, the signed-in user must have this app role in the token's `roles` claim. This replaced the older group-based check.
|
|
183
183
|
- `ENTRA_TEAM_LEAD_ID` — optional for regular engineers, who can omit it entirely. Set it only for the one person who should be treated as `team-lead` for managerial functions such as `/api/team/report` and protected OKR/KPI/team wiki writes. Without it, the signed-in user is treated as `engineer`, which is the correct role for normal team members.
|
|
184
184
|
|
|
185
|
+
### WorkIQ MCP server (Entra only)
|
|
186
|
+
|
|
187
|
+
When `ENTRA_AUTH_ENABLED=true`, Chapterhouse automatically adds a `workiq` entry to `~/.copilot/mcp-config.json` at daemon startup. This gives the orchestrator access to Microsoft 365 tools (Teams, Outlook, Calendar, etc.) via the `@microsoft/workiq` MCP server without any manual configuration.
|
|
188
|
+
|
|
189
|
+
The entry uses `npx -y @microsoft/workiq` so no global npm install is required — npx fetches the server on first use.
|
|
190
|
+
|
|
191
|
+
| Behaviour | Detail |
|
|
192
|
+
|-----------|--------|
|
|
193
|
+
| **Trigger** | `ENTRA_AUTH_ENABLED=true` + `ENTRA_TENANT_ID` set |
|
|
194
|
+
| **Idempotent** | Safe to restart; entry is only written if `workiq` key is absent |
|
|
195
|
+
| **Opt-out** | Set `CHAPTERHOUSE_WORKIQ_AUTO_INSTALL=false` to disable |
|
|
196
|
+
| **Failure-safe** | If the write fails (permissions, read-only FS), a structured warning is logged and the daemon continues |
|
|
197
|
+
|
|
198
|
+
**`CHAPTERHOUSE_WORKIQ_AUTO_INSTALL`** — `true` (default) or `false`. Set to `false` to manage the workiq MCP entry manually.
|
|
199
|
+
|
|
185
200
|
## Docker (Personal)
|
|
186
201
|
|
|
187
202
|
For a single-user local deployment, use the personal compose file. It binds port `7788`, runs the daemon as the non-root `node` user, and persists state in `CHAPTERHOUSE_HOME` (default: `$HOME/.chapterhouse` on macOS/Linux).
|
|
@@ -239,9 +254,47 @@ The deployment assets for the shared instance set Entra auth and `CHAPTERHOUSE_M
|
|
|
239
254
|
| `chapterhouse update --check-only` | Print current/latest version without updating |
|
|
240
255
|
| `chapterhouse update --ref <ver>` | Install a specific version |
|
|
241
256
|
| `chapterhouse daemon <sub>` | Manage the persistent background service |
|
|
257
|
+
| `chapterhouse squad init` | Initialize a new Squad team for this project (guided dialog) |
|
|
242
258
|
| `chapterhouse squad worktree <sub>` | Manage per-agent git worktrees for concurrent squad work |
|
|
243
259
|
| `chapterhouse help` | Show available commands |
|
|
244
260
|
|
|
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
|
+
|
|
245
298
|
### Squad Worktree Commands
|
|
246
299
|
|
|
247
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:
|
|
@@ -319,9 +372,21 @@ Chapterhouse enforces a **3-layer timing contract** so in-flight LLM streams can
|
|
|
319
372
|
|
|
320
373
|
**Rule:** each layer must exceed the one above it. Do not tighten `CHAPTERHOUSE_SHUTDOWN_TIMEOUT_MS` below `CHAPTERHOUSE_ORCHESTRATOR_TIMEOUT_MS`, and do not reduce `TimeoutStopSec` below `CHAPTERHOUSE_SHUTDOWN_TIMEOUT_MS`.
|
|
321
374
|
|
|
375
|
+
#### Session lifecycle
|
|
376
|
+
|
|
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:
|
|
378
|
+
|
|
379
|
+
| Config | What it controls | Default |
|
|
380
|
+
|--------|-----------------|---------|
|
|
381
|
+
| `CHAPTERHOUSE_SESSION_IDLE_TTL_MS` | How long an idle session (no in-flight turn, empty queue) is kept before being disconnected | `1800000` (30 min) |
|
|
382
|
+
| `CHAPTERHOUSE_SESSION_MAX_ACTIVE` | Maximum number of simultaneously active sessions; when reached, the least-recently-used *idle* session is evicted to make room | `20` |
|
|
383
|
+
|
|
384
|
+
Busy sessions (processing a turn or with items queued) are never evicted by either mechanism.
|
|
385
|
+
|
|
322
386
|
#### Daemon PATH
|
|
323
387
|
|
|
324
388
|
The generated systemd unit and launchd plist compose a rich `PATH` that includes:
|
|
389
|
+
|
|
325
390
|
- The installing shell's `$PATH` (captured at install time)
|
|
326
391
|
- The binary's own directory
|
|
327
392
|
- Linuxbrew (`/home/linuxbrew/.linuxbrew/bin`), Homebrew (`/opt/homebrew/bin`, `/usr/local/bin`)
|
|
@@ -343,7 +408,7 @@ The browser app at `http://localhost:7788` is split into a few views:
|
|
|
343
408
|
|
|
344
409
|
## How it Works
|
|
345
410
|
|
|
346
|
-
```
|
|
411
|
+
```text
|
|
347
412
|
Browser ──HTTP / SSE──► Chapterhouse Daemon
|
|
348
413
|
│
|
|
349
414
|
Orchestrator Session (Copilot SDK)
|
|
@@ -457,6 +522,12 @@ npm run dev:web
|
|
|
457
522
|
|
|
458
523
|
# Build everything
|
|
459
524
|
npm run build
|
|
525
|
+
|
|
526
|
+
# Run tests
|
|
527
|
+
npm test
|
|
528
|
+
|
|
529
|
+
# Lint user-facing markdown (README, CHANGELOG, docs/, .github/)
|
|
530
|
+
npm run lint:md
|
|
460
531
|
```
|
|
461
532
|
|
|
462
533
|
The web UI lives in `web/`. Production builds emit to `web/dist/`, which the Express server serves out of in `src/api/server.ts`.
|
|
@@ -482,5 +553,6 @@ git push origin main --follow-tags
|
|
|
482
553
|
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`.
|
|
483
554
|
|
|
484
555
|
This is automatically enforced:
|
|
556
|
+
|
|
485
557
|
- **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.
|
|
486
558
|
- **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/auth.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import jwt from "jsonwebtoken";
|
|
2
2
|
import jwksClient from "jwks-rsa";
|
|
3
|
+
import { childLogger } from "../util/logger.js";
|
|
4
|
+
const log = childLogger("auth");
|
|
3
5
|
function getIssuer(tenantId) {
|
|
4
6
|
return `https://login.microsoftonline.com/${tenantId}/v2.0`;
|
|
5
7
|
}
|
|
@@ -137,8 +139,7 @@ export function createAuthMiddleware(options) {
|
|
|
137
139
|
try {
|
|
138
140
|
const claims = await verifyBearerToken(token, options.config);
|
|
139
141
|
if (options.config.entraRequiredRole && !hasRequiredRole(claims, options.config.entraRequiredRole)) {
|
|
140
|
-
|
|
141
|
-
`required role "${options.config.entraRequiredRole}" not found in roles=${JSON.stringify(claims.roles ?? [])}`);
|
|
142
|
+
log.warn({ username: claims.preferred_username || claims.oid, requiredRole: options.config.entraRequiredRole, roles: claims.roles ?? [] }, "token rejected: required app role not found");
|
|
142
143
|
unauthorized(res, "Unauthorized: user does not have the required app role");
|
|
143
144
|
return;
|
|
144
145
|
}
|
|
@@ -151,7 +152,7 @@ export function createAuthMiddleware(options) {
|
|
|
151
152
|
next();
|
|
152
153
|
}
|
|
153
154
|
catch (err) {
|
|
154
|
-
|
|
155
|
+
log.error({ err }, "JWT verification failed");
|
|
155
156
|
unauthorized(res, "Unauthorized: invalid or expired token");
|
|
156
157
|
}
|
|
157
158
|
};
|
package/dist/api/auth.test.js
CHANGED
|
@@ -323,46 +323,34 @@ test("returns a generic message when JWT verification fails", async () => {
|
|
|
323
323
|
const auth = await loadAuthModule();
|
|
324
324
|
assert.ok(auth, "auth module should exist");
|
|
325
325
|
assert.equal(typeof auth.createAuthMiddleware, "function", "createAuthMiddleware should be exported");
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
326
|
+
const middleware = auth.createAuthMiddleware({
|
|
327
|
+
apiToken: "legacy-token",
|
|
328
|
+
config: {
|
|
329
|
+
entraAuthEnabled: true,
|
|
330
|
+
standaloneMode: false,
|
|
331
|
+
entraClientId: "client-id",
|
|
332
|
+
entraTenantId: "tenant-id",
|
|
333
|
+
entraRequiredRole: "",
|
|
334
|
+
entraTeamLeadId: "",
|
|
335
|
+
},
|
|
336
|
+
verifyBearerToken: async () => {
|
|
337
|
+
throw new Error("jwt malformed");
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
const req = {
|
|
341
|
+
path: "/api/message",
|
|
342
|
+
headers: { authorization: "Bearer bad-token" },
|
|
343
|
+
query: {},
|
|
344
|
+
socket: { remoteAddress: "127.0.0.1" },
|
|
330
345
|
};
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
entraRequiredRole: "",
|
|
340
|
-
entraTeamLeadId: "",
|
|
341
|
-
},
|
|
342
|
-
verifyBearerToken: async () => {
|
|
343
|
-
throw new Error("jwt malformed");
|
|
344
|
-
},
|
|
345
|
-
});
|
|
346
|
-
const req = {
|
|
347
|
-
path: "/api/message",
|
|
348
|
-
headers: { authorization: "Bearer bad-token" },
|
|
349
|
-
query: {},
|
|
350
|
-
socket: { remoteAddress: "127.0.0.1" },
|
|
351
|
-
};
|
|
352
|
-
const res = createMockResponse();
|
|
353
|
-
let nextCalled = false;
|
|
354
|
-
await middleware(req, res, () => {
|
|
355
|
-
nextCalled = true;
|
|
356
|
-
});
|
|
357
|
-
assert.equal(nextCalled, false);
|
|
358
|
-
assert.equal(res.statusCode, 401);
|
|
359
|
-
assert.deepEqual(res.body, { error: "Unauthorized: invalid or expired token" });
|
|
360
|
-
assert.equal(logged.length, 1);
|
|
361
|
-
assert.equal(logged[0]?.[0], "[auth] JWT verification failed:");
|
|
362
|
-
}
|
|
363
|
-
finally {
|
|
364
|
-
console.error = originalConsoleError;
|
|
365
|
-
}
|
|
346
|
+
const res = createMockResponse();
|
|
347
|
+
let nextCalled = false;
|
|
348
|
+
await middleware(req, res, () => {
|
|
349
|
+
nextCalled = true;
|
|
350
|
+
});
|
|
351
|
+
assert.equal(nextCalled, false);
|
|
352
|
+
assert.equal(res.statusCode, 401);
|
|
353
|
+
assert.deepEqual(res.body, { error: "Unauthorized: invalid or expired token" });
|
|
366
354
|
});
|
|
367
355
|
// ---------------------------------------------------------------------------
|
|
368
356
|
// Auth edge cases: JWKS cache + Entra error paths
|
package/dist/api/server.js
CHANGED
|
@@ -5,7 +5,7 @@ import { existsSync, statSync, readdirSync } from "fs";
|
|
|
5
5
|
import { join, dirname } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { z } from "zod";
|
|
8
|
-
import { sendToOrchestrator, getAgentInfo, cancelCurrentMessage, getLastRouteResult, getCurrentSessionKey } from "../copilot/orchestrator.js";
|
|
8
|
+
import { sendToOrchestrator, getAgentInfo, cancelCurrentMessage, getLastRouteResult, getCurrentSessionKey, subscribeTaskEvents } from "../copilot/orchestrator.js";
|
|
9
9
|
import { getAgentRegistry } from "../copilot/agents.js";
|
|
10
10
|
import { config, persistModel } from "../config.js";
|
|
11
11
|
import { getRouterConfig, updateRouterConfig } from "../copilot/router.js";
|
|
@@ -19,7 +19,7 @@ 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, getSessionMessages } from "../store/db.js";
|
|
22
|
+
import { getDb, getSessionMessages, getTaskEvents } 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";
|
|
@@ -266,9 +266,12 @@ app.get("/api/workers/:taskId", (req, res) => {
|
|
|
266
266
|
if (!row) {
|
|
267
267
|
throw new NotFoundError("Task not found");
|
|
268
268
|
}
|
|
269
|
+
const registry = getAgentRegistry();
|
|
270
|
+
const agent = registry.find((a) => a.slug === row.agent_slug);
|
|
269
271
|
res.json({
|
|
270
272
|
taskId: row.task_id,
|
|
271
273
|
agentSlug: row.agent_slug,
|
|
274
|
+
name: agent?.name || row.agent_slug,
|
|
272
275
|
description: row.description,
|
|
273
276
|
status: row.status,
|
|
274
277
|
result: row.result,
|
|
@@ -276,6 +279,44 @@ app.get("/api/workers/:taskId", (req, res) => {
|
|
|
276
279
|
completedAt: row.completed_at,
|
|
277
280
|
});
|
|
278
281
|
});
|
|
282
|
+
// Historical event log for a task (catch-up on page load)
|
|
283
|
+
app.get("/api/workers/:taskId/events", (req, res) => {
|
|
284
|
+
const taskId = req.params.taskId;
|
|
285
|
+
const afterSeqRaw = req.query.afterSeq;
|
|
286
|
+
const afterSeq = typeof afterSeqRaw === "string" && !isNaN(Number(afterSeqRaw)) ? Number(afterSeqRaw) : 0;
|
|
287
|
+
const taskRow = getDb()
|
|
288
|
+
.prepare(`SELECT task_id FROM agent_tasks WHERE task_id = ?`)
|
|
289
|
+
.get(taskId);
|
|
290
|
+
if (!taskRow) {
|
|
291
|
+
throw new NotFoundError("Task not found");
|
|
292
|
+
}
|
|
293
|
+
const events = getTaskEvents(taskId, afterSeq);
|
|
294
|
+
res.json({ taskId, events });
|
|
295
|
+
});
|
|
296
|
+
// SSE stream for per-task live tool-call activity
|
|
297
|
+
app.get("/api/workers/:taskId/events/stream", (req, res) => {
|
|
298
|
+
const taskId = req.params.taskId;
|
|
299
|
+
const taskRow = getDb()
|
|
300
|
+
.prepare(`SELECT task_id FROM agent_tasks WHERE task_id = ?`)
|
|
301
|
+
.get(taskId);
|
|
302
|
+
if (!taskRow) {
|
|
303
|
+
throw new NotFoundError("Task not found");
|
|
304
|
+
}
|
|
305
|
+
res.writeHead(200, {
|
|
306
|
+
"Content-Type": "text/event-stream",
|
|
307
|
+
"Cache-Control": "no-cache",
|
|
308
|
+
Connection: "keep-alive",
|
|
309
|
+
});
|
|
310
|
+
res.write(formatSseData({ type: "connected", taskId }));
|
|
311
|
+
const heartbeat = setInterval(() => { res.write(`:ping\n\n`); }, 20_000);
|
|
312
|
+
const unsub = subscribeTaskEvents(taskId, (event) => {
|
|
313
|
+
res.write(formatSseData({ type: "task_event", taskId, ...event }));
|
|
314
|
+
});
|
|
315
|
+
req.on("close", () => {
|
|
316
|
+
clearInterval(heartbeat);
|
|
317
|
+
unsub();
|
|
318
|
+
});
|
|
319
|
+
});
|
|
279
320
|
// ---------------------------------------------------------------------------
|
|
280
321
|
// SSE stream for real-time chat
|
|
281
322
|
// ---------------------------------------------------------------------------
|
package/dist/api/team.js
CHANGED
|
@@ -9,6 +9,7 @@ import { BadRequestError, ForbiddenError, InternalServerError, asBadRequest, par
|
|
|
9
9
|
import { assertPagePath, ensureWikiStructure, getWikiDir, listPages, readPage, writePage, } from "../wiki/fs.js";
|
|
10
10
|
import { withWikiWrite } from "../wiki/lock.js";
|
|
11
11
|
import { TeamsNotifier, TEAMS_MILESTONE_THRESHOLDS } from "../integrations/teams-notify.js";
|
|
12
|
+
import { childLogger } from "../util/logger.js";
|
|
12
13
|
const wikiPageRoute = /^\/wiki\/(.+)$/;
|
|
13
14
|
const updateSchema = z.object({
|
|
14
15
|
engineerId: z.string().min(1),
|
|
@@ -111,10 +112,11 @@ function getCrossedMilestone(kr, delta) {
|
|
|
111
112
|
}
|
|
112
113
|
export function createTeamRouter(options) {
|
|
113
114
|
const router = express.Router();
|
|
115
|
+
const log = childLogger("team");
|
|
114
116
|
const now = options.now ?? (() => new Date());
|
|
115
117
|
const teamsNotifier = options.teamsNotifier ?? new TeamsNotifier();
|
|
116
118
|
const adoClient = options.adoClient ?? (config.adoPat ? new AdoClient() : undefined);
|
|
117
|
-
const warn = options.warn ?? ((message) =>
|
|
119
|
+
const warn = options.warn ?? ((message) => log.warn(message));
|
|
118
120
|
router.use(options.authMiddleware);
|
|
119
121
|
router.post("/update", async (req, res) => {
|
|
120
122
|
const input = parseRequest(updateSchema, req.body);
|
|
@@ -187,7 +189,7 @@ export function createTeamRouter(options) {
|
|
|
187
189
|
res.json({ ok: true, report });
|
|
188
190
|
}
|
|
189
191
|
catch (error) {
|
|
190
|
-
|
|
192
|
+
log.error({ err: error instanceof Error ? error.message : error }, "failed to generate report");
|
|
191
193
|
throw new InternalServerError();
|
|
192
194
|
}
|
|
193
195
|
});
|
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ 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 (worktree management)
|
|
28
|
+
squad <sub> Squad agent tools (init, worktree management)
|
|
29
29
|
help Show this help message
|
|
30
30
|
|
|
31
31
|
Flags (start):
|
|
@@ -182,9 +182,13 @@ switch (command) {
|
|
|
182
182
|
case "squad": {
|
|
183
183
|
const squadSub = args[1];
|
|
184
184
|
if (squadSub === 'worktree') {
|
|
185
|
-
const { runWorktreeCli
|
|
185
|
+
const { runWorktreeCli } = await import("./squad/worktree.js");
|
|
186
186
|
await runWorktreeCli(args.slice(2));
|
|
187
187
|
}
|
|
188
|
+
else if (squadSub === 'init') {
|
|
189
|
+
const { runInitCli } = await import("./squad/init-cli.js");
|
|
190
|
+
await runInitCli(args.slice(2));
|
|
191
|
+
}
|
|
188
192
|
else {
|
|
189
193
|
if (squadSub) {
|
|
190
194
|
console.error(`Unknown squad subcommand: ${squadSub}\n`);
|
|
@@ -193,8 +197,10 @@ switch (command) {
|
|
|
193
197
|
chapterhouse squad — Squad agent tools
|
|
194
198
|
|
|
195
199
|
Subcommands:
|
|
200
|
+
init Initialize a new Squad team for this project (guided dialog)
|
|
196
201
|
worktree Manage per-agent git worktrees (create / list / remove / prune)
|
|
197
202
|
|
|
203
|
+
Run \`chapterhouse squad init\` to scaffold a .squad/ directory with a named team.
|
|
198
204
|
Run \`chapterhouse squad worktree\` for worktree subcommand help.
|
|
199
205
|
`.trim());
|
|
200
206
|
if (squadSub)
|
package/dist/config.js
CHANGED
|
@@ -46,6 +46,7 @@ const configSchema = z.object({
|
|
|
46
46
|
API_RATE_LIMIT_AUTH_MAX: z.string().optional(),
|
|
47
47
|
API_RATE_LIMIT_SSE_MAX_CONNECTIONS: z.string().optional(),
|
|
48
48
|
ENABLE_SQUAD: z.string().optional(),
|
|
49
|
+
CHAPTERHOUSE_WORKIQ_AUTO_INSTALL: z.string().optional(),
|
|
49
50
|
});
|
|
50
51
|
export const DEFAULT_MODEL = "claude-sonnet-4.6";
|
|
51
52
|
export const DEFAULT_TEAM_WIKI_CACHE_TTL_MINUTES = 60;
|
|
@@ -219,6 +220,7 @@ export function parseRuntimeConfig(env, options = {}) {
|
|
|
219
220
|
apiRateLimitAuthMax,
|
|
220
221
|
apiRateLimitSseMaxConnections,
|
|
221
222
|
squadEnabled: raw.ENABLE_SQUAD === "1",
|
|
223
|
+
workiqAutoInstall: parseBooleanEnv("CHAPTERHOUSE_WORKIQ_AUTO_INSTALL", raw.CHAPTERHOUSE_WORKIQ_AUTO_INSTALL, true),
|
|
222
224
|
};
|
|
223
225
|
}
|
|
224
226
|
const runtimeConfig = parseRuntimeConfig(process.env);
|
|
@@ -258,6 +260,7 @@ export const config = {
|
|
|
258
260
|
apiRateLimitAuthMax: runtimeConfig.apiRateLimitAuthMax,
|
|
259
261
|
apiRateLimitSseMaxConnections: runtimeConfig.apiRateLimitSseMaxConnections,
|
|
260
262
|
squadEnabled: runtimeConfig.squadEnabled,
|
|
263
|
+
workiqAutoInstall: runtimeConfig.workiqAutoInstall,
|
|
261
264
|
copilotAuthToken: runtimeConfig.copilotAuthToken,
|
|
262
265
|
get copilotModel() {
|
|
263
266
|
return _copilotModel;
|
|
@@ -12,6 +12,8 @@ import { addToIndex } from "../wiki/index-manager.js";
|
|
|
12
12
|
import { appendLog } from "../wiki/log-manager.js";
|
|
13
13
|
import { withWikiWrite } from "../wiki/lock.js";
|
|
14
14
|
import { setBusy, setIdle } from "../status.js";
|
|
15
|
+
import { childLogger } from "../util/logger.js";
|
|
16
|
+
const log = childLogger("episode-writer");
|
|
15
17
|
const EPISODE_MODEL = "gpt-4.1";
|
|
16
18
|
const EPISODE_TIMEOUT_MS = 30_000;
|
|
17
19
|
const MIN_TURNS_FOR_SUMMARY = 10;
|
|
@@ -193,10 +195,10 @@ async function runEpisode(client) {
|
|
|
193
195
|
setState(LAST_SUMMARY_TIME_KEY, String(now));
|
|
194
196
|
setState(IN_PROGRESS_KEY, "");
|
|
195
197
|
});
|
|
196
|
-
|
|
198
|
+
log.info({ rows: rows.length, startId, endId }, "summarized turns");
|
|
197
199
|
}
|
|
198
200
|
catch (err) {
|
|
199
|
-
|
|
201
|
+
log.warn({ err: err instanceof Error ? err.message : err }, "episode writer error (non-fatal)");
|
|
200
202
|
if (episodeSession) {
|
|
201
203
|
episodeSession.destroy().catch(() => { });
|
|
202
204
|
episodeSession = undefined;
|