maestro-agent 0.0.1 → 0.0.3

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 (111) hide show
  1. package/README.md +316 -2
  2. package/bin/maestro.ts +5 -0
  3. package/dist/maestro +0 -0
  4. package/dist/web/apple-touch-icon.png +0 -0
  5. package/dist/web/assets/Connections-BMA04Ycg.js +11 -0
  6. package/dist/web/assets/GanttView-DXjh0gxg.js +49 -0
  7. package/dist/web/assets/Home-Ct3Ho0Qt.js +1 -0
  8. package/dist/web/assets/HooksCrons--0kyVJcR.js +11 -0
  9. package/dist/web/assets/ProjectDetail-B_IqEpFu.js +1 -0
  10. package/dist/web/assets/Roles-D1tIQzto.js +24 -0
  11. package/dist/web/assets/Settings-yts4LUmH.js +11 -0
  12. package/dist/web/assets/Skills-DbuNLjIV.js +12 -0
  13. package/dist/web/assets/Wizard-vJol8-Y4.js +11 -0
  14. package/dist/web/assets/WorkspaceChat-DrsLs4m2.js +56 -0
  15. package/dist/web/assets/WorkspaceDashboard-B9vgrd2Z.js +6 -0
  16. package/dist/web/assets/WorkspaceNew-DoNGYHCG.js +1 -0
  17. package/dist/web/assets/WorkspaceProjects-DDp3mUse.js +6 -0
  18. package/dist/web/assets/WorkspaceSchedules-BTjmCbYG.js +1 -0
  19. package/dist/web/assets/WorkspaceTasks-mPU-bhKR.js +41 -0
  20. package/dist/web/assets/activity-CIA8bIA4.js +6 -0
  21. package/dist/web/assets/addon-fit-BlxrFPDK.js +1 -0
  22. package/dist/web/assets/arrow-right-S7ID7nDp.js +6 -0
  23. package/dist/web/assets/badge-DDTUzWIi.js +1 -0
  24. package/dist/web/assets/circle-check-B3P1qK0Z.js +6 -0
  25. package/dist/web/assets/clock-f9aYZox0.js +6 -0
  26. package/dist/web/assets/index-BRo4Du_s.js +11 -0
  27. package/dist/web/assets/index-C7kx39S9.js +196 -0
  28. package/dist/web/assets/index-D6LSdZea.css +1 -0
  29. package/dist/web/assets/plus-BHnOxbns.js +6 -0
  30. package/dist/web/assets/refresh-cw-BWX04Hg3.js +6 -0
  31. package/dist/web/assets/save-BLbb_9xz.js +6 -0
  32. package/dist/web/assets/sparkles-CDr6Dw1e.js +6 -0
  33. package/dist/web/assets/trash-2-9-ThEdey.js +6 -0
  34. package/dist/web/assets/useEventStream-DXt2Hmei.js +1 -0
  35. package/dist/web/assets/x-DVdKPXXy.js +6 -0
  36. package/dist/web/assets/xterm-DYP7pi_n.css +32 -0
  37. package/dist/web/assets/xterm-DlVFs1Kw.js +9 -0
  38. package/dist/web/favicon-512.png +0 -0
  39. package/dist/web/favicon.png +0 -0
  40. package/dist/web/index.html +15 -0
  41. package/package.json +49 -6
  42. package/src/api/agents.ts +76 -0
  43. package/src/api/audit.ts +19 -0
  44. package/src/api/autopilot.ts +73 -0
  45. package/src/api/chat.ts +801 -0
  46. package/src/api/chief.ts +84 -0
  47. package/src/api/config.ts +39 -0
  48. package/src/api/gantt.ts +72 -0
  49. package/src/api/hooks.ts +54 -0
  50. package/src/api/inbox.ts +125 -0
  51. package/src/api/lark.ts +32 -0
  52. package/src/api/memory.ts +37 -0
  53. package/src/api/ops.ts +89 -0
  54. package/src/api/projects.ts +105 -0
  55. package/src/api/roles.ts +123 -0
  56. package/src/api/runtimes.ts +62 -0
  57. package/src/api/scheduled-tasks.ts +203 -0
  58. package/src/api/sessions.ts +479 -0
  59. package/src/api/skills.ts +386 -0
  60. package/src/api/tasks.ts +457 -0
  61. package/src/api/telegram.ts +94 -0
  62. package/src/api/templates.ts +36 -0
  63. package/src/api/webhooks.ts +20 -0
  64. package/src/api/workspaces.ts +150 -0
  65. package/src/bridges/lark/index.ts +213 -0
  66. package/src/bridges/telegram/index.ts +273 -0
  67. package/src/bridges/telegram/polling.ts +185 -0
  68. package/src/chat/index.ts +86 -0
  69. package/src/chief/index.ts +461 -0
  70. package/src/core/cli.ts +333 -0
  71. package/src/core/db.ts +53 -0
  72. package/src/core/event-bus.ts +33 -0
  73. package/src/core/index.ts +6 -0
  74. package/src/core/migrations.ts +303 -0
  75. package/src/core/router.ts +69 -0
  76. package/src/core/schema.sql +232 -0
  77. package/src/core/server.ts +308 -0
  78. package/src/core/validate.ts +22 -0
  79. package/src/discovery/index.ts +194 -0
  80. package/src/gateway/adapters/telegram.ts +148 -0
  81. package/src/gateway/index.ts +31 -0
  82. package/src/gateway/manager.ts +176 -0
  83. package/src/gateway/types.ts +77 -0
  84. package/src/inbox/index.ts +500 -0
  85. package/src/ops/artifact-sync.ts +65 -0
  86. package/src/ops/autopilot.ts +338 -0
  87. package/src/ops/gc.ts +252 -0
  88. package/src/ops/index.ts +226 -0
  89. package/src/ops/project-serial.ts +52 -0
  90. package/src/ops/role-dispatch.ts +111 -0
  91. package/src/ops/runtime-scheduler.ts +447 -0
  92. package/src/ops/task-blocking.ts +65 -0
  93. package/src/ops/task-deps.ts +37 -0
  94. package/src/ops/task-workspace.ts +60 -0
  95. package/src/roles/index.ts +258 -0
  96. package/src/roles/prompt-assembler.ts +85 -0
  97. package/src/roles/workspace-role.ts +155 -0
  98. package/src/scheduler/index.ts +461 -0
  99. package/src/session/output-parser.ts +75 -0
  100. package/src/session/realtime-parser.ts +40 -0
  101. package/src/skills/builtin.ts +155 -0
  102. package/src/skills/skill-extractor.ts +452 -0
  103. package/src/skills/skill-md.ts +282 -0
  104. package/src/transport/http-api.ts +75 -0
  105. package/src/transport/index.ts +4 -0
  106. package/src/transport/local-pty.ts +119 -0
  107. package/src/transport/ssh.ts +176 -0
  108. package/src/transport/types.ts +20 -0
  109. package/src/workflows/index.ts +231 -0
  110. package/index.js +0 -1
  111. package/maestro-agent-0.0.1.tgz +0 -0
@@ -0,0 +1,801 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { generateId, now } from "../core/db";
5
+ import type { Router } from "../core/router";
6
+ import { body, json } from "../core/router";
7
+ import type { HubContext } from "../core/server";
8
+ import { createWorkspaceRole } from "../roles/workspace-role";
9
+ import { computeNextFireAt, validateScheduleInput } from "../scheduler";
10
+ import { getConfig } from "./config";
11
+ import { claimTaskForAgent } from "./tasks";
12
+
13
+ // ─── Types ───────────────────────────────────────────────────────────────────
14
+
15
+ interface ConversationRow {
16
+ workspace_id: string;
17
+ messages_json: string;
18
+ model: string;
19
+ system_prompt: string | null;
20
+ updated_at: number;
21
+ }
22
+
23
+ // ─── Constants ───────────────────────────────────────────────────────────────
24
+
25
+ const DEFAULT_MODEL = "claude-sonnet-4-20250514";
26
+ const MAX_TURNS = 10;
27
+
28
+ const DEFAULT_SYSTEM = `You are MAESTRO Agent, an AI assistant collaborating with the user on their workspace.
29
+ You can use tools to manage tasks, agents, and projects within the MAESTRO system.
30
+ Be concise, helpful, and action-oriented. When given instructions, execute them directly.`;
31
+
32
+ // ─── Route Registration ──────────────────────────────────────────────────────
33
+
34
+ export function registerChatRoutes(router: Router, ctx: HubContext) {
35
+ // 查历史消息
36
+ router.get("/api/chat/messages", (req) => {
37
+ const url = new URL(req.url);
38
+ const workspaceId = url.searchParams.get("workspace_id");
39
+ if (!workspaceId) return json({ error: "workspace_id required" }, 400);
40
+
41
+ const limit = Math.min(Number(url.searchParams.get("limit") || "50"), 200);
42
+ const beforeSeq = url.searchParams.get("before_seq");
43
+
44
+ let sql = "SELECT * FROM chat_message WHERE workspace_id = ?";
45
+ const params: any[] = [workspaceId];
46
+ if (beforeSeq) {
47
+ sql += " AND seq < ?";
48
+ params.push(Number(beforeSeq));
49
+ }
50
+ sql += " ORDER BY seq DESC LIMIT ?";
51
+ params.push(limit);
52
+
53
+ const rows = ctx.db.query(sql).all(...params) as any[];
54
+ return json(rows.reverse());
55
+ });
56
+
57
+ // 发送消息 — 检测 @mention 并触发 agentic loop
58
+ router.post("/api/chat/messages", async (req) => {
59
+ const input = await body(req);
60
+ const { workspace_id, content } = input;
61
+ if (!workspace_id || !content) {
62
+ return json({ error: "workspace_id and content are required" }, 400);
63
+ }
64
+
65
+ // 写入人类消息
66
+ const humanMsg = insertChatMessage(ctx, workspace_id, "human", "user", content);
67
+ ctx.bus.publish("chat.message", { workspace_id, message_id: humanMsg.id, seq: humanMsg.seq, sender_type: "human" });
68
+
69
+ // 触发 agentic loop(@mention 信息会通过 system prompt 注入,让主 Agent 决定如何拆分任务)
70
+ runAgentLoop(ctx, workspace_id).catch((err) => {
71
+ console.error(`[chat] agent loop error for ${workspace_id}:`, err.message);
72
+ insertChatMessage(ctx, workspace_id, "agent", "system", `[Error] ${err.message}`);
73
+ ctx.bus.publish("chat.message", { workspace_id, sender_type: "agent", error: true });
74
+ });
75
+
76
+ return json(humanMsg, 201);
77
+ });
78
+
79
+ // 获取/设置 conversation 配置
80
+ router.get("/api/chat/conversation/:workspaceId", (req, params) => {
81
+ const conv = getOrCreateConversation(ctx, params.workspaceId);
82
+ return json({ workspace_id: conv.workspace_id, model: getEffectiveModel(ctx, conv), system_prompt: conv.system_prompt, updated_at: conv.updated_at });
83
+ });
84
+
85
+ router.patch("/api/chat/conversation/:workspaceId", async (req, params) => {
86
+ const input = await body(req);
87
+ const conv = getOrCreateConversation(ctx, params.workspaceId);
88
+ if (input.model) {
89
+ ctx.db.run("UPDATE chat_conversation SET model = ?, updated_at = ? WHERE workspace_id = ?", [input.model, now(), params.workspaceId]);
90
+ }
91
+ if (input.system_prompt !== undefined) {
92
+ ctx.db.run("UPDATE chat_conversation SET system_prompt = ?, updated_at = ? WHERE workspace_id = ?", [input.system_prompt, now(), params.workspaceId]);
93
+ }
94
+ if (input.reset_conversation) {
95
+ ctx.db.run("UPDATE chat_conversation SET messages_json = '[]', updated_at = ? WHERE workspace_id = ?", [now(), params.workspaceId]);
96
+ }
97
+ return json({ ok: true });
98
+ });
99
+ }
100
+
101
+ // ─── Agentic Loop ────────────────────────────────────────────────────────────
102
+
103
+ export async function runAgentLoop(ctx: HubContext, workspaceId: string) {
104
+ const apiKey = getConfig(ctx, "chief_api_key", "") || process.env.ANTHROPIC_API_KEY || "";
105
+ if (!apiKey) {
106
+ insertChatMessage(ctx, workspaceId, "agent", "system", "[Error] API Key not configured. Set it in Settings.");
107
+ ctx.bus.publish("chat.message", { workspace_id: workspaceId, sender_type: "agent", error: true });
108
+ return;
109
+ }
110
+
111
+ const baseURL = getConfig(ctx, "chief_base_url", "") || process.env.MAESTRO_CHIEF_BASE_URL || undefined;
112
+ const client = new Anthropic({ apiKey, baseURL });
113
+ const conv = getOrCreateConversation(ctx, workspaceId);
114
+ const model = getEffectiveModel(ctx, conv);
115
+ const systemPrompt = conv.system_prompt || buildSystemPrompt(ctx, workspaceId);
116
+
117
+ // 获取 agent 显示名(从 workspace 关联的角色)
118
+ const agentDisplayName = getAgentDisplayName(ctx, workspaceId);
119
+
120
+ // 加载 conversation state
121
+ let messages: Anthropic.MessageParam[] = JSON.parse(conv.messages_json);
122
+
123
+ // 追加最新的人类消息(从 chat_message 表取最近一条 human 消息的 content)
124
+ const latestHuman = ctx.db.query(
125
+ "SELECT content FROM chat_message WHERE workspace_id = ? AND sender_type = 'human' ORDER BY seq DESC LIMIT 1"
126
+ ).get(workspaceId) as any;
127
+
128
+ if (latestHuman) {
129
+ messages.push({ role: "user", content: latestHuman.content });
130
+ }
131
+
132
+ // Agentic loop
133
+ for (let turn = 0; turn < MAX_TURNS; turn++) {
134
+ const response = await client.messages.create({
135
+ model,
136
+ max_tokens: 4096,
137
+ system: systemPrompt,
138
+ messages,
139
+ tools: chatTools(ctx, workspaceId),
140
+ });
141
+
142
+ // 提取文本回复
143
+ const textBlocks = response.content.filter(
144
+ (b): b is Anthropic.TextBlock => b.type === "text"
145
+ );
146
+ const toolUses = response.content.filter(
147
+ (b): b is Anthropic.ToolUseBlock => b.type === "tool_use"
148
+ );
149
+
150
+ // 如果有文本回复,写入 chat_message
151
+ if (textBlocks.length > 0) {
152
+ const text = textBlocks.map((b) => b.text).join("\n");
153
+ if (text.trim()) {
154
+ const agentMsg = insertChatMessage(ctx, workspaceId, "agent", agentDisplayName, text);
155
+ ctx.bus.publish("chat.message", { workspace_id: workspaceId, message_id: agentMsg.id, seq: agentMsg.seq, sender_type: "agent" });
156
+ }
157
+ }
158
+
159
+ // 追加 assistant 回复到 conversation
160
+ messages.push({ role: "assistant", content: response.content });
161
+
162
+ // 如果没有 tool_use,结束循环
163
+ if (toolUses.length === 0 || response.stop_reason !== "tool_use") {
164
+ break;
165
+ }
166
+
167
+ // 执行 tools 并构造 tool_result
168
+ const toolResults: Anthropic.ToolResultBlockParam[] = [];
169
+ for (const tu of toolUses) {
170
+ const result = await executeToolCall(ctx, workspaceId, tu.name, tu.input as Record<string, any>);
171
+ toolResults.push({
172
+ type: "tool_result",
173
+ tool_use_id: tu.id,
174
+ content: JSON.stringify(result),
175
+ });
176
+ }
177
+
178
+ messages.push({ role: "user", content: toolResults });
179
+ }
180
+
181
+ // 持久化 conversation state
182
+ saveConversation(ctx, workspaceId, messages);
183
+ }
184
+
185
+ // ─── Tool Definitions ────────────────────────────────────────────────────────
186
+
187
+ export function chatTools(_ctx: HubContext, _workspaceId: string): Anthropic.Tool[] {
188
+ return [
189
+ {
190
+ name: "create_task",
191
+ description: "Create a new task in the workspace's project. When the user @mentions a role, use assignee_role to assign the task to that role's agent.",
192
+ input_schema: {
193
+ type: "object" as const,
194
+ properties: {
195
+ title: { type: "string", description: "Task title" },
196
+ description: { type: "string", description: "Task description" },
197
+ priority: { type: "number", description: "Priority (0=normal, 1=high, -1=low)" },
198
+ assignee_role: { type: "string", description: "Role name to assign this task to (e.g. 'Engineer', 'Critic')" },
199
+ },
200
+ required: ["title"],
201
+ },
202
+ },
203
+ {
204
+ name: "create_scheduled_task",
205
+ description: "Create a scheduled task in the current workspace. Use cron_expr for recurring schedules or run_at for a one-time trigger. Use assignee_role to assign the created task to that role's agent.",
206
+ input_schema: {
207
+ type: "object" as const,
208
+ properties: {
209
+ title: { type: "string" },
210
+ description: { type: "string" },
211
+ priority: { type: "number" },
212
+ project_id: { type: "string" },
213
+ schedule_type: { type: "string", description: "cron or once" },
214
+ cron_expr: { type: "string", description: "5-field cron expression, e.g. 0 9 * * 1" },
215
+ run_at: { type: "number", description: "Unix timestamp in milliseconds for one-time schedules" },
216
+ assignee_role: { type: "string", description: "Role name to assign the task to when it fires" },
217
+ },
218
+ required: ["title"],
219
+ },
220
+ },
221
+ {
222
+ name: "list_scheduled_tasks",
223
+ description: "List scheduled tasks in this workspace.",
224
+ input_schema: { type: "object" as const, properties: {} },
225
+ },
226
+ {
227
+ name: "update_scheduled_task",
228
+ description: "Edit, enable, or disable an existing scheduled task.",
229
+ input_schema: {
230
+ type: "object" as const,
231
+ properties: {
232
+ scheduled_task_id: { type: "string" },
233
+ title: { type: "string" },
234
+ description: { type: "string" },
235
+ priority: { type: "number" },
236
+ schedule_type: { type: "string" },
237
+ cron_expr: { type: "string" },
238
+ run_at: { type: "number" },
239
+ assignee_role: { type: "string" },
240
+ enabled: { type: "boolean" },
241
+ },
242
+ required: ["scheduled_task_id"],
243
+ },
244
+ },
245
+ {
246
+ name: "delete_scheduled_task",
247
+ description: "Delete a scheduled task.",
248
+ input_schema: {
249
+ type: "object" as const,
250
+ properties: { scheduled_task_id: { type: "string" } },
251
+ required: ["scheduled_task_id"],
252
+ },
253
+ },
254
+ {
255
+ name: "list_tasks",
256
+ description: "List tasks in this workspace.",
257
+ input_schema: {
258
+ type: "object" as const,
259
+ properties: {
260
+ status: { type: "string", description: "Filter by status: open, done, blocked" },
261
+ },
262
+ },
263
+ },
264
+ {
265
+ name: "update_task",
266
+ description: "Update a task's status or details.",
267
+ input_schema: {
268
+ type: "object" as const,
269
+ properties: {
270
+ task_id: { type: "string" },
271
+ status: { type: "string", description: "New status: open, in_progress, done, blocked" },
272
+ summary: { type: "string", description: "Completion summary" },
273
+ },
274
+ required: ["task_id"],
275
+ },
276
+ },
277
+ {
278
+ name: "list_agents",
279
+ description: "List all agents and their status.",
280
+ input_schema: { type: "object" as const, properties: {} },
281
+ },
282
+ {
283
+ name: "list_runtimes",
284
+ description: "List available agent runtimes. Use this first to get a real runtime_id before calling spawn_agent.",
285
+ input_schema: { type: "object" as const, properties: {} },
286
+ },
287
+ {
288
+ name: "spawn_agent",
289
+ description: "Start a new agent session for a specific role. Call list_runtimes first and pass the returned agent_runtime.id as runtime_id.",
290
+ input_schema: {
291
+ type: "object" as const,
292
+ properties: {
293
+ role_id: { type: "string" },
294
+ runtime_id: { type: "string" },
295
+ },
296
+ required: ["role_id", "runtime_id"],
297
+ },
298
+ },
299
+ {
300
+ name: "create_role",
301
+ description: "Create a workspace role. A role is the primary object; runtime_id selects the agent runtime used for the role's initial agent.",
302
+ input_schema: {
303
+ type: "object" as const,
304
+ properties: {
305
+ name: { type: "string" },
306
+ headcount: { type: "number" },
307
+ capabilities: { type: "array", items: { type: "string" } },
308
+ preferred_runtimes: { type: "array", items: { type: "string" } },
309
+ runtime_id: { type: "string" },
310
+ },
311
+ required: ["name"],
312
+ },
313
+ },
314
+ {
315
+ name: "list_projects",
316
+ description: "List projects in this workspace.",
317
+ input_schema: { type: "object" as const, properties: {} },
318
+ },
319
+ {
320
+ name: "create_project",
321
+ description: "Create a project in the current workspace. Use this before creating tasks when no suitable project exists.",
322
+ input_schema: {
323
+ type: "object" as const,
324
+ properties: {
325
+ name: { type: "string" },
326
+ rationale: { type: "string" },
327
+ charter_template: { type: "string" },
328
+ auto_kickoff: { type: "boolean" },
329
+ },
330
+ required: ["name"],
331
+ },
332
+ },
333
+ {
334
+ name: "workspace_info",
335
+ description: "Get information about the current workspace.",
336
+ input_schema: { type: "object" as const, properties: {} },
337
+ },
338
+ ];
339
+ }
340
+
341
+ // ─── Tool Execution ──────────────────────────────────────────────────────────
342
+
343
+ export async function executeToolCall(ctx: HubContext, workspaceId: string, toolName: string, args: Record<string, any>): Promise<any> {
344
+ try {
345
+ switch (toolName) {
346
+ case "create_task": {
347
+ const project = ctx.db.query("SELECT id FROM project WHERE workspace_id = ? LIMIT 1").get(workspaceId) as any;
348
+ if (!project) return { error: "No project found for this workspace. Create a project first." };
349
+
350
+ // 如果指定了 assignee_role,找到对应 agent
351
+ let assigneeAgentId: string | null = null;
352
+ if (args.assignee_role) {
353
+ const role = ctx.db.query(
354
+ "SELECT id FROM role WHERE workspace_id = ? AND LOWER(name) = LOWER(?)"
355
+ ).get(workspaceId, args.assignee_role) as any;
356
+ if (role) {
357
+ const agent = ctx.db.query(
358
+ "SELECT id FROM agent WHERE role_id = ? ORDER BY last_active_at DESC LIMIT 1"
359
+ ).get(role.id) as any;
360
+ assigneeAgentId = agent?.id || null;
361
+ }
362
+ }
363
+
364
+ const id = generateId("task");
365
+ const ts = now();
366
+ ctx.db.run(
367
+ `INSERT INTO task (id, project_id, title, description, status, priority, assignee_agent_id, required_capabilities_json, lineage_depth, created_by, created_at, updated_at)
368
+ VALUES (?, ?, ?, ?, 'open', ?, ?, '[]', 0, 'agent', ?, ?)`,
369
+ [id, project.id, args.title, args.description || "", args.priority || 0, assigneeAgentId, ts, ts]
370
+ );
371
+ ctx.bus.publish("task.created", { task_id: id, project_id: project.id });
372
+ if (assigneeAgentId) {
373
+ const claimResult = await claimTaskForAgent(ctx, id, assigneeAgentId, { deferIfProjectBusy: true });
374
+ if ("error" in claimResult) {
375
+ return {
376
+ ok: false,
377
+ task_id: id,
378
+ title: args.title,
379
+ assigned_to: args.assignee_role || null,
380
+ assigned_agent_id: assigneeAgentId,
381
+ error: claimResult.error,
382
+ };
383
+ }
384
+ if ("deferred" in claimResult) {
385
+ return {
386
+ ok: true,
387
+ task_id: id,
388
+ title: args.title,
389
+ status: claimResult.task.status,
390
+ assigned_to: args.assignee_role || null,
391
+ assigned_agent_id: assigneeAgentId,
392
+ assignment_deferred: true,
393
+ blocked_by_task_id: claimResult.blocked_by_task_id,
394
+ };
395
+ }
396
+ return {
397
+ ok: true,
398
+ task_id: id,
399
+ title: args.title,
400
+ status: claimResult.task.status,
401
+ assigned_to: args.assignee_role || null,
402
+ assigned_agent_id: assigneeAgentId,
403
+ session_id: claimResult.session_id,
404
+ };
405
+ }
406
+ return { ok: true, task_id: id, title: args.title, status: "open", assigned_to: args.assignee_role || null };
407
+ }
408
+
409
+ case "create_scheduled_task": {
410
+ const normalized = normalizeScheduledTaskToolArgs(ctx, workspaceId, args);
411
+ if ("error" in normalized) return normalized;
412
+ const id = generateId("sched");
413
+ const ts = now();
414
+ const nextFireAt = computeNextFireAt(normalized, ts);
415
+ ctx.db.run(
416
+ `INSERT INTO scheduled_task (
417
+ id, workspace_id, project_id, title, description, priority, schedule_type, cron_expr, run_at,
418
+ assignee_role_id, assignee_role_name, enabled, next_fire_at, created_by, created_at, updated_at
419
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, 'agent', ?, ?)`,
420
+ [
421
+ id,
422
+ workspaceId,
423
+ normalized.project_id,
424
+ normalized.title,
425
+ normalized.description || null,
426
+ normalized.priority,
427
+ normalized.schedule_type,
428
+ normalized.cron_expr || null,
429
+ normalized.run_at || null,
430
+ normalized.assignee_role_id || null,
431
+ normalized.assignee_role_name || null,
432
+ nextFireAt,
433
+ ts,
434
+ ts,
435
+ ],
436
+ );
437
+ ctx.bus?.publish?.("scheduled_task.created", { id, workspace_id: workspaceId, next_fire_at: nextFireAt });
438
+ return { ok: true, scheduled_task_id: id, next_fire_at: nextFireAt, assigned_to: normalized.assignee_role_name };
439
+ }
440
+
441
+ case "list_scheduled_tasks": {
442
+ const rows = ctx.db.query(
443
+ `SELECT scheduled_task.*, project.name AS project_name, role.name AS resolved_role_name
444
+ FROM scheduled_task
445
+ LEFT JOIN project ON project.id = scheduled_task.project_id
446
+ LEFT JOIN role ON role.id = scheduled_task.assignee_role_id
447
+ WHERE scheduled_task.workspace_id = ?
448
+ ORDER BY scheduled_task.enabled DESC, scheduled_task.next_fire_at ASC, scheduled_task.created_at DESC`,
449
+ ).all(workspaceId);
450
+ return { scheduled_tasks: rows };
451
+ }
452
+
453
+ case "update_scheduled_task": {
454
+ const existing = ctx.db.query("SELECT * FROM scheduled_task WHERE id = ? AND workspace_id = ?").get(args.scheduled_task_id, workspaceId) as any;
455
+ if (!existing) return { error: `Scheduled task not found: ${args.scheduled_task_id}` };
456
+ const normalized = normalizeScheduledTaskToolArgs(ctx, workspaceId, { ...existing, ...args, project_id: args.project_id || existing.project_id }, existing);
457
+ if ("error" in normalized) return normalized;
458
+ const ts = now();
459
+ const scheduleChanged = args.schedule_type !== undefined || args.cron_expr !== undefined || args.run_at !== undefined || args.enabled !== undefined;
460
+ const enabled = args.enabled === undefined ? Number(existing.enabled) : (args.enabled ? 1 : 0);
461
+ const nextFireAt = enabled ? (scheduleChanged ? computeNextFireAt(normalized, ts) : existing.next_fire_at) : null;
462
+ ctx.db.run(
463
+ `UPDATE scheduled_task SET title = ?, description = ?, priority = ?, schedule_type = ?, cron_expr = ?, run_at = ?,
464
+ assignee_role_id = ?, assignee_role_name = ?, enabled = ?, next_fire_at = ?, updated_at = ?
465
+ WHERE id = ?`,
466
+ [
467
+ normalized.title,
468
+ normalized.description || null,
469
+ normalized.priority,
470
+ normalized.schedule_type,
471
+ normalized.cron_expr || null,
472
+ normalized.run_at || null,
473
+ normalized.assignee_role_id || null,
474
+ normalized.assignee_role_name || null,
475
+ enabled,
476
+ nextFireAt,
477
+ ts,
478
+ existing.id,
479
+ ],
480
+ );
481
+ return { ok: true, scheduled_task_id: existing.id, next_fire_at: nextFireAt, enabled };
482
+ }
483
+
484
+ case "delete_scheduled_task": {
485
+ const existing = ctx.db.query("SELECT * FROM scheduled_task WHERE id = ? AND workspace_id = ?").get(args.scheduled_task_id, workspaceId) as any;
486
+ if (!existing) return { error: `Scheduled task not found: ${args.scheduled_task_id}` };
487
+ ctx.db.run("DELETE FROM scheduled_task WHERE id = ?", [existing.id]);
488
+ return { ok: true, scheduled_task_id: existing.id, deleted: true };
489
+ }
490
+
491
+ case "list_tasks": {
492
+ const projects = ctx.db.query("SELECT id FROM project WHERE workspace_id = ?").all(workspaceId) as any[];
493
+ if (projects.length === 0) return { tasks: [] };
494
+ const projectIds = projects.map(p => p.id);
495
+ const placeholders = projectIds.map(() => "?").join(",");
496
+ let sql = `SELECT id, title, status, priority, assigned_to FROM task WHERE project_id IN (${placeholders})`;
497
+ const params: any[] = [...projectIds];
498
+ if (args.status) { sql += " AND status = ?"; params.push(args.status); }
499
+ sql += " ORDER BY priority DESC, created_at DESC LIMIT 20";
500
+ return { tasks: ctx.db.query(sql).all(...params) };
501
+ }
502
+
503
+ case "update_task": {
504
+ const task = ctx.db.query("SELECT * FROM task WHERE id = ?").get(args.task_id) as any;
505
+ if (!task) return { error: `Task not found: ${args.task_id}` };
506
+ const updates: string[] = [];
507
+ const values: any[] = [];
508
+ if (args.status) { updates.push("status = ?"); values.push(args.status); }
509
+ if (args.summary) { updates.push("acceptance_criteria = ?"); values.push(args.summary); }
510
+ updates.push("updated_at = ?"); values.push(now());
511
+ values.push(args.task_id);
512
+ ctx.db.run(`UPDATE task SET ${updates.join(", ")} WHERE id = ?`, values);
513
+ return { ok: true, task_id: args.task_id, status: args.status || task.status };
514
+ }
515
+
516
+ case "list_agents": {
517
+ const agents = ctx.db.query("SELECT id, name, status, role_id FROM agent WHERE status != 'offline' ORDER BY created_at DESC LIMIT 20").all();
518
+ return { agents };
519
+ }
520
+
521
+ case "list_runtimes": {
522
+ const runtimes = ctx.db.query(`
523
+ SELECT
524
+ id,
525
+ type,
526
+ transport,
527
+ status,
528
+ capacity,
529
+ target
530
+ FROM agent_runtime
531
+ ORDER BY created_at DESC
532
+ LIMIT 20
533
+ `).all();
534
+ return { runtimes };
535
+ }
536
+
537
+ case "spawn_agent": {
538
+ const role = ctx.db.query("SELECT * FROM role WHERE id = ?").get(args.role_id) as any;
539
+ const runtime = ctx.db.query("SELECT * FROM agent_runtime WHERE id = ?").get(args.runtime_id) as any;
540
+ if (!role) return { error: `Role not found: ${args.role_id}` };
541
+ if (!runtime) return { error: `Agent runtime not found: ${args.runtime_id}` };
542
+ const id = generateId("agent");
543
+ const ts = now();
544
+ ctx.db.run(
545
+ "INSERT INTO agent (id, role_id, runtime_id, name, status, metrics_json, created_at, last_active_at) VALUES (?, ?, ?, ?, 'idle', '{}', ?, ?)",
546
+ [id, role.id, runtime.id, `${role.name}-${id.slice(-4)}`, ts, ts]
547
+ );
548
+ return { ok: true, agent_id: id, name: `${role.name}-${id.slice(-4)}` };
549
+ }
550
+
551
+ case "create_role": {
552
+ const created = createWorkspaceRole(ctx.db, {
553
+ workspaceId,
554
+ name: String(args.name || "").trim(),
555
+ capabilities: Array.isArray(args.capabilities) ? args.capabilities : [],
556
+ preferredRuntimes: Array.isArray(args.preferred_runtimes) ? args.preferred_runtimes : [],
557
+ headcount: Number(args.headcount || 1),
558
+ runtimeId: args.runtime_id || null,
559
+ hubDir: ctx.hubDir,
560
+ }, ctx.bus?.publish ? ctx.bus.publish.bind(ctx.bus) : undefined);
561
+ return {
562
+ ok: true,
563
+ agents: created.agents,
564
+ role: {
565
+ id: created.role.id,
566
+ workspace_id: workspaceId,
567
+ name: created.role.name,
568
+ headcount: created.role.headcount,
569
+ role_md_path: created.role.role_md_path,
570
+ capabilities: Array.isArray(args.capabilities) ? args.capabilities : [],
571
+ preferred_runtimes: Array.isArray(args.preferred_runtimes) ? args.preferred_runtimes : [],
572
+ },
573
+ };
574
+ }
575
+
576
+ case "list_projects": {
577
+ const projects = ctx.db.query("SELECT id, name, status FROM project WHERE workspace_id = ?").all(workspaceId);
578
+ return { projects };
579
+ }
580
+
581
+ case "create_project": {
582
+ const workspace = ctx.db.query("SELECT * FROM workspace WHERE id = ?").get(workspaceId) as any;
583
+ if (!workspace) return { error: `Workspace not found: ${workspaceId}` };
584
+ const name = String(args.name || "").trim();
585
+ if (!name) return { error: "Project name is required" };
586
+ const id = generateId("proj");
587
+ const ts = now();
588
+ ctx.db.run(
589
+ "INSERT INTO project (id, workspace_id, name, charter_template, status, rationale, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, 'active', ?, 'agent', ?, ?)",
590
+ [id, workspaceId, name, args.charter_template || null, args.rationale || null, ts, ts],
591
+ );
592
+ ctx.bus?.publish?.("project.created", { id, workspace_id: workspaceId, name });
593
+ const initialTasks = args.auto_kickoff ? createKickoffTasks(ctx, id, ts) : undefined;
594
+ return {
595
+ ok: true,
596
+ project: {
597
+ id,
598
+ workspace_id: workspaceId,
599
+ name,
600
+ status: "active",
601
+ rationale: args.rationale || null,
602
+ created_at: ts,
603
+ },
604
+ ...(initialTasks ? { initial_tasks: initialTasks } : {}),
605
+ };
606
+ }
607
+
608
+ case "workspace_info": {
609
+ const workspace = ctx.db.query("SELECT * FROM workspace WHERE id = ?").get(workspaceId);
610
+ return workspace || { error: "Workspace not found" };
611
+ }
612
+
613
+ default:
614
+ return { error: `Unknown tool: ${toolName}` };
615
+ }
616
+ } catch (err: any) {
617
+ return { error: err.message };
618
+ }
619
+ }
620
+
621
+ function createKickoffTasks(ctx: HubContext, projectId: string, ts: number): any[] {
622
+ const templatePath = join(import.meta.dir, "../../templates/kickoff-tasks.json");
623
+ if (!existsSync(templatePath)) return [];
624
+ const initialTasks: any[] = [];
625
+ try {
626
+ const tasks = JSON.parse(readFileSync(templatePath, "utf-8")) as any[];
627
+ for (const task of tasks) {
628
+ const taskId = generateId("task");
629
+ ctx.db.run(
630
+ `INSERT INTO task (id, project_id, title, description, status, required_capabilities_json, priority, lineage_depth, created_by, created_at, updated_at)
631
+ VALUES (?, ?, ?, ?, 'open', ?, ?, 0, 'template', ?, ?)`,
632
+ [
633
+ taskId,
634
+ projectId,
635
+ task.title,
636
+ task.description || null,
637
+ JSON.stringify(task.skill ? [task.skill] : []),
638
+ task.priority || 0,
639
+ ts,
640
+ ts,
641
+ ],
642
+ );
643
+ initialTasks.push({ id: taskId, title: task.title, status: "open", priority: task.priority || 0 });
644
+ ctx.bus?.publish?.("task.created", { id: taskId, project_id: projectId, title: task.title });
645
+ }
646
+ } catch {}
647
+ return initialTasks;
648
+ }
649
+
650
+ function normalizeScheduledTaskToolArgs(ctx: HubContext, workspaceId: string, args: Record<string, any>, existing?: any):
651
+ | {
652
+ project_id: string;
653
+ title: string;
654
+ description: string;
655
+ priority: number;
656
+ schedule_type: "cron" | "once";
657
+ cron_expr: string | null;
658
+ run_at: number | null;
659
+ assignee_role_id: string | null;
660
+ assignee_role_name: string | null;
661
+ }
662
+ | { error: string } {
663
+ const projectId = resolveToolProjectId(ctx, workspaceId, args.project_id || existing?.project_id);
664
+ if (!projectId) return { error: "No project found for this workspace. Create a project first." };
665
+
666
+ const title = String(args.title || existing?.title || "").trim();
667
+ if (!title) return { error: "title is required" };
668
+
669
+ let scheduleType: "cron" | "once";
670
+ try {
671
+ scheduleType = validateScheduleInput(args) as "cron" | "once";
672
+ } catch (err: any) {
673
+ return { error: err.message };
674
+ }
675
+
676
+ const assigneeRoleName = String(args.assignee_role || args.assignee_role_name || existing?.assignee_role_name || "").trim();
677
+ let assigneeRoleId: string | null = args.assignee_role_id || existing?.assignee_role_id || null;
678
+ let resolvedRoleName: string | null = assigneeRoleName || existing?.assignee_role_name || null;
679
+ if (assigneeRoleName) {
680
+ const role = ctx.db.query(
681
+ "SELECT * FROM role WHERE workspace_id = ? AND LOWER(name) = LOWER(?) ORDER BY created_at ASC LIMIT 1",
682
+ ).get(workspaceId, assigneeRoleName) as any;
683
+ if (!role) return { error: `Assignee role not found: ${assigneeRoleName}` };
684
+ assigneeRoleId = role.id;
685
+ resolvedRoleName = role.name;
686
+ }
687
+
688
+ return {
689
+ project_id: projectId,
690
+ title,
691
+ description: String(args.description ?? existing?.description ?? ""),
692
+ priority: Number(args.priority ?? existing?.priority ?? 0),
693
+ schedule_type: scheduleType,
694
+ cron_expr: scheduleType === "cron" ? String(args.cron_expr || existing?.cron_expr || "").trim() : null,
695
+ run_at: scheduleType === "once" ? Number(args.run_at ?? existing?.run_at) : null,
696
+ assignee_role_id: assigneeRoleId,
697
+ assignee_role_name: resolvedRoleName,
698
+ };
699
+ }
700
+
701
+ function resolveToolProjectId(ctx: HubContext, workspaceId: string, projectId?: string | null) {
702
+ if (projectId) {
703
+ const project = ctx.db.query("SELECT id FROM project WHERE id = ? AND workspace_id = ?").get(projectId, workspaceId) as any;
704
+ return project?.id || null;
705
+ }
706
+ const project = ctx.db.query("SELECT id FROM project WHERE workspace_id = ? ORDER BY created_at ASC LIMIT 1").get(workspaceId) as any;
707
+ return project?.id || null;
708
+ }
709
+
710
+ // ─── Conversation State ──────────────────────────────────────────────────────
711
+
712
+ function getOrCreateConversation(ctx: HubContext, workspaceId: string): ConversationRow {
713
+ let conv = ctx.db.query("SELECT * FROM chat_conversation WHERE workspace_id = ?").get(workspaceId) as ConversationRow | null;
714
+ if (!conv) {
715
+ const ts = now();
716
+ const model = getConfig(ctx, "chief_model", "") || DEFAULT_MODEL;
717
+ ctx.db.run(
718
+ "INSERT INTO chat_conversation (workspace_id, messages_json, model, system_prompt, updated_at) VALUES (?, '[]', ?, NULL, ?)",
719
+ [workspaceId, model, ts]
720
+ );
721
+ conv = ctx.db.query("SELECT * FROM chat_conversation WHERE workspace_id = ?").get(workspaceId) as ConversationRow;
722
+ }
723
+ return conv;
724
+ }
725
+
726
+ function getEffectiveModel(ctx: HubContext, conv: ConversationRow): string {
727
+ return getConfig(ctx, "chief_model", "") || conv.model || DEFAULT_MODEL;
728
+ }
729
+
730
+ function saveConversation(ctx: HubContext, workspaceId: string, messages: Anthropic.MessageParam[]) {
731
+ // 只保留最近 50 轮对话,防止 token 爆炸
732
+ const trimmed = messages.slice(-100);
733
+ ctx.db.run(
734
+ "UPDATE chat_conversation SET messages_json = ?, updated_at = ? WHERE workspace_id = ?",
735
+ [JSON.stringify(trimmed), now(), workspaceId]
736
+ );
737
+ }
738
+
739
+ function buildSystemPrompt(ctx: HubContext, workspaceId: string): string {
740
+ const workspace = ctx.db.query("SELECT name, goal FROM workspace WHERE id = ?").get(workspaceId) as any;
741
+ const roles = ctx.db.query("SELECT name FROM role WHERE workspace_id = ? ORDER BY name").all(workspaceId) as any[];
742
+ // 排除 Chief — 用户直接对话就是和主 Agent 说话,不需要 @mention
743
+ const assignableRoles = roles.filter((r: any) => r.name.toLowerCase() !== "chief");
744
+ const roleList = assignableRoles.map((r: any) => r.name).join(", ");
745
+
746
+ let prompt = `${DEFAULT_SYSTEM}
747
+
748
+ Current Workspace: "${workspace?.name || "Unknown"}"
749
+ Goal: ${workspace?.goal || "Not specified"}
750
+ Workspace ID: ${workspaceId}`;
751
+
752
+ if (roleList) {
753
+ prompt += `
754
+
755
+ Available Roles for task assignment: ${roleList}
756
+
757
+ When the user @mentions a role name (e.g. "@Engineer do X"), you MUST use the create_task tool with assignee_role set to that role name.
758
+ If multiple roles are mentioned, create separate tasks for each with appropriate subtask descriptions.
759
+ When the user asks to run work at a future time or on a recurrence, use create_scheduled_task and set assignee_role when a role is specified.
760
+ Confirm to the user what tasks were created and who they were assigned to.`;
761
+ }
762
+
763
+ return prompt;
764
+ }
765
+
766
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
767
+
768
+ function getAgentDisplayName(ctx: HubContext, workspaceId: string): string {
769
+ // 优先从 workspace.primary_agent_id 关联的 agent 取名
770
+ const workspace = ctx.db.query("SELECT primary_agent_id FROM workspace WHERE id = ?").get(workspaceId) as any;
771
+ if (workspace?.primary_agent_id) {
772
+ const agent = ctx.db.query("SELECT name FROM agent WHERE id = ?").get(workspace.primary_agent_id) as any;
773
+ if (agent?.name) return agent.name;
774
+ }
775
+ // 从 workspace 关联的 role 取名
776
+ const role = ctx.db.query(`
777
+ SELECT r.name FROM role r
778
+ JOIN project p ON r.workspace_id = p.workspace_id
779
+ WHERE p.workspace_id = ?
780
+ ORDER BY r.created_at ASC LIMIT 1
781
+ `).get(workspaceId) as any;
782
+ if (role?.name) return role.name;
783
+ return "MAESTRO";
784
+ }
785
+
786
+ export function insertChatMessage(ctx: HubContext, workspaceId: string, senderType: string, senderId: string, content: string) {
787
+ const id = generateId("cm");
788
+ const ts = now();
789
+
790
+ const maxSeq = (ctx.db.query(
791
+ "SELECT MAX(seq) as s FROM chat_message WHERE workspace_id = ?"
792
+ ).get(workspaceId) as any)?.s || 0;
793
+ const seq = maxSeq + 1;
794
+
795
+ ctx.db.run(`
796
+ INSERT INTO chat_message (id, workspace_id, sender_type, sender_id, content, mentions_json, seq, created_at)
797
+ VALUES (?, ?, ?, ?, ?, '[]', ?, ?)
798
+ `, [id, workspaceId, senderType, senderId, content, seq, ts]);
799
+
800
+ return { id, workspace_id: workspaceId, sender_type: senderType, sender_id: senderId, content, seq, created_at: ts };
801
+ }