chapterhouse 0.9.1 → 0.10.0
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 +1 -1
- package/agents/korg.agent.md +20 -0
- package/dist/api/auth.js +11 -1
- package/dist/api/auth.test.js +29 -0
- package/dist/api/errors.js +23 -0
- package/dist/api/route-coverage.test.js +61 -21
- package/dist/api/routes/agents.js +472 -0
- package/dist/api/routes/memory.js +299 -0
- package/dist/api/routes/projects.js +170 -0
- package/dist/api/routes/sessions.js +347 -0
- package/dist/api/routes/system.js +82 -0
- package/dist/api/routes/wiki.js +455 -0
- package/dist/api/routes/wiki.test.js +49 -0
- package/dist/api/send-json.js +16 -0
- package/dist/api/send-json.test.js +18 -0
- package/dist/api/server-runtime.js +45 -3
- package/dist/api/server.js +34 -1764
- package/dist/api/server.test.js +239 -8
- package/dist/api/sse-hub.js +37 -0
- package/dist/cli.js +1 -1
- package/dist/config.js +151 -58
- package/dist/config.test.js +29 -0
- package/dist/copilot/okr-mapper.js +2 -11
- package/dist/copilot/orchestrator.js +358 -352
- package/dist/copilot/orchestrator.test.js +139 -4
- package/dist/copilot/prompt-date.js +2 -1
- package/dist/copilot/session-manager.js +25 -23
- package/dist/copilot/session-manager.test.js +35 -1
- package/dist/copilot/standup.js +2 -2
- package/dist/copilot/task-event-log.js +7 -1
- package/dist/copilot/task-event-log.test.js +13 -0
- package/dist/copilot/tools/agent.js +608 -0
- package/dist/copilot/tools/index.js +19 -0
- package/dist/copilot/tools/memory.js +678 -0
- package/dist/copilot/tools/models.js +2 -0
- package/dist/copilot/tools/okr.js +171 -0
- package/dist/copilot/tools/wiki.js +333 -0
- package/dist/copilot/tools-deps.js +4 -0
- package/dist/copilot/tools.agent.test.js +10 -8
- package/dist/copilot/tools.inventory.test.js +76 -0
- package/dist/copilot/tools.js +1 -1725
- package/dist/copilot/tools.okr.test.js +31 -0
- package/dist/copilot/tools.wiki.test.js +358 -6
- package/dist/copilot/turn-event-log.js +31 -4
- package/dist/copilot/turn-event-log.test.js +24 -2
- package/dist/copilot/workiq-installer.test.js +2 -2
- package/dist/daemon-install.js +3 -2
- package/dist/daemon.js +9 -17
- package/dist/integrations/ado-client.js +90 -9
- package/dist/integrations/ado-client.test.js +56 -0
- package/dist/integrations/team-push.js +1 -0
- package/dist/integrations/team-push.test.js +6 -0
- package/dist/integrations/teams-notify.js +1 -0
- package/dist/integrations/teams-notify.test.js +5 -0
- package/dist/memory/active-scope.test.js +0 -1
- package/dist/memory/checkpoint.js +89 -72
- package/dist/memory/checkpoint.test.js +23 -3
- package/dist/memory/eot.js +194 -89
- package/dist/memory/eot.test.js +186 -3
- package/dist/memory/hooks.js +2 -4
- package/dist/memory/housekeeping-scheduler.js +1 -1
- package/dist/memory/housekeeping-scheduler.test.js +1 -2
- package/dist/memory/housekeeping.js +100 -3
- package/dist/memory/housekeeping.test.js +33 -2
- package/dist/memory/reflect.test.js +2 -0
- package/dist/memory/scope-lock.js +26 -0
- package/dist/memory/scope-lock.test.js +118 -0
- package/dist/memory/scopes.test.js +0 -1
- package/dist/mode-context.js +58 -5
- package/dist/mode-context.test.js +68 -0
- package/dist/paths.js +1 -0
- package/dist/setup.js +3 -2
- package/dist/shared/api-schemas.js +48 -5
- package/dist/store/connection.js +96 -0
- package/dist/store/db.js +5 -1498
- package/dist/store/db.test.js +182 -1
- package/dist/store/migrations.js +460 -0
- package/dist/store/repositories/memory.js +281 -0
- package/dist/store/repositories/okr.js +3 -0
- package/dist/store/repositories/projects.js +5 -0
- package/dist/store/repositories/sessions.js +284 -0
- package/dist/store/repositories/wiki.js +60 -0
- package/dist/store/schema.js +501 -0
- package/dist/util/logger.js +3 -2
- package/dist/wiki/consolidation.js +50 -9
- package/dist/wiki/consolidation.test.js +45 -0
- package/dist/wiki/frontmatter.js +45 -14
- package/dist/wiki/frontmatter.test.js +26 -1
- package/dist/wiki/fs.js +16 -4
- package/dist/wiki/fs.test.js +84 -0
- package/dist/wiki/index-manager.js +30 -2
- package/dist/wiki/index-manager.test.js +43 -12
- package/dist/wiki/ingest.js +17 -1
- package/dist/wiki/lock.js +11 -1
- package/dist/wiki/log-manager.js +2 -7
- package/dist/wiki/migrate.js +44 -17
- package/dist/wiki/project-registry.js +10 -5
- package/dist/wiki/project-registry.test.js +14 -0
- package/dist/wiki/scheduler.js +1 -1
- package/dist/wiki/seed-team-wiki.js +2 -1
- package/dist/wiki/team-sync.js +31 -6
- package/dist/wiki/team-sync.test.js +81 -0
- package/package.json +1 -1
- package/web/dist/assets/WikiEdit-BZXAdarz.js +30 -0
- package/web/dist/assets/WikiEdit-BZXAdarz.js.map +1 -0
- package/web/dist/assets/WikiGraph-KrCYco4v.js +2 -0
- package/web/dist/assets/WikiGraph-KrCYco4v.js.map +1 -0
- package/web/dist/assets/index-CUm2Wbuh.js +250 -0
- package/web/dist/assets/index-CUm2Wbuh.js.map +1 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-iQrv3lQN.js +0 -286
- package/web/dist/assets/index-iQrv3lQN.js.map +0 -1
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { approveAll, defineTool } from "@github/copilot-sdk";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { config, persistModel } from "../../config.js";
|
|
7
|
+
import { getDb, appendTaskOutputDeltaEvent, appendTaskStatusEvent, updateTaskResult } from "../../store/db.js";
|
|
8
|
+
import { TeamsNotifier } from "../../integrations/teams-notify.js";
|
|
9
|
+
import { childLogger } from "../../util/logger.js";
|
|
10
|
+
import { agentEventBus } from "../agent-event-bus.js";
|
|
11
|
+
import { getAgentRegistry, getAgent, createEphemeralAgentSession, getAgentSessionStatus, getTask, registerTask, completeTask, failTask, createTaskId, createAgentFile, removeAgentFile, loadAgents, } from "../tools-deps.js";
|
|
12
|
+
import { getCurrentSourceChannel, getCurrentActivityCallback, getCurrentActiveProjectRules, getCurrentSessionKey, sendToAgentSession, switchSessionModel, } from "../tools-deps.js";
|
|
13
|
+
import { detectProjectRuleWarnings } from "../project-rule-warnings.js";
|
|
14
|
+
import { renderDelegatedProjectRulesPreamble } from "../project-rules-injection.js";
|
|
15
|
+
import { getRouterConfig, updateRouterConfig } from "../router.js";
|
|
16
|
+
import { listSkills, createSkill, removeSkill } from "../skills.js";
|
|
17
|
+
const log = childLogger("tools");
|
|
18
|
+
export function createAgentTools(deps, getAllTools) {
|
|
19
|
+
return [
|
|
20
|
+
defineTool("delegate_to_agent", {
|
|
21
|
+
description: "Delegate a task to a specialist agent. The task runs in the background — you'll be notified when it's done. " +
|
|
22
|
+
"Available agents: use show_agent_roster to see the roster. For @general-purpose, specify model_override based on task complexity.",
|
|
23
|
+
parameters: z.object({
|
|
24
|
+
agent_name: z.string().describe("Name or slug of the agent to delegate to (e.g. 'coder', 'designer', 'general-purpose')"),
|
|
25
|
+
task: z.string().describe("Detailed task description for the agent"),
|
|
26
|
+
summary: z.string().describe("Short human-readable summary of the task (under 80 chars, e.g. 'Fix login button styling')"),
|
|
27
|
+
model_override: z.string().optional().describe("Model override for agents with model 'auto' (e.g. 'gpt-4.1', 'claude-sonnet-4.6', 'claude-opus-4.6')"),
|
|
28
|
+
}),
|
|
29
|
+
handler: async (args) => {
|
|
30
|
+
const agent = getAgent(args.agent_name);
|
|
31
|
+
if (agent?.slug === "chapterhouse") {
|
|
32
|
+
return "Cannot delegate to yourself. Handle this directly or pick a specialist agent.";
|
|
33
|
+
}
|
|
34
|
+
if (!agent) {
|
|
35
|
+
const available = getAgentRegistry().map((a) => a.slug).join(", ");
|
|
36
|
+
return `Agent '${args.agent_name}' not found. Available agents: ${available}`;
|
|
37
|
+
}
|
|
38
|
+
const delegatedSlug = agent.slug;
|
|
39
|
+
const taskId = createTaskId();
|
|
40
|
+
const task = registerTask(delegatedSlug, args.summary, getCurrentSourceChannel(), taskId);
|
|
41
|
+
const activeProjectRules = getCurrentActiveProjectRules();
|
|
42
|
+
const warningLines = activeProjectRules
|
|
43
|
+
? detectProjectRuleWarnings(args.task, activeProjectRules.rules.hard)
|
|
44
|
+
: [];
|
|
45
|
+
const warningBlock = warningLines.length > 0 ? `${warningLines.join("\n")}\n\n` : "";
|
|
46
|
+
const taskPrompt = activeProjectRules
|
|
47
|
+
? `${warningBlock}${renderDelegatedProjectRulesPreamble(activeProjectRules.project.slug, activeProjectRules.project.path, activeProjectRules.rules.path, activeProjectRules.rules.hard, activeProjectRules.rules.soft)}\n\n${args.task}`
|
|
48
|
+
: args.task;
|
|
49
|
+
// Persist task to DB
|
|
50
|
+
const db = getDb();
|
|
51
|
+
db.prepare(`INSERT INTO agent_tasks (task_id, agent_slug, description, prompt, status, origin_channel, session_key, source)
|
|
52
|
+
VALUES (?, ?, ?, ?, 'running', ?, ?, 'adhoc')`).run(task.taskId, delegatedSlug, args.summary, args.task, task.originChannel || null, getCurrentSessionKey());
|
|
53
|
+
if (agent.persistent) {
|
|
54
|
+
(async () => {
|
|
55
|
+
try {
|
|
56
|
+
const output = await sendToAgentSession(delegatedSlug, taskPrompt, task.taskId);
|
|
57
|
+
completeTask(task.taskId, output);
|
|
58
|
+
updateTaskResult(task.taskId, "completed", output);
|
|
59
|
+
const statusEvent = appendTaskStatusEvent(task.taskId, "completed");
|
|
60
|
+
if (statusEvent) {
|
|
61
|
+
void agentEventBus.emit({
|
|
62
|
+
type: "session:tool_call",
|
|
63
|
+
sessionId: task.taskId,
|
|
64
|
+
payload: {
|
|
65
|
+
toolName: "",
|
|
66
|
+
toolArgs: {},
|
|
67
|
+
_kind: statusEvent.kind,
|
|
68
|
+
_seq: statusEvent.seq,
|
|
69
|
+
_ts: statusEvent.ts,
|
|
70
|
+
_summary: statusEvent.summary,
|
|
71
|
+
_text: statusEvent.text,
|
|
72
|
+
_status: statusEvent.status,
|
|
73
|
+
},
|
|
74
|
+
timestamp: new Date(statusEvent.ts),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
deps.onAgentTaskComplete(task.taskId, delegatedSlug, output);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
81
|
+
failTask(task.taskId, msg);
|
|
82
|
+
updateTaskResult(task.taskId, "error", msg);
|
|
83
|
+
const statusEvent = appendTaskStatusEvent(task.taskId, "error", msg);
|
|
84
|
+
if (statusEvent) {
|
|
85
|
+
void agentEventBus.emit({
|
|
86
|
+
type: "session:tool_call",
|
|
87
|
+
sessionId: task.taskId,
|
|
88
|
+
payload: {
|
|
89
|
+
toolName: "",
|
|
90
|
+
toolArgs: {},
|
|
91
|
+
_kind: statusEvent.kind,
|
|
92
|
+
_seq: statusEvent.seq,
|
|
93
|
+
_ts: statusEvent.ts,
|
|
94
|
+
_summary: statusEvent.summary,
|
|
95
|
+
_text: statusEvent.text,
|
|
96
|
+
_status: statusEvent.status,
|
|
97
|
+
},
|
|
98
|
+
timestamp: new Date(statusEvent.ts),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
deps.onAgentTaskComplete(task.taskId, delegatedSlug, `Error: ${msg}`);
|
|
102
|
+
}
|
|
103
|
+
})();
|
|
104
|
+
const model = (args.model_override && args.model_override.length > 0)
|
|
105
|
+
? args.model_override
|
|
106
|
+
: (agent.model === "auto" ? "claude-sonnet-4.6" : agent.model || "claude-sonnet-4.6");
|
|
107
|
+
return `Task delegated to @${delegatedSlug} (${model}). Task ID: ${task.taskId}. I'll notify you when it's done.`;
|
|
108
|
+
}
|
|
109
|
+
let session;
|
|
110
|
+
try {
|
|
111
|
+
const allTools = getAllTools();
|
|
112
|
+
session = await createEphemeralAgentSession(agent.slug, deps.client, allTools, args.model_override, undefined, taskId);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
116
|
+
return `Failed to create session for @${delegatedSlug}: ${msg}`;
|
|
117
|
+
}
|
|
118
|
+
// Capture the parent's activity callback so the child session can stream
|
|
119
|
+
// its events back to the originating SSE connection. This survives past
|
|
120
|
+
// the parent assistant turn — the child runs long after the parent's
|
|
121
|
+
// `executeOnSession` finishes.
|
|
122
|
+
const parentActivity = getCurrentActivityCallback();
|
|
123
|
+
const childUnsubs = [];
|
|
124
|
+
const emitTaskLogEvent = (taskEvent) => {
|
|
125
|
+
void agentEventBus.emit({
|
|
126
|
+
type: "session:tool_call",
|
|
127
|
+
sessionId: task.taskId,
|
|
128
|
+
payload: {
|
|
129
|
+
toolName: "",
|
|
130
|
+
toolArgs: {},
|
|
131
|
+
_kind: taskEvent.kind,
|
|
132
|
+
_seq: taskEvent.seq,
|
|
133
|
+
_ts: taskEvent.ts,
|
|
134
|
+
_summary: taskEvent.summary,
|
|
135
|
+
_text: taskEvent.text,
|
|
136
|
+
_status: taskEvent.status,
|
|
137
|
+
},
|
|
138
|
+
timestamp: new Date(taskEvent.ts),
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
let workerOutput = "";
|
|
142
|
+
childUnsubs.push(session.on("assistant.message_delta", (event) => {
|
|
143
|
+
const delta = typeof event.data.deltaContent === "string" ? event.data.deltaContent : "";
|
|
144
|
+
if (!delta)
|
|
145
|
+
return;
|
|
146
|
+
workerOutput += delta;
|
|
147
|
+
const taskEvent = appendTaskOutputDeltaEvent(task.taskId, delta);
|
|
148
|
+
if (!taskEvent)
|
|
149
|
+
return;
|
|
150
|
+
emitTaskLogEvent(taskEvent);
|
|
151
|
+
}));
|
|
152
|
+
if (parentActivity) {
|
|
153
|
+
childUnsubs.push(session.on("assistant.reasoning_delta", (event) => {
|
|
154
|
+
parentActivity({
|
|
155
|
+
kind: "thinking_delta",
|
|
156
|
+
reasoningId: event.data.reasoningId,
|
|
157
|
+
deltaContent: event.data.deltaContent,
|
|
158
|
+
agentSlug: delegatedSlug,
|
|
159
|
+
});
|
|
160
|
+
}), session.on("tool.execution_start", (event) => {
|
|
161
|
+
const data = event.data;
|
|
162
|
+
parentActivity({
|
|
163
|
+
kind: "tool_start",
|
|
164
|
+
toolCallId: data.toolCallId,
|
|
165
|
+
toolName: data.toolName,
|
|
166
|
+
mcpServerName: data.mcpServerName,
|
|
167
|
+
arguments: data.arguments,
|
|
168
|
+
agentSlug: delegatedSlug,
|
|
169
|
+
});
|
|
170
|
+
}), session.on("tool.execution_complete", (event) => {
|
|
171
|
+
const data = event.data;
|
|
172
|
+
const result = data.result;
|
|
173
|
+
const resultPreview = typeof result?.content === "string" ? result.content.slice(0, 400) : undefined;
|
|
174
|
+
const detailedContent = typeof result?.detailedContent === "string"
|
|
175
|
+
? result.detailedContent
|
|
176
|
+
: typeof result?.content === "string"
|
|
177
|
+
? result.content
|
|
178
|
+
: undefined;
|
|
179
|
+
parentActivity({
|
|
180
|
+
kind: "tool_complete",
|
|
181
|
+
toolCallId: data.toolCallId,
|
|
182
|
+
success: data.success,
|
|
183
|
+
resultPreview,
|
|
184
|
+
detailedContent,
|
|
185
|
+
agentSlug: delegatedSlug,
|
|
186
|
+
});
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
const timeoutMs = config.workerTimeoutMs;
|
|
190
|
+
// Non-blocking: dispatch and return immediately. Session is always destroyed after.
|
|
191
|
+
(async () => {
|
|
192
|
+
try {
|
|
193
|
+
const result = await session.sendAndWait({ prompt: taskPrompt }, timeoutMs);
|
|
194
|
+
const output = workerOutput || result?.data?.content || "No response";
|
|
195
|
+
completeTask(task.taskId, output);
|
|
196
|
+
updateTaskResult(task.taskId, "completed", output);
|
|
197
|
+
const statusEvent = appendTaskStatusEvent(task.taskId, "completed");
|
|
198
|
+
if (statusEvent)
|
|
199
|
+
emitTaskLogEvent(statusEvent);
|
|
200
|
+
deps.onAgentTaskComplete(task.taskId, delegatedSlug, output);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
204
|
+
failTask(task.taskId, msg);
|
|
205
|
+
updateTaskResult(task.taskId, "error", msg);
|
|
206
|
+
const statusEvent = appendTaskStatusEvent(task.taskId, "error", msg);
|
|
207
|
+
if (statusEvent)
|
|
208
|
+
emitTaskLogEvent(statusEvent);
|
|
209
|
+
deps.onAgentTaskComplete(task.taskId, delegatedSlug, `Error: ${msg}`);
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
for (const unsub of childUnsubs) {
|
|
213
|
+
try {
|
|
214
|
+
unsub();
|
|
215
|
+
}
|
|
216
|
+
catch { /* best effort */ }
|
|
217
|
+
}
|
|
218
|
+
session.destroy().catch(() => { });
|
|
219
|
+
}
|
|
220
|
+
})();
|
|
221
|
+
const model = (args.model_override && args.model_override.length > 0)
|
|
222
|
+
? args.model_override
|
|
223
|
+
: (agent.model === "auto" ? "claude-sonnet-4.6" : agent.model || "claude-sonnet-4.6");
|
|
224
|
+
return `Task delegated to @${delegatedSlug} (${model}). Task ID: ${task.taskId}. I'll notify you when it's done.`;
|
|
225
|
+
},
|
|
226
|
+
}),
|
|
227
|
+
defineTool("check_agent_status", {
|
|
228
|
+
description: "Check the status of an agent or a specific delegated task.",
|
|
229
|
+
parameters: z.object({
|
|
230
|
+
agent_name: z.string().optional().describe("Agent name/slug to check"),
|
|
231
|
+
task_id: z.string().optional().describe("Specific task ID to check"),
|
|
232
|
+
}),
|
|
233
|
+
handler: async (args) => {
|
|
234
|
+
if (args.task_id) {
|
|
235
|
+
const task = getTask(args.task_id);
|
|
236
|
+
if (!task)
|
|
237
|
+
return `Task '${args.task_id}' not found.`;
|
|
238
|
+
const elapsed = Math.round((Date.now() - task.startedAt) / 1000);
|
|
239
|
+
let info = `Task ${task.taskId} (@${task.agentSlug})\nStatus: ${task.status}\nDescription: ${task.description}\nElapsed: ${elapsed}s`;
|
|
240
|
+
if (task.result)
|
|
241
|
+
info += `\n\nResult:\n${task.result.slice(0, 2000)}`;
|
|
242
|
+
return info;
|
|
243
|
+
}
|
|
244
|
+
if (args.agent_name) {
|
|
245
|
+
const agent = getAgent(args.agent_name);
|
|
246
|
+
if (!agent)
|
|
247
|
+
return `Agent '${args.agent_name}' not found.`;
|
|
248
|
+
const status = getAgentSessionStatus(agent.slug);
|
|
249
|
+
let info = `@${agent.slug} (${agent.name})\nModel: ${agent.model}`;
|
|
250
|
+
if (status.tasks.length > 0) {
|
|
251
|
+
info += `\n\nActive tasks (${status.tasks.length}):`;
|
|
252
|
+
for (const t of status.tasks) {
|
|
253
|
+
info += `\n• ${t.taskId}: ${t.description} (${t.status})`;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return info;
|
|
257
|
+
}
|
|
258
|
+
// Show all agents
|
|
259
|
+
const agents = getAgentRegistry();
|
|
260
|
+
const lines = agents.map((a) => {
|
|
261
|
+
const status = getAgentSessionStatus(a.slug);
|
|
262
|
+
const runningTasks = status.tasks.filter((t) => t.status === "running");
|
|
263
|
+
const sessionBadge = runningTasks.length > 0 ? "●" : "○";
|
|
264
|
+
const taskInfo = runningTasks.length > 0 ? ` (${runningTasks.length} task(s) running)` : "";
|
|
265
|
+
return `${sessionBadge} @${a.slug} — ${a.description} [${a.model}]${taskInfo}`;
|
|
266
|
+
});
|
|
267
|
+
return `Agents (${agents.length}):\n${lines.join("\n")}`;
|
|
268
|
+
},
|
|
269
|
+
}),
|
|
270
|
+
defineTool("get_agent_result", {
|
|
271
|
+
description: "Get the result of a completed agent task.",
|
|
272
|
+
parameters: z.object({
|
|
273
|
+
task_id: z.string().describe("The task ID (from delegate_to_agent)"),
|
|
274
|
+
}),
|
|
275
|
+
handler: async (args) => {
|
|
276
|
+
const task = getTask(args.task_id);
|
|
277
|
+
if (!task) {
|
|
278
|
+
// Check DB for completed tasks that may have been cleared from memory
|
|
279
|
+
const db = getDb();
|
|
280
|
+
const row = db.prepare(`SELECT * FROM agent_tasks WHERE task_id = ?`).get(args.task_id);
|
|
281
|
+
if (!row)
|
|
282
|
+
return `Task '${args.task_id}' not found.`;
|
|
283
|
+
return `Task ${row.task_id} (@${row.agent_slug})\nStatus: ${row.status}\nDescription: ${row.description}\n\nResult:\n${row.result || "(no result)"}`;
|
|
284
|
+
}
|
|
285
|
+
if (task.status === "running") {
|
|
286
|
+
const elapsed = Math.round((Date.now() - task.startedAt) / 1000);
|
|
287
|
+
return `Task ${task.taskId} is still running (${elapsed}s elapsed).`;
|
|
288
|
+
}
|
|
289
|
+
return `Task ${task.taskId} (@${task.agentSlug}) — ${task.status}\n\nResult:\n${task.result || "(no result)"}`;
|
|
290
|
+
},
|
|
291
|
+
}),
|
|
292
|
+
defineTool("show_agent_roster", {
|
|
293
|
+
description: "List all registered agents with their name, model, status, and current tasks.",
|
|
294
|
+
parameters: z.object({}),
|
|
295
|
+
handler: async () => {
|
|
296
|
+
const agents = getAgentRegistry();
|
|
297
|
+
const chLines = agents.map((a) => {
|
|
298
|
+
const status = getAgentSessionStatus(a.slug);
|
|
299
|
+
const runningTasks = status.tasks.filter((t) => t.status === "running");
|
|
300
|
+
const badge = runningTasks.length > 0 ? "● working" : "○ idle";
|
|
301
|
+
const taskInfo = runningTasks.length > 0
|
|
302
|
+
? `\n Tasks: ${runningTasks.map((t) => `${t.taskId}: ${t.description}`).join(", ")}`
|
|
303
|
+
: "";
|
|
304
|
+
return `• @${a.slug} (${a.name}) — ${a.model} — ${badge}${taskInfo}\n ${a.description}`;
|
|
305
|
+
});
|
|
306
|
+
if (chLines.length === 0)
|
|
307
|
+
return "No agents registered.";
|
|
308
|
+
return `Registered agents (${chLines.length}):\n${chLines.join("\n")}`;
|
|
309
|
+
},
|
|
310
|
+
}),
|
|
311
|
+
defineTool("teams_notify", {
|
|
312
|
+
description: "Send a notification to the team Microsoft Teams channel",
|
|
313
|
+
parameters: z.object({
|
|
314
|
+
title: z.string().min(1).describe("Notification title"),
|
|
315
|
+
message: z.string().min(1).describe("Notification body"),
|
|
316
|
+
}),
|
|
317
|
+
handler: async (args) => {
|
|
318
|
+
const notifier = new TeamsNotifier();
|
|
319
|
+
const sent = await notifier.sendMessage(args.title, args.message);
|
|
320
|
+
return sent
|
|
321
|
+
? "Sent notification to the team Microsoft Teams channel."
|
|
322
|
+
: "Teams notifications are disabled or TEAMS_WEBHOOK_URL is not configured.";
|
|
323
|
+
},
|
|
324
|
+
}),
|
|
325
|
+
defineTool("hire_agent", {
|
|
326
|
+
description: "Create a new custom agent by writing an .agent.md file to ~/.chapterhouse/agents/. " +
|
|
327
|
+
"The agent will be available immediately after creation.",
|
|
328
|
+
parameters: z.object({
|
|
329
|
+
slug: z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/).describe("Kebab-case identifier, e.g. 'data-analyst'"),
|
|
330
|
+
name: z.string().describe("Human-readable name"),
|
|
331
|
+
description: z.string().describe("One-line description of the agent's specialty"),
|
|
332
|
+
model: z.string().describe("Model to use (e.g. 'claude-sonnet-4.6', 'gpt-5.4', or 'auto')"),
|
|
333
|
+
system_prompt: z.string().describe("The agent's system prompt (markdown)"),
|
|
334
|
+
skills: z.array(z.string()).optional().describe("Skills to attach to this agent"),
|
|
335
|
+
tools: z.array(z.string()).optional().describe("Tool allowlist (omit for all execution tools)"),
|
|
336
|
+
}),
|
|
337
|
+
handler: async (args) => {
|
|
338
|
+
const err = createAgentFile(args.slug, args.name, args.description, args.model, args.system_prompt, args.skills, args.tools);
|
|
339
|
+
if (err)
|
|
340
|
+
return err;
|
|
341
|
+
// Reload registry
|
|
342
|
+
loadAgents();
|
|
343
|
+
return `Agent @${args.slug} created. It's ready for delegation.`;
|
|
344
|
+
},
|
|
345
|
+
}),
|
|
346
|
+
defineTool("fire_agent", {
|
|
347
|
+
description: "Remove a custom agent's .agent.md file and destroy its session. Cannot remove built-in agents.",
|
|
348
|
+
parameters: z.object({
|
|
349
|
+
slug: z.string().describe("The agent slug to remove"),
|
|
350
|
+
}),
|
|
351
|
+
handler: async (args) => {
|
|
352
|
+
const err = removeAgentFile(args.slug);
|
|
353
|
+
if (err)
|
|
354
|
+
return err;
|
|
355
|
+
loadAgents();
|
|
356
|
+
return `Agent @${args.slug} removed.`;
|
|
357
|
+
},
|
|
358
|
+
}),
|
|
359
|
+
defineTool("list_machine_sessions", {
|
|
360
|
+
description: "List ALL Copilot CLI sessions on this machine — including sessions started from VS Code, " +
|
|
361
|
+
"the terminal, or other tools. Shows session ID, summary, working directory. " +
|
|
362
|
+
"Use this when the user asks about existing sessions running on the machine. " +
|
|
363
|
+
"By default shows the 20 most recently active sessions.",
|
|
364
|
+
parameters: z.object({
|
|
365
|
+
cwd_filter: z.string().optional().describe("Optional: only show sessions whose working directory contains this string"),
|
|
366
|
+
limit: z.number().int().min(1).max(100).optional().describe("Chapterhouse sessions to return (default 20)"),
|
|
367
|
+
}),
|
|
368
|
+
handler: async (args) => {
|
|
369
|
+
const sessionStateDir = join(homedir(), ".copilot", "session-state");
|
|
370
|
+
const limit = args.limit || 20;
|
|
371
|
+
let entries = [];
|
|
372
|
+
try {
|
|
373
|
+
const dirs = readdirSync(sessionStateDir);
|
|
374
|
+
for (const dir of dirs) {
|
|
375
|
+
const yamlPath = join(sessionStateDir, dir, "workspace.yaml");
|
|
376
|
+
try {
|
|
377
|
+
const content = readFileSync(yamlPath, "utf-8");
|
|
378
|
+
const parsed = parseSimpleYaml(content);
|
|
379
|
+
if (args.cwd_filter && !parsed.cwd?.includes(args.cwd_filter))
|
|
380
|
+
continue;
|
|
381
|
+
entries.push({
|
|
382
|
+
id: parsed.id || dir,
|
|
383
|
+
cwd: parsed.cwd || "unknown",
|
|
384
|
+
summary: parsed.summary || "",
|
|
385
|
+
updatedAt: parsed.updated_at ? new Date(parsed.updated_at) : new Date(0),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// Skip dirs without valid workspace.yaml
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
395
|
+
return "No Copilot sessions found on this machine (session state directory does not exist yet).";
|
|
396
|
+
}
|
|
397
|
+
return "Could not read session state directory.";
|
|
398
|
+
}
|
|
399
|
+
// Sort by most recently updated
|
|
400
|
+
entries.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
401
|
+
entries = entries.slice(0, limit);
|
|
402
|
+
if (entries.length === 0) {
|
|
403
|
+
return "No Copilot sessions found on this machine.";
|
|
404
|
+
}
|
|
405
|
+
const lines = entries.map((s) => {
|
|
406
|
+
const age = formatAge(s.updatedAt);
|
|
407
|
+
const summary = s.summary ? ` — ${s.summary}` : "";
|
|
408
|
+
return `• ID: ${s.id}\n ${s.cwd} (${age})${summary}`;
|
|
409
|
+
});
|
|
410
|
+
return `Found ${entries.length} session(s) (most recent first):\n${lines.join("\n")}`;
|
|
411
|
+
},
|
|
412
|
+
}),
|
|
413
|
+
defineTool("attach_machine_session", {
|
|
414
|
+
description: "Attach to an existing Copilot CLI session on this machine (e.g. one started from VS Code or terminal). " +
|
|
415
|
+
"Resumes the session so you can observe or interact with it.",
|
|
416
|
+
parameters: z.object({
|
|
417
|
+
session_id: z.string().describe("The session ID to attach to (from list_machine_sessions)"),
|
|
418
|
+
name: z.string().describe("A short name to reference this session by, e.g. 'vscode-main'"),
|
|
419
|
+
}),
|
|
420
|
+
handler: async (args) => {
|
|
421
|
+
try {
|
|
422
|
+
await deps.client.resumeSession(args.session_id, {
|
|
423
|
+
model: config.copilotModel,
|
|
424
|
+
onPermissionRequest: approveAll,
|
|
425
|
+
});
|
|
426
|
+
const db = getDb();
|
|
427
|
+
db.prepare(`INSERT OR REPLACE INTO agent_sessions (slug, copilot_session_id, model, status)
|
|
428
|
+
VALUES (?, ?, ?, 'idle')`).run(args.name, args.session_id, config.copilotModel);
|
|
429
|
+
return `Attached to session ${args.session_id.slice(0, 8)}… as '${args.name}'.`;
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
433
|
+
return `Failed to attach to session: ${msg}`;
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
}),
|
|
437
|
+
defineTool("list_skills", {
|
|
438
|
+
description: "List all available skills that Chapterhouse knows. Skills are instruction documents that teach Chapterhouse " +
|
|
439
|
+
"how to use external tools and services (e.g. Gmail, browser automation, YouTube transcripts). " +
|
|
440
|
+
"Shows skill name, description, and whether it's a local or global skill.",
|
|
441
|
+
parameters: z.object({}),
|
|
442
|
+
handler: async () => {
|
|
443
|
+
const skills = listSkills();
|
|
444
|
+
if (skills.length === 0) {
|
|
445
|
+
return "No skills installed yet. Use learn_skill to teach me something new.";
|
|
446
|
+
}
|
|
447
|
+
const lines = skills.map((s) => `• ${s.name} (${s.source}) — ${s.description}`);
|
|
448
|
+
return `Available skills (${skills.length}):\n${lines.join("\n")}`;
|
|
449
|
+
},
|
|
450
|
+
}),
|
|
451
|
+
defineTool("learn_skill", {
|
|
452
|
+
description: "Teach Chapterhouse a new skill by creating a SKILL.md instruction file. Use this when the user asks Chapterhouse " +
|
|
453
|
+
"to do something it doesn't know how to do yet (e.g. 'check my email', 'search the web'). " +
|
|
454
|
+
"First, use a worker session to research what CLI tools are available on the system (run 'which', " +
|
|
455
|
+
"'--help', etc.), then create the skill with the instructions you've learned. " +
|
|
456
|
+
"The skill becomes available on the next message (no restart needed).",
|
|
457
|
+
parameters: z.object({
|
|
458
|
+
slug: z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/).describe("Short kebab-case identifier for the skill, e.g. 'gmail', 'web-search'"),
|
|
459
|
+
name: z.string().refine(s => !s.includes('\n'), "must be single-line").describe("Human-readable name for the skill, e.g. 'Gmail', 'Web Search'"),
|
|
460
|
+
description: z.string().refine(s => !s.includes('\n'), "must be single-line").describe("One-line description of when to use this skill"),
|
|
461
|
+
instructions: z.string().describe("Markdown instructions for how to use the skill. Include: what CLI tool to use, " +
|
|
462
|
+
"common commands with examples, authentication steps if needed, tips and gotchas. " +
|
|
463
|
+
"This becomes the SKILL.md content body."),
|
|
464
|
+
}),
|
|
465
|
+
handler: async (args) => {
|
|
466
|
+
return createSkill(args.slug, args.name, args.description, args.instructions);
|
|
467
|
+
},
|
|
468
|
+
}),
|
|
469
|
+
defineTool("uninstall_skill", {
|
|
470
|
+
description: "Remove a skill from Chapterhouse's local skills directory (~/.chapterhouse/skills/). " +
|
|
471
|
+
"The skill will no longer be available on the next message. " +
|
|
472
|
+
"Only works for local skills — bundled and global skills cannot be removed this way.",
|
|
473
|
+
parameters: z.object({
|
|
474
|
+
slug: z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/).describe("The kebab-case slug of the skill to remove, e.g. 'gmail', 'web-search'"),
|
|
475
|
+
}),
|
|
476
|
+
handler: async (args) => {
|
|
477
|
+
const result = removeSkill(args.slug);
|
|
478
|
+
return result.message;
|
|
479
|
+
},
|
|
480
|
+
}),
|
|
481
|
+
defineTool("list_models", {
|
|
482
|
+
description: "List all available Copilot models. Shows model id, name, and billing tier. " +
|
|
483
|
+
"Marks the currently active model. Use when the user asks what models are available " +
|
|
484
|
+
"or wants to know which model is in use.",
|
|
485
|
+
parameters: z.object({}),
|
|
486
|
+
handler: async () => {
|
|
487
|
+
try {
|
|
488
|
+
const models = await deps.client.listModels();
|
|
489
|
+
if (models.length === 0) {
|
|
490
|
+
return "No models available.";
|
|
491
|
+
}
|
|
492
|
+
const current = config.copilotModel;
|
|
493
|
+
const lines = models.map((m) => {
|
|
494
|
+
const active = m.id === current ? " ← active" : "";
|
|
495
|
+
const billing = m.billing ? ` (${m.billing.multiplier}x)` : "";
|
|
496
|
+
return `• ${m.id}${billing}${active}`;
|
|
497
|
+
});
|
|
498
|
+
return `Available models (${models.length}):\n${lines.join("\n")}\n\nCurrent: ${current}`;
|
|
499
|
+
}
|
|
500
|
+
catch (err) {
|
|
501
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
502
|
+
return `Failed to list models: ${msg}`;
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
}),
|
|
506
|
+
defineTool("switch_model", {
|
|
507
|
+
description: "Switch the Copilot model Chapterhouse uses for conversations. Takes effect on the next message. " +
|
|
508
|
+
"The change is persisted across restarts. Use when the user asks to change or switch models.",
|
|
509
|
+
parameters: z.object({
|
|
510
|
+
model_id: z.string().describe("The model id to switch to (from list_models)"),
|
|
511
|
+
}),
|
|
512
|
+
handler: async (args) => {
|
|
513
|
+
try {
|
|
514
|
+
const models = await deps.client.listModels();
|
|
515
|
+
const match = models.find((m) => m.id === args.model_id);
|
|
516
|
+
if (!match) {
|
|
517
|
+
const suggestions = models
|
|
518
|
+
.filter((m) => m.id.includes(args.model_id) || m.id.toLowerCase().includes(args.model_id.toLowerCase()))
|
|
519
|
+
.map((m) => m.id);
|
|
520
|
+
const hint = suggestions.length > 0
|
|
521
|
+
? ` Did you mean: ${suggestions.join(", ")}?`
|
|
522
|
+
: " Use list_models to see available options.";
|
|
523
|
+
return `Model '${args.model_id}' not found.${hint}`;
|
|
524
|
+
}
|
|
525
|
+
const previous = config.copilotModel;
|
|
526
|
+
config.copilotModel = args.model_id;
|
|
527
|
+
persistModel(args.model_id);
|
|
528
|
+
// Apply model change to the live session immediately
|
|
529
|
+
try {
|
|
530
|
+
await switchSessionModel(args.model_id);
|
|
531
|
+
}
|
|
532
|
+
catch (err) {
|
|
533
|
+
log.warn({ err: err instanceof Error ? err.message : err }, "setModel() failed during switch_model, will apply on next session");
|
|
534
|
+
}
|
|
535
|
+
// Disable router when manually switching — user has explicit preference
|
|
536
|
+
if (getRouterConfig().enabled) {
|
|
537
|
+
updateRouterConfig({ enabled: false });
|
|
538
|
+
return `Switched model from '${previous}' to '${args.model_id}'. Auto-routing disabled (use /auto or toggle_auto to re-enable).`;
|
|
539
|
+
}
|
|
540
|
+
return `Switched model from '${previous}' to '${args.model_id}'.`;
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
544
|
+
return `Failed to switch model: ${msg}`;
|
|
545
|
+
}
|
|
546
|
+
},
|
|
547
|
+
}),
|
|
548
|
+
defineTool("toggle_auto", {
|
|
549
|
+
description: "Enable or disable automatic model routing (auto mode). When enabled, Chapterhouse automatically picks " +
|
|
550
|
+
"the best model (fast/standard/premium) for each message to save cost and optimize speed. " +
|
|
551
|
+
"Use when the user asks to turn auto-routing on or off.",
|
|
552
|
+
parameters: z.object({
|
|
553
|
+
enabled: z.boolean().describe("true to enable auto-routing, false to disable"),
|
|
554
|
+
}),
|
|
555
|
+
handler: async (args) => {
|
|
556
|
+
const updated = updateRouterConfig({ enabled: args.enabled });
|
|
557
|
+
if (args.enabled) {
|
|
558
|
+
const tiers = updated.tierModels;
|
|
559
|
+
return `Auto-routing enabled. Tier models:\n• fast: ${tiers.fast}\n• standard: ${tiers.standard}\n• premium: ${tiers.premium}\n\nMax will automatically pick the best model for each message.`;
|
|
560
|
+
}
|
|
561
|
+
return `Auto-routing disabled. Using fixed model: ${config.copilotModel}`;
|
|
562
|
+
},
|
|
563
|
+
}),
|
|
564
|
+
defineTool("restart_chapterhouse", {
|
|
565
|
+
description: "Restart the Chapterhouse daemon process. Use when the user asks Chapterhouse to restart himself, " +
|
|
566
|
+
"or when a restart is needed to pick up configuration changes. " +
|
|
567
|
+
"Spawns a new process and exits the current one.",
|
|
568
|
+
parameters: z.object({
|
|
569
|
+
reason: z.string().optional().describe("Optional reason for the restart"),
|
|
570
|
+
}),
|
|
571
|
+
handler: async (args) => {
|
|
572
|
+
const reason = args.reason ? ` (${args.reason})` : "";
|
|
573
|
+
// Dynamic import to avoid circular dependency
|
|
574
|
+
const { restartDaemon } = await import("../../daemon.js");
|
|
575
|
+
// Schedule restart after returning the response
|
|
576
|
+
setTimeout(() => {
|
|
577
|
+
restartDaemon().catch((err) => {
|
|
578
|
+
log.error({ err: err instanceof Error ? err.message : err }, "Restart failed");
|
|
579
|
+
});
|
|
580
|
+
}, 1000);
|
|
581
|
+
return `Restarting Chapterhouse${reason}. I'll be back in a few seconds.`;
|
|
582
|
+
},
|
|
583
|
+
}),
|
|
584
|
+
];
|
|
585
|
+
}
|
|
586
|
+
function formatAge(date) {
|
|
587
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
588
|
+
if (seconds < 60)
|
|
589
|
+
return "just now";
|
|
590
|
+
if (seconds < 3600)
|
|
591
|
+
return `${Math.floor(seconds / 60)}m ago`;
|
|
592
|
+
if (seconds < 86400)
|
|
593
|
+
return `${Math.floor(seconds / 3600)}h ago`;
|
|
594
|
+
return `${Math.floor(seconds / 86400)}d ago`;
|
|
595
|
+
}
|
|
596
|
+
function parseSimpleYaml(content) {
|
|
597
|
+
const result = {};
|
|
598
|
+
for (const line of content.split("\n")) {
|
|
599
|
+
const idx = line.indexOf(": ");
|
|
600
|
+
if (idx > 0) {
|
|
601
|
+
const key = line.slice(0, idx).trim();
|
|
602
|
+
const value = line.slice(idx + 2).trim();
|
|
603
|
+
result[key] = value;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return result;
|
|
607
|
+
}
|
|
608
|
+
//# sourceMappingURL=agent.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createAgentTools } from "./agent.js";
|
|
2
|
+
import { createMemoryTools } from "./memory.js";
|
|
3
|
+
import { createOkrTools } from "./okr.js";
|
|
4
|
+
import { createWikiTools } from "./wiki.js";
|
|
5
|
+
export { createAgentTools } from "./agent.js";
|
|
6
|
+
export { createMemoryTools } from "./memory.js";
|
|
7
|
+
export { createOkrTools, getMyOkrsSummary } from "./okr.js";
|
|
8
|
+
export { createWikiTools } from "./wiki.js";
|
|
9
|
+
export function createTools(deps) {
|
|
10
|
+
let allTools = [];
|
|
11
|
+
allTools = [
|
|
12
|
+
...createAgentTools(deps, () => allTools),
|
|
13
|
+
...createOkrTools(deps),
|
|
14
|
+
...createMemoryTools(deps),
|
|
15
|
+
...createWikiTools(deps),
|
|
16
|
+
];
|
|
17
|
+
return allTools;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=index.js.map
|