chapterhouse 0.3.1 → 0.3.2

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
@@ -239,9 +239,47 @@ The deployment assets for the shared instance set Entra auth and `CHAPTERHOUSE_M
239
239
  | `chapterhouse update --check-only` | Print current/latest version without updating |
240
240
  | `chapterhouse update --ref <ver>` | Install a specific version |
241
241
  | `chapterhouse daemon <sub>` | Manage the persistent background service |
242
+ | `chapterhouse squad init` | Initialize a new Squad team for this project (guided dialog) |
242
243
  | `chapterhouse squad worktree <sub>` | Manage per-agent git worktrees for concurrent squad work |
243
244
  | `chapterhouse help` | Show available commands |
244
245
 
246
+ ### Squad Init Command
247
+
248
+ 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:
249
+
250
+ ```sh
251
+ chapterhouse squad init
252
+ # Runs the guided dialog: project name, stack, goal, team size, universe.
253
+ # Proposes a named roster, then writes .squad/ on confirmation.
254
+
255
+ chapterhouse squad init --force
256
+ # Re-scaffolds even if .squad/ already exists (overwrites existing files).
257
+
258
+ chapterhouse squad init --yes
259
+ # Skip the confirmation prompt (non-interactive / CI mode).
260
+
261
+ chapterhouse squad init --universe Firefly
262
+ # Use a specific naming universe instead of the default (Dune).
263
+ # Available: Dune (default), Firefly, Star Wars, The Matrix, Breaking Bad
264
+ ```
265
+
266
+ **What gets written:**
267
+
268
+ | Path | Purpose |
269
+ |------|---------|
270
+ | `.squad/team.md` | Roster with `## Members` table (required by GitHub workflows) |
271
+ | `.squad/routing.md` | Task routing rules by role |
272
+ | `.squad/ceremonies.md` | Sprint rhythm and review gates |
273
+ | `.squad/decisions.md` | Append-only decision log |
274
+ | `.squad/agents/{name}/charter.md` | Per-agent charter with project context |
275
+ | `.squad/agents/{name}/history.md` | Per-agent day-1 seed history |
276
+ | `.squad/casting/policy.json` | Universe allowlist and capacity |
277
+ | `.squad/casting/registry.json` | Persistent name registry (cast names survive re-runs) |
278
+ | `.squad/casting/history.json` | Universe assignment audit trail |
279
+ | `.gitattributes` | Union merge rules for append-only Squad files |
280
+
281
+ **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.
282
+
245
283
  ### Squad Worktree Commands
246
284
 
247
285
  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,6 +357,17 @@ Chapterhouse enforces a **3-layer timing contract** so in-flight LLM streams can
319
357
 
320
358
  **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
359
 
360
+ #### Session lifecycle
361
+
362
+ 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:
363
+
364
+ | Config | What it controls | Default |
365
+ |--------|-----------------|---------|
366
+ | `CHAPTERHOUSE_SESSION_IDLE_TTL_MS` | How long an idle session (no in-flight turn, empty queue) is kept before being disconnected | `1800000` (30 min) |
367
+ | `CHAPTERHOUSE_SESSION_MAX_ACTIVE` | Maximum number of simultaneously active sessions; when reached, the least-recently-used *idle* session is evicted to make room | `20` |
368
+
369
+ Busy sessions (processing a turn or with items queued) are never evicted by either mechanism.
370
+
322
371
  #### Daemon PATH
323
372
 
324
373
  The generated systemd unit and launchd plist compose a rich `PATH` that includes:
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
- console.warn(`[auth] Token for ${claims.preferred_username || claims.oid} rejected: ` +
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
- console.error("[auth] JWT verification failed:", err);
155
+ log.error({ err }, "JWT verification failed");
155
156
  unauthorized(res, "Unauthorized: invalid or expired token");
156
157
  }
157
158
  };
@@ -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 originalConsoleError = console.error;
327
- const logged = [];
328
- console.error = (...args) => {
329
- logged.push(args);
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
- try {
332
- const middleware = auth.createAuthMiddleware({
333
- apiToken: "legacy-token",
334
- config: {
335
- entraAuthEnabled: true,
336
- standaloneMode: false,
337
- entraClientId: "client-id",
338
- entraTenantId: "tenant-id",
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
@@ -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,
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) => console.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
- console.error("[team] Failed to generate report:", error);
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, printWorktreeHelp } = await import("./squad/worktree.js");
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)
@@ -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
- console.log(`[chapterhouse] Episode writer: summarized ${rows.length} turns (ids ${startId}-${endId}) pages/conversations/${new Date().toISOString().slice(0, 10)}.md`);
198
+ log.info({ rows: rows.length, startId, endId }, "summarized turns");
197
199
  }
198
200
  catch (err) {
199
- console.log(`[chapterhouse] Episode writer error (non-fatal): ${err instanceof Error ? err.message : err}`);
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;