chapterhouse 0.3.19 → 0.3.21
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/dist/api/server.js +154 -3
- package/dist/api/server.test.js +461 -14
- package/dist/copilot/orchestrator.js +97 -13
- package/dist/copilot/orchestrator.test.js +409 -1
- package/dist/copilot/project-resolution.js +73 -0
- package/dist/copilot/project-resolution.test.js +124 -0
- package/dist/copilot/project-rule-warnings.js +73 -0
- package/dist/copilot/project-rule-warnings.test.js +46 -0
- package/dist/copilot/project-rules-injection.js +71 -0
- package/dist/copilot/project-rules-injection.test.js +84 -0
- package/dist/copilot/tools.agent.test.js +214 -0
- package/dist/copilot/tools.js +14 -3
- package/dist/store/db.js +4 -0
- package/dist/store/db.test.js +30 -0
- package/dist/wiki/frontmatter.js +1 -1
- package/dist/wiki/lint.js +37 -10
- package/dist/wiki/lint.test.js +72 -0
- package/dist/wiki/project-registry.js +160 -0
- package/dist/wiki/project-registry.test.js +72 -0
- package/dist/wiki/project-rules.js +155 -0
- package/dist/wiki/project-rules.test.js +217 -0
- package/package.json +1 -1
- package/web/dist/assets/{index-9We9vWBC.js → index-B8dZzlmE.js} +66 -66
- package/web/dist/assets/index-B8dZzlmE.js.map +1 -0
- package/web/dist/assets/index-D9flFppK.css +10 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-9We9vWBC.js.map +0 -1
- package/web/dist/assets/index-DYx2idiH.css +0 -10
|
@@ -19,6 +19,11 @@ import { agentEventBus } from "./agent-event-bus.js";
|
|
|
19
19
|
import { initTaskEventLog } from "./task-event-log.js";
|
|
20
20
|
import { emitTurnEvent, persistTurnEvents, scheduleClearTurnLog, } from "./turn-event-log.js";
|
|
21
21
|
import { SessionManager, SessionRegistry, SESSION_IDLE_TTL_MS, SESSION_MAX_ACTIVE, } from "./session-manager.js";
|
|
22
|
+
import { ActiveProjectRulesLoadError, renderActiveProjectRulesBlock, } from "./project-rules-injection.js";
|
|
23
|
+
import { detectProjectRuleWarnings } from "./project-rule-warnings.js";
|
|
24
|
+
import { loadRegistry } from "../wiki/project-registry.js";
|
|
25
|
+
import { loadProjectRules } from "../wiki/project-rules.js";
|
|
26
|
+
import { resolveProject } from "./project-resolution.js";
|
|
22
27
|
const log = childLogger("orchestrator");
|
|
23
28
|
const MAX_RETRIES = 3;
|
|
24
29
|
const RECONNECT_DELAYS_MS = [1_000, 3_000, 10_000];
|
|
@@ -33,6 +38,19 @@ let proactiveNotifyFn;
|
|
|
33
38
|
export function setProactiveNotify(fn) {
|
|
34
39
|
proactiveNotifyFn = fn;
|
|
35
40
|
}
|
|
41
|
+
function usesSessionTurnLifecycle(source) {
|
|
42
|
+
return source.type === "background" || source.type === "sse-web";
|
|
43
|
+
}
|
|
44
|
+
function finalizeTurnEvent(sessionKey, event) {
|
|
45
|
+
if (event.type === "turn:complete") {
|
|
46
|
+
emitTurnEvent(sessionKey, { type: "turn:complete", turnId: event.turnId, sessionKey, finalMessage: event.finalMessage });
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
emitTurnEvent(sessionKey, { type: "turn:error", turnId: event.turnId, sessionKey, error: event.error });
|
|
50
|
+
}
|
|
51
|
+
persistTurnEvents(event.turnId, sessionKey);
|
|
52
|
+
scheduleClearTurnLog(event.turnId);
|
|
53
|
+
}
|
|
36
54
|
const turnContextStorage = new AsyncLocalStorage();
|
|
37
55
|
// ---------------------------------------------------------------------------
|
|
38
56
|
// Module-level state (not per-session)
|
|
@@ -105,6 +123,9 @@ export function getCurrentChannelKey() {
|
|
|
105
123
|
export function getCurrentActivityCallback() {
|
|
106
124
|
return turnContextStorage.getStore()?.activityCallback;
|
|
107
125
|
}
|
|
126
|
+
export function getCurrentActiveProjectRules() {
|
|
127
|
+
return turnContextStorage.getStore()?.activeProjectRules ?? null;
|
|
128
|
+
}
|
|
108
129
|
export function getCurrentAuthenticatedUser() {
|
|
109
130
|
return turnContextStorage.getStore()?.authUser ?? currentAuthenticatedUser;
|
|
110
131
|
}
|
|
@@ -347,6 +368,14 @@ export const ORCHESTRATOR_TIMEOUT_MS = (() => {
|
|
|
347
368
|
async function executeOnSession(manager, item) {
|
|
348
369
|
const { sessionKey } = manager;
|
|
349
370
|
const session = await manager.ensureSession();
|
|
371
|
+
const activeProjectRules = getActiveProjectRules(item.prompt, item.projectPath);
|
|
372
|
+
const warningLines = activeProjectRules
|
|
373
|
+
? detectProjectRuleWarnings(item.prompt, activeProjectRules.rules.hard)
|
|
374
|
+
: [];
|
|
375
|
+
const warningBlock = warningLines.length > 0 ? `${warningLines.join("\n")}\n\n` : "";
|
|
376
|
+
const sessionPrompt = activeProjectRules
|
|
377
|
+
? `${warningBlock}${renderActiveProjectRulesBlock(activeProjectRules.project.slug, activeProjectRules.project.path, activeProjectRules.rules.path, activeProjectRules.rules.hard, activeProjectRules.rules.soft)}\n\n${item.prompt}`
|
|
378
|
+
: item.prompt;
|
|
350
379
|
// Update last-seen globals (backwards compat — for callers that inspect after a turn ends)
|
|
351
380
|
currentAuthenticatedUser = item.authUser;
|
|
352
381
|
currentAuthorizationHeader = item.authHeader;
|
|
@@ -357,6 +386,7 @@ async function executeOnSession(manager, item) {
|
|
|
357
386
|
authUser: item.authUser,
|
|
358
387
|
authHeader: item.authHeader,
|
|
359
388
|
activityCallback: item.onActivity,
|
|
389
|
+
activeProjectRules,
|
|
360
390
|
}, async () => {
|
|
361
391
|
let accumulated = "";
|
|
362
392
|
let toolCallExecuted = false;
|
|
@@ -373,6 +403,11 @@ async function executeOnSession(manager, item) {
|
|
|
373
403
|
spawnArgsMap.set(data.toolCallId, {
|
|
374
404
|
name: typeof args.name === "string" ? args.name : undefined,
|
|
375
405
|
description: typeof args.description === "string" ? args.description : undefined,
|
|
406
|
+
prompt: typeof args.prompt === "string"
|
|
407
|
+
? args.prompt
|
|
408
|
+
: typeof args.task === "string"
|
|
409
|
+
? args.task
|
|
410
|
+
: undefined,
|
|
376
411
|
});
|
|
377
412
|
}
|
|
378
413
|
});
|
|
@@ -509,7 +544,12 @@ async function executeOnSession(manager, item) {
|
|
|
509
544
|
const description = (typeof spawnArgs?.description === "string"
|
|
510
545
|
? spawnArgs.description
|
|
511
546
|
: data.agentDescription || data.agentDisplayName || `Agent dispatch: ${agentSlug}`).slice(0, 500);
|
|
512
|
-
|
|
547
|
+
const prompt = typeof spawnArgs?.prompt === "string" ? spawnArgs.prompt : null;
|
|
548
|
+
db.prepare(`INSERT OR IGNORE INTO agent_tasks (task_id, agent_slug, description, prompt, status, origin_channel, session_key, source)
|
|
549
|
+
VALUES (?, ?, ?, ?, 'running', ?, ?, 'adhoc')`).run(data.toolCallId, agentSlug, description, prompt, item.sourceChannel || null, sessionKey);
|
|
550
|
+
if (prompt) {
|
|
551
|
+
db.prepare(`UPDATE agent_tasks SET prompt = COALESCE(prompt, ?) WHERE task_id = ?`).run(prompt, data.toolCallId);
|
|
552
|
+
}
|
|
513
553
|
activeSubagentTaskIds.add(data.toolCallId);
|
|
514
554
|
void agentEventBus.emit({
|
|
515
555
|
type: "session:created",
|
|
@@ -649,8 +689,22 @@ async function executeOnSession(manager, item) {
|
|
|
649
689
|
});
|
|
650
690
|
});
|
|
651
691
|
try {
|
|
652
|
-
|
|
653
|
-
|
|
692
|
+
if (warningBlock) {
|
|
693
|
+
accumulated = warningBlock;
|
|
694
|
+
item.callback(accumulated, false, item.turnId);
|
|
695
|
+
emitTurnEvent(sessionKey, {
|
|
696
|
+
type: "turn:delta",
|
|
697
|
+
turnId: item.turnId,
|
|
698
|
+
sessionKey,
|
|
699
|
+
part: { type: "text", text: warningBlock },
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
const result = await session.sendAndWait({ prompt: sessionPrompt, ...(item.attachments && item.attachments.length > 0 ? { attachments: item.attachments } : {}) }, ORCHESTRATOR_TIMEOUT_MS);
|
|
703
|
+
const streamedContent = warningBlock && accumulated.startsWith(warningBlock)
|
|
704
|
+
? accumulated.slice(warningBlock.length)
|
|
705
|
+
: accumulated;
|
|
706
|
+
const responseContent = result?.data?.content || streamedContent || "(No response)";
|
|
707
|
+
const finalContent = warningBlock ? `${warningBlock}${responseContent}` : responseContent;
|
|
654
708
|
return finalContent;
|
|
655
709
|
}
|
|
656
710
|
catch (err) {
|
|
@@ -730,6 +784,30 @@ async function processItem(item, manager) {
|
|
|
730
784
|
lastRouteResult = routeResult;
|
|
731
785
|
return executeOnSession(manager, item);
|
|
732
786
|
}
|
|
787
|
+
function getActiveProjectRules(prompt, projectPath) {
|
|
788
|
+
const registry = loadRegistry();
|
|
789
|
+
const project = resolveProject(prompt, { projectPath }, registry);
|
|
790
|
+
if (!project) {
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
try {
|
|
794
|
+
const rules = loadProjectRules(project.slug);
|
|
795
|
+
if (!rules.found) {
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
return { project, rules };
|
|
799
|
+
}
|
|
800
|
+
catch (err) {
|
|
801
|
+
if (err instanceof ActiveProjectRulesLoadError || err instanceof Error) {
|
|
802
|
+
log.warn({
|
|
803
|
+
slug: err instanceof ActiveProjectRulesLoadError ? err.slug : project.slug,
|
|
804
|
+
err: err.message,
|
|
805
|
+
}, "Project rules could not be loaded; continuing without injection");
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
throw err;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
733
811
|
function isRecoverableError(err) {
|
|
734
812
|
const msg = err instanceof Error ? err.message : String(err);
|
|
735
813
|
if (/timeout/i.test(msg))
|
|
@@ -765,6 +843,10 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
765
843
|
// sse-web carries user identity just like web (Fix 3).
|
|
766
844
|
const authUser = (source.type === "web" || source.type === "sse-web") ? source.user : undefined;
|
|
767
845
|
const authHeader = (source.type === "web" || source.type === "sse-web") ? source.authorizationHeader?.trim() || undefined : undefined;
|
|
846
|
+
const emitSessionLifecycle = usesSessionTurnLifecycle(source);
|
|
847
|
+
if (emitSessionLifecycle) {
|
|
848
|
+
emitTurnEvent(sessionKey, { type: "turn:started", turnId, sessionKey, prompt, attachments });
|
|
849
|
+
}
|
|
768
850
|
const manager = registry.getOrCreate(sessionKey);
|
|
769
851
|
void (async () => {
|
|
770
852
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
@@ -772,6 +854,7 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
772
854
|
const finalContent = await new Promise((resolve, reject) => {
|
|
773
855
|
manager.enqueue({
|
|
774
856
|
prompt: taggedPrompt,
|
|
857
|
+
projectPath: source.type === "web" ? source.projectPath : undefined,
|
|
775
858
|
attachments,
|
|
776
859
|
callback,
|
|
777
860
|
// Cast: QueuedMessage.onActivity uses a wide event type to avoid circular
|
|
@@ -793,6 +876,9 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
793
876
|
});
|
|
794
877
|
});
|
|
795
878
|
callback(finalContent, true, turnId);
|
|
879
|
+
if (emitSessionLifecycle) {
|
|
880
|
+
finalizeTurnEvent(sessionKey, { type: "turn:complete", turnId, finalMessage: finalContent });
|
|
881
|
+
}
|
|
796
882
|
try {
|
|
797
883
|
logMessage("out", sourceLabel, finalContent);
|
|
798
884
|
}
|
|
@@ -829,6 +915,9 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
829
915
|
}
|
|
830
916
|
log.error({ msg }, "Error processing message");
|
|
831
917
|
callback(`Error: ${msg}`, true, turnId);
|
|
918
|
+
if (emitSessionLifecycle) {
|
|
919
|
+
finalizeTurnEvent(sessionKey, { type: "turn:error", turnId, error: msg });
|
|
920
|
+
}
|
|
832
921
|
return;
|
|
833
922
|
}
|
|
834
923
|
}
|
|
@@ -873,6 +962,7 @@ export async function interruptCurrentTurn(sessionKey, newPrompt, source, callba
|
|
|
873
962
|
const finalContent = await new Promise((resolve, reject) => {
|
|
874
963
|
manager.enqueue({
|
|
875
964
|
prompt: taggedPrompt,
|
|
965
|
+
projectPath: source.type === "web" ? source.projectPath : undefined,
|
|
876
966
|
attachments,
|
|
877
967
|
callback,
|
|
878
968
|
onActivity: onActivity,
|
|
@@ -938,7 +1028,7 @@ export async function interruptCurrentTurn(sessionKey, newPrompt, source, callba
|
|
|
938
1028
|
*
|
|
939
1029
|
* Unlike `sendToOrchestrator`, this function:
|
|
940
1030
|
* - Returns the `turnId` immediately without waiting for the turn to complete.
|
|
941
|
-
* -
|
|
1031
|
+
* - Routes through the shared lifecycle emitter in sendToOrchestrator.
|
|
942
1032
|
* - Does NOT write to sseClients — the SSE channel delivers events via subscribeSession().
|
|
943
1033
|
* - Supports interrupt: true which calls interruptCurrentTurn under the hood.
|
|
944
1034
|
*
|
|
@@ -949,15 +1039,9 @@ export function enqueueForSse(opts) {
|
|
|
949
1039
|
const turnId = randomUUID();
|
|
950
1040
|
// sse-web carries auth and enables onQueued — unlike "background" (Fixes 2 & 3).
|
|
951
1041
|
const source = { type: "sse-web", sessionKey, user: authUser, authorizationHeader: authHeader };
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
if (done) {
|
|
956
|
-
emitTurnEvent(sessionKey, { type: "turn:complete", turnId: tid, sessionKey, finalMessage: text });
|
|
957
|
-
persistTurnEvents(tid, sessionKey);
|
|
958
|
-
scheduleClearTurnLog(tid);
|
|
959
|
-
}
|
|
960
|
-
// Note: mid-turn text deltas are emitted by executeOnSession's delta handler
|
|
1042
|
+
const callback = (_text, _done, _tid) => {
|
|
1043
|
+
// Note: sendToOrchestrator now owns turn:started/turn:complete/turn:error emission.
|
|
1044
|
+
// Mid-turn text deltas are still emitted by executeOnSession's delta handler.
|
|
961
1045
|
};
|
|
962
1046
|
const onQueued = (position, tid) => {
|
|
963
1047
|
emitTurnEvent(sessionKey, { type: "turn:queued", turnId: tid, sessionKey, position });
|