chapterhouse 0.3.13 → 0.3.14
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 +2 -69
- package/dist/api/server.js +8 -155
- package/dist/api/server.test.js +1 -1
- package/dist/cli.js +0 -30
- package/dist/config.js +0 -3
- package/dist/copilot/agent-event-bus.js +41 -0
- package/dist/copilot/agent-event-bus.test.js +23 -0
- package/dist/copilot/agents.js +4 -59
- package/dist/copilot/orchestrator.js +20 -39
- package/dist/copilot/orchestrator.test.js +73 -158
- package/dist/copilot/task-event-log.js +5 -5
- package/dist/copilot/task-event-log.test.js +68 -142
- package/dist/copilot/tools.js +9 -85
- package/dist/daemon.js +0 -22
- package/dist/store/db.js +2 -50
- package/dist/store/db.test.js +0 -45
- package/package.json +1 -3
- package/web/dist/assets/index-BlIWCM11.js +217 -0
- package/web/dist/assets/index-BlIWCM11.js.map +1 -0
- package/web/dist/assets/{index-BtAcw3EP.css → index-lvHFM_ut.css} +1 -1
- package/web/dist/index.html +2 -2
- package/dist/api/ralph.js +0 -153
- package/dist/api/ralph.test.js +0 -101
- package/dist/copilot/agents.squad.test.js +0 -72
- package/dist/copilot/hooks.js +0 -157
- package/dist/copilot/hooks.test.js +0 -315
- package/dist/copilot/squad-event-bus.js +0 -27
- package/dist/copilot/tools.squad.test.js +0 -168
- package/dist/squad/charter.js +0 -125
- package/dist/squad/charter.test.js +0 -89
- package/dist/squad/context.js +0 -48
- package/dist/squad/context.test.js +0 -59
- package/dist/squad/discovery.js +0 -268
- package/dist/squad/discovery.test.js +0 -154
- package/dist/squad/index.js +0 -9
- package/dist/squad/init-cli.js +0 -109
- package/dist/squad/init.js +0 -395
- package/dist/squad/init.test.js +0 -351
- package/dist/squad/mirror.js +0 -83
- package/dist/squad/mirror.scheduler.js +0 -80
- package/dist/squad/mirror.scheduler.test.js +0 -197
- package/dist/squad/mirror.test.js +0 -172
- package/dist/squad/registry.js +0 -162
- package/dist/squad/registry.test.js +0 -31
- package/dist/squad/squad-coordinator-system-message.test.js +0 -190
- package/dist/squad/squad-session-routing.test.js +0 -260
- package/dist/squad/types.js +0 -4
- package/dist/squad/worktree.js +0 -295
- package/dist/squad/worktree.test.js +0 -189
- package/dist/store/squad-sessions.test.js +0 -341
- package/web/dist/assets/index-IgSOXx_a.js +0 -219
- package/web/dist/assets/index-IgSOXx_a.js.map +0 -1
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
* 10. Multiple tasks tracked independently
|
|
15
15
|
* 11. RING_BUFFER_CAPACITY constant: 500 items, evicts oldest
|
|
16
16
|
*
|
|
17
|
-
* Uses node:test (same pattern as hooks.test.ts
|
|
17
|
+
* Uses node:test (same pattern as hooks.test.ts) so the
|
|
18
18
|
* daemon's `npm test` runner (node --test) picks these up from dist/.
|
|
19
19
|
*/
|
|
20
20
|
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
21
21
|
import assert from "node:assert/strict";
|
|
22
|
-
import {
|
|
23
|
-
import { RingBuffer, RING_BUFFER_CAPACITY, } from "./task-event-log.js";
|
|
22
|
+
import { agentEventBus } from "./agent-event-bus.js";
|
|
23
|
+
import { RingBuffer, RING_BUFFER_CAPACITY, initTaskEventLog, getTaskLogEvents, subscribeTaskLog, clearTaskLog, taskLogSize, } from "./task-event-log.js";
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
// Helpers
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
@@ -48,87 +48,6 @@ function makeDestroyedEvent(sessionId) {
|
|
|
48
48
|
timestamp: new Date(),
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Builds an isolated task-event-log wired to a fresh EventBus so tests don't
|
|
53
|
-
* share state through the process-level squadEventBus singleton.
|
|
54
|
-
*/
|
|
55
|
-
function buildIsolatedLog(bus) {
|
|
56
|
-
const CAPACITY = RING_BUFFER_CAPACITY;
|
|
57
|
-
const buffers = new Map();
|
|
58
|
-
const listeners = new Map();
|
|
59
|
-
function getOrCreate(taskId) {
|
|
60
|
-
let b = buffers.get(taskId);
|
|
61
|
-
if (!b) {
|
|
62
|
-
b = new RingBuffer(CAPACITY);
|
|
63
|
-
buffers.set(taskId, b);
|
|
64
|
-
}
|
|
65
|
-
return b;
|
|
66
|
-
}
|
|
67
|
-
function notify(taskId, event) {
|
|
68
|
-
const ls = listeners.get(taskId);
|
|
69
|
-
if (!ls)
|
|
70
|
-
return;
|
|
71
|
-
for (const fn of ls) {
|
|
72
|
-
try {
|
|
73
|
-
fn(event);
|
|
74
|
-
}
|
|
75
|
-
catch { /* non-fatal */ }
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const unsubToolCall = bus.subscribe("session:tool_call", (event) => {
|
|
79
|
-
const taskId = event.sessionId;
|
|
80
|
-
if (!taskId)
|
|
81
|
-
return;
|
|
82
|
-
const p = event.payload;
|
|
83
|
-
const ev = {
|
|
84
|
-
id: 0, taskId,
|
|
85
|
-
seq: p._seq ?? 0,
|
|
86
|
-
ts: p._ts ?? Date.now(),
|
|
87
|
-
kind: p._kind === "tool_complete" ? "tool_complete" : "tool_start",
|
|
88
|
-
toolName: p.toolName ?? null,
|
|
89
|
-
summary: p._summary ?? null,
|
|
90
|
-
};
|
|
91
|
-
getOrCreate(taskId).push(ev);
|
|
92
|
-
notify(taskId, ev);
|
|
93
|
-
});
|
|
94
|
-
const unsubDestroyed = bus.subscribe("session:destroyed", (event) => {
|
|
95
|
-
if (event.sessionId) {
|
|
96
|
-
buffers.delete(event.sessionId);
|
|
97
|
-
listeners.delete(event.sessionId);
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
return {
|
|
101
|
-
getEvents: (taskId, afterSeq = 0) => {
|
|
102
|
-
const buf = buffers.get(taskId);
|
|
103
|
-
if (!buf)
|
|
104
|
-
return [];
|
|
105
|
-
const all = buf.getAll();
|
|
106
|
-
return afterSeq === 0 ? all : all.filter((e) => e.seq > afterSeq);
|
|
107
|
-
},
|
|
108
|
-
subscribe: (taskId, fn) => {
|
|
109
|
-
let ls = listeners.get(taskId);
|
|
110
|
-
if (!ls) {
|
|
111
|
-
ls = new Set();
|
|
112
|
-
listeners.set(taskId, ls);
|
|
113
|
-
}
|
|
114
|
-
ls.add(fn);
|
|
115
|
-
return () => {
|
|
116
|
-
const lss = listeners.get(taskId);
|
|
117
|
-
if (lss) {
|
|
118
|
-
lss.delete(fn);
|
|
119
|
-
if (lss.size === 0)
|
|
120
|
-
listeners.delete(taskId);
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
},
|
|
124
|
-
clear: (taskId) => {
|
|
125
|
-
buffers.delete(taskId);
|
|
126
|
-
listeners.delete(taskId);
|
|
127
|
-
},
|
|
128
|
-
size: () => buffers.size,
|
|
129
|
-
shutdown: () => { unsubToolCall(); unsubDestroyed(); },
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
51
|
// ---------------------------------------------------------------------------
|
|
133
52
|
// RingBuffer — pure class tests
|
|
134
53
|
// ---------------------------------------------------------------------------
|
|
@@ -189,87 +108,94 @@ describe("RingBuffer", () => {
|
|
|
189
108
|
// Task event log (bus-wired) tests
|
|
190
109
|
// ---------------------------------------------------------------------------
|
|
191
110
|
describe("task event log — bus-wired", () => {
|
|
192
|
-
let
|
|
193
|
-
let log;
|
|
111
|
+
let shutdown;
|
|
194
112
|
beforeEach(() => {
|
|
195
|
-
|
|
196
|
-
log = buildIsolatedLog(bus);
|
|
113
|
+
shutdown = initTaskEventLog();
|
|
197
114
|
});
|
|
198
115
|
afterEach(() => {
|
|
199
|
-
|
|
116
|
+
shutdown();
|
|
200
117
|
});
|
|
201
118
|
it("returns [] for an unknown taskId", () => {
|
|
202
|
-
assert.deepEqual(
|
|
119
|
+
assert.deepEqual(getTaskLogEvents("no-such-task"), []);
|
|
203
120
|
});
|
|
204
|
-
it("populates ring buffer on session:tool_call bus event",
|
|
205
|
-
|
|
206
|
-
const events =
|
|
121
|
+
it("populates ring buffer on session:tool_call bus event", () => {
|
|
122
|
+
agentEventBus.emit(makeToolCallEvent({ sessionId: "task-001", kind: "tool_start", seq: 1, toolName: "view" }));
|
|
123
|
+
const events = getTaskLogEvents("task-001");
|
|
207
124
|
assert.equal(events.length, 1);
|
|
208
125
|
assert.equal(events[0].kind, "tool_start");
|
|
209
126
|
assert.equal(events[0].toolName, "view");
|
|
210
127
|
assert.equal(events[0].taskId, "task-001");
|
|
211
128
|
assert.equal(events[0].seq, 1);
|
|
129
|
+
clearTaskLog("task-001");
|
|
212
130
|
});
|
|
213
|
-
it("accumulates multiple events in order",
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const events =
|
|
131
|
+
it("accumulates multiple events in order", () => {
|
|
132
|
+
agentEventBus.emit(makeToolCallEvent({ sessionId: "task-002", kind: "tool_start", seq: 1, toolName: "bash" }));
|
|
133
|
+
agentEventBus.emit(makeToolCallEvent({ sessionId: "task-002", kind: "tool_complete", seq: 2, summary: "ok" }));
|
|
134
|
+
agentEventBus.emit(makeToolCallEvent({ sessionId: "task-002", kind: "tool_start", seq: 3, toolName: "view" }));
|
|
135
|
+
const events = getTaskLogEvents("task-002");
|
|
218
136
|
assert.equal(events.length, 3);
|
|
219
137
|
assert.equal(events[0].seq, 1);
|
|
220
138
|
assert.equal(events[2].seq, 3);
|
|
221
139
|
assert.equal(events[1].kind, "tool_complete");
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
assert.equal(
|
|
229
|
-
assert.equal(
|
|
230
|
-
|
|
231
|
-
|
|
140
|
+
clearTaskLog("task-002");
|
|
141
|
+
});
|
|
142
|
+
it("getEvents filters by afterSeq", () => {
|
|
143
|
+
agentEventBus.emit(makeToolCallEvent({ sessionId: "task-003", seq: 1 }));
|
|
144
|
+
agentEventBus.emit(makeToolCallEvent({ sessionId: "task-003", seq: 2 }));
|
|
145
|
+
agentEventBus.emit(makeToolCallEvent({ sessionId: "task-003", seq: 3 }));
|
|
146
|
+
assert.equal(getTaskLogEvents("task-003", 1).length, 2);
|
|
147
|
+
assert.equal(getTaskLogEvents("task-003", 2).length, 1);
|
|
148
|
+
assert.equal(getTaskLogEvents("task-003", 3).length, 0);
|
|
149
|
+
clearTaskLog("task-003");
|
|
150
|
+
});
|
|
151
|
+
it("subscribe delivers events as they arrive", () => {
|
|
232
152
|
const received = [];
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
153
|
+
const unsub = subscribeTaskLog("task-004", (e) => received.push(e.toolName ?? "?"));
|
|
154
|
+
agentEventBus.emit(makeToolCallEvent({ sessionId: "task-004", toolName: "bash" }));
|
|
155
|
+
agentEventBus.emit(makeToolCallEvent({ sessionId: "task-004", toolName: "view" }));
|
|
156
|
+
unsub();
|
|
236
157
|
assert.deepEqual(received, ["bash", "view"]);
|
|
158
|
+
clearTaskLog("task-004");
|
|
237
159
|
});
|
|
238
|
-
it("subscribe unsub stops delivery",
|
|
160
|
+
it("subscribe unsub stops delivery", () => {
|
|
239
161
|
const received = [];
|
|
240
|
-
const unsub =
|
|
241
|
-
|
|
162
|
+
const unsub = subscribeTaskLog("task-005", (e) => received.push(e.toolName ?? "?"));
|
|
163
|
+
agentEventBus.emit(makeToolCallEvent({ sessionId: "task-005", toolName: "bash" }));
|
|
242
164
|
unsub();
|
|
243
|
-
|
|
165
|
+
agentEventBus.emit(makeToolCallEvent({ sessionId: "task-005", toolName: "view" }));
|
|
244
166
|
assert.equal(received.length, 1);
|
|
245
167
|
assert.equal(received[0], "bash");
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
assert.equal(
|
|
266
|
-
assert.equal(
|
|
267
|
-
assert.equal(
|
|
268
|
-
assert.equal(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
168
|
+
clearTaskLog("task-005");
|
|
169
|
+
});
|
|
170
|
+
it("clearTaskLog removes buffer and listeners", () => {
|
|
171
|
+
agentEventBus.emit(makeToolCallEvent({ sessionId: "task-006", seq: 1 }));
|
|
172
|
+
assert.equal(getTaskLogEvents("task-006").length, 1);
|
|
173
|
+
clearTaskLog("task-006");
|
|
174
|
+
assert.equal(getTaskLogEvents("task-006").length, 0);
|
|
175
|
+
});
|
|
176
|
+
it("session:destroyed auto-clears ring buffer", () => {
|
|
177
|
+
agentEventBus.emit(makeToolCallEvent({ sessionId: "task-007", seq: 1 }));
|
|
178
|
+
agentEventBus.emit(makeToolCallEvent({ sessionId: "task-007", seq: 2 }));
|
|
179
|
+
assert.equal(getTaskLogEvents("task-007").length, 2);
|
|
180
|
+
agentEventBus.emit(makeDestroyedEvent("task-007"));
|
|
181
|
+
assert.equal(getTaskLogEvents("task-007").length, 0);
|
|
182
|
+
});
|
|
183
|
+
it("tracks multiple tasks independently", () => {
|
|
184
|
+
agentEventBus.emit(makeToolCallEvent({ sessionId: "task-A", seq: 1, toolName: "bash" }));
|
|
185
|
+
agentEventBus.emit(makeToolCallEvent({ sessionId: "task-B", seq: 1, toolName: "view" }));
|
|
186
|
+
agentEventBus.emit(makeToolCallEvent({ sessionId: "task-A", seq: 2, toolName: "edit" }));
|
|
187
|
+
assert.equal(getTaskLogEvents("task-A").length, 2);
|
|
188
|
+
assert.equal(getTaskLogEvents("task-B").length, 1);
|
|
189
|
+
assert.equal(getTaskLogEvents("task-A")[0].toolName, "bash");
|
|
190
|
+
assert.equal(getTaskLogEvents("task-B")[0].toolName, "view");
|
|
191
|
+
assert.equal(taskLogSize(), 2);
|
|
192
|
+
clearTaskLog("task-A");
|
|
193
|
+
clearTaskLog("task-B");
|
|
194
|
+
});
|
|
195
|
+
it("events for wrong taskId are not visible to other tasks", () => {
|
|
196
|
+
agentEventBus.emit(makeToolCallEvent({ sessionId: "task-X", seq: 1 }));
|
|
197
|
+
assert.equal(getTaskLogEvents("task-Y").length, 0);
|
|
198
|
+
clearTaskLog("task-X");
|
|
273
199
|
});
|
|
274
200
|
});
|
|
275
201
|
//# sourceMappingURL=task-event-log.test.js.map
|
package/dist/copilot/tools.js
CHANGED
|
@@ -6,23 +6,18 @@ import { join } from "path";
|
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
import { listSkills, createSkill, removeSkill } from "./skills.js";
|
|
8
8
|
import { config, persistModel } from "../config.js";
|
|
9
|
-
import {
|
|
10
|
-
import { getCurrentSourceChannel, getCurrentActivityCallback, getCurrentAuthenticatedUser, getLastAuthenticatedUser, getCurrentAuthorizationHeader, getCurrentChannelKey, getCurrentSessionKey, switchSessionModel, } from "./orchestrator.js";
|
|
9
|
+
import { getCurrentSourceChannel, getCurrentActivityCallback, getCurrentAuthenticatedUser, getLastAuthenticatedUser, getCurrentAuthorizationHeader, getCurrentSessionKey, switchSessionModel, } from "./orchestrator.js";
|
|
11
10
|
import { getRouterConfig, updateRouterConfig } from "./router.js";
|
|
12
11
|
import { ensureWikiStructure, readPage, writePage, deletePage, listPages, writeRawSource, listSources, assertPagePath } from "../wiki/fs.js";
|
|
13
12
|
import { searchIndex, addToIndex, removeFromIndex, parseIndex, buildIndexEntryForPage } from "../wiki/index-manager.js";
|
|
14
13
|
import { appendLog } from "../wiki/log-manager.js";
|
|
15
14
|
import { withWikiWrite } from "../wiki/lock.js";
|
|
16
15
|
import { readWikiPage, teamWikiSync } from "../wiki/team-sync.js";
|
|
17
|
-
import { getAgentRegistry, getAgent, createEphemeralAgentSession,
|
|
16
|
+
import { getAgentRegistry, getAgent, createEphemeralAgentSession, getAgentSessionStatus, getTask, registerTask, completeTask, failTask, createAgentFile, removeAgentFile, loadAgents, } from "./agents.js";
|
|
18
17
|
import { adoGetOkrs, adoOkrSummary, adoUpdateKr } from "../integrations/ado-skill.js";
|
|
19
18
|
import { TeamsNotifier } from "../integrations/teams-notify.js";
|
|
20
19
|
import { TeamPushClient } from "../integrations/team-push.js";
|
|
21
20
|
import { OKRMapper, parseOKRPageContent } from "./okr-mapper.js";
|
|
22
|
-
import { getChannelProject } from "../squad/context.js";
|
|
23
|
-
import { findSquadAgent } from "../squad/registry.js";
|
|
24
|
-
import { buildSquadSystemPrefix } from "../squad/charter.js";
|
|
25
|
-
import { mirrorDecisionToWiki, syncDecisionsFileToWiki } from "../squad/mirror.js";
|
|
26
21
|
import { childLogger } from "../util/logger.js";
|
|
27
22
|
const log = childLogger("tools");
|
|
28
23
|
function getCategoryDir(category) {
|
|
@@ -112,45 +107,15 @@ export function createTools(deps) {
|
|
|
112
107
|
if (agent?.slug === "chapterhouse") {
|
|
113
108
|
return "Cannot delegate to yourself. Handle this directly or pick a specialist agent.";
|
|
114
109
|
}
|
|
115
|
-
|
|
116
|
-
const projectRoot = config.squadEnabled ? (getChannelProject(channelKey) ?? undefined) : undefined;
|
|
117
|
-
const squadDescriptor = (!agent && projectRoot)
|
|
118
|
-
? (() => { try {
|
|
119
|
-
return findSquadAgent(projectRoot, args.agent_name);
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
return null;
|
|
123
|
-
} })()
|
|
124
|
-
: null;
|
|
125
|
-
if (!agent && !squadDescriptor) {
|
|
110
|
+
if (!agent) {
|
|
126
111
|
const available = getAgentRegistry().map((a) => a.slug).join(", ");
|
|
127
112
|
return `Agent '${args.agent_name}' not found. Available agents: ${available}`;
|
|
128
113
|
}
|
|
129
|
-
const delegatedSlug = agent
|
|
114
|
+
const delegatedSlug = agent.slug;
|
|
130
115
|
let session;
|
|
131
|
-
let isSquadAgent = false;
|
|
132
116
|
try {
|
|
133
117
|
const allTools = createTools(deps);
|
|
134
|
-
|
|
135
|
-
const squadContext = {
|
|
136
|
-
projectRoot,
|
|
137
|
-
squadDir: join(projectRoot, ".squad"),
|
|
138
|
-
teamDir: join(projectRoot, ".squad"),
|
|
139
|
-
personalDir: null,
|
|
140
|
-
mode: "local",
|
|
141
|
-
projectKey: null,
|
|
142
|
-
config: {},
|
|
143
|
-
agents: [squadDescriptor],
|
|
144
|
-
decisionsPath: join(projectRoot, ".squad", "decisions.md"),
|
|
145
|
-
loadedAt: new Date().toISOString(),
|
|
146
|
-
};
|
|
147
|
-
const charterPrefix = await buildSquadSystemPrefix(squadContext, squadDescriptor);
|
|
148
|
-
session = await createSquadAgentSession(delegatedSlug, deps.client, allTools, charterPrefix, args.model_override || squadDescriptor.modelPreference);
|
|
149
|
-
isSquadAgent = true;
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
session = await createEphemeralAgentSession(agent.slug, deps.client, allTools, args.model_override);
|
|
153
|
-
}
|
|
118
|
+
session = await createEphemeralAgentSession(agent.slug, deps.client, allTools, args.model_override);
|
|
154
119
|
}
|
|
155
120
|
catch (err) {
|
|
156
121
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -212,28 +177,6 @@ export function createTools(deps) {
|
|
|
212
177
|
completeTask(task.taskId, output);
|
|
213
178
|
db.prepare(`UPDATE agent_tasks SET status = 'completed', result = ?, completed_at = CURRENT_TIMESTAMP WHERE task_id = ?`).run(output.slice(0, 10000), task.taskId);
|
|
214
179
|
deps.onAgentTaskComplete(task.taskId, delegatedSlug, output);
|
|
215
|
-
if (isSquadAgent && projectRoot && squadDescriptor) {
|
|
216
|
-
try {
|
|
217
|
-
const wikiDecisionPath = await mirrorDecisionToWiki({
|
|
218
|
-
taskId: task.taskId,
|
|
219
|
-
projectRoot,
|
|
220
|
-
squadAgentSlug: squadDescriptor.slug,
|
|
221
|
-
wikiDecisionPath: "",
|
|
222
|
-
}, args.summary, output);
|
|
223
|
-
db.prepare(`INSERT OR REPLACE INTO squad_task_links (task_id, project_root, squad_agent_slug, wiki_decision_path)
|
|
224
|
-
VALUES (?, ?, ?, ?)`).run(task.taskId, projectRoot, squadDescriptor.slug, wikiDecisionPath);
|
|
225
|
-
// Sync the full decisions.md file to the wiki so the page reflects
|
|
226
|
-
// all 89+ entries, not just this task-completion entry.
|
|
227
|
-
syncDecisionsFileToWiki(projectRoot).then(syncResult => {
|
|
228
|
-
if (syncResult) {
|
|
229
|
-
log.info({ entriesSynced: syncResult.entriesSynced, wikiPath: syncResult.wikiPath }, "Post-task squad decisions synced to wiki");
|
|
230
|
-
}
|
|
231
|
-
}).catch(() => { });
|
|
232
|
-
}
|
|
233
|
-
catch (mirrorErr) {
|
|
234
|
-
log.error({ err: mirrorErr instanceof Error ? mirrorErr.message : mirrorErr }, "Failed to mirror squad decision to wiki (non-fatal)");
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
180
|
}
|
|
238
181
|
catch (err) {
|
|
239
182
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -253,7 +196,7 @@ export function createTools(deps) {
|
|
|
253
196
|
})();
|
|
254
197
|
const model = (args.model_override && args.model_override.length > 0)
|
|
255
198
|
? args.model_override
|
|
256
|
-
:
|
|
199
|
+
: (agent.model === "auto" ? "claude-sonnet-4.6" : agent.model || "claude-sonnet-4.6");
|
|
257
200
|
return `Task delegated to @${delegatedSlug} (${model}). Task ID: ${task.taskId}. I'll notify you when it's done.`;
|
|
258
201
|
},
|
|
259
202
|
}),
|
|
@@ -323,12 +266,10 @@ export function createTools(deps) {
|
|
|
323
266
|
},
|
|
324
267
|
}),
|
|
325
268
|
defineTool("show_agent_roster", {
|
|
326
|
-
description: "List all registered agents with their name, model, status, and current tasks.
|
|
269
|
+
description: "List all registered agents with their name, model, status, and current tasks.",
|
|
327
270
|
parameters: z.object({}),
|
|
328
271
|
handler: async () => {
|
|
329
272
|
const agents = getAgentRegistry();
|
|
330
|
-
const channelKey = getCurrentChannelKey() ?? "default";
|
|
331
|
-
const projectRoot = config.squadEnabled ? (getChannelProject(channelKey) ?? undefined) : undefined;
|
|
332
273
|
const chLines = agents.map((a) => {
|
|
333
274
|
const status = getAgentSessionStatus(a.slug);
|
|
334
275
|
const runningTasks = status.tasks.filter((t) => t.status === "running");
|
|
@@ -338,25 +279,9 @@ export function createTools(deps) {
|
|
|
338
279
|
: "";
|
|
339
280
|
return `• @${a.slug} (${a.name}) — ${a.model} — ${badge}${taskInfo}\n ${a.description}`;
|
|
340
281
|
});
|
|
341
|
-
|
|
342
|
-
if (projectRoot) {
|
|
343
|
-
try {
|
|
344
|
-
const { renderProjectAgentRoster } = await import("../squad/registry.js");
|
|
345
|
-
const squadRoster = renderProjectAgentRoster(projectRoot);
|
|
346
|
-
if (squadRoster) {
|
|
347
|
-
squadSection = `\n\nSquad agents (project: ${projectRoot}):\n${squadRoster}`;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
catch {
|
|
351
|
-
// squad unavailable — skip
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
else if (config.squadEnabled) {
|
|
355
|
-
squadSection = "\n\n(Squad is enabled — select a project context to see squad agents)";
|
|
356
|
-
}
|
|
357
|
-
if (chLines.length === 0 && !squadSection)
|
|
282
|
+
if (chLines.length === 0)
|
|
358
283
|
return "No agents registered.";
|
|
359
|
-
return `Registered agents (${chLines.length}):\n${chLines.join("\n")}
|
|
284
|
+
return `Registered agents (${chLines.length}):\n${chLines.join("\n")}`;
|
|
360
285
|
},
|
|
361
286
|
}),
|
|
362
287
|
defineTool("teams_notify", {
|
|
@@ -584,7 +509,6 @@ export function createTools(deps) {
|
|
|
584
509
|
const session = await deps.client.resumeSession(args.session_id, {
|
|
585
510
|
model: config.copilotModel,
|
|
586
511
|
onPermissionRequest: approveAll,
|
|
587
|
-
hooks: createSessionHooks(args.name),
|
|
588
512
|
});
|
|
589
513
|
const db = getDb();
|
|
590
514
|
db.prepare(`INSERT OR REPLACE INTO agent_sessions (slug, copilot_session_id, model, status)
|
package/dist/daemon.js
CHANGED
|
@@ -2,7 +2,6 @@ import { getClient, stopClient } from "./copilot/client.js";
|
|
|
2
2
|
import { initOrchestrator, setMessageLogger, setProactiveNotify, getAgentInfo, shutdownAgents } from "./copilot/orchestrator.js";
|
|
3
3
|
import { stopEpisodeWriter } from "./copilot/episode-writer.js";
|
|
4
4
|
import { startApiServer, broadcastToSSE } from "./api/server.js";
|
|
5
|
-
import { killRalphOnShutdown } from "./api/ralph.js";
|
|
6
5
|
import { getDb, closeDb, getState } from "./store/db.js";
|
|
7
6
|
import { config } from "./config.js";
|
|
8
7
|
import { spawn } from "child_process";
|
|
@@ -15,7 +14,6 @@ import { shouldMigrate, migrateMemoriesToWiki, shouldReorganize, reorganizeWiki
|
|
|
15
14
|
import { SESSIONS_DIR } from "./paths.js";
|
|
16
15
|
import { getDisplayHost } from "./api/server-runtime.js";
|
|
17
16
|
import { StandupScheduler } from "./copilot/standup.js";
|
|
18
|
-
import { DecisionsSyncScheduler } from "./squad/mirror.scheduler.js";
|
|
19
17
|
import { registerShutdownSignals } from "./shutdown-signals.js";
|
|
20
18
|
import { logger } from "./util/logger.js";
|
|
21
19
|
import { CHAPTERHOUSE_VERSION } from "./version.js";
|
|
@@ -148,9 +146,6 @@ async function main() {
|
|
|
148
146
|
if (config.chapterhouseMode === "personal" && (config.adoPat || config.teamChapterhouseUrl)) {
|
|
149
147
|
new StandupScheduler().schedule();
|
|
150
148
|
}
|
|
151
|
-
// Start periodic decisions→wiki sync for all registered projects
|
|
152
|
-
decisionsSyncScheduler = new DecisionsSyncScheduler();
|
|
153
|
-
decisionsSyncScheduler.start();
|
|
154
149
|
const url = `http://${getDisplayHost(config.apiHost)}:${config.apiPort}`;
|
|
155
150
|
log.info({ url }, "Chapterhouse is fully operational");
|
|
156
151
|
if (process.env.CHAPTERHOUSE_OPEN_BROWSER === "1") {
|
|
@@ -177,7 +172,6 @@ async function main() {
|
|
|
177
172
|
}
|
|
178
173
|
// Graceful shutdown
|
|
179
174
|
let shutdownState = "idle";
|
|
180
|
-
let decisionsSyncScheduler;
|
|
181
175
|
async function shutdown() {
|
|
182
176
|
if (shutdownState === "shutting_down") {
|
|
183
177
|
log.warn("Forced exit");
|
|
@@ -202,10 +196,6 @@ async function shutdown() {
|
|
|
202
196
|
forceTimer.unref();
|
|
203
197
|
// Destroy all active agent sessions
|
|
204
198
|
await shutdownAgents();
|
|
205
|
-
try {
|
|
206
|
-
decisionsSyncScheduler?.stop();
|
|
207
|
-
}
|
|
208
|
-
catch { /* best effort */ }
|
|
209
199
|
try {
|
|
210
200
|
stopEpisodeWriter();
|
|
211
201
|
}
|
|
@@ -214,10 +204,6 @@ async function shutdown() {
|
|
|
214
204
|
await stopClient();
|
|
215
205
|
}
|
|
216
206
|
catch { /* best effort */ }
|
|
217
|
-
try {
|
|
218
|
-
killRalphOnShutdown();
|
|
219
|
-
}
|
|
220
|
-
catch { /* best effort */ }
|
|
221
207
|
closeDb();
|
|
222
208
|
log.info("Goodbye");
|
|
223
209
|
process.exit(0);
|
|
@@ -231,10 +217,6 @@ export async function restartDaemon() {
|
|
|
231
217
|
}
|
|
232
218
|
// Destroy all active agent sessions
|
|
233
219
|
await shutdownAgents();
|
|
234
|
-
try {
|
|
235
|
-
decisionsSyncScheduler?.stop();
|
|
236
|
-
}
|
|
237
|
-
catch { /* best effort */ }
|
|
238
220
|
try {
|
|
239
221
|
stopEpisodeWriter();
|
|
240
222
|
}
|
|
@@ -243,10 +225,6 @@ export async function restartDaemon() {
|
|
|
243
225
|
await stopClient();
|
|
244
226
|
}
|
|
245
227
|
catch { /* best effort */ }
|
|
246
|
-
try {
|
|
247
|
-
killRalphOnShutdown();
|
|
248
|
-
}
|
|
249
|
-
catch { /* best effort */ }
|
|
250
228
|
closeDb();
|
|
251
229
|
// Spawn a detached replacement process with the same args (include execArgv for tsx/loaders)
|
|
252
230
|
const child = spawn(process.execPath, [...process.execArgv, ...process.argv.slice(1)], {
|
package/dist/store/db.js
CHANGED
|
@@ -68,42 +68,6 @@ export function getDb() {
|
|
|
68
68
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
69
69
|
last_accessed DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
70
70
|
)
|
|
71
|
-
`);
|
|
72
|
-
db.exec(`
|
|
73
|
-
CREATE TABLE IF NOT EXISTS project_squads (
|
|
74
|
-
project_root TEXT PRIMARY KEY,
|
|
75
|
-
squad_dir TEXT NOT NULL,
|
|
76
|
-
team_dir TEXT NOT NULL,
|
|
77
|
-
personal_dir TEXT,
|
|
78
|
-
mode TEXT NOT NULL CHECK(mode IN ('local', 'remote')),
|
|
79
|
-
project_key TEXT,
|
|
80
|
-
config_source TEXT,
|
|
81
|
-
registered INTEGER NOT NULL DEFAULT 0,
|
|
82
|
-
loaded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
83
|
-
)
|
|
84
|
-
`);
|
|
85
|
-
db.exec(`
|
|
86
|
-
CREATE TABLE IF NOT EXISTS squad_agents (
|
|
87
|
-
project_root TEXT NOT NULL,
|
|
88
|
-
slug TEXT NOT NULL,
|
|
89
|
-
role TEXT NOT NULL,
|
|
90
|
-
description TEXT,
|
|
91
|
-
charter_path TEXT NOT NULL,
|
|
92
|
-
model_preference TEXT,
|
|
93
|
-
tools_json TEXT,
|
|
94
|
-
capabilities_json TEXT,
|
|
95
|
-
stale INTEGER NOT NULL DEFAULT 0,
|
|
96
|
-
loaded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
97
|
-
PRIMARY KEY (project_root, slug)
|
|
98
|
-
)
|
|
99
|
-
`);
|
|
100
|
-
db.exec(`
|
|
101
|
-
CREATE TABLE IF NOT EXISTS squad_task_links (
|
|
102
|
-
task_id TEXT PRIMARY KEY,
|
|
103
|
-
project_root TEXT NOT NULL,
|
|
104
|
-
squad_agent_slug TEXT NOT NULL,
|
|
105
|
-
wiki_decision_path TEXT NOT NULL
|
|
106
|
-
)
|
|
107
71
|
`);
|
|
108
72
|
// Migrate: if the table already existed with a stricter CHECK, recreate it
|
|
109
73
|
try {
|
|
@@ -146,7 +110,7 @@ export function getDb() {
|
|
|
146
110
|
if (!taskCols.some((c) => c.name === 'session_key')) {
|
|
147
111
|
db.exec(`ALTER TABLE agent_tasks ADD COLUMN session_key TEXT NOT NULL DEFAULT 'default'`);
|
|
148
112
|
}
|
|
149
|
-
// Migrate: add source column to agent_tasks ('adhoc'
|
|
113
|
+
// Migrate: add source column to agent_tasks ('adhoc') if not present
|
|
150
114
|
if (!taskCols.some((c) => c.name === 'source')) {
|
|
151
115
|
db.exec(`ALTER TABLE agent_tasks ADD COLUMN source TEXT NOT NULL DEFAULT 'adhoc'`);
|
|
152
116
|
}
|
|
@@ -168,13 +132,6 @@ export function getDb() {
|
|
|
168
132
|
if (!taskColsNow.some((c) => c.name === 'event_seq')) {
|
|
169
133
|
db.exec(`ALTER TABLE agent_tasks ADD COLUMN event_seq INTEGER NOT NULL DEFAULT 0`);
|
|
170
134
|
}
|
|
171
|
-
// Migrate: add last_used_at column to project_squads (epoch ms, nullable)
|
|
172
|
-
const projectCols = db.prepare(`PRAGMA table_info(project_squads)`).all();
|
|
173
|
-
if (!projectCols.some((c) => c.name === 'last_used_at')) {
|
|
174
|
-
db.exec(`ALTER TABLE project_squads ADD COLUMN last_used_at INTEGER`);
|
|
175
|
-
// Backfill from loaded_at so sidebar is not empty for existing projects
|
|
176
|
-
db.exec(`UPDATE project_squads SET last_used_at = CAST((julianday(loaded_at) - 2440587.5) * 86400000 AS INTEGER) WHERE last_used_at IS NULL`);
|
|
177
|
-
}
|
|
178
135
|
// turn_events: append-only per-turn event log for the SSE chat channel (#130).
|
|
179
136
|
// Events are written on turn completion; ring buffer serves live/recent replay.
|
|
180
137
|
db.exec(`
|
|
@@ -297,7 +254,7 @@ export function getTaskSessionKey(taskId) {
|
|
|
297
254
|
* Get recent conversation history formatted for injection into system message.
|
|
298
255
|
*
|
|
299
256
|
* When `sessionKey` is provided, only rows belonging to that session are
|
|
300
|
-
* returned — useful for
|
|
257
|
+
* returned — useful for per-session isolation that must not bleed context
|
|
301
258
|
* from other sessions. When omitted, rows from all sessions are returned
|
|
302
259
|
* (legacy behavior for callers that don't care about session isolation).
|
|
303
260
|
*/
|
|
@@ -408,11 +365,6 @@ export function getTaskEvents(taskId, afterSeq = 0) {
|
|
|
408
365
|
summary: r.summary,
|
|
409
366
|
}));
|
|
410
367
|
}
|
|
411
|
-
export function bumpProjectLastUsed(projectRoot) {
|
|
412
|
-
getDb()
|
|
413
|
-
.prepare(`UPDATE project_squads SET last_used_at = ? WHERE project_root = ?`)
|
|
414
|
-
.run(Date.now(), projectRoot);
|
|
415
|
-
}
|
|
416
368
|
export function closeDb() {
|
|
417
369
|
if (db) {
|
|
418
370
|
db.close();
|
package/dist/store/db.test.js
CHANGED
|
@@ -167,51 +167,6 @@ test("getSessionMessages returns structured messages in chronological order, exc
|
|
|
167
167
|
}
|
|
168
168
|
});
|
|
169
169
|
// ---------------------------------------------------------------------------
|
|
170
|
-
// #26 — bumpProjectLastUsed
|
|
171
|
-
// ---------------------------------------------------------------------------
|
|
172
|
-
test("migration adds last_used_at column to project_squads when absent", async () => {
|
|
173
|
-
const dbModule = await loadDbModule();
|
|
174
|
-
try {
|
|
175
|
-
const db = dbModule.getDb();
|
|
176
|
-
const cols = db.prepare(`PRAGMA table_info(project_squads)`).all();
|
|
177
|
-
assert.equal(cols.some((c) => c.name === "last_used_at"), true, "last_used_at column should exist after migration");
|
|
178
|
-
}
|
|
179
|
-
finally {
|
|
180
|
-
dbModule.closeDb();
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
test("bumpProjectLastUsed updates last_used_at for the given project_root", async () => {
|
|
184
|
-
const dbModule = await loadDbModule();
|
|
185
|
-
try {
|
|
186
|
-
const db = dbModule.getDb();
|
|
187
|
-
db.prepare(`INSERT INTO project_squads (project_root, squad_dir, team_dir, mode, registered) VALUES (?, ?, ?, 'local', 1)`).run("/home/user/test-proj", "/home/user/test-proj/.squad", "/home/user/test-proj/.squad");
|
|
188
|
-
const before = db
|
|
189
|
-
.prepare(`SELECT last_used_at FROM project_squads WHERE project_root = ?`)
|
|
190
|
-
.get("/home/user/test-proj");
|
|
191
|
-
const beforeTs = before.last_used_at ?? 0;
|
|
192
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
193
|
-
dbModule.bumpProjectLastUsed("/home/user/test-proj");
|
|
194
|
-
const after = db
|
|
195
|
-
.prepare(`SELECT last_used_at FROM project_squads WHERE project_root = ?`)
|
|
196
|
-
.get("/home/user/test-proj");
|
|
197
|
-
assert.ok(after.last_used_at !== null, "last_used_at should not be null after bump");
|
|
198
|
-
assert.ok(after.last_used_at > beforeTs, "last_used_at should advance after bump");
|
|
199
|
-
}
|
|
200
|
-
finally {
|
|
201
|
-
dbModule.closeDb();
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
test("bumpProjectLastUsed is a no-op for unknown project_root (no throw)", async () => {
|
|
205
|
-
const dbModule = await loadDbModule();
|
|
206
|
-
try {
|
|
207
|
-
dbModule.getDb();
|
|
208
|
-
dbModule.bumpProjectLastUsed("/does/not/exist");
|
|
209
|
-
}
|
|
210
|
-
finally {
|
|
211
|
-
dbModule.closeDb();
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
// ---------------------------------------------------------------------------
|
|
215
170
|
// #86: agent_task_events — appendTaskEvent and getTaskEvents
|
|
216
171
|
// ---------------------------------------------------------------------------
|
|
217
172
|
test("#86: appendTaskEvent inserts a row and getTaskEvents returns it ordered by seq", async () => {
|