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.
Files changed (112) hide show
  1. package/README.md +1 -1
  2. package/agents/korg.agent.md +20 -0
  3. package/dist/api/auth.js +11 -1
  4. package/dist/api/auth.test.js +29 -0
  5. package/dist/api/errors.js +23 -0
  6. package/dist/api/route-coverage.test.js +61 -21
  7. package/dist/api/routes/agents.js +472 -0
  8. package/dist/api/routes/memory.js +299 -0
  9. package/dist/api/routes/projects.js +170 -0
  10. package/dist/api/routes/sessions.js +347 -0
  11. package/dist/api/routes/system.js +82 -0
  12. package/dist/api/routes/wiki.js +455 -0
  13. package/dist/api/routes/wiki.test.js +49 -0
  14. package/dist/api/send-json.js +16 -0
  15. package/dist/api/send-json.test.js +18 -0
  16. package/dist/api/server-runtime.js +45 -3
  17. package/dist/api/server.js +34 -1764
  18. package/dist/api/server.test.js +239 -8
  19. package/dist/api/sse-hub.js +37 -0
  20. package/dist/cli.js +1 -1
  21. package/dist/config.js +151 -58
  22. package/dist/config.test.js +29 -0
  23. package/dist/copilot/okr-mapper.js +2 -11
  24. package/dist/copilot/orchestrator.js +358 -352
  25. package/dist/copilot/orchestrator.test.js +139 -4
  26. package/dist/copilot/prompt-date.js +2 -1
  27. package/dist/copilot/session-manager.js +25 -23
  28. package/dist/copilot/session-manager.test.js +35 -1
  29. package/dist/copilot/standup.js +2 -2
  30. package/dist/copilot/task-event-log.js +7 -1
  31. package/dist/copilot/task-event-log.test.js +13 -0
  32. package/dist/copilot/tools/agent.js +608 -0
  33. package/dist/copilot/tools/index.js +19 -0
  34. package/dist/copilot/tools/memory.js +678 -0
  35. package/dist/copilot/tools/models.js +2 -0
  36. package/dist/copilot/tools/okr.js +171 -0
  37. package/dist/copilot/tools/wiki.js +333 -0
  38. package/dist/copilot/tools-deps.js +4 -0
  39. package/dist/copilot/tools.agent.test.js +10 -8
  40. package/dist/copilot/tools.inventory.test.js +76 -0
  41. package/dist/copilot/tools.js +1 -1725
  42. package/dist/copilot/tools.okr.test.js +31 -0
  43. package/dist/copilot/tools.wiki.test.js +358 -6
  44. package/dist/copilot/turn-event-log.js +31 -4
  45. package/dist/copilot/turn-event-log.test.js +24 -2
  46. package/dist/copilot/workiq-installer.test.js +2 -2
  47. package/dist/daemon-install.js +3 -2
  48. package/dist/daemon.js +9 -17
  49. package/dist/integrations/ado-client.js +90 -9
  50. package/dist/integrations/ado-client.test.js +56 -0
  51. package/dist/integrations/team-push.js +1 -0
  52. package/dist/integrations/team-push.test.js +6 -0
  53. package/dist/integrations/teams-notify.js +1 -0
  54. package/dist/integrations/teams-notify.test.js +5 -0
  55. package/dist/memory/active-scope.test.js +0 -1
  56. package/dist/memory/checkpoint.js +89 -72
  57. package/dist/memory/checkpoint.test.js +23 -3
  58. package/dist/memory/eot.js +194 -89
  59. package/dist/memory/eot.test.js +186 -3
  60. package/dist/memory/hooks.js +2 -4
  61. package/dist/memory/housekeeping-scheduler.js +1 -1
  62. package/dist/memory/housekeeping-scheduler.test.js +1 -2
  63. package/dist/memory/housekeeping.js +100 -3
  64. package/dist/memory/housekeeping.test.js +33 -2
  65. package/dist/memory/reflect.test.js +2 -0
  66. package/dist/memory/scope-lock.js +26 -0
  67. package/dist/memory/scope-lock.test.js +118 -0
  68. package/dist/memory/scopes.test.js +0 -1
  69. package/dist/mode-context.js +58 -5
  70. package/dist/mode-context.test.js +68 -0
  71. package/dist/paths.js +1 -0
  72. package/dist/setup.js +3 -2
  73. package/dist/shared/api-schemas.js +48 -5
  74. package/dist/store/connection.js +96 -0
  75. package/dist/store/db.js +5 -1498
  76. package/dist/store/db.test.js +182 -1
  77. package/dist/store/migrations.js +460 -0
  78. package/dist/store/repositories/memory.js +281 -0
  79. package/dist/store/repositories/okr.js +3 -0
  80. package/dist/store/repositories/projects.js +5 -0
  81. package/dist/store/repositories/sessions.js +284 -0
  82. package/dist/store/repositories/wiki.js +60 -0
  83. package/dist/store/schema.js +501 -0
  84. package/dist/util/logger.js +3 -2
  85. package/dist/wiki/consolidation.js +50 -9
  86. package/dist/wiki/consolidation.test.js +45 -0
  87. package/dist/wiki/frontmatter.js +45 -14
  88. package/dist/wiki/frontmatter.test.js +26 -1
  89. package/dist/wiki/fs.js +16 -4
  90. package/dist/wiki/fs.test.js +84 -0
  91. package/dist/wiki/index-manager.js +30 -2
  92. package/dist/wiki/index-manager.test.js +43 -12
  93. package/dist/wiki/ingest.js +17 -1
  94. package/dist/wiki/lock.js +11 -1
  95. package/dist/wiki/log-manager.js +2 -7
  96. package/dist/wiki/migrate.js +44 -17
  97. package/dist/wiki/project-registry.js +10 -5
  98. package/dist/wiki/project-registry.test.js +14 -0
  99. package/dist/wiki/scheduler.js +1 -1
  100. package/dist/wiki/seed-team-wiki.js +2 -1
  101. package/dist/wiki/team-sync.js +31 -6
  102. package/dist/wiki/team-sync.test.js +81 -0
  103. package/package.json +1 -1
  104. package/web/dist/assets/WikiEdit-BZXAdarz.js +30 -0
  105. package/web/dist/assets/WikiEdit-BZXAdarz.js.map +1 -0
  106. package/web/dist/assets/WikiGraph-KrCYco4v.js +2 -0
  107. package/web/dist/assets/WikiGraph-KrCYco4v.js.map +1 -0
  108. package/web/dist/assets/index-CUm2Wbuh.js +250 -0
  109. package/web/dist/assets/index-CUm2Wbuh.js.map +1 -0
  110. package/web/dist/index.html +1 -1
  111. package/web/dist/assets/index-iQrv3lQN.js +0 -286
  112. 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