chapterhouse 0.3.13 → 0.3.15
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 +11 -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/system-message.js +7 -0
- package/dist/copilot/task-event-log.js +5 -5
- package/dist/copilot/task-event-log.test.js +68 -142
- package/dist/copilot/tools.js +72 -132
- package/dist/daemon.js +6 -22
- package/dist/store/db.js +2 -50
- package/dist/store/db.test.js +0 -45
- package/dist/wiki/fs.js +5 -0
- package/dist/wiki/index-manager.js +92 -17
- package/dist/wiki/index-manager.test.js +19 -0
- package/dist/wiki/migrate-topics.js +132 -0
- package/dist/wiki/migrate-topics.test.js +57 -0
- package/dist/wiki/topic-structure.js +167 -0
- package/dist/wiki/topic-structure.test.js +74 -0
- 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,36 +6,21 @@ 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";
|
|
14
|
+
import { getCategoryDir, topicPagePath, slugify, entityCategories, FLAT_CATEGORIES } from "../wiki/topic-structure.js";
|
|
15
15
|
import { withWikiWrite } from "../wiki/lock.js";
|
|
16
16
|
import { readWikiPage, teamWikiSync } from "../wiki/team-sync.js";
|
|
17
|
-
import { getAgentRegistry, getAgent, createEphemeralAgentSession,
|
|
17
|
+
import { getAgentRegistry, getAgent, createEphemeralAgentSession, getAgentSessionStatus, getTask, registerTask, completeTask, failTask, createAgentFile, removeAgentFile, loadAgents, } from "./agents.js";
|
|
18
18
|
import { adoGetOkrs, adoOkrSummary, adoUpdateKr } from "../integrations/ado-skill.js";
|
|
19
19
|
import { TeamsNotifier } from "../integrations/teams-notify.js";
|
|
20
20
|
import { TeamPushClient } from "../integrations/team-push.js";
|
|
21
21
|
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
22
|
import { childLogger } from "../util/logger.js";
|
|
27
23
|
const log = childLogger("tools");
|
|
28
|
-
function getCategoryDir(category) {
|
|
29
|
-
const map = {
|
|
30
|
-
person: "people",
|
|
31
|
-
project: "projects",
|
|
32
|
-
preference: "preferences",
|
|
33
|
-
fact: "facts",
|
|
34
|
-
routine: "routines",
|
|
35
|
-
decision: "decisions",
|
|
36
|
-
};
|
|
37
|
-
return map[category] || category;
|
|
38
|
-
}
|
|
39
24
|
/** Escape a string for safe inclusion as a single-line YAML scalar value. */
|
|
40
25
|
function yamlEscape(value) {
|
|
41
26
|
// Always quote and escape backslashes, double quotes, and newlines.
|
|
@@ -112,45 +97,15 @@ export function createTools(deps) {
|
|
|
112
97
|
if (agent?.slug === "chapterhouse") {
|
|
113
98
|
return "Cannot delegate to yourself. Handle this directly or pick a specialist agent.";
|
|
114
99
|
}
|
|
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) {
|
|
100
|
+
if (!agent) {
|
|
126
101
|
const available = getAgentRegistry().map((a) => a.slug).join(", ");
|
|
127
102
|
return `Agent '${args.agent_name}' not found. Available agents: ${available}`;
|
|
128
103
|
}
|
|
129
|
-
const delegatedSlug = agent
|
|
104
|
+
const delegatedSlug = agent.slug;
|
|
130
105
|
let session;
|
|
131
|
-
let isSquadAgent = false;
|
|
132
106
|
try {
|
|
133
107
|
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
|
-
}
|
|
108
|
+
session = await createEphemeralAgentSession(agent.slug, deps.client, allTools, args.model_override);
|
|
154
109
|
}
|
|
155
110
|
catch (err) {
|
|
156
111
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -212,28 +167,6 @@ export function createTools(deps) {
|
|
|
212
167
|
completeTask(task.taskId, output);
|
|
213
168
|
db.prepare(`UPDATE agent_tasks SET status = 'completed', result = ?, completed_at = CURRENT_TIMESTAMP WHERE task_id = ?`).run(output.slice(0, 10000), task.taskId);
|
|
214
169
|
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
170
|
}
|
|
238
171
|
catch (err) {
|
|
239
172
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -253,7 +186,7 @@ export function createTools(deps) {
|
|
|
253
186
|
})();
|
|
254
187
|
const model = (args.model_override && args.model_override.length > 0)
|
|
255
188
|
? args.model_override
|
|
256
|
-
:
|
|
189
|
+
: (agent.model === "auto" ? "claude-sonnet-4.6" : agent.model || "claude-sonnet-4.6");
|
|
257
190
|
return `Task delegated to @${delegatedSlug} (${model}). Task ID: ${task.taskId}. I'll notify you when it's done.`;
|
|
258
191
|
},
|
|
259
192
|
}),
|
|
@@ -323,12 +256,10 @@ export function createTools(deps) {
|
|
|
323
256
|
},
|
|
324
257
|
}),
|
|
325
258
|
defineTool("show_agent_roster", {
|
|
326
|
-
description: "List all registered agents with their name, model, status, and current tasks.
|
|
259
|
+
description: "List all registered agents with their name, model, status, and current tasks.",
|
|
327
260
|
parameters: z.object({}),
|
|
328
261
|
handler: async () => {
|
|
329
262
|
const agents = getAgentRegistry();
|
|
330
|
-
const channelKey = getCurrentChannelKey() ?? "default";
|
|
331
|
-
const projectRoot = config.squadEnabled ? (getChannelProject(channelKey) ?? undefined) : undefined;
|
|
332
263
|
const chLines = agents.map((a) => {
|
|
333
264
|
const status = getAgentSessionStatus(a.slug);
|
|
334
265
|
const runningTasks = status.tasks.filter((t) => t.status === "running");
|
|
@@ -338,25 +269,9 @@ export function createTools(deps) {
|
|
|
338
269
|
: "";
|
|
339
270
|
return `• @${a.slug} (${a.name}) — ${a.model} — ${badge}${taskInfo}\n ${a.description}`;
|
|
340
271
|
});
|
|
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)
|
|
272
|
+
if (chLines.length === 0)
|
|
358
273
|
return "No agents registered.";
|
|
359
|
-
return `Registered agents (${chLines.length}):\n${chLines.join("\n")}
|
|
274
|
+
return `Registered agents (${chLines.length}):\n${chLines.join("\n")}`;
|
|
360
275
|
},
|
|
361
276
|
}),
|
|
362
277
|
defineTool("teams_notify", {
|
|
@@ -584,7 +499,6 @@ export function createTools(deps) {
|
|
|
584
499
|
const session = await deps.client.resumeSession(args.session_id, {
|
|
585
500
|
model: config.copilotModel,
|
|
586
501
|
onPermissionRequest: approveAll,
|
|
587
|
-
hooks: createSessionHooks(args.name),
|
|
588
502
|
});
|
|
589
503
|
const db = getDb();
|
|
590
504
|
db.prepare(`INSERT OR REPLACE INTO agent_sessions (slug, copilot_session_id, model, status)
|
|
@@ -726,55 +640,73 @@ export function createTools(deps) {
|
|
|
726
640
|
}),
|
|
727
641
|
// ----- Wiki-backed memory facades (preserve existing remember/recall/forget UX) -----
|
|
728
642
|
defineTool("remember", {
|
|
729
|
-
description: "Save a fact, preference, or detail to the wiki. Routes to
|
|
643
|
+
description: "Save a fact, preference, or detail to the wiki. Routes to topic pages automatically. " +
|
|
730
644
|
"Use for discrete facts ('The team prefers dark mode', 'Project uses Vercel'). " +
|
|
645
|
+
"Entity categories (project/person/org/tool/topic/area) are filed under pages/<category>/<topic>/index.md; " +
|
|
646
|
+
"pass `entity` for those, and `facet` to file under a sub-page like pages/projects/chapterhouse/feature-ideas.md. " +
|
|
647
|
+
"A `decision` is always recorded against an entity: pass both `about` (which entity category) and `entity`; " +
|
|
648
|
+
"it lands at pages/<about>/<entity>/decisions.md. " +
|
|
731
649
|
"For richer knowledge pages, use wiki_update instead.",
|
|
732
650
|
parameters: z.object({
|
|
733
|
-
category: z.enum(["preference", "fact", "project", "person", "routine", "decision"])
|
|
734
|
-
.describe("Category
|
|
651
|
+
category: z.enum(["preference", "fact", "project", "person", "routine", "decision", "org", "tool", "topic", "area"])
|
|
652
|
+
.describe("Category. Entity categories (need `entity`): project (codebase/repo), person (people), org (companies/teams), tool (software/technologies), topic (knowledge areas), area (areas of responsibility). decision (a choice made — needs `about` + `entity`). Flat categories: preference (likes/dislikes/settings), fact (general knowledge), routine (schedules/habits)."),
|
|
735
653
|
content: z.string().describe("The thing to remember — a concise, self-contained statement"),
|
|
736
|
-
entity: z.string().optional().describe("
|
|
654
|
+
entity: z.string().optional().describe("Required for entity categories and for decisions: the specific topic this is about (e.g. 'chapterhouse', 'vercel', 'Brian'). Routes to pages/<category>/<topic-slug>/…"),
|
|
655
|
+
about: z.enum(["project", "person", "org", "tool", "topic", "area"]).optional()
|
|
656
|
+
.describe("Required only when category='decision': which entity category the decision concerns. The decision is filed at pages/<about>/<entity>/decisions.md."),
|
|
657
|
+
facet: z.string().optional().describe("Optional sub-page within the topic directory (e.g. 'feature-ideas', 'architecture'). Only meaningful with an entity category + `entity`. Defaults to the topic's index page. (For decisions use `about` instead.)"),
|
|
737
658
|
related: z.array(z.string()).optional().describe("Wiki page paths this connects to, for cross-referencing"),
|
|
738
659
|
}),
|
|
739
660
|
handler: async (args) => {
|
|
740
661
|
return withWikiWrite(async () => {
|
|
741
662
|
ensureWikiStructure();
|
|
742
663
|
const now = new Date().toISOString().slice(0, 10);
|
|
743
|
-
|
|
664
|
+
const titleCase = (s) => s.split(/[-_\s]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
665
|
+
// Routing: code-authoritative slugification and page lookup.
|
|
744
666
|
let pagePath;
|
|
745
667
|
let title;
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
// Check for existing page with fuzzy match before creating new
|
|
751
|
-
const existingPages = searchIndex(args.entity, 5);
|
|
752
|
-
const existingMatch = existingPages.find((p) => {
|
|
753
|
-
const pSlug = p.path.split("/").pop()?.replace(".md", "") || "";
|
|
754
|
-
return pSlug === slug || p.title.toLowerCase() === args.entity.toLowerCase();
|
|
755
|
-
});
|
|
756
|
-
if (existingMatch) {
|
|
757
|
-
pagePath = existingMatch.path;
|
|
758
|
-
title = existingMatch.title;
|
|
668
|
+
let routedCategoryDir; // the directory under pages/ this ends up in
|
|
669
|
+
if (args.category === "decision") {
|
|
670
|
+
if (!args.about || !args.entity) {
|
|
671
|
+
return `A decision needs both 'about' (the entity category it concerns — one of: ${entityCategories().join(", ")}) and 'entity' (the specific one, e.g. "chapterhouse"). It will be filed at pages/<about>/<entity>/decisions.md.`;
|
|
759
672
|
}
|
|
760
|
-
|
|
761
|
-
|
|
673
|
+
routedCategoryDir = getCategoryDir(args.about);
|
|
674
|
+
if (!entityCategories().includes(routedCategoryDir)) {
|
|
675
|
+
return `'${args.about}' isn't a known entity category. Use one of: ${entityCategories().join(", ")}.`;
|
|
762
676
|
}
|
|
677
|
+
pagePath = topicPagePath(args.about, args.entity, "decisions");
|
|
678
|
+
const existingMatch = searchIndex(args.entity, 5).find((p) => p.path === pagePath);
|
|
679
|
+
title = existingMatch ? existingMatch.title : `${titleCase(args.entity)} — Decisions`;
|
|
763
680
|
}
|
|
764
681
|
else {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
682
|
+
routedCategoryDir = getCategoryDir(args.category);
|
|
683
|
+
const isFlat = FLAT_CATEGORIES.includes(routedCategoryDir);
|
|
684
|
+
if (!isFlat) {
|
|
685
|
+
if (!args.entity) {
|
|
686
|
+
return `The '${args.category}' category needs an 'entity' (the specific ${args.category} this is about) so it can be filed under pages/${routedCategoryDir}/<topic>/. Re-call remember with an entity, e.g. entity: "chapterhouse".`;
|
|
687
|
+
}
|
|
688
|
+
const facet = args.facet ? slugify(args.facet) : "index";
|
|
689
|
+
pagePath = topicPagePath(args.category, args.entity, facet);
|
|
690
|
+
// Reuse an existing page if the index already has one at (or matching) this path.
|
|
691
|
+
const existingPages = searchIndex(args.entity, 5);
|
|
692
|
+
const existingMatch = existingPages.find((p) => p.path === pagePath ||
|
|
693
|
+
(facet === "index" && p.title.toLowerCase() === args.entity.toLowerCase() && p.path.startsWith(`pages/${routedCategoryDir}/`)));
|
|
694
|
+
if (existingMatch) {
|
|
695
|
+
pagePath = existingMatch.path;
|
|
696
|
+
title = existingMatch.title;
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
const topicTitle = titleCase(args.entity);
|
|
700
|
+
title = facet === "index" ? topicTitle : `${topicTitle} — ${titleCase(facet)}`;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
pagePath = `pages/${routedCategoryDir}.md`;
|
|
705
|
+
title = titleCase(routedCategoryDir);
|
|
706
|
+
}
|
|
775
707
|
}
|
|
776
708
|
// Defense-in-depth: pagePath is constructed from controlled parts but
|
|
777
|
-
// assertPagePath
|
|
709
|
+
// assertPagePath also enforces the topic-structure rules.
|
|
778
710
|
assertPagePath(pagePath);
|
|
779
711
|
const existing = readPage(pagePath);
|
|
780
712
|
if (existing) {
|
|
@@ -829,7 +761,7 @@ export function createTools(deps) {
|
|
|
829
761
|
"Use when you need to look up something the user told you, or when asked 'do you remember...?'",
|
|
830
762
|
parameters: z.object({
|
|
831
763
|
keyword: z.string().optional().describe("Search term to match against wiki pages"),
|
|
832
|
-
category: z.enum(["preference", "fact", "project", "person", "routine", "decision"]).optional()
|
|
764
|
+
category: z.enum(["preference", "fact", "project", "person", "routine", "decision", "org", "tool", "topic", "area"]).optional()
|
|
833
765
|
.describe("Optional: filter by category"),
|
|
834
766
|
}),
|
|
835
767
|
handler: async (args) => {
|
|
@@ -992,9 +924,12 @@ export function createTools(deps) {
|
|
|
992
924
|
}),
|
|
993
925
|
defineTool("wiki_read", {
|
|
994
926
|
description: "Read a specific wiki page by path. Use after wiki_search to read full page content. " +
|
|
995
|
-
"Paths are relative to the wiki root (
|
|
927
|
+
"Paths are relative to the wiki root. Layout: entity categories (projects, people, orgs, tools, " +
|
|
928
|
+
"topics, areas) live at 'pages/<category>/<topic>/index.md' (the topic overview) plus optional " +
|
|
929
|
+
"'pages/<category>/<topic>/<facet>.md' sub-pages; flat categories at 'pages/<category>.md' " +
|
|
930
|
+
"(preferences, facts, routines, decisions); daily summaries at 'pages/conversations/YYYY-MM-DD.md'.",
|
|
996
931
|
parameters: z.object({
|
|
997
|
-
path: z.string().describe("Path to the wiki page (e.g. 'pages/people/brian.md', 'index.md')"),
|
|
932
|
+
path: z.string().describe("Path to the wiki page (e.g. 'pages/people/brian/index.md', 'pages/projects/chapterhouse/decisions.md', 'index.md')"),
|
|
998
933
|
}),
|
|
999
934
|
handler: async (args) => {
|
|
1000
935
|
ensureWikiStructure();
|
|
@@ -1008,9 +943,14 @@ export function createTools(deps) {
|
|
|
1008
943
|
description: "Create or update a wiki page. You provide the full page content (markdown with optional " +
|
|
1009
944
|
"YAML frontmatter). The page will be written to disk and the index updated. Use this for " +
|
|
1010
945
|
"rich knowledge pages, entity pages, synthesis documents — anything more structured than " +
|
|
1011
|
-
"a quick 'remember' call. After creating/updating a page, the index is automatically updated."
|
|
946
|
+
"a quick 'remember' call. After creating/updating a page, the index is automatically updated. " +
|
|
947
|
+
"PATH RULES: entity-category pages MUST be 'pages/<category>/<topic-slug>/<page>.md' where " +
|
|
948
|
+
"category is one of projects, people, orgs, tools, topics, areas, '<page>' is 'index' for the " +
|
|
949
|
+
"topic overview or a facet name (e.g. 'decisions', 'feature-ideas') — exactly one topic level, " +
|
|
950
|
+
"lowercase slugs only. Flat-category pages MUST be 'pages/<category>.md' (preferences, facts, " +
|
|
951
|
+
"routines, decisions). Bad paths are rejected with a suggested correction.",
|
|
1012
952
|
parameters: z.object({
|
|
1013
|
-
path: z.string().describe("Page path relative to wiki root (e.g. 'pages/projects/
|
|
953
|
+
path: z.string().describe("Page path relative to wiki root (e.g. 'pages/projects/chapterhouse/index.md', 'pages/projects/chapterhouse/decisions.md', 'pages/people/brian/index.md')"),
|
|
1014
954
|
title: z.string().describe("Page title for the index"),
|
|
1015
955
|
summary: z.string().describe("One-line summary for the index"),
|
|
1016
956
|
section: z.string().optional().describe("Index section (default: 'Knowledge')"),
|