chapterhouse 0.1.1 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/server.js +37 -8
- package/dist/copilot/orchestrator.js +30 -1
- package/dist/copilot/orchestrator.test.js +106 -1
- package/dist/copilot/tools.js +1 -1
- package/dist/squad/discovery.test.js +61 -0
- package/dist/store/db.js +4 -0
- package/package.json +1 -1
- package/web/dist/chapterhouse-icon.svg +1 -1
package/dist/api/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import cors from "cors";
|
|
2
2
|
import express from "express";
|
|
3
3
|
import helmet from "helmet";
|
|
4
|
-
import { existsSync, statSync } from "fs";
|
|
4
|
+
import { existsSync, statSync, readdirSync } from "fs";
|
|
5
5
|
import { join, dirname } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { z } from "zod";
|
|
@@ -23,6 +23,7 @@ import { getDb } from "../store/db.js";
|
|
|
23
23
|
import { getStatus, onStatusChange } from "../status.js";
|
|
24
24
|
import { formatSseData, formatSseEvent } from "./sse.js";
|
|
25
25
|
import { syncDecisionsFileToWiki } from "../squad/mirror.js";
|
|
26
|
+
import { resolveProjectSquad } from "../squad/discovery.js";
|
|
26
27
|
import { assertAuthenticationConfigured, createHealthPayload, createPublicConfigPayload, buildHistoryEntries, getDisplayHost, resolveApiToken, shouldServeSpaPath, } from "./server-runtime.js";
|
|
27
28
|
import { BadRequestError, ForbiddenError, InternalServerError, NotFoundError, apiNotFoundHandler, asBadRequest, createApiErrorHandler, parseRequest, } from "./errors.js";
|
|
28
29
|
void searchIndex; // re-exported by index-manager; reference here documents the dep
|
|
@@ -528,6 +529,31 @@ app.post("/api/restart", (_req, res) => {
|
|
|
528
529
|
const projectRegisterSchema = z.object({
|
|
529
530
|
projectRoot: requiredString("projectRoot must be a non-empty string"),
|
|
530
531
|
}).strict();
|
|
532
|
+
/**
|
|
533
|
+
* Count squad agents on disk for a project.
|
|
534
|
+
* Authoritative source: each subdirectory of <projectRoot>/.squad/agents/ that
|
|
535
|
+
* contains a charter.md is one agent. Never relies on the SQLite cache so the
|
|
536
|
+
* badge is always accurate even before the cache is warm.
|
|
537
|
+
*/
|
|
538
|
+
function countAgentsOnDisk(projectRoot) {
|
|
539
|
+
const agentsDir = join(projectRoot, ".squad", "agents");
|
|
540
|
+
if (!existsSync(agentsDir))
|
|
541
|
+
return 0;
|
|
542
|
+
try {
|
|
543
|
+
return readdirSync(agentsDir).filter((entry) => {
|
|
544
|
+
try {
|
|
545
|
+
return statSync(join(agentsDir, entry)).isDirectory() &&
|
|
546
|
+
existsSync(join(agentsDir, entry, "charter.md"));
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
}).length;
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
return 0;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
531
557
|
app.get("/api/projects", (_req, res) => {
|
|
532
558
|
if (!config.squadEnabled) {
|
|
533
559
|
res.status(503).json({ error: "Squad integration is disabled. Set ENABLE_SQUAD=1 to enable." });
|
|
@@ -535,18 +561,16 @@ app.get("/api/projects", (_req, res) => {
|
|
|
535
561
|
}
|
|
536
562
|
const db = getDb();
|
|
537
563
|
const rows = db.prepare(`
|
|
538
|
-
SELECT
|
|
539
|
-
COUNT(squad_agents.slug) as agent_count, project_squads.loaded_at
|
|
564
|
+
SELECT project_root, squad_dir, loaded_at
|
|
540
565
|
FROM project_squads
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
GROUP BY project_squads.project_root
|
|
544
|
-
ORDER BY project_squads.loaded_at DESC
|
|
566
|
+
WHERE registered = 1
|
|
567
|
+
ORDER BY loaded_at DESC
|
|
545
568
|
`).all();
|
|
546
569
|
res.json(rows.map((r) => ({
|
|
547
570
|
projectRoot: r.project_root,
|
|
548
571
|
squadDir: r.squad_dir,
|
|
549
|
-
|
|
572
|
+
// Count from live filesystem — authoritative per Squad SDK rule: repo files win over cache.
|
|
573
|
+
agentCount: countAgentsOnDisk(r.project_root),
|
|
550
574
|
loadedAt: r.loaded_at,
|
|
551
575
|
})));
|
|
552
576
|
});
|
|
@@ -575,6 +599,11 @@ app.post("/api/projects", async (req, res) => {
|
|
|
575
599
|
}).catch(err => {
|
|
576
600
|
console.warn('[squad] syncDecisionsFileToWiki failed during registration (non-fatal):', err instanceof Error ? err.message : err);
|
|
577
601
|
});
|
|
602
|
+
// Fire-and-forget: populate squad_agents cache from disk so future queries have
|
|
603
|
+
// something to work with (non-fatal — GET /api/projects counts live from disk anyway).
|
|
604
|
+
resolveProjectSquad(projectRoot).catch(err => {
|
|
605
|
+
console.warn('[squad] resolveProjectSquad failed during registration (non-fatal):', err instanceof Error ? err.message : err);
|
|
606
|
+
});
|
|
578
607
|
res.status(201).json({ projectRoot, message: "Project registered successfully" });
|
|
579
608
|
});
|
|
580
609
|
app.delete("/api/projects/:projectRoot", (req, res) => {
|
|
@@ -5,7 +5,7 @@ import { config, DEFAULT_MODEL } from "../config.js";
|
|
|
5
5
|
import { loadMcpConfig } from "./mcp-config.js";
|
|
6
6
|
import { getSkillDirectories } from "./skills.js";
|
|
7
7
|
import { resetClient } from "./client.js";
|
|
8
|
-
import { logConversation, getState, setState, deleteState, getCopilotSession, upsertCopilotSession, getTaskSessionKey } from "../store/db.js";
|
|
8
|
+
import { logConversation, getState, setState, deleteState, getCopilotSession, upsertCopilotSession, getTaskSessionKey, getDb } from "../store/db.js";
|
|
9
9
|
import { maybeWriteEpisode } from "./episode-writer.js";
|
|
10
10
|
import { getWikiSummary } from "../wiki/context.js";
|
|
11
11
|
import { SESSIONS_DIR } from "../paths.js";
|
|
@@ -403,6 +403,32 @@ async function executeOnSession(sessionKey, prompt, callback, attachments, onAct
|
|
|
403
403
|
});
|
|
404
404
|
})
|
|
405
405
|
: () => { };
|
|
406
|
+
// Always persist SDK subagent dispatches to agent_tasks so Workers tab shows them.
|
|
407
|
+
// These fire when the built-in `task` tool (Squad coordinator) routes work to a
|
|
408
|
+
// specialist — separate from `delegate_to_agent` which handles CH-registry agents.
|
|
409
|
+
const db = getDb();
|
|
410
|
+
const unsubSubStartDb = session.on("subagent.started", (event) => {
|
|
411
|
+
try {
|
|
412
|
+
const data = event.data;
|
|
413
|
+
const agentSlug = (data.agentName || "unknown").toLowerCase().replace(/\s+/g, "-");
|
|
414
|
+
const description = (data.agentDescription || data.agentDisplayName || `Squad dispatch: ${agentSlug}`).slice(0, 500);
|
|
415
|
+
db.prepare(`INSERT OR IGNORE INTO agent_tasks (task_id, agent_slug, description, status, origin_channel, session_key, source) VALUES (?, ?, ?, 'running', ?, ?, 'squad')`).run(data.toolCallId, agentSlug, description, currentSourceChannel || null, sessionKey);
|
|
416
|
+
}
|
|
417
|
+
catch { /* non-fatal */ }
|
|
418
|
+
});
|
|
419
|
+
const unsubSubDoneDb = session.on("subagent.completed", (event) => {
|
|
420
|
+
try {
|
|
421
|
+
db.prepare(`UPDATE agent_tasks SET status = 'completed', completed_at = CURRENT_TIMESTAMP WHERE task_id = ?`).run(event.data.toolCallId);
|
|
422
|
+
}
|
|
423
|
+
catch { /* non-fatal */ }
|
|
424
|
+
});
|
|
425
|
+
const unsubSubFailDb = session.on("subagent.failed", (event) => {
|
|
426
|
+
try {
|
|
427
|
+
const data = event.data;
|
|
428
|
+
db.prepare(`UPDATE agent_tasks SET status = 'error', result = ?, completed_at = CURRENT_TIMESTAMP WHERE task_id = ?`).run(data.error || "Subagent failed", data.toolCallId);
|
|
429
|
+
}
|
|
430
|
+
catch { /* non-fatal */ }
|
|
431
|
+
});
|
|
406
432
|
const unsubDelta = session.on("assistant.message_delta", (event) => {
|
|
407
433
|
// After a tool call completes, ensure a line break separates the text blocks
|
|
408
434
|
// so they don't visually run together in the rendered chat.
|
|
@@ -455,6 +481,9 @@ async function executeOnSession(sessionKey, prompt, callback, attachments, onAct
|
|
|
455
481
|
unsubSubStart();
|
|
456
482
|
unsubSubDone();
|
|
457
483
|
unsubSubFail();
|
|
484
|
+
unsubSubStartDb();
|
|
485
|
+
unsubSubDoneDb();
|
|
486
|
+
unsubSubFailDb();
|
|
458
487
|
currentCallback = undefined;
|
|
459
488
|
currentActivityCallback = undefined;
|
|
460
489
|
currentProcessingSessionKey = undefined;
|
|
@@ -13,6 +13,11 @@ function createFakeClient(state) {
|
|
|
13
13
|
this.listeners.set(eventName, next);
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
+
emit(eventName, data) {
|
|
17
|
+
for (const handler of this.listeners.get(eventName) || []) {
|
|
18
|
+
handler({ data });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
16
21
|
async sendAndWait(request, _timeoutMs) {
|
|
17
22
|
state.sessionPrompts.push(request);
|
|
18
23
|
if (state.sendResult === "__PENDING__") {
|
|
@@ -45,7 +50,11 @@ function createFakeClient(state) {
|
|
|
45
50
|
if (state.createSessionError) {
|
|
46
51
|
throw new Error(state.createSessionError);
|
|
47
52
|
}
|
|
48
|
-
|
|
53
|
+
const session = new FakeSession();
|
|
54
|
+
state.lastSession = {
|
|
55
|
+
emit: (eventName, data) => session.emit(eventName, data),
|
|
56
|
+
};
|
|
57
|
+
return session;
|
|
49
58
|
},
|
|
50
59
|
async resumeSession(savedId, options) {
|
|
51
60
|
state.resumeSessionCalls.push({ savedId, options });
|
|
@@ -71,6 +80,7 @@ async function loadOrchestratorModule(t, overrides = {}) {
|
|
|
71
80
|
disconnectCalls: 0,
|
|
72
81
|
conversationLogs: [],
|
|
73
82
|
dbLogs: [],
|
|
83
|
+
dbWrites: [],
|
|
74
84
|
episodeWrites: 0,
|
|
75
85
|
ensureDefaultAgentsCalls: 0,
|
|
76
86
|
loadAgentsCalls: 0,
|
|
@@ -162,6 +172,16 @@ async function loadOrchestratorModule(t, overrides = {}) {
|
|
|
162
172
|
getCopilotSession: (_sessionKey) => undefined,
|
|
163
173
|
upsertCopilotSession: (_sessionKey, _mode, _copilotSessionId, _projectRoot, _model) => { },
|
|
164
174
|
getTaskSessionKey: (taskId) => state.taskSessionKeys?.get(taskId) ?? "default",
|
|
175
|
+
getDb: () => ({
|
|
176
|
+
prepare: (sql) => ({
|
|
177
|
+
run: (...args) => {
|
|
178
|
+
state.dbWrites.push({ sql, args });
|
|
179
|
+
return {};
|
|
180
|
+
},
|
|
181
|
+
get: () => undefined,
|
|
182
|
+
all: () => [],
|
|
183
|
+
}),
|
|
184
|
+
}),
|
|
165
185
|
},
|
|
166
186
|
});
|
|
167
187
|
t.mock.module("./episode-writer.js", {
|
|
@@ -520,4 +540,89 @@ test("ensureOrchestratorSession cleans up in-flight promise on session creation
|
|
|
520
540
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
521
541
|
assert.ok(state.createSessionCalls.length > countAfterFirst, "a second message must trigger a new createSession attempt, proving sessionCreatePromises was cleaned up");
|
|
522
542
|
});
|
|
543
|
+
// ---------------------------------------------------------------------------
|
|
544
|
+
// S5-01: SDK subagent events persist to agent_tasks (squad dispatch tracking)
|
|
545
|
+
// Root cause: executeOnSession subscribed to subagent.* events for the UI
|
|
546
|
+
// activity feed but never wrote rows to agent_tasks. Workers tab only reads
|
|
547
|
+
// agent_tasks, so squad coordinator dispatches were invisible.
|
|
548
|
+
// Fix: unconditional DB subscriptions in executeOnSession write/update rows.
|
|
549
|
+
// ---------------------------------------------------------------------------
|
|
550
|
+
test("S5-01: subagent.started event inserts a squad row into agent_tasks", async (t) => {
|
|
551
|
+
const { orchestrator, state, client } = await loadOrchestratorModule(t, {
|
|
552
|
+
sendResult: "__PENDING__",
|
|
553
|
+
});
|
|
554
|
+
await orchestrator.initOrchestrator(client);
|
|
555
|
+
// Start a message so executeOnSession runs and registers event handlers
|
|
556
|
+
const callbackResults = [];
|
|
557
|
+
orchestrator.sendToOrchestrator("dispatch something", { type: "background" }, (text, done) => {
|
|
558
|
+
callbackResults.push({ text, done });
|
|
559
|
+
});
|
|
560
|
+
// Yield so the async IIFE reaches sendAndWait (which is pending)
|
|
561
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
562
|
+
assert.ok(state.lastSession, "FakeSession should have been created");
|
|
563
|
+
// Fire a subagent.started event — simulates the SDK's task tool starting a squad dispatch
|
|
564
|
+
state.lastSession.emit("subagent.started", {
|
|
565
|
+
toolCallId: "subagent-call-001",
|
|
566
|
+
agentName: "Kaylee",
|
|
567
|
+
agentDisplayName: "Kaylee — Backend Dev",
|
|
568
|
+
agentDescription: "Implement S5-01 backend fix",
|
|
569
|
+
});
|
|
570
|
+
// The handler is synchronous — DB write should be in state.dbWrites immediately
|
|
571
|
+
const insertWrite = state.dbWrites.find((w) => w.sql.includes("INSERT") && w.sql.includes("agent_tasks"));
|
|
572
|
+
assert.ok(insertWrite, "subagent.started must INSERT a row into agent_tasks");
|
|
573
|
+
assert.ok((insertWrite.sql + JSON.stringify(insertWrite.args)).includes("squad"), "inserted row must carry source='squad'");
|
|
574
|
+
assert.ok(JSON.stringify(insertWrite.args).includes("subagent-call-001"), "task_id must equal the toolCallId from the event");
|
|
575
|
+
// Resolve the pending sendAndWait so the test can clean up
|
|
576
|
+
state.pendingReject?.(new Error("test teardown"));
|
|
577
|
+
});
|
|
578
|
+
test("S5-01: subagent.completed event updates agent_tasks status to completed", async (t) => {
|
|
579
|
+
const { orchestrator, state, client } = await loadOrchestratorModule(t, {
|
|
580
|
+
sendResult: "__PENDING__",
|
|
581
|
+
});
|
|
582
|
+
await orchestrator.initOrchestrator(client);
|
|
583
|
+
orchestrator.sendToOrchestrator("dispatch something", { type: "background" }, () => { });
|
|
584
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
585
|
+
// First, start the subagent
|
|
586
|
+
state.lastSession.emit("subagent.started", {
|
|
587
|
+
toolCallId: "subagent-call-002",
|
|
588
|
+
agentName: "Wash",
|
|
589
|
+
agentDisplayName: "Wash — Frontend Dev",
|
|
590
|
+
agentDescription: "Fix Workers tab UI",
|
|
591
|
+
});
|
|
592
|
+
// Then complete it
|
|
593
|
+
state.lastSession.emit("subagent.completed", {
|
|
594
|
+
toolCallId: "subagent-call-002",
|
|
595
|
+
agentName: "Wash",
|
|
596
|
+
agentDisplayName: "Wash — Frontend Dev",
|
|
597
|
+
durationMs: 1234,
|
|
598
|
+
});
|
|
599
|
+
const updateWrite = state.dbWrites.find((w) => w.sql.includes("UPDATE") && w.sql.includes("agent_tasks") && w.sql.includes("completed"));
|
|
600
|
+
assert.ok(updateWrite, "subagent.completed must UPDATE agent_tasks to completed");
|
|
601
|
+
assert.ok(JSON.stringify(updateWrite.args).includes("subagent-call-002"), "UPDATE must target the correct task_id");
|
|
602
|
+
state.pendingReject?.(new Error("test teardown"));
|
|
603
|
+
});
|
|
604
|
+
test("S5-01: subagent.failed event updates agent_tasks status to error", async (t) => {
|
|
605
|
+
const { orchestrator, state, client } = await loadOrchestratorModule(t, {
|
|
606
|
+
sendResult: "__PENDING__",
|
|
607
|
+
});
|
|
608
|
+
await orchestrator.initOrchestrator(client);
|
|
609
|
+
orchestrator.sendToOrchestrator("dispatch something", { type: "background" }, () => { });
|
|
610
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
611
|
+
state.lastSession.emit("subagent.started", {
|
|
612
|
+
toolCallId: "subagent-call-003",
|
|
613
|
+
agentName: "Zoe",
|
|
614
|
+
agentDisplayName: "Zoe — QA",
|
|
615
|
+
agentDescription: "Run test suite",
|
|
616
|
+
});
|
|
617
|
+
state.lastSession.emit("subagent.failed", {
|
|
618
|
+
toolCallId: "subagent-call-003",
|
|
619
|
+
agentName: "Zoe",
|
|
620
|
+
agentDisplayName: "Zoe — QA",
|
|
621
|
+
error: "Timeout after 600s",
|
|
622
|
+
});
|
|
623
|
+
const errorWrite = state.dbWrites.find((w) => w.sql.includes("UPDATE") && w.sql.includes("agent_tasks") && w.sql.includes("error"));
|
|
624
|
+
assert.ok(errorWrite, "subagent.failed must UPDATE agent_tasks to error status");
|
|
625
|
+
assert.ok(JSON.stringify(errorWrite.args).includes("subagent-call-003"), "UPDATE must target the correct task_id");
|
|
626
|
+
state.pendingReject?.(new Error("test teardown"));
|
|
627
|
+
});
|
|
523
628
|
//# sourceMappingURL=orchestrator.test.js.map
|
package/dist/copilot/tools.js
CHANGED
|
@@ -156,7 +156,7 @@ export function createTools(deps) {
|
|
|
156
156
|
const task = registerTask(delegatedSlug, args.summary, getCurrentSourceChannel());
|
|
157
157
|
// Persist task to DB
|
|
158
158
|
const db = getDb();
|
|
159
|
-
db.prepare(`INSERT INTO agent_tasks (task_id, agent_slug, description, status, origin_channel, session_key) VALUES (?, ?, ?, 'running', ?,
|
|
159
|
+
db.prepare(`INSERT INTO agent_tasks (task_id, agent_slug, description, status, origin_channel, session_key, source) VALUES (?, ?, ?, 'running', ?, ?, 'adhoc')`).run(task.taskId, delegatedSlug, args.summary, task.originChannel || null, getCurrentSessionKey());
|
|
160
160
|
// Capture the parent's activity callback so the child session can stream
|
|
161
161
|
// its events back to the originating SSE connection. This survives past
|
|
162
162
|
// the parent assistant turn — the child runs long after the parent's
|
|
@@ -90,4 +90,65 @@ test("invalidateProjectSquad marks agents stale — next loadProjectSquad re-res
|
|
|
90
90
|
// Both should have agents (content integrity survives invalidation + re-resolve)
|
|
91
91
|
assert.ok(after && typeof after === "object" && "agents" in after, "re-resolved context should include agents");
|
|
92
92
|
});
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// S5-02: agent count from filesystem is always authoritative
|
|
95
|
+
// These tests guard the fix: GET /api/projects must count agents from disk,
|
|
96
|
+
// not from the SQLite squad_agents cache (which is never populated on a fresh
|
|
97
|
+
// registration, causing the badge to show 0).
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
test("S5-02: resolveProjectSquad counts 0 agents for an empty agents dir", async () => {
|
|
100
|
+
const m = await loadDiscovery();
|
|
101
|
+
const dir = join(repoRoot, ".test-work", `s502-empty-${process.pid}`);
|
|
102
|
+
mkdirSync(join(dir, ".squad", "agents"), { recursive: true });
|
|
103
|
+
try {
|
|
104
|
+
const result = await m.resolveProjectSquad(dir);
|
|
105
|
+
assert.ok(result !== null, "should return context even with no agents");
|
|
106
|
+
assert.equal(result.agents.length, 0, "empty agents dir → 0 agents");
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
rmSync(dir, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
test("S5-02: resolveProjectSquad counts N agents matching charter-bearing subdirs", async () => {
|
|
113
|
+
const m = await loadDiscovery();
|
|
114
|
+
const dir = join(repoRoot, ".test-work", `s502-n-agents-${process.pid}`);
|
|
115
|
+
// Create 3 agents (with charter.md) and 1 directory without charter.md
|
|
116
|
+
for (const slug of ["alpha", "beta", "gamma"]) {
|
|
117
|
+
mkdirSync(join(dir, ".squad", "agents", slug), { recursive: true });
|
|
118
|
+
const { writeFileSync } = await import("node:fs");
|
|
119
|
+
writeFileSync(join(dir, ".squad", "agents", slug, "charter.md"), `# ${slug}\n\n**Role:** Specialist\n`);
|
|
120
|
+
}
|
|
121
|
+
mkdirSync(join(dir, ".squad", "agents", "no-charter"), { recursive: true }); // no charter.md
|
|
122
|
+
try {
|
|
123
|
+
const result = await m.resolveProjectSquad(dir);
|
|
124
|
+
assert.ok(result !== null, "should return context");
|
|
125
|
+
assert.equal(result.agents.length, 3, "only dirs with charter.md count as agents");
|
|
126
|
+
const slugs = result.agents.map((a) => a.slug).sort();
|
|
127
|
+
assert.deepEqual(slugs, ["alpha", "beta", "gamma"]);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
rmSync(dir, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
test("S5-02: resolveProjectSquad re-reads filesystem after agent added (no stale count)", async () => {
|
|
134
|
+
const m = await loadDiscovery();
|
|
135
|
+
const dir = join(repoRoot, ".test-work", `s502-refresh-${process.pid}`);
|
|
136
|
+
const { writeFileSync } = await import("node:fs");
|
|
137
|
+
// Start with 1 agent
|
|
138
|
+
mkdirSync(join(dir, ".squad", "agents", "first"), { recursive: true });
|
|
139
|
+
writeFileSync(join(dir, ".squad", "agents", "first", "charter.md"), "# first\n\n**Role:** Scout\n");
|
|
140
|
+
try {
|
|
141
|
+
const before = await m.resolveProjectSquad(dir);
|
|
142
|
+
assert.equal(before.agents.length, 1, "before: 1 agent on disk");
|
|
143
|
+
// Add a second agent to disk
|
|
144
|
+
mkdirSync(join(dir, ".squad", "agents", "second"), { recursive: true });
|
|
145
|
+
writeFileSync(join(dir, ".squad", "agents", "second", "charter.md"), "# second\n\n**Role:** Recon\n");
|
|
146
|
+
// resolveProjectSquad always reads from disk (no TTL — direct filesystem read)
|
|
147
|
+
const after = await m.resolveProjectSquad(dir);
|
|
148
|
+
assert.equal(after.agents.length, 2, "after adding agent to disk: 2 agents");
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
rmSync(dir, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
93
154
|
//# sourceMappingURL=discovery.test.js.map
|
package/dist/store/db.js
CHANGED
|
@@ -146,6 +146,10 @@ export function getDb() {
|
|
|
146
146
|
if (!taskCols.some((c) => c.name === 'session_key')) {
|
|
147
147
|
db.exec(`ALTER TABLE agent_tasks ADD COLUMN session_key TEXT NOT NULL DEFAULT 'default'`);
|
|
148
148
|
}
|
|
149
|
+
// Migrate: add source column to agent_tasks ('adhoc' | 'squad') if not present
|
|
150
|
+
if (!taskCols.some((c) => c.name === 'source')) {
|
|
151
|
+
db.exec(`ALTER TABLE agent_tasks ADD COLUMN source TEXT NOT NULL DEFAULT 'adhoc'`);
|
|
152
|
+
}
|
|
149
153
|
// Prune conversation log at startup — keep more history for better recovery
|
|
150
154
|
db.prepare(`DELETE FROM conversation_log WHERE id NOT IN (SELECT id FROM conversation_log ORDER BY id DESC LIMIT 1000)`).run();
|
|
151
155
|
// Set up FTS5 for memory search (graceful fallback if not available)
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
width="512px" height="512px" viewBox="0 0 1024.000000 1024.000000"
|
|
5
5
|
preserveAspectRatio="xMidYMid meet">
|
|
6
6
|
<g transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)"
|
|
7
|
-
fill="#
|
|
7
|
+
fill="#CD7F32" stroke="none">
|
|
8
8
|
<path d="M4960 9676 c-415 -143 -684 -258 -935 -401 -530 -300 -969 -739
|
|
9
9
|
-1277 -1275 -48 -83 -186 -364 -231 -471 -214 -503 -382 -1155 -491 -1909 -25
|
|
10
10
|
-172 -73 -606 -101 -909 -18 -192 -32 -297 -43 -325 -26 -62 -23 -283 6 -397
|