chapterhouse 0.3.12 → 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 +15 -157
- package/dist/api/server.test.js +1 -1
- package/dist/api/turn-sse.integration.test.js +36 -0
- 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 +60 -65
- 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-BR2cks94.js +0 -219
- package/web/dist/assets/index-BR2cks94.js.map +0 -1
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for src/copilot/hooks.ts
|
|
3
|
-
*
|
|
4
|
-
* Tests per Mal's spec §6:
|
|
5
|
-
* - One test per hook firing (block cases)
|
|
6
|
-
* - One test per hook bypass attempt (allow cases)
|
|
7
|
-
* - Integration: createSessionHooks adapter translates HookPipeline results to SDK format
|
|
8
|
-
*/
|
|
9
|
-
import { describe, it, before, after } from "node:test";
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import { HookPipeline } from "@bradygaster/squad-sdk/hooks";
|
|
12
|
-
import { initHookPipeline, getHookPipeline, createSessionHooks, DEFAULT_ALLOWED_PATHS } from "./hooks.js";
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// HookPipeline unit tests — directly exercising the squad-sdk class
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
describe("HookPipeline — file-write guard", () => {
|
|
17
|
-
it("blocks write to path outside allowlist", async () => {
|
|
18
|
-
const pipeline = new HookPipeline({ allowedWritePaths: ["src/**"] });
|
|
19
|
-
const result = await pipeline.runPreToolHooks({
|
|
20
|
-
toolName: "edit",
|
|
21
|
-
arguments: { path: "/etc/passwd" },
|
|
22
|
-
agentName: "test-agent",
|
|
23
|
-
sessionId: "test-1",
|
|
24
|
-
});
|
|
25
|
-
assert.equal(result.action, "block");
|
|
26
|
-
assert.ok(result.reason?.includes("/etc/passwd"), `reason should mention path, got: ${result.reason}`);
|
|
27
|
-
});
|
|
28
|
-
it("allows write to path inside allowlist", async () => {
|
|
29
|
-
const pipeline = new HookPipeline({ allowedWritePaths: ["src/**"] });
|
|
30
|
-
const result = await pipeline.runPreToolHooks({
|
|
31
|
-
toolName: "edit",
|
|
32
|
-
arguments: { path: "src/copilot/hooks.ts" },
|
|
33
|
-
agentName: "test-agent",
|
|
34
|
-
sessionId: "test-1",
|
|
35
|
-
});
|
|
36
|
-
assert.equal(result.action, "allow");
|
|
37
|
-
});
|
|
38
|
-
it("allows non-write tools regardless of path", async () => {
|
|
39
|
-
const pipeline = new HookPipeline({ allowedWritePaths: ["src/**"] });
|
|
40
|
-
const result = await pipeline.runPreToolHooks({
|
|
41
|
-
toolName: "bash",
|
|
42
|
-
arguments: { command: "echo hello", path: "/etc/passwd" },
|
|
43
|
-
agentName: "test-agent",
|
|
44
|
-
sessionId: "test-1",
|
|
45
|
-
});
|
|
46
|
-
// File-write guard only fires on edit/create/write_file/create_file
|
|
47
|
-
// This will hit the shell restriction instead (echo is safe)
|
|
48
|
-
assert.ok(result.action === "allow" || result.action === "block");
|
|
49
|
-
if (result.action === "block") {
|
|
50
|
-
// Should be shell restriction, not file-write
|
|
51
|
-
assert.ok(!result.reason?.includes("/etc/passwd"));
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
describe("HookPipeline — shell command restriction", () => {
|
|
56
|
-
it("blocks rm -rf", async () => {
|
|
57
|
-
const pipeline = new HookPipeline({});
|
|
58
|
-
const result = await pipeline.runPreToolHooks({
|
|
59
|
-
toolName: "bash",
|
|
60
|
-
arguments: { command: "rm -rf /important" },
|
|
61
|
-
agentName: "test-agent",
|
|
62
|
-
sessionId: "test-2",
|
|
63
|
-
});
|
|
64
|
-
assert.equal(result.action, "block");
|
|
65
|
-
assert.ok(result.reason?.toLowerCase().includes("rm -rf"), `reason should mention rm -rf, got: ${result.reason}`);
|
|
66
|
-
});
|
|
67
|
-
it("blocks git push --force", async () => {
|
|
68
|
-
const pipeline = new HookPipeline({});
|
|
69
|
-
const result = await pipeline.runPreToolHooks({
|
|
70
|
-
toolName: "bash",
|
|
71
|
-
arguments: { command: "git push --force origin main" },
|
|
72
|
-
agentName: "test-agent",
|
|
73
|
-
sessionId: "test-2",
|
|
74
|
-
});
|
|
75
|
-
assert.equal(result.action, "block");
|
|
76
|
-
assert.ok(result.reason?.toLowerCase().includes("git push --force"), `reason should mention force push, got: ${result.reason}`);
|
|
77
|
-
});
|
|
78
|
-
it("allows safe shell commands", async () => {
|
|
79
|
-
const pipeline = new HookPipeline({});
|
|
80
|
-
const result = await pipeline.runPreToolHooks({
|
|
81
|
-
toolName: "bash",
|
|
82
|
-
arguments: { command: "npm test" },
|
|
83
|
-
agentName: "test-agent",
|
|
84
|
-
sessionId: "test-2",
|
|
85
|
-
});
|
|
86
|
-
assert.equal(result.action, "allow");
|
|
87
|
-
});
|
|
88
|
-
it("allows non-shell tools regardless of args", async () => {
|
|
89
|
-
const pipeline = new HookPipeline({});
|
|
90
|
-
const result = await pipeline.runPreToolHooks({
|
|
91
|
-
toolName: "read_file",
|
|
92
|
-
arguments: { command: "rm -rf /" },
|
|
93
|
-
agentName: "test-agent",
|
|
94
|
-
sessionId: "test-2",
|
|
95
|
-
});
|
|
96
|
-
// Shell restriction only fires on bash/powershell/shell/exec
|
|
97
|
-
assert.equal(result.action, "allow");
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
describe("HookPipeline — PII scrubber", () => {
|
|
101
|
-
it("redacts email addresses in tool output", async () => {
|
|
102
|
-
const pipeline = new HookPipeline({ scrubPii: true });
|
|
103
|
-
const result = await pipeline.runPostToolHooks({
|
|
104
|
-
toolName: "bash",
|
|
105
|
-
arguments: {},
|
|
106
|
-
result: "User email is brian@example.com and also admin@test.org",
|
|
107
|
-
agentName: "test-agent",
|
|
108
|
-
sessionId: "test-3",
|
|
109
|
-
});
|
|
110
|
-
assert.ok(typeof result.result === "string" && !result.result.includes("brian@example.com"), `Email should be redacted, got: ${result.result}`);
|
|
111
|
-
assert.ok(typeof result.result === "string" && result.result.includes("[EMAIL_REDACTED]"), `Should contain redaction marker, got: ${result.result}`);
|
|
112
|
-
});
|
|
113
|
-
it("passes through clean output unchanged", async () => {
|
|
114
|
-
const pipeline = new HookPipeline({ scrubPii: true });
|
|
115
|
-
const original = "Build succeeded. 405 tests passed.";
|
|
116
|
-
const result = await pipeline.runPostToolHooks({
|
|
117
|
-
toolName: "bash",
|
|
118
|
-
arguments: {},
|
|
119
|
-
result: original,
|
|
120
|
-
agentName: "test-agent",
|
|
121
|
-
sessionId: "test-3",
|
|
122
|
-
});
|
|
123
|
-
assert.equal(result.result, original);
|
|
124
|
-
});
|
|
125
|
-
it("does not scrub when scrubPii is false", async () => {
|
|
126
|
-
const pipeline = new HookPipeline({ scrubPii: false });
|
|
127
|
-
const original = "Email: test@example.com";
|
|
128
|
-
const result = await pipeline.runPostToolHooks({
|
|
129
|
-
toolName: "bash",
|
|
130
|
-
arguments: {},
|
|
131
|
-
result: original,
|
|
132
|
-
agentName: "test-agent",
|
|
133
|
-
sessionId: "test-3",
|
|
134
|
-
});
|
|
135
|
-
assert.equal(result.result, original);
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
describe("HookPipeline — reviewer lockout", () => {
|
|
139
|
-
it("blocks locked-out agent from editing artifact", async () => {
|
|
140
|
-
const pipeline = new HookPipeline({ reviewerLockout: true });
|
|
141
|
-
pipeline.getReviewerLockout().lockout("src/auth.ts", "kaylee");
|
|
142
|
-
const result = await pipeline.runPreToolHooks({
|
|
143
|
-
toolName: "edit",
|
|
144
|
-
arguments: { path: "src/auth.ts" },
|
|
145
|
-
agentName: "kaylee",
|
|
146
|
-
sessionId: "test-4",
|
|
147
|
-
});
|
|
148
|
-
assert.equal(result.action, "block");
|
|
149
|
-
assert.ok(result.reason?.includes("kaylee"), `reason should name agent, got: ${result.reason}`);
|
|
150
|
-
});
|
|
151
|
-
it("allows non-locked-out agent to edit same artifact", async () => {
|
|
152
|
-
const pipeline = new HookPipeline({ reviewerLockout: true, allowedWritePaths: ["src/**"] });
|
|
153
|
-
pipeline.getReviewerLockout().lockout("src/auth.ts", "kaylee");
|
|
154
|
-
const result = await pipeline.runPreToolHooks({
|
|
155
|
-
toolName: "edit",
|
|
156
|
-
arguments: { path: "src/auth.ts" },
|
|
157
|
-
agentName: "zoe",
|
|
158
|
-
sessionId: "test-4",
|
|
159
|
-
});
|
|
160
|
-
assert.equal(result.action, "allow");
|
|
161
|
-
});
|
|
162
|
-
it("allows locked-out agent to edit different artifact", async () => {
|
|
163
|
-
const pipeline = new HookPipeline({ reviewerLockout: true, allowedWritePaths: ["src/**"] });
|
|
164
|
-
pipeline.getReviewerLockout().lockout("src/auth.ts", "kaylee");
|
|
165
|
-
const result = await pipeline.runPreToolHooks({
|
|
166
|
-
toolName: "edit",
|
|
167
|
-
arguments: { path: "src/copilot/hooks.ts" },
|
|
168
|
-
agentName: "kaylee",
|
|
169
|
-
sessionId: "test-4",
|
|
170
|
-
});
|
|
171
|
-
assert.equal(result.action, "allow");
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
|
-
// initHookPipeline / getHookPipeline — singleton lifecycle
|
|
176
|
-
// ---------------------------------------------------------------------------
|
|
177
|
-
describe("initHookPipeline", () => {
|
|
178
|
-
it("initializes the singleton pipeline", () => {
|
|
179
|
-
const p = initHookPipeline();
|
|
180
|
-
assert.ok(p instanceof HookPipeline);
|
|
181
|
-
assert.equal(getHookPipeline(), p);
|
|
182
|
-
});
|
|
183
|
-
it("reinitializes on second call (returns new instance)", () => {
|
|
184
|
-
const first = initHookPipeline();
|
|
185
|
-
const second = initHookPipeline();
|
|
186
|
-
assert.ok(second instanceof HookPipeline);
|
|
187
|
-
// Both are valid instances; the singleton points to the latest
|
|
188
|
-
assert.equal(getHookPipeline(), second);
|
|
189
|
-
});
|
|
190
|
-
it("respects policy overrides", async () => {
|
|
191
|
-
// Override: disable PII scrubbing
|
|
192
|
-
initHookPipeline({ scrubPii: false });
|
|
193
|
-
const p = getHookPipeline();
|
|
194
|
-
const result = await p.runPostToolHooks({
|
|
195
|
-
toolName: "bash",
|
|
196
|
-
arguments: {},
|
|
197
|
-
result: "email: test@example.com",
|
|
198
|
-
agentName: "test-agent",
|
|
199
|
-
sessionId: "test-5",
|
|
200
|
-
});
|
|
201
|
-
// With scrubPii: false, email is NOT redacted
|
|
202
|
-
assert.equal(result.result, "email: test@example.com");
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
// ---------------------------------------------------------------------------
|
|
206
|
-
// createSessionHooks — adapter tests (SDK interface bridging)
|
|
207
|
-
// ---------------------------------------------------------------------------
|
|
208
|
-
describe("createSessionHooks", () => {
|
|
209
|
-
before(() => {
|
|
210
|
-
// Fresh pipeline with defaults for adapter tests
|
|
211
|
-
initHookPipeline({
|
|
212
|
-
allowedWritePaths: DEFAULT_ALLOWED_PATHS,
|
|
213
|
-
scrubPii: true,
|
|
214
|
-
reviewerLockout: true,
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
const fakeInvocation = { sessionId: "adapter-test-session" };
|
|
218
|
-
it("returns deny for blocked write (SDK adapter output)", async () => {
|
|
219
|
-
const hooks = createSessionHooks("test-agent");
|
|
220
|
-
const result = await hooks.onPreToolUse({
|
|
221
|
-
toolName: "edit",
|
|
222
|
-
toolArgs: { path: "/etc/passwd" },
|
|
223
|
-
timestamp: Date.now(),
|
|
224
|
-
cwd: process.cwd(),
|
|
225
|
-
}, fakeInvocation);
|
|
226
|
-
assert.ok(result, "Should return a result object");
|
|
227
|
-
assert.equal(result.permissionDecision, "deny");
|
|
228
|
-
assert.ok(result.permissionDecisionReason?.includes("[BLOCKED]"), `reason should start with [BLOCKED], got: ${result.permissionDecisionReason}`);
|
|
229
|
-
});
|
|
230
|
-
it("returns undefined (allow) for safe write inside default paths", async () => {
|
|
231
|
-
const hooks = createSessionHooks("test-agent");
|
|
232
|
-
const result = await hooks.onPreToolUse({
|
|
233
|
-
toolName: "edit",
|
|
234
|
-
toolArgs: { path: "src/copilot/hooks.ts" },
|
|
235
|
-
timestamp: Date.now(),
|
|
236
|
-
cwd: process.cwd(),
|
|
237
|
-
}, fakeInvocation);
|
|
238
|
-
// Allowed — should return void/undefined
|
|
239
|
-
assert.ok(result == null || result.permissionDecision !== "deny");
|
|
240
|
-
});
|
|
241
|
-
it("returns deny for blocked shell command", async () => {
|
|
242
|
-
const hooks = createSessionHooks("test-agent");
|
|
243
|
-
const result = await hooks.onPreToolUse({
|
|
244
|
-
toolName: "bash",
|
|
245
|
-
toolArgs: { command: "rm -rf /" },
|
|
246
|
-
timestamp: Date.now(),
|
|
247
|
-
cwd: process.cwd(),
|
|
248
|
-
}, fakeInvocation);
|
|
249
|
-
assert.ok(result, "Should return a result object");
|
|
250
|
-
assert.equal(result.permissionDecision, "deny");
|
|
251
|
-
});
|
|
252
|
-
it("strips CWD prefix from absolute paths before hook check", async () => {
|
|
253
|
-
const hooks = createSessionHooks("test-agent");
|
|
254
|
-
const absPath = `${process.cwd()}/src/copilot/hooks.ts`;
|
|
255
|
-
const result = await hooks.onPreToolUse({
|
|
256
|
-
toolName: "edit",
|
|
257
|
-
toolArgs: { path: absPath },
|
|
258
|
-
timestamp: Date.now(),
|
|
259
|
-
cwd: process.cwd(),
|
|
260
|
-
}, fakeInvocation);
|
|
261
|
-
// After stripping CWD, "src/copilot/hooks.ts" matches "src/**" → allow
|
|
262
|
-
assert.ok(result == null || result.permissionDecision !== "deny", "Absolute path within project should be allowed");
|
|
263
|
-
});
|
|
264
|
-
it("scrubs PII from tool output via onPostToolUse adapter", async () => {
|
|
265
|
-
const hooks = createSessionHooks("test-agent");
|
|
266
|
-
const fakeToolResult = {
|
|
267
|
-
textResultForLlm: "Contact admin@example.com for support",
|
|
268
|
-
resultType: "success",
|
|
269
|
-
};
|
|
270
|
-
const result = await hooks.onPostToolUse({
|
|
271
|
-
toolName: "bash",
|
|
272
|
-
toolArgs: {},
|
|
273
|
-
toolResult: fakeToolResult,
|
|
274
|
-
timestamp: Date.now(),
|
|
275
|
-
cwd: process.cwd(),
|
|
276
|
-
}, fakeInvocation);
|
|
277
|
-
assert.ok(result, "Should return a modified result");
|
|
278
|
-
assert.ok(result.modifiedResult?.textResultForLlm.includes("[EMAIL_REDACTED]"), `Should redact email, got: ${result.modifiedResult?.textResultForLlm}`);
|
|
279
|
-
});
|
|
280
|
-
it("returns undefined from onPostToolUse when nothing to scrub", async () => {
|
|
281
|
-
const hooks = createSessionHooks("test-agent");
|
|
282
|
-
const fakeToolResult = {
|
|
283
|
-
textResultForLlm: "All 405 tests passed.",
|
|
284
|
-
resultType: "success",
|
|
285
|
-
};
|
|
286
|
-
const result = await hooks.onPostToolUse({
|
|
287
|
-
toolName: "bash",
|
|
288
|
-
toolArgs: {},
|
|
289
|
-
toolResult: fakeToolResult,
|
|
290
|
-
timestamp: Date.now(),
|
|
291
|
-
cwd: process.cwd(),
|
|
292
|
-
}, fakeInvocation);
|
|
293
|
-
// No PII → no modification → return undefined
|
|
294
|
-
assert.ok(result == null, "Should return undefined when no scrubbing needed");
|
|
295
|
-
});
|
|
296
|
-
it("uses per-agent allowedPaths when provided", async () => {
|
|
297
|
-
// Agent restricted to .squad/** only
|
|
298
|
-
const hooks = createSessionHooks("restricted-agent", [".squad/**"]);
|
|
299
|
-
const result = await hooks.onPreToolUse({
|
|
300
|
-
toolName: "edit",
|
|
301
|
-
toolArgs: { path: "src/copilot/hooks.ts" },
|
|
302
|
-
timestamp: Date.now(),
|
|
303
|
-
cwd: process.cwd(),
|
|
304
|
-
}, fakeInvocation);
|
|
305
|
-
// src/** is in DEFAULT_ALLOWED_PATHS (global) but NOT in restricted agent's paths
|
|
306
|
-
// The per-agent check runs first and should block it
|
|
307
|
-
assert.ok(result, "Should return a result object");
|
|
308
|
-
assert.equal(result.permissionDecision, "deny");
|
|
309
|
-
});
|
|
310
|
-
after(() => {
|
|
311
|
-
// Restore default pipeline for any tests that follow
|
|
312
|
-
initHookPipeline();
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
//# sourceMappingURL=hooks.test.js.map
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Process-scoped singleton EventBus for squad agent lifecycle events.
|
|
3
|
-
*
|
|
4
|
-
* The SDK `EventBus` is a plain class — no hidden global state. We own
|
|
5
|
-
* the singleton here so the entire daemon process shares one bus. All
|
|
6
|
-
* code that emits or subscribes to squad events imports from this module.
|
|
7
|
-
*
|
|
8
|
-
* Cross-project reach: NOT supported with a single bus instance. Requires
|
|
9
|
-
* the SDK's WS bridge (`startWSBridge`) or a shared IPC channel. Out of
|
|
10
|
-
* scope for #101 — see mal-101-architecture.md §Forward Implications.
|
|
11
|
-
*
|
|
12
|
-
* Exported event types mirror `SquadEventPayloadMap` from the SDK:
|
|
13
|
-
* session:created — sub-agent spawned
|
|
14
|
-
* session:idle — sub-agent waiting / between turns
|
|
15
|
-
* session:error — sub-agent failed
|
|
16
|
-
* session:destroyed — sub-agent finished (reason: complete | error | abort)
|
|
17
|
-
* session:tool_call — tool invoked (resultType undefined=start, defined=end)
|
|
18
|
-
* agent:milestone — model fallback / exhaustion
|
|
19
|
-
* coordinator:routing — coordinator routing decision phases
|
|
20
|
-
* pool:health — session pool stats
|
|
21
|
-
*
|
|
22
|
-
* @module copilot/squad-event-bus
|
|
23
|
-
*/
|
|
24
|
-
import { EventBus } from "@bradygaster/squad-sdk/runtime/event-bus";
|
|
25
|
-
/** Process-level singleton. Import this everywhere instead of newing EventBus. */
|
|
26
|
-
export const squadEventBus = new EventBus();
|
|
27
|
-
//# sourceMappingURL=squad-event-bus.js.map
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
const fixtureProjectRoot = `${process.cwd()}/src/test/fixtures/mock-squad-repo`;
|
|
4
|
-
async function loadToolsModule(t, state) {
|
|
5
|
-
const fakeSession = {
|
|
6
|
-
on() {
|
|
7
|
-
return () => { };
|
|
8
|
-
},
|
|
9
|
-
async sendAndWait() {
|
|
10
|
-
return { data: { content: "Squad result" } };
|
|
11
|
-
},
|
|
12
|
-
async destroy() { },
|
|
13
|
-
};
|
|
14
|
-
t.mock.module("../config.js", {
|
|
15
|
-
namedExports: {
|
|
16
|
-
config: {
|
|
17
|
-
workerTimeoutMs: 1_000,
|
|
18
|
-
chapterhouseMode: "personal",
|
|
19
|
-
adoPat: "",
|
|
20
|
-
squadEnabled: true,
|
|
21
|
-
copilotModel: "claude-sonnet-4.6",
|
|
22
|
-
},
|
|
23
|
-
persistModel: () => { },
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
t.mock.module("./orchestrator.js", {
|
|
27
|
-
namedExports: {
|
|
28
|
-
getCurrentSourceChannel: () => "web",
|
|
29
|
-
getCurrentActivityCallback: () => undefined,
|
|
30
|
-
getCurrentAuthenticatedUser: () => undefined,
|
|
31
|
-
getLastAuthenticatedUser: () => undefined,
|
|
32
|
-
getCurrentAuthorizationHeader: () => undefined,
|
|
33
|
-
getCurrentChannelKey: () => "conn-squad",
|
|
34
|
-
getCurrentSessionKey: () => "default",
|
|
35
|
-
switchSessionModel: async () => { },
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
t.mock.module("./agents.js", {
|
|
39
|
-
namedExports: {
|
|
40
|
-
getAgentRegistry: () => [{ slug: "coder" }, { slug: "designer" }],
|
|
41
|
-
getAgent: () => undefined,
|
|
42
|
-
createEphemeralAgentSession: async () => {
|
|
43
|
-
throw new Error("should not create a Chapterhouse registry agent session");
|
|
44
|
-
},
|
|
45
|
-
createSquadAgentSession: async (slug, _client, allTools, systemMessagePrefix, modelOverride) => {
|
|
46
|
-
state.createSquadSessionCalls.push({
|
|
47
|
-
slug,
|
|
48
|
-
toolNames: allTools.map((tool) => tool.name),
|
|
49
|
-
systemMessagePrefix,
|
|
50
|
-
modelOverride,
|
|
51
|
-
});
|
|
52
|
-
return fakeSession;
|
|
53
|
-
},
|
|
54
|
-
getAgentSessionStatus: () => ({ taskCount: 0, tasks: [] }),
|
|
55
|
-
getActiveTasks: () => [],
|
|
56
|
-
getTask: () => undefined,
|
|
57
|
-
registerTask: (agentSlug, description, originChannel) => ({
|
|
58
|
-
taskId: "task-squad-1",
|
|
59
|
-
agentSlug,
|
|
60
|
-
description,
|
|
61
|
-
status: "running",
|
|
62
|
-
startedAt: Date.now(),
|
|
63
|
-
originChannel,
|
|
64
|
-
}),
|
|
65
|
-
completeTask: () => { },
|
|
66
|
-
failTask: () => { },
|
|
67
|
-
createAgentFile: () => null,
|
|
68
|
-
removeAgentFile: () => null,
|
|
69
|
-
loadAgents: () => [],
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
t.mock.module("../squad/context.js", {
|
|
73
|
-
namedExports: {
|
|
74
|
-
getChannelProject: () => fixtureProjectRoot,
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
|
-
t.mock.module("../squad/registry.js", {
|
|
78
|
-
namedExports: {
|
|
79
|
-
findSquadAgent: (_projectRoot, mention) => mention === "ripley"
|
|
80
|
-
? {
|
|
81
|
-
slug: "ripley",
|
|
82
|
-
mention: "@ripley",
|
|
83
|
-
role: "Lead",
|
|
84
|
-
charterPath: `${fixtureProjectRoot}/.squad/agents/ripley/charter.md`,
|
|
85
|
-
origin: "project-squad",
|
|
86
|
-
}
|
|
87
|
-
: null,
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
t.mock.module("../squad/charter.js", {
|
|
91
|
-
namedExports: {
|
|
92
|
-
buildSquadSystemPrefix: (context, descriptor) => {
|
|
93
|
-
state.charterCalls.push({ projectRoot: context.projectRoot, slug: descriptor.slug });
|
|
94
|
-
return "SQUAD SYSTEM PREFIX";
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
t.mock.module("../squad/mirror.js", {
|
|
99
|
-
namedExports: {
|
|
100
|
-
mirrorDecisionToWiki: async (link, taskSummary, resultSummary) => {
|
|
101
|
-
state.mirrorCalls.push({ link, taskSummary, resultSummary });
|
|
102
|
-
return "pages/projects/mock-squad-repo/decisions.md";
|
|
103
|
-
},
|
|
104
|
-
syncDecisionsFileToWiki: async (_projectRoot) => null,
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
t.mock.module("../store/db.js", {
|
|
108
|
-
namedExports: {
|
|
109
|
-
getDb: () => ({
|
|
110
|
-
prepare: (sql) => ({
|
|
111
|
-
run: (...args) => {
|
|
112
|
-
state.dbWrites.push({ sql, args });
|
|
113
|
-
return {};
|
|
114
|
-
},
|
|
115
|
-
get: () => undefined,
|
|
116
|
-
all: () => [],
|
|
117
|
-
}),
|
|
118
|
-
}),
|
|
119
|
-
getState: () => undefined,
|
|
120
|
-
setState: () => { },
|
|
121
|
-
logConversation: () => { },
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
t.mock.module("../wiki/team-sync.js", {
|
|
125
|
-
namedExports: {
|
|
126
|
-
readWikiPage: async () => undefined,
|
|
127
|
-
teamWikiSync: {
|
|
128
|
-
isEnabled: () => false,
|
|
129
|
-
syncAll: async () => { },
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
return await import(new URL(`./tools.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
134
|
-
}
|
|
135
|
-
test("delegate_to_agent routes squad virtual agents with charter injection and mirrors results", async (t) => {
|
|
136
|
-
const state = {
|
|
137
|
-
taskCompleteCalls: [],
|
|
138
|
-
createSquadSessionCalls: [],
|
|
139
|
-
charterCalls: [],
|
|
140
|
-
mirrorCalls: [],
|
|
141
|
-
dbWrites: [],
|
|
142
|
-
};
|
|
143
|
-
const toolsModule = await loadToolsModule(t, state);
|
|
144
|
-
const tools = toolsModule.createTools({
|
|
145
|
-
client: {},
|
|
146
|
-
onAgentTaskComplete: (taskId, agentSlug, result) => {
|
|
147
|
-
state.taskCompleteCalls.push({ taskId, agentSlug, result });
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
const delegateTool = tools.find((tool) => tool.name === "delegate_to_agent");
|
|
151
|
-
assert.ok(delegateTool, "delegate_to_agent should exist");
|
|
152
|
-
const result = await delegateTool.handler({
|
|
153
|
-
agent_name: "ripley",
|
|
154
|
-
task: "Audit the squad routing",
|
|
155
|
-
summary: "Audit squad routing",
|
|
156
|
-
}, {});
|
|
157
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
158
|
-
assert.match(String(result), /@ripley/);
|
|
159
|
-
assert.equal(state.createSquadSessionCalls.length, 1);
|
|
160
|
-
assert.deepEqual(state.charterCalls, [{ projectRoot: fixtureProjectRoot, slug: "ripley" }]);
|
|
161
|
-
assert.equal(state.taskCompleteCalls[0]?.result, "Squad result");
|
|
162
|
-
assert.equal(state.mirrorCalls.length, 1);
|
|
163
|
-
assert.equal(state.mirrorCalls[0]?.taskSummary, "Audit squad routing");
|
|
164
|
-
assert.equal(state.mirrorCalls[0]?.resultSummary, "Squad result");
|
|
165
|
-
assert.ok(state.dbWrites.some((write) => write.sql.includes("INSERT INTO agent_tasks")));
|
|
166
|
-
assert.ok(state.dbWrites.some((write) => write.sql.includes("squad_task_links")));
|
|
167
|
-
});
|
|
168
|
-
//# sourceMappingURL=tools.squad.test.js.map
|
package/dist/squad/charter.js
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from 'fs';
|
|
2
|
-
/**
|
|
3
|
-
* Read `context.decisionsPath`, return the last `maxChars` characters.
|
|
4
|
-
* Returns an empty string if the file doesn't exist or cannot be read.
|
|
5
|
-
*/
|
|
6
|
-
export function readRecentDecisions(context, maxChars = 4000) {
|
|
7
|
-
try {
|
|
8
|
-
if (!existsSync(context.decisionsPath))
|
|
9
|
-
return '';
|
|
10
|
-
const content = readFileSync(context.decisionsPath, 'utf-8');
|
|
11
|
-
if (content.length <= maxChars)
|
|
12
|
-
return content;
|
|
13
|
-
return content.slice(content.length - maxChars);
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
return '';
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Build the system prefix that is prepended to a squad agent's system prompt.
|
|
21
|
-
* Includes the project context, the agent's charter, and recent decisions.
|
|
22
|
-
*/
|
|
23
|
-
export function buildSquadSystemPrefix(context, agent) {
|
|
24
|
-
let charter = '';
|
|
25
|
-
try {
|
|
26
|
-
charter = readFileSync(agent.charterPath, 'utf-8');
|
|
27
|
-
}
|
|
28
|
-
catch { /* non-fatal */ }
|
|
29
|
-
const decisions = readRecentDecisions(context) || '(none)';
|
|
30
|
-
return [
|
|
31
|
-
'## Squad project context',
|
|
32
|
-
`Project root: ${context.projectRoot}`,
|
|
33
|
-
`Squad dir: ${context.squadDir}`,
|
|
34
|
-
'',
|
|
35
|
-
'## Agent charter',
|
|
36
|
-
charter,
|
|
37
|
-
'',
|
|
38
|
-
'## Recent squad decisions',
|
|
39
|
-
decisions,
|
|
40
|
-
].join('\n');
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Build the delegation task prompt that is sent to a squad agent when Chapterhouse
|
|
44
|
-
* delegates work on behalf of the user.
|
|
45
|
-
*/
|
|
46
|
-
export function buildSquadDelegationTask(context, agent, task) {
|
|
47
|
-
return [
|
|
48
|
-
`You are ${agent.mention} (${agent.role}) working on project: ${context.projectRoot}`,
|
|
49
|
-
'',
|
|
50
|
-
task,
|
|
51
|
-
].join('\n');
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Build the system message for a Squad Coordinator session attached to a specific project.
|
|
55
|
-
*
|
|
56
|
-
* Resolution order for coordinator instructions:
|
|
57
|
-
* 1. ${projectRoot}/.github/agents/squad.agent.md
|
|
58
|
-
* 2. ${projectRoot}/squad.agent.md
|
|
59
|
-
* Throws if neither is found.
|
|
60
|
-
*/
|
|
61
|
-
export async function getSquadCoordinatorSystemMessage(projectRoot) {
|
|
62
|
-
// 1. Coordinator instructions
|
|
63
|
-
const candidatePaths = [
|
|
64
|
-
`${projectRoot}/.github/agents/squad.agent.md`,
|
|
65
|
-
`${projectRoot}/squad.agent.md`,
|
|
66
|
-
];
|
|
67
|
-
let agentInstructions;
|
|
68
|
-
for (const p of candidatePaths) {
|
|
69
|
-
if (existsSync(p)) {
|
|
70
|
-
agentInstructions = readFileSync(p, 'utf-8');
|
|
71
|
-
break;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (!agentInstructions) {
|
|
75
|
-
throw new Error(`Squad coordinator instructions not found in ${projectRoot}. ` +
|
|
76
|
-
`Expected .github/agents/squad.agent.md or squad.agent.md.`);
|
|
77
|
-
}
|
|
78
|
-
// 2. Project charter
|
|
79
|
-
const teamMdPath = `${projectRoot}/.squad/team.md`;
|
|
80
|
-
const teamContent = existsSync(teamMdPath)
|
|
81
|
-
? readFileSync(teamMdPath, 'utf-8')
|
|
82
|
-
: '(not found — create .squad/team.md to provide project charter context)';
|
|
83
|
-
// 3. Identity files — what the team is focused on and what patterns we use
|
|
84
|
-
const nowMdPath = `${projectRoot}/.squad/identity/now.md`;
|
|
85
|
-
const nowContent = existsSync(nowMdPath)
|
|
86
|
-
? readFileSync(nowMdPath, 'utf-8')
|
|
87
|
-
: '(not found — create .squad/identity/now.md to share current focus)';
|
|
88
|
-
const wisdomMdPath = `${projectRoot}/.squad/identity/wisdom.md`;
|
|
89
|
-
const wisdomContent = existsSync(wisdomMdPath)
|
|
90
|
-
? readFileSync(wisdomMdPath, 'utf-8')
|
|
91
|
-
: '(not found — create .squad/identity/wisdom.md to capture team patterns)';
|
|
92
|
-
// 4. Recent decisions — last ~4000 chars
|
|
93
|
-
const decisionsMdPath = `${projectRoot}/.squad/decisions.md`;
|
|
94
|
-
let decisionsContent = '(no decisions recorded yet)';
|
|
95
|
-
if (existsSync(decisionsMdPath)) {
|
|
96
|
-
const raw = readFileSync(decisionsMdPath, 'utf-8');
|
|
97
|
-
decisionsContent = raw.length > 4000 ? raw.slice(raw.length - 4000) : raw;
|
|
98
|
-
}
|
|
99
|
-
return [
|
|
100
|
-
agentInstructions,
|
|
101
|
-
'',
|
|
102
|
-
'## Chapterhouse Project Session Context',
|
|
103
|
-
'',
|
|
104
|
-
`- **Project Root:** ${projectRoot}`,
|
|
105
|
-
`- **Squad Dir:** ${projectRoot}/.squad`,
|
|
106
|
-
`- **Session Mode:** project (Squad Coordinator)`,
|
|
107
|
-
'',
|
|
108
|
-
'## Project Charter',
|
|
109
|
-
'',
|
|
110
|
-
teamContent,
|
|
111
|
-
'',
|
|
112
|
-
'## Team Current Focus (What We\'re Doing Now)',
|
|
113
|
-
'',
|
|
114
|
-
nowContent,
|
|
115
|
-
'',
|
|
116
|
-
'## Team Wisdom (Patterns & Lessons)',
|
|
117
|
-
'',
|
|
118
|
-
wisdomContent,
|
|
119
|
-
'',
|
|
120
|
-
'## Recent Decisions',
|
|
121
|
-
'',
|
|
122
|
-
decisionsContent,
|
|
123
|
-
].join('\n');
|
|
124
|
-
}
|
|
125
|
-
//# sourceMappingURL=charter.js.map
|