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 +49 -0
- package/dist/api/auth.js +4 -3
- package/dist/api/auth.test.js +27 -39
- package/dist/api/server.js +3 -0
- package/dist/api/team.js +4 -2
- package/dist/cli.js +8 -2
- package/dist/copilot/episode-writer.js +4 -2
- package/dist/copilot/orchestrator.js +300 -355
- package/dist/copilot/orchestrator.test.js +39 -0
- package/dist/copilot/session-manager.js +307 -0
- package/dist/copilot/session-manager.test.js +292 -0
- package/dist/copilot/system-message.js +2 -1
- package/dist/copilot/system-message.test.js +8 -0
- package/dist/daemon.js +2 -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/version.js +7 -0
- package/dist/wiki/team-sync.js +3 -1
- package/package.json +2 -3
- package/web/dist/assets/index-Dpt-MCe8.css +10 -0
- package/web/dist/assets/index-NmxVWGY1.js +205 -0
- package/web/dist/assets/index-NmxVWGY1.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
|
@@ -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
|
-
|
|
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
|
@@ -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) =>
|
|
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)
|
|
@@ -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;
|