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,500 @@
1
+ import { generateId, getDb, now } from "../core/db";
2
+ import { getWorkflowForTask, listTaskActions, transitionTask, workflowStatusesForProject } from "../workflows";
3
+ import { findProjectExecutionBlocker, isTaskAssignedToAgent } from "../ops/project-serial";
4
+
5
+ export interface InboxCommandOptions {
6
+ hubDir: string;
7
+ agentId?: string;
8
+ }
9
+
10
+ export interface InboxCommandResult {
11
+ status: number;
12
+ stdout: string;
13
+ stderr: string;
14
+ }
15
+
16
+ export async function runInboxCommand(args: string[], opts: InboxCommandOptions): Promise<InboxCommandResult> {
17
+ const db = getDb(opts.hubDir);
18
+ const jsonMode = hasFlag(args, "--json");
19
+ const command = args[0];
20
+ const agentId = opts.agentId || process.env.MAESTRO_AGENT_ID || "";
21
+
22
+ try {
23
+ if (command === "whoami") {
24
+ requireAgent(agentId);
25
+ const agent = db.query("SELECT * FROM agent WHERE id = ?").get(agentId);
26
+ if (!agent) return fail(`Agent not found: ${agentId}`);
27
+ return ok(agent, jsonMode, `agent ${agentId}`);
28
+ }
29
+
30
+ if (command === "roles") {
31
+ const roles = db.query("SELECT * FROM role ORDER BY id ASC").all();
32
+ return ok(roles, jsonMode, renderRows(roles as any[], "no roles", (role) => `${role.id}\t${role.name}`));
33
+ }
34
+
35
+ if (command === "agents") {
36
+ const agents = db.query("SELECT * FROM agent ORDER BY status DESC, created_at ASC").all();
37
+ return ok(agents, jsonMode, renderRows(agents as any[], "no agents", (agent) => `${agent.id}\t${agent.status}\t${agent.name}`));
38
+ }
39
+
40
+ if (command === "tasks") {
41
+ const mode = args.includes("--mine") ? "mine" : args.includes("--available") ? "available" : args.includes("--watching") ? "watching" : "";
42
+ const skill = flagValue(args, "--skill");
43
+ const rows = db.query("SELECT * FROM task ORDER BY priority DESC, created_at ASC").all() as any[];
44
+ const tasks = rows.filter((task) => {
45
+ if (mode === "mine") return task.assignee_agent_id === agentId;
46
+ if (mode === "available") return isOpenTaskAvailableForAgent(db, task, agentId) && skillMatches(task, skill);
47
+ if (mode === "watching") return isWatchingTask(db, task.id, agentId);
48
+ return true;
49
+ });
50
+ return ok(tasks, jsonMode, renderTaskList(tasks));
51
+ }
52
+
53
+ if (command === "work") {
54
+ requireAgent(agentId);
55
+ const skill = flagValue(args, "--skill");
56
+ const wait = hasFlag(args, "--wait");
57
+ const interval = Number(flagValue(args, "--interval") || "3000");
58
+ const timeout = Number(flagValue(args, "--timeout") || "300000");
59
+
60
+ const hubUrl = process.env.MAESTRO_HUB_URL;
61
+
62
+ // If hub URL available, use long-poll API
63
+ if (wait && hubUrl) {
64
+ const deadline = Date.now() + timeout;
65
+ while (Date.now() < deadline) {
66
+ const pollTimeout = Math.min(30000, deadline - Date.now());
67
+ try {
68
+ const url = `${hubUrl}/api/inbox/poll?agent_id=${encodeURIComponent(agentId)}&timeout=${pollTimeout}${skill ? `&skill=${encodeURIComponent(skill)}` : ""}`;
69
+ const resp = await fetch(url);
70
+ if (resp.status === 200) {
71
+ const task = await resp.json() as any;
72
+ claimTask(db, task.id, agentId);
73
+ const claimed = db.query("SELECT * FROM task WHERE id = ?").get(task.id);
74
+ return ok(claimed, jsonMode, `claimed ${task.id}`);
75
+ }
76
+ // 204 = timeout, loop again
77
+ } catch {
78
+ // Network error, fallback to sleep
79
+ await Bun.sleep(interval);
80
+ }
81
+ }
82
+ return ok(null, jsonMode, "timeout");
83
+ }
84
+
85
+ let task = nextAvailableTask(db, skill, agentId);
86
+ if (!task && wait) {
87
+ const deadline = Date.now() + timeout;
88
+ while (!task && Date.now() < deadline) {
89
+ await Bun.sleep(interval);
90
+ task = nextAvailableTask(db, skill, agentId);
91
+ }
92
+ }
93
+ if (!task) return ok(null, jsonMode, wait ? "timeout" : "no available task");
94
+ claimTask(db, task.id, agentId);
95
+ const claimed = db.query("SELECT * FROM task WHERE id = ?").get(task.id) as any;
96
+ const workPacket = buildWorkPacket(db, claimed);
97
+ return ok(workPacket, jsonMode, `claimed ${task.id}`);
98
+ }
99
+
100
+ if (command === "task") {
101
+ return runTaskCommand(args.slice(1), opts, jsonMode);
102
+ }
103
+
104
+ if (command === "memory") {
105
+ return runMemoryCommand(args.slice(1), opts, jsonMode);
106
+ }
107
+
108
+ if (command === "notify-user") {
109
+ requireAgent(agentId);
110
+ const kind = flagValue(args, "--kind") || "report";
111
+ const messageBody = flagValue(args, "--body");
112
+ if (!messageBody) return fail("notify-user requires --body");
113
+ const id = generateId("msg");
114
+ const ts = now();
115
+ db.run(
116
+ `INSERT INTO inbox_message (id, kind, from_actor, to_actor, subject, body, ref_json, status, created_at)
117
+ VALUES (?, ?, ?, 'user', ?, ?, ?, 'unread', ?)`,
118
+ [id, kind, agentId, `Agent ${kind}`, messageBody, JSON.stringify({ agent_id: agentId }), ts],
119
+ );
120
+ const row = db.query("SELECT * FROM inbox_message WHERE id = ?").get(id);
121
+ return ok(row, jsonMode, `notified user: ${kind}`);
122
+ }
123
+
124
+ if (command === "status") {
125
+ requireAgent(agentId);
126
+ const assignedTasks = db.query("SELECT * FROM task WHERE assignee_agent_id = ? ORDER BY updated_at DESC").all(agentId) as any[];
127
+ const currentTask = assignedTasks.find((task) => !isTerminalTask(db, task));
128
+ const activeSession = db.query("SELECT * FROM session WHERE agent_id = ? AND status = 'running' LIMIT 1").get(agentId);
129
+ const recentDone = assignedTasks.filter((task) => isDoneTask(db, task)).slice(0, 3);
130
+ const agent = db.query("SELECT * FROM agent WHERE id = ?").get(agentId) as any;
131
+ const result = { agent_id: agentId, agent_status: agent?.status, current_task: currentTask, active_session: activeSession, recent_done: recentDone };
132
+ return ok(result, jsonMode, renderStatus(result));
133
+ }
134
+
135
+ if (command === "heartbeat") {
136
+ requireAgent(agentId);
137
+ const ts = now();
138
+ db.run("UPDATE agent SET last_active_at = ? WHERE id = ?", [ts, agentId]);
139
+ return ok({ agent_id: agentId, heartbeat_at: ts }, jsonMode, `heartbeat ${agentId}`);
140
+ }
141
+
142
+ if (command === "retire") {
143
+ requireAgent(agentId);
144
+ const ts = now();
145
+ db.run("UPDATE agent SET status = 'offline', last_active_at = ? WHERE id = ?", [ts, agentId]);
146
+ return ok({ agent_id: agentId, status: "offline" }, jsonMode, `retired ${agentId}`);
147
+ }
148
+
149
+ return fail(`Unknown inbox command: ${command || ""}`);
150
+ } catch (err: any) {
151
+ return fail(err.message);
152
+ }
153
+ }
154
+
155
+ function runTaskCommand(args: string[], opts: InboxCommandOptions, jsonMode: boolean): InboxCommandResult {
156
+ const db = getDb(opts.hubDir);
157
+ const subcommand = args[0];
158
+ const agentId = opts.agentId || process.env.MAESTRO_AGENT_ID || "agent";
159
+
160
+ if (subcommand === "show") {
161
+ const task = taskWithThread(db, args[1]);
162
+ if (!task) return fail(`Task not found: ${args[1]}`);
163
+ return ok(task, jsonMode, renderTask(task));
164
+ }
165
+
166
+ if (subcommand === "claim") {
167
+ requireAgent(agentId);
168
+ const taskId = args[1];
169
+ claimTask(db, taskId, agentId);
170
+ const task = db.query("SELECT * FROM task WHERE id = ?").get(taskId);
171
+ return ok(task, jsonMode, `claimed ${taskId}`);
172
+ }
173
+
174
+ if (subcommand === "actions") {
175
+ const taskId = args[1];
176
+ if (!taskId) return fail("task actions requires <id>");
177
+ try {
178
+ const actions = listTaskActions(db, taskId);
179
+ return ok(actions, jsonMode, renderTaskActions(actions));
180
+ } catch (err: any) {
181
+ return fail(err.message);
182
+ }
183
+ }
184
+
185
+ if (subcommand === "transition") {
186
+ const taskId = args[1];
187
+ const actionId = args[2];
188
+ if (!taskId || !actionId) return fail("task transition requires <id> <action_id>");
189
+ const input = collectTransitionInput(args.slice(3));
190
+ const result = transitionTask(
191
+ { db, bus: { publish: () => {} } } as any,
192
+ taskId,
193
+ actionId,
194
+ input,
195
+ { actorId: agentId },
196
+ );
197
+ if (!result.ok) return fail(result.error);
198
+ return ok(result, jsonMode, `transitioned ${taskId} via ${actionId}`);
199
+ }
200
+
201
+ if (subcommand === "progress") {
202
+ const taskId = args[1];
203
+ const content = args.slice(2).join(" ");
204
+ addThreadItem(db, taskId, "status_update", agentId, content);
205
+ return ok({ task_id: taskId, kind: "status_update", content }, jsonMode, `progress ${taskId}`);
206
+ }
207
+
208
+ if (subcommand === "comment") {
209
+ const taskId = args[1];
210
+ const content = args.slice(2).join(" ");
211
+ addThreadItem(db, taskId, "comment", agentId, content);
212
+ return ok({ task_id: taskId, kind: "comment", content }, jsonMode, `commented ${taskId}`);
213
+ }
214
+
215
+ if (subcommand === "attach") {
216
+ const taskId = args[1];
217
+ const ref = args[2];
218
+ if (!taskId || !ref) return fail("task attach requires <id> <path|url>");
219
+ const kind = flagValue(args, "--kind") || "file";
220
+ const id = generateId("art");
221
+ const ts = now();
222
+ const isUrl = /^https?:\/\//.test(ref);
223
+ db.run(
224
+ "INSERT INTO artifact (id, task_id, kind, path, url, meta_json, created_at) VALUES (?, ?, ?, ?, ?, '{}', ?)",
225
+ [id, taskId, kind, isUrl ? null : ref, isUrl ? ref : null, ts],
226
+ );
227
+ addThreadItem(db, taskId, "artifact", agentId, ref, id);
228
+ const artifact = db.query("SELECT * FROM artifact WHERE id = ?").get(id);
229
+ return ok(artifact, jsonMode, `attached ${ref}`);
230
+ }
231
+
232
+ if (subcommand === "handoff") {
233
+ const taskId = args[1];
234
+ const to = flagValue(args, "--to");
235
+ const reason = flagValue(args, "--reason") || "";
236
+ if (!taskId || !to) return fail("task handoff requires <id> --to <agent_or_role>");
237
+ const ts = now();
238
+ const workflow = getWorkflowForTask(db, taskId);
239
+ db.run("UPDATE task SET assignee_agent_id = ?, status = ?, updated_at = ? WHERE id = ?", [to, workflow.initial_status, ts, taskId]);
240
+ addThreadItem(db, taskId, "handoff", agentId, reason, to);
241
+ const task = db.query("SELECT * FROM task WHERE id = ?").get(taskId);
242
+ return ok(task, jsonMode, `handoff ${taskId} to ${to}`);
243
+ }
244
+
245
+ if (subcommand === "review") {
246
+ const taskId = args[1];
247
+ const reviewBody = flagValue(args, "--body") || args.slice(2).filter(a => !a.startsWith("--")).join(" ");
248
+ if (!taskId || !reviewBody) return fail("task review requires <id> --body <text>");
249
+ addThreadItem(db, taskId, "review", agentId, reviewBody);
250
+ return ok({ task_id: taskId, kind: "review", content: reviewBody }, jsonMode, `reviewed ${taskId}`);
251
+ }
252
+
253
+ if (subcommand === "spawn") {
254
+ const parentId = flagValue(args, "--parent");
255
+ const title = flagValue(args, "--title");
256
+ if (!parentId || !title) return fail("task spawn requires --parent and --title");
257
+ const parent = db.query("SELECT * FROM task WHERE id = ?").get(parentId) as any;
258
+ if (!parent) return fail(`Task not found: ${parentId}`);
259
+ const skill = flagValue(args, "--skill");
260
+ const id = generateId("task");
261
+ const ts = now();
262
+ const lineageDepth = (parent.lineage_depth || 0) + 1;
263
+ db.run(
264
+ `INSERT INTO task (id, project_id, parent_task_id, title, description, status, required_capabilities_json, priority, lineage_depth, created_by, created_at, updated_at)
265
+ VALUES (?, ?, ?, ?, '', ?, ?, 0, ?, ?, ?, ?)`,
266
+ [id, parent.project_id, parentId, title, workflowStatusesForProject(db, parent.project_id)[0]?.id || "open", JSON.stringify(skill ? [skill] : []), lineageDepth, agentId, ts, ts]
267
+ );
268
+ addThreadItem(db, parentId, "spawn_task", agentId, title, id);
269
+ const task = db.query("SELECT * FROM task WHERE id = ?").get(id);
270
+ return ok(task, jsonMode, `spawned ${id}`);
271
+ }
272
+
273
+ return fail(`Unknown task command: ${subcommand || ""}`);
274
+ }
275
+
276
+ function runMemoryCommand(args: string[], opts: InboxCommandOptions, jsonMode: boolean): InboxCommandResult {
277
+ const db = getDb(opts.hubDir);
278
+ const subcommand = args[0];
279
+ const key = args[1];
280
+ const agentId = opts.agentId || process.env.MAESTRO_AGENT_ID || "";
281
+ const scope = flagValue(args, "--scope") || "project";
282
+ const scopeId = resolveMemoryScopeId(db, agentId, scope);
283
+ if (!key) return fail("memory command requires <key>");
284
+
285
+ if (subcommand === "get") {
286
+ const row = db.query("SELECT * FROM memory WHERE scope = ? AND scope_id = ? AND key = ?").get(scope, scopeId, key);
287
+ return ok(row || null, jsonMode, row ? `${key}=${(row as any).value}` : "not found");
288
+ }
289
+
290
+ if (subcommand === "set") {
291
+ const value = args.slice(2).filter((part) => !part.startsWith("--")).join(" ");
292
+ if (!value) return fail("memory set requires <value>");
293
+ const id = generateId("mem");
294
+ const ts = now();
295
+ db.run(
296
+ `INSERT INTO memory (id, scope, scope_id, key, value, updated_at)
297
+ VALUES (?, ?, ?, ?, ?, ?)
298
+ ON CONFLICT(scope, scope_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
299
+ [id, scope, scopeId, key, value, ts],
300
+ );
301
+ const row = db.query("SELECT * FROM memory WHERE scope = ? AND scope_id = ? AND key = ?").get(scope, scopeId, key);
302
+ return ok(row, jsonMode, `set ${key}`);
303
+ }
304
+
305
+ return fail(`Unknown memory command: ${subcommand || ""}`);
306
+ }
307
+
308
+ function buildWorkPacket(db: ReturnType<typeof getDb>, task: any) {
309
+ const artifacts = db.query("SELECT * FROM artifact WHERE task_id = ? ORDER BY created_at DESC").all(task.id);
310
+ const dependencies = db.query(
311
+ "SELECT t.id, t.title, t.status FROM task_dependency d JOIN task t ON t.id = d.depends_on WHERE d.task_id = ?"
312
+ ).all(task.id);
313
+ const thread_recent_5 = db.query(
314
+ "SELECT * FROM task_thread_item WHERE task_id = ? ORDER BY created_at DESC LIMIT 5"
315
+ ).all(task.id);
316
+ return {
317
+ task,
318
+ done_when: task.acceptance_criteria || "",
319
+ artifacts,
320
+ dependencies,
321
+ thread_recent_5,
322
+ };
323
+ }
324
+
325
+ function claimTask(db: ReturnType<typeof getDb>, taskId: string, agentId: string) {
326
+ const task = db.query("SELECT * FROM task WHERE id = ?").get(taskId) as any;
327
+ if (!task) throw new Error(`Task not found: ${taskId}`);
328
+ const workflow = getWorkflowForTask(db, taskId);
329
+ const claimAction = workflow.actions.find((action) => action.id === "claim" && action.from.includes(task.status));
330
+ if (task.status !== workflow.initial_status && task.assignee_agent_id !== agentId) {
331
+ throw new Error(`Task is not available: ${taskId}`);
332
+ }
333
+ if (!claimAction && task.assignee_agent_id !== agentId) {
334
+ throw new Error(`No claim action is available from ${task.status}`);
335
+ }
336
+ if (task.status === workflow.initial_status) {
337
+ const blocker = findProjectExecutionBlocker(db, {
338
+ projectId: task.project_id,
339
+ excludeTaskId: task.id,
340
+ claimedStatus: claimAction?.to,
341
+ });
342
+ if (blocker) throw new Error(`Project already has active task: ${blocker.id}`);
343
+ }
344
+
345
+ const ts = now();
346
+ db.run(
347
+ "UPDATE task SET status = ?, assignee_agent_id = ?, claim_token = ?, updated_at = ? WHERE id = ?",
348
+ [claimAction?.to || task.status, agentId, crypto.randomUUID().slice(0, 8), ts, taskId]
349
+ );
350
+ }
351
+
352
+ function nextAvailableTask(db: ReturnType<typeof getDb>, skill: string | undefined, agentId: string) {
353
+ const rows = db.query("SELECT * FROM task ORDER BY priority DESC, created_at ASC").all() as any[];
354
+ return rows.find((task) => {
355
+ return isOpenTaskAvailableForAgent(db, task, agentId) && skillMatches(task, skill);
356
+ });
357
+ }
358
+
359
+ function isOpenTaskAvailableForAgent(db: ReturnType<typeof getDb>, task: any, agentId: string) {
360
+ const workflow = getWorkflowForTask(db, task.id);
361
+ if (task.status !== workflow.initial_status) return false;
362
+ if (!isTaskAssignedToAgent(task, agentId)) return false;
363
+ const claimAction = workflow.actions.find((action) => action.id === "claim" && action.from.includes(task.status));
364
+ return !findProjectExecutionBlocker(db, {
365
+ projectId: task.project_id,
366
+ excludeTaskId: task.id,
367
+ claimedStatus: claimAction?.to,
368
+ });
369
+ }
370
+
371
+ function taskWithThread(db: ReturnType<typeof getDb>, taskId: string) {
372
+ const task = db.query("SELECT * FROM task WHERE id = ?").get(taskId) as any;
373
+ if (!task) return null;
374
+ const thread = db.query("SELECT * FROM task_thread_item WHERE task_id = ? ORDER BY created_at ASC").all(taskId);
375
+ return { ...task, thread };
376
+ }
377
+
378
+ function addThreadItem(
379
+ db: ReturnType<typeof getDb>,
380
+ taskId: string,
381
+ kind: string,
382
+ author: string,
383
+ content: string,
384
+ refId?: string,
385
+ ) {
386
+ const ts = now();
387
+ db.run(
388
+ "INSERT INTO task_thread_item (id, task_id, kind, author, content, ref_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
389
+ [generateId("ti"), taskId, kind, author || "agent", content, refId || null, ts]
390
+ );
391
+ }
392
+
393
+ function isWatchingTask(db: ReturnType<typeof getDb>, taskId: string, agentId: string) {
394
+ if (!agentId) return false;
395
+ const row = db.query("SELECT id FROM task_thread_item WHERE task_id = ? AND author = ? LIMIT 1").get(taskId, agentId);
396
+ return Boolean(row);
397
+ }
398
+
399
+ function resolveMemoryScopeId(db: ReturnType<typeof getDb>, agentId: string, scope: string): string {
400
+ if (scope === "agent") return agentId;
401
+ const task = db.query("SELECT project_id FROM task WHERE assignee_agent_id = ? ORDER BY updated_at DESC LIMIT 1").get(agentId) as any;
402
+ if (scope === "workspace") {
403
+ const projectId = task?.project_id || (db.query("SELECT id FROM project ORDER BY created_at ASC LIMIT 1").get() as any)?.id;
404
+ return (db.query("SELECT workspace_id FROM project WHERE id = ?").get(projectId) as any)?.workspace_id || "global";
405
+ }
406
+ return task?.project_id || (db.query("SELECT id FROM project ORDER BY created_at ASC LIMIT 1").get() as any)?.id || "global";
407
+ }
408
+
409
+ function skillMatches(task: any, skill?: string) {
410
+ if (!skill) return true;
411
+ const requiredCapabilities = JSON.parse(task.required_capabilities_json || "[]");
412
+ return requiredCapabilities.length === 0 || requiredCapabilities.includes(skill);
413
+ }
414
+
415
+ function flagValue(args: string[], flag: string): string | undefined {
416
+ const index = args.indexOf(flag);
417
+ if (index === -1) return undefined;
418
+ return args[index + 1];
419
+ }
420
+
421
+ function hasFlag(args: string[], flag: string): boolean {
422
+ return args.includes(flag);
423
+ }
424
+
425
+ function collectTransitionInput(args: string[]): Record<string, string> {
426
+ const input: Record<string, string> = {};
427
+ for (let index = 0; index < args.length; index++) {
428
+ const part = args[index];
429
+ if (!part.startsWith("--")) continue;
430
+ const key = part.slice(2).replace(/-/g, "_");
431
+ const value = args[index + 1];
432
+ if (value && !value.startsWith("--")) {
433
+ input[key] = value;
434
+ index++;
435
+ } else {
436
+ input[key] = "true";
437
+ }
438
+ }
439
+ return input;
440
+ }
441
+
442
+ function requireAgent(agentId: string) {
443
+ if (!agentId) throw new Error("Missing agent identity. Set MAESTRO_AGENT_ID.");
444
+ }
445
+
446
+ function ok(data: unknown, jsonMode: boolean, text: string): InboxCommandResult {
447
+ return {
448
+ status: 0,
449
+ stdout: jsonMode ? `${JSON.stringify(data)}\n` : `${text}\n`,
450
+ stderr: "",
451
+ };
452
+ }
453
+
454
+ function fail(message: string): InboxCommandResult {
455
+ return { status: 1, stdout: "", stderr: `${message}\n` };
456
+ }
457
+
458
+ function renderTaskList(tasks: any[]) {
459
+ if (tasks.length === 0) return "no tasks";
460
+ return tasks.map((task) => `${task.id}\t${task.status}\t${task.title}`).join("\n");
461
+ }
462
+
463
+ function renderTask(task: any) {
464
+ return `${task.id}\t${task.status}\t${task.title}`;
465
+ }
466
+
467
+ function renderTaskActions(result: any) {
468
+ if (result.actions.length === 0) return "no actions";
469
+ return result.actions
470
+ .map((action: any) => `${action.id}\t${result.status} -> ${action.to}\t${(action.requires || []).join(",")}`)
471
+ .join("\n");
472
+ }
473
+
474
+ function renderStatus(result: any) {
475
+ const lines: string[] = [];
476
+ lines.push(`agent: ${result.agent_id} (${result.agent_status || "unknown"})`);
477
+ if (result.current_task) lines.push(`task: ${result.current_task.id}\t${result.current_task.title}`);
478
+ else lines.push("task: none");
479
+ if (result.active_session) lines.push(`session: ${result.active_session.id} (running)`);
480
+ else lines.push("session: none");
481
+ if (result.recent_done?.length) {
482
+ lines.push(`done: ${result.recent_done.map((t: any) => t.id).join(", ")}`);
483
+ }
484
+ return lines.join("\n");
485
+ }
486
+
487
+ function renderRows(rows: any[], empty: string, render: (row: any) => string) {
488
+ if (rows.length === 0) return empty;
489
+ return rows.map(render).join("\n");
490
+ }
491
+
492
+ function isTerminalTask(db: ReturnType<typeof getDb>, task: any): boolean {
493
+ const workflow = getWorkflowForTask(db, task.id);
494
+ return Boolean(workflow.statuses.find((status) => status.id === task.status)?.terminal);
495
+ }
496
+
497
+ function isDoneTask(db: ReturnType<typeof getDb>, task: any): boolean {
498
+ const workflow = getWorkflowForTask(db, task.id);
499
+ return task.status === workflow.done_status;
500
+ }
@@ -0,0 +1,65 @@
1
+ import { copyFileSync, existsSync, mkdirSync } from "fs";
2
+ import { join, basename } from "path";
3
+ import type { Database } from "bun:sqlite";
4
+
5
+ export interface SyncResult {
6
+ synced: number;
7
+ failed: number;
8
+ errors: string[];
9
+ }
10
+
11
+ export async function syncArtifacts(hubDir: string, db: Database, agentId: string): Promise<SyncResult> {
12
+ const agent = db.query("SELECT * FROM agent WHERE id = ?").get(agentId) as any;
13
+ if (!agent) return { synced: 0, failed: 0, errors: ["Agent not found"] };
14
+
15
+ const runtime = db.query("SELECT * FROM agent_runtime WHERE id = ?").get(agent.runtime_id) as any;
16
+ if (!runtime) return { synced: 0, failed: 0, errors: ["Runtime not found"] };
17
+
18
+ const artifacts = db.query(
19
+ "SELECT a.* FROM artifact a JOIN task t ON t.id = a.task_id WHERE t.assignee_agent_id = ? AND a.path IS NOT NULL"
20
+ ).all(agentId) as any[];
21
+
22
+ const destDir = join(hubDir, "artifacts", agentId);
23
+ mkdirSync(destDir, { recursive: true });
24
+
25
+ const result: SyncResult = { synced: 0, failed: 0, errors: [] };
26
+
27
+ for (const artifact of artifacts) {
28
+ try {
29
+ if (runtime.transport === "local-pty") {
30
+ // Local: direct copy
31
+ if (existsSync(artifact.path)) {
32
+ copyFileSync(artifact.path, join(destDir, basename(artifact.path)));
33
+ result.synced++;
34
+ } else {
35
+ result.errors.push(`File not found: ${artifact.path}`);
36
+ result.failed++;
37
+ }
38
+ } else if (runtime.transport === "ssh" && runtime.target) {
39
+ // SSH: use scp
40
+ const dest = join(destDir, basename(artifact.path));
41
+ const proc = Bun.spawn(["scp", "-o", "StrictHostKeyChecking=no", `${runtime.target}:${artifact.path}`, dest]);
42
+ const code = await proc.exited;
43
+ if (code === 0) result.synced++;
44
+ else { result.failed++; result.errors.push(`scp failed for ${artifact.path}`); }
45
+ } else if (runtime.transport === "http-api" && artifact.url) {
46
+ // HTTP: download
47
+ const resp = await fetch(artifact.url);
48
+ if (resp.ok) {
49
+ const buf = await resp.arrayBuffer();
50
+ const dest = join(destDir, basename(artifact.url));
51
+ Bun.write(dest, buf);
52
+ result.synced++;
53
+ } else {
54
+ result.failed++;
55
+ result.errors.push(`HTTP download failed: ${artifact.url}`);
56
+ }
57
+ }
58
+ } catch (err: any) {
59
+ result.failed++;
60
+ result.errors.push(err.message);
61
+ }
62
+ }
63
+
64
+ return result;
65
+ }