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
|
@@ -5,7 +5,7 @@ import { config, DEFAULT_MODEL } from "../config.js";
|
|
|
5
5
|
import { loadMcpConfig } from "./mcp-config.js";
|
|
6
6
|
import { getSkillDirectories } from "./skills.js";
|
|
7
7
|
import { resetClient } from "./client.js";
|
|
8
|
-
import { logConversation, getState, setState, deleteState, getCopilotSession, upsertCopilotSession, getTaskSessionKey, getDb } from "../store/db.js";
|
|
8
|
+
import { logConversation, getState, setState, deleteState, getCopilotSession, upsertCopilotSession, getTaskSessionKey, getDb, bumpProjectLastUsed } from "../store/db.js";
|
|
9
9
|
import { maybeWriteEpisode } from "./episode-writer.js";
|
|
10
10
|
import { getWikiSummary } from "../wiki/context.js";
|
|
11
11
|
import { SESSIONS_DIR } from "../paths.js";
|
|
@@ -13,6 +13,8 @@ import { resolveModel } from "./router.js";
|
|
|
13
13
|
import { loadAgents, ensureDefaultAgents, clearActiveTasks, getAgentRegistry, setActiveAgent, parseAtMention, buildAgentRoster, getActiveTasks, } from "./agents.js";
|
|
14
14
|
import { normalizeProjectPath, setChannelProject } from "../squad/context.js";
|
|
15
15
|
import { getSquadCoordinatorSystemMessage } from "../squad/charter.js";
|
|
16
|
+
import { childLogger } from "../util/logger.js";
|
|
17
|
+
const log = childLogger("orchestrator");
|
|
16
18
|
/**
|
|
17
19
|
* Permission handler for the orchestrator session.
|
|
18
20
|
* Approves all tool requests so @chapterhouse has full access to all tools.
|
|
@@ -160,9 +162,9 @@ async function ensureClient() {
|
|
|
160
162
|
return copilotClient;
|
|
161
163
|
}
|
|
162
164
|
if (!resetPromise) {
|
|
163
|
-
|
|
165
|
+
log.info({ state: copilotClient?.getState() ?? "null" }, "Client not connected, resetting");
|
|
164
166
|
resetPromise = resetClient().then((c) => {
|
|
165
|
-
|
|
167
|
+
log.info({ state: c.getState() }, "Client reset successful");
|
|
166
168
|
copilotClient = c;
|
|
167
169
|
return c;
|
|
168
170
|
}).finally(() => { resetPromise = undefined; });
|
|
@@ -179,7 +181,7 @@ function startHealthCheck() {
|
|
|
179
181
|
try {
|
|
180
182
|
const state = copilotClient.getState();
|
|
181
183
|
if (state !== "connected") {
|
|
182
|
-
|
|
184
|
+
log.info({ state }, "Health check: client not connected, resetting");
|
|
183
185
|
await ensureClient();
|
|
184
186
|
// Session may need recovery after client reset
|
|
185
187
|
sessionMap.clear();
|
|
@@ -187,7 +189,7 @@ function startHealthCheck() {
|
|
|
187
189
|
}
|
|
188
190
|
}
|
|
189
191
|
catch (err) {
|
|
190
|
-
|
|
192
|
+
log.error({ err: err instanceof Error ? err.message : err }, "Health check error");
|
|
191
193
|
}
|
|
192
194
|
}, HEALTH_CHECK_INTERVAL_MS);
|
|
193
195
|
}
|
|
@@ -237,7 +239,7 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
237
239
|
const savedSessionId = stored?.copilotSessionId ?? (sessionKey === "default" ? getState(ORCHESTRATOR_SESSION_KEY) : undefined);
|
|
238
240
|
if (savedSessionId) {
|
|
239
241
|
try {
|
|
240
|
-
|
|
242
|
+
log.info({ sessionKey, sessionId: savedSessionId.slice(0, 8) }, "Resuming session");
|
|
241
243
|
const session = await client.resumeSession(savedSessionId, {
|
|
242
244
|
model: config.copilotModel,
|
|
243
245
|
configDir: SESSIONS_DIR,
|
|
@@ -249,19 +251,19 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
249
251
|
onPermissionRequest: orchestratorPermissionHandler,
|
|
250
252
|
infiniteSessions,
|
|
251
253
|
});
|
|
252
|
-
|
|
254
|
+
log.info({ sessionKey }, "Session resumed successfully");
|
|
253
255
|
upsertCopilotSession(sessionKey, isProjectSession ? "project" : "default", session.sessionId, projectRoot, config.copilotModel);
|
|
254
256
|
sessionModelMap.set(sessionKey, config.copilotModel);
|
|
255
257
|
return session;
|
|
256
258
|
}
|
|
257
259
|
catch (err) {
|
|
258
|
-
|
|
260
|
+
log.warn({ sessionKey, err: err instanceof Error ? err.message : err }, "Could not resume session, creating new");
|
|
259
261
|
if (sessionKey === "default")
|
|
260
262
|
deleteState(ORCHESTRATOR_SESSION_KEY);
|
|
261
263
|
}
|
|
262
264
|
}
|
|
263
265
|
// Create a fresh session
|
|
264
|
-
|
|
266
|
+
log.info({ sessionKey }, "Creating new session");
|
|
265
267
|
const session = await client.createSession({
|
|
266
268
|
model: config.copilotModel,
|
|
267
269
|
configDir: SESSIONS_DIR,
|
|
@@ -273,7 +275,7 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
273
275
|
onPermissionRequest: orchestratorPermissionHandler,
|
|
274
276
|
infiniteSessions,
|
|
275
277
|
});
|
|
276
|
-
|
|
278
|
+
log.info({ sessionKey, sessionId: session.sessionId.slice(0, 8) }, "Session created");
|
|
277
279
|
upsertCopilotSession(sessionKey, isProjectSession ? "project" : "default", session.sessionId, projectRoot, config.copilotModel);
|
|
278
280
|
// Backward compat: also persist the default session to the legacy state key
|
|
279
281
|
if (sessionKey === "default")
|
|
@@ -287,30 +289,30 @@ export async function initOrchestrator(client) {
|
|
|
287
289
|
// Initialize agent system
|
|
288
290
|
ensureDefaultAgents();
|
|
289
291
|
const agents = loadAgents();
|
|
290
|
-
|
|
292
|
+
log.info({ count: agents.length, agents: agents.map((a) => `@${a.slug}`) }, "Agents loaded");
|
|
291
293
|
// Validate configured model against available models
|
|
292
294
|
try {
|
|
293
295
|
const models = await client.listModels();
|
|
294
296
|
const configured = config.copilotModel;
|
|
295
297
|
const isAvailable = models.some((m) => m.id === configured);
|
|
296
298
|
if (!isAvailable) {
|
|
297
|
-
|
|
299
|
+
log.warn({ configured, fallback: DEFAULT_MODEL }, "Configured model not available, falling back");
|
|
298
300
|
config.copilotModel = DEFAULT_MODEL;
|
|
299
301
|
}
|
|
300
302
|
}
|
|
301
303
|
catch (err) {
|
|
302
|
-
|
|
304
|
+
log.warn({ model: config.copilotModel, err: err instanceof Error ? err.message : err }, "Could not validate model, using as-is");
|
|
303
305
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
306
|
+
log.info({ mcpServerCount: Object.keys(mcpServers).length, mcpServers: Object.keys(mcpServers) }, "Loading MCP servers");
|
|
307
|
+
log.info({ skillDirectories }, "Skill directories");
|
|
308
|
+
log.info("Persistent session mode — conversation history maintained by SDK");
|
|
307
309
|
startHealthCheck();
|
|
308
310
|
// Eagerly create/resume the default orchestrator session
|
|
309
311
|
try {
|
|
310
312
|
await ensureOrchestratorSession("default");
|
|
311
313
|
}
|
|
312
314
|
catch (err) {
|
|
313
|
-
|
|
315
|
+
log.error({ err: err instanceof Error ? err.message : err }, "Failed to create initial session (will retry on first message)");
|
|
314
316
|
}
|
|
315
317
|
}
|
|
316
318
|
/** How long to wait for the orchestrator to finish a turn (10 min). */
|
|
@@ -450,22 +452,22 @@ async function executeOnSession(sessionKey, prompt, callback, attachments, onAct
|
|
|
450
452
|
// session and may have been (partially) processed. Return what we have.
|
|
451
453
|
if (/timeout/i.test(msg)) {
|
|
452
454
|
if (accumulated.length > 0) {
|
|
453
|
-
|
|
455
|
+
log.warn({ timeoutSec: ORCHESTRATOR_TIMEOUT_MS / 1000, charCount: accumulated.length }, "Timeout with partial response — returning partial");
|
|
454
456
|
return accumulated;
|
|
455
457
|
}
|
|
456
458
|
// No text yet but tool calls ran — the session is working in the background
|
|
457
459
|
// (e.g. delegate_to_agent dispatched). Don't error out.
|
|
458
460
|
if (toolCallCount > 0) {
|
|
459
|
-
|
|
461
|
+
log.warn({ timeoutSec: ORCHESTRATOR_TIMEOUT_MS / 1000, toolCallCount }, "Timeout — tool calls ran but no text yet, session still working");
|
|
460
462
|
return "I'm still working on this — I've started processing but it's taking longer than expected. I'll send you the results when I'm done.";
|
|
461
463
|
}
|
|
462
464
|
// No text, no tool calls — the session is truly stuck
|
|
463
|
-
|
|
465
|
+
log.warn({ timeoutSec: ORCHESTRATOR_TIMEOUT_MS / 1000 }, "Timeout with no activity — session may be stuck");
|
|
464
466
|
return "Sorry, that request timed out before I could start working on it. Try again or break it into smaller pieces?";
|
|
465
467
|
}
|
|
466
468
|
// If the session is broken, invalidate it so it's recreated on next attempt
|
|
467
469
|
if (/closed|destroy|disposed|invalid|expired|not found/i.test(msg)) {
|
|
468
|
-
|
|
470
|
+
log.warn({ sessionKey, msg }, "Session appears dead, will recreate");
|
|
469
471
|
sessionMap.delete(sessionKey);
|
|
470
472
|
sessionModelMap.delete(sessionKey);
|
|
471
473
|
if (sessionKey === "default")
|
|
@@ -493,7 +495,7 @@ async function executeOnSession(sessionKey, prompt, callback, attachments, onAct
|
|
|
493
495
|
async function processQueue() {
|
|
494
496
|
if (processing) {
|
|
495
497
|
if (messageQueue.length > 0) {
|
|
496
|
-
|
|
498
|
+
log.debug({ queueLength: messageQueue.length }, "Message queued, orchestrator is busy");
|
|
497
499
|
}
|
|
498
500
|
return;
|
|
499
501
|
}
|
|
@@ -515,17 +517,17 @@ async function processQueue() {
|
|
|
515
517
|
const currentModel = sessionModelMap.get(sessionKey) ?? config.copilotModel;
|
|
516
518
|
const routeResult = await resolveModel(item.prompt, currentModel, recentTiers);
|
|
517
519
|
if (routeResult.switched) {
|
|
518
|
-
|
|
520
|
+
log.info({ model: routeResult.model, tier: routeResult.overrideName || routeResult.tier }, "Auto-routing: switching model");
|
|
519
521
|
config.copilotModel = routeResult.model;
|
|
520
522
|
const existingSession = sessionMap.get(sessionKey);
|
|
521
523
|
if (existingSession) {
|
|
522
524
|
try {
|
|
523
525
|
await existingSession.setModel(routeResult.model);
|
|
524
526
|
sessionModelMap.set(sessionKey, routeResult.model);
|
|
525
|
-
|
|
527
|
+
log.info({ sessionKey }, "Model switched in-place");
|
|
526
528
|
}
|
|
527
529
|
catch (err) {
|
|
528
|
-
|
|
530
|
+
log.warn({ sessionKey, err: err instanceof Error ? err.message : err }, "setModel() failed, will recreate session");
|
|
529
531
|
sessionMap.delete(sessionKey);
|
|
530
532
|
if (sessionKey === "default")
|
|
531
533
|
deleteState(ORCHESTRATOR_SESSION_KEY);
|
|
@@ -570,6 +572,8 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
570
572
|
sessionKey = "project:" + normalizeProjectPath(source.projectPath);
|
|
571
573
|
// Keep the legacy channel-project map in sync for tools that read it
|
|
572
574
|
setChannelProject(source.connectionId, normalizeProjectPath(source.projectPath));
|
|
575
|
+
// Bump last-used timestamp so sidebar can sort by real activity
|
|
576
|
+
bumpProjectLastUsed(normalizeProjectPath(source.projectPath));
|
|
573
577
|
}
|
|
574
578
|
else if (source.type === "background" && source.sessionKey) {
|
|
575
579
|
sessionKey = source.sessionKey;
|
|
@@ -621,7 +625,7 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
621
625
|
// the user reply path.
|
|
622
626
|
if (copilotClient) {
|
|
623
627
|
maybeWriteEpisode(copilotClient).catch((err) => {
|
|
624
|
-
|
|
628
|
+
log.error({ err: err instanceof Error ? err.message : err }, "Episode write failed (non-fatal)");
|
|
625
629
|
});
|
|
626
630
|
}
|
|
627
631
|
return;
|
|
@@ -634,7 +638,7 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
634
638
|
}
|
|
635
639
|
if (isRecoverableError(err) && attempt < MAX_RETRIES) {
|
|
636
640
|
const delay = RECONNECT_DELAYS_MS[Math.min(attempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
637
|
-
|
|
641
|
+
log.warn({ msg, attempt: attempt + 1, maxRetries: MAX_RETRIES, delayMs: delay }, "Recoverable error, retrying");
|
|
638
642
|
await sleep(delay);
|
|
639
643
|
// Reset client before retry in case the connection is stale
|
|
640
644
|
try {
|
|
@@ -643,7 +647,7 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
643
647
|
catch { /* will fail again on next attempt */ }
|
|
644
648
|
continue;
|
|
645
649
|
}
|
|
646
|
-
|
|
650
|
+
log.error({ msg }, "Error processing message");
|
|
647
651
|
callback(`Error: ${msg}`, true);
|
|
648
652
|
return;
|
|
649
653
|
}
|
|
@@ -663,11 +667,11 @@ export async function cancelCurrentMessage() {
|
|
|
663
667
|
if (activeSession && currentCallback) {
|
|
664
668
|
try {
|
|
665
669
|
await activeSession.abort();
|
|
666
|
-
|
|
670
|
+
log.info({ sessionKey: currentProcessingSessionKey }, "Aborted in-flight request");
|
|
667
671
|
return true;
|
|
668
672
|
}
|
|
669
673
|
catch (err) {
|
|
670
|
-
|
|
674
|
+
log.error({ err: err instanceof Error ? err.message : err }, "Abort failed");
|
|
671
675
|
}
|
|
672
676
|
}
|
|
673
677
|
return drained > 0;
|
|
@@ -704,7 +708,7 @@ export async function shutdownAgents() {
|
|
|
704
708
|
await session.disconnect();
|
|
705
709
|
}
|
|
706
710
|
catch (err) {
|
|
707
|
-
|
|
711
|
+
log.error({ sessionKey: key, err: err instanceof Error ? err.message : err }, "Error disconnecting session during shutdown");
|
|
708
712
|
}
|
|
709
713
|
}
|
|
710
714
|
sessionMap.clear();
|
package/dist/copilot/router.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getState, setState } from "../store/db.js";
|
|
2
2
|
import { classifyWithLLM } from "./classifier.js";
|
|
3
|
+
import { childLogger } from "../util/logger.js";
|
|
4
|
+
const log = childLogger("router");
|
|
3
5
|
// ---------------------------------------------------------------------------
|
|
4
6
|
// Default configuration
|
|
5
7
|
// ---------------------------------------------------------------------------
|
|
@@ -96,12 +98,12 @@ async function classifyMessage(prompt, recentTiers, client) {
|
|
|
96
98
|
if (client) {
|
|
97
99
|
const tier = await classifyWithLLM(client, text);
|
|
98
100
|
if (tier) {
|
|
99
|
-
|
|
101
|
+
log.debug({ tier }, "LLM classifier result");
|
|
100
102
|
return tier;
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
// Fallback — standard is always safe
|
|
104
|
-
|
|
106
|
+
log.debug({ tier: "standard" }, "Classifier fallback: using standard tier");
|
|
105
107
|
return "standard";
|
|
106
108
|
}
|
|
107
109
|
// ---------------------------------------------------------------------------
|
package/dist/copilot/tools.js
CHANGED
|
@@ -22,6 +22,8 @@ import { getChannelProject } from "../squad/context.js";
|
|
|
22
22
|
import { findSquadAgent } from "../squad/registry.js";
|
|
23
23
|
import { buildSquadSystemPrefix } from "../squad/charter.js";
|
|
24
24
|
import { mirrorDecisionToWiki, syncDecisionsFileToWiki } from "../squad/mirror.js";
|
|
25
|
+
import { childLogger } from "../util/logger.js";
|
|
26
|
+
const log = childLogger("tools");
|
|
25
27
|
function getCategoryDir(category) {
|
|
26
28
|
const map = {
|
|
27
29
|
person: "people",
|
|
@@ -223,12 +225,12 @@ export function createTools(deps) {
|
|
|
223
225
|
// all 89+ entries, not just this task-completion entry.
|
|
224
226
|
syncDecisionsFileToWiki(projectRoot).then(syncResult => {
|
|
225
227
|
if (syncResult) {
|
|
226
|
-
|
|
228
|
+
log.info({ entriesSynced: syncResult.entriesSynced, wikiPath: syncResult.wikiPath }, "Post-task squad decisions synced to wiki");
|
|
227
229
|
}
|
|
228
230
|
}).catch(() => { });
|
|
229
231
|
}
|
|
230
232
|
catch (mirrorErr) {
|
|
231
|
-
|
|
233
|
+
log.error({ err: mirrorErr instanceof Error ? mirrorErr.message : mirrorErr }, "Failed to mirror squad decision to wiki (non-fatal)");
|
|
232
234
|
}
|
|
233
235
|
}
|
|
234
236
|
}
|
|
@@ -689,7 +691,7 @@ export function createTools(deps) {
|
|
|
689
691
|
await switchSessionModel(args.model_id);
|
|
690
692
|
}
|
|
691
693
|
catch (err) {
|
|
692
|
-
|
|
694
|
+
log.warn({ err: err instanceof Error ? err.message : err }, "setModel() failed during switch_model, will apply on next session");
|
|
693
695
|
}
|
|
694
696
|
// Disable router when manually switching — user has explicit preference
|
|
695
697
|
if (getRouterConfig().enabled) {
|
|
@@ -1169,7 +1171,7 @@ export function createTools(deps) {
|
|
|
1169
1171
|
// Schedule restart after returning the response
|
|
1170
1172
|
setTimeout(() => {
|
|
1171
1173
|
restartDaemon().catch((err) => {
|
|
1172
|
-
|
|
1174
|
+
log.error({ err: err instanceof Error ? err.message : err }, "Restart failed");
|
|
1173
1175
|
});
|
|
1174
1176
|
}, 1000);
|
|
1175
1177
|
return `Restarting Chapterhouse${reason}. I'll be back in a few seconds.`;
|