maestro-agent 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +316 -2
- package/bin/maestro.ts +5 -0
- package/dist/maestro +0 -0
- package/dist/web/assets/Connections-DV2Kql1Z.js +1 -0
- package/dist/web/assets/GanttView-CCT_rFpY.js +39 -0
- package/dist/web/assets/Home-BFbUIh2z.js +1 -0
- package/dist/web/assets/HooksCrons-ASM5-jDm.js +1 -0
- package/dist/web/assets/ProjectDetail-KZZi6IAd.js +1 -0
- package/dist/web/assets/Roles-KQ94PG3H.js +4 -0
- package/dist/web/assets/ScheduledTasks-CdJHJpEV.js +1 -0
- package/dist/web/assets/Settings-CTflMta-.js +1 -0
- package/dist/web/assets/Skills-D09W1mwX.js +2 -0
- package/dist/web/assets/Wizard-CW6B0wc3.js +1 -0
- package/dist/web/assets/WorkspaceChat-CthETL_A.js +1 -0
- package/dist/web/assets/WorkspaceDashboard-DTAesQuT.js +1 -0
- package/dist/web/assets/WorkspaceNew-Em4msIKn.js +1 -0
- package/dist/web/assets/WorkspaceProjects-Dxg2BpQy.js +1 -0
- package/dist/web/assets/WorkspaceTasks-C20mnnkP.js +1 -0
- package/dist/web/assets/index-B1k33vcR.js +11 -0
- package/dist/web/assets/index-Bk2hHz7P.css +1 -0
- package/dist/web/assets/index-Ddy5AJwx.js +61 -0
- package/dist/web/assets/useEventStream-DTID465I.js +1 -0
- package/dist/web/index.html +13 -0
- package/package.json +49 -6
- package/src/api/agents.ts +76 -0
- package/src/api/audit.ts +19 -0
- package/src/api/autopilot.ts +73 -0
- package/src/api/chat.ts +801 -0
- package/src/api/chief.ts +84 -0
- package/src/api/config.ts +39 -0
- package/src/api/gantt.ts +72 -0
- package/src/api/hooks.ts +54 -0
- package/src/api/inbox.ts +125 -0
- package/src/api/lark.ts +32 -0
- package/src/api/memory.ts +37 -0
- package/src/api/ops.ts +89 -0
- package/src/api/projects.ts +105 -0
- package/src/api/roles.ts +123 -0
- package/src/api/runtimes.ts +62 -0
- package/src/api/scheduled-tasks.ts +203 -0
- package/src/api/sessions.ts +479 -0
- package/src/api/skills.ts +386 -0
- package/src/api/tasks.ts +457 -0
- package/src/api/telegram.ts +94 -0
- package/src/api/templates.ts +36 -0
- package/src/api/webhooks.ts +20 -0
- package/src/api/workspaces.ts +150 -0
- package/src/bridges/lark/index.ts +213 -0
- package/src/bridges/telegram/index.ts +273 -0
- package/src/bridges/telegram/polling.ts +185 -0
- package/src/chat/index.ts +86 -0
- package/src/chief/index.ts +461 -0
- package/src/core/cli.ts +333 -0
- package/src/core/db.ts +53 -0
- package/src/core/event-bus.ts +33 -0
- package/src/core/index.ts +6 -0
- package/src/core/migrations.ts +303 -0
- package/src/core/router.ts +69 -0
- package/src/core/schema.sql +232 -0
- package/src/core/server.ts +308 -0
- package/src/core/validate.ts +22 -0
- package/src/discovery/index.ts +194 -0
- package/src/gateway/adapters/telegram.ts +148 -0
- package/src/gateway/index.ts +31 -0
- package/src/gateway/manager.ts +176 -0
- package/src/gateway/types.ts +77 -0
- package/src/inbox/index.ts +500 -0
- package/src/ops/artifact-sync.ts +65 -0
- package/src/ops/autopilot.ts +338 -0
- package/src/ops/gc.ts +252 -0
- package/src/ops/index.ts +226 -0
- package/src/ops/project-serial.ts +52 -0
- package/src/ops/role-dispatch.ts +111 -0
- package/src/ops/runtime-scheduler.ts +447 -0
- package/src/ops/task-blocking.ts +65 -0
- package/src/ops/task-deps.ts +37 -0
- package/src/ops/task-workspace.ts +60 -0
- package/src/roles/index.ts +258 -0
- package/src/roles/prompt-assembler.ts +85 -0
- package/src/roles/workspace-role.ts +155 -0
- package/src/scheduler/index.ts +461 -0
- package/src/session/output-parser.ts +75 -0
- package/src/session/realtime-parser.ts +40 -0
- package/src/skills/builtin.ts +155 -0
- package/src/skills/skill-extractor.ts +452 -0
- package/src/skills/skill-md.ts +282 -0
- package/src/transport/http-api.ts +75 -0
- package/src/transport/index.ts +4 -0
- package/src/transport/local-pty.ts +119 -0
- package/src/transport/ssh.ts +176 -0
- package/src/transport/types.ts +20 -0
- package/src/workflows/index.ts +231 -0
- package/index.js +0 -1
- package/maestro-agent-0.0.1.tgz +0 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
3
|
+
import { generateId, now } from "../core/db";
|
|
4
|
+
import { createWorkspaceRole } from "../roles/workspace-role";
|
|
5
|
+
import { countTasksByPredicate, isBlockedTaskStatus, isInitialTaskStatus } from "../workflows";
|
|
6
|
+
|
|
7
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export interface RecommendedRole {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
rationale: string;
|
|
13
|
+
headcount: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ChiefDecision {
|
|
17
|
+
tool: string;
|
|
18
|
+
args: Record<string, any>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ChiefHeartbeatOptions {
|
|
22
|
+
planner?: (metrics: OrganizationalMetrics) => Promise<ChiefDecision[]> | ChiefDecision[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface OrganizationalMetrics {
|
|
26
|
+
workspaces: number;
|
|
27
|
+
projects: number;
|
|
28
|
+
open_tasks: number;
|
|
29
|
+
blocked_tasks: number;
|
|
30
|
+
working_agents: number;
|
|
31
|
+
idle_agents: number;
|
|
32
|
+
offline_agents: number;
|
|
33
|
+
agent_runtimes: number;
|
|
34
|
+
unread_inbox: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ChiefConfig {
|
|
38
|
+
apiKey: string;
|
|
39
|
+
baseURL?: string;
|
|
40
|
+
model?: string;
|
|
41
|
+
maxTurns?: number;
|
|
42
|
+
/** 用于测试注入,生产环境不传 */
|
|
43
|
+
_client?: Anthropic;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Role Recommendation (rule-based) ────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
const ROLE_RULES: Array<{ id: string; name: string; keywords: string[]; rationale: string }> = [
|
|
49
|
+
{ id: "engineer", name: "Engineer", keywords: ["api", "build", "code", "ship", "implement"], rationale: "Needed for implementation and integration work." },
|
|
50
|
+
{ id: "writer", name: "Writer", keywords: ["doc", "docs", "launch", "write", "content"], rationale: "Needed for documentation and launch communication." },
|
|
51
|
+
{ id: "researcher", name: "Researcher", keywords: ["research", "compare", "investigate"], rationale: "Needed for discovery and option analysis." },
|
|
52
|
+
{ id: "critic", name: "Critic", keywords: ["review", "qa", "test", "quality"], rationale: "Needed for review, testing, and risk checks." },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export function recommendRoles(goal: string): RecommendedRole[] {
|
|
56
|
+
const text = goal.toLowerCase();
|
|
57
|
+
const matches = ROLE_RULES.filter((rule) => rule.keywords.some((keyword) => text.includes(keyword)));
|
|
58
|
+
const roles = matches.length > 0 ? matches : [ROLE_RULES[0], ROLE_RULES[3]];
|
|
59
|
+
return roles.map((role) => ({
|
|
60
|
+
id: role.id,
|
|
61
|
+
name: role.name,
|
|
62
|
+
rationale: role.rationale,
|
|
63
|
+
headcount: 1,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function proposeRoster(db: Database, workspaceId: string) {
|
|
68
|
+
const workspace = db.query("SELECT * FROM workspace WHERE id = ?").get(workspaceId) as any;
|
|
69
|
+
if (!workspace) throw new Error(`Workspace not found: ${workspaceId}`);
|
|
70
|
+
const roles = recommendRoles(`${workspace.name} ${workspace.goal || ""}`);
|
|
71
|
+
const id = generateId("msg");
|
|
72
|
+
const ts = now();
|
|
73
|
+
const body = roles
|
|
74
|
+
.map((role) => `${role.id} x${role.headcount}: ${role.rationale}`)
|
|
75
|
+
.join("\n");
|
|
76
|
+
const ref = JSON.stringify({ workspace_id: workspaceId, roles });
|
|
77
|
+
db.run(
|
|
78
|
+
`INSERT INTO inbox_message (id, kind, from_actor, to_actor, subject, body, ref_json, status, created_at)
|
|
79
|
+
VALUES (?, 'proposal', 'chief', 'user', ?, ?, ?, 'unread', ?)`,
|
|
80
|
+
[id, `Roster recommendation for ${workspace.name}`, body, ref, ts],
|
|
81
|
+
);
|
|
82
|
+
return db.query("SELECT * FROM inbox_message WHERE id = ?").get(id) as any;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Chief Heartbeat ─────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
export async function runChiefHeartbeat(db: Database, opts: ChiefHeartbeatOptions = {}) {
|
|
88
|
+
const metrics = collectOrganizationalMetrics(db);
|
|
89
|
+
const decisions = opts.planner
|
|
90
|
+
? await opts.planner(metrics)
|
|
91
|
+
: defaultChiefPlanner(db, metrics);
|
|
92
|
+
const results = decisions.map((decision) => applyChiefDecision(db, decision));
|
|
93
|
+
return {
|
|
94
|
+
metrics,
|
|
95
|
+
decisions: decisions.length,
|
|
96
|
+
applied: results.filter((result) => result.applied).length,
|
|
97
|
+
results,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Claude-backed Chief Agent (agentic loop) ───────────────────────────────
|
|
102
|
+
|
|
103
|
+
const CHIEF_SYSTEM = `You are MAESTRO Chief, an autonomous project manager agent.
|
|
104
|
+
Your job is to review organizational health metrics and take actions to keep work flowing.
|
|
105
|
+
Rules:
|
|
106
|
+
- At most 5 tool calls per heartbeat.
|
|
107
|
+
- Prefer proposals over direct actions (send to Inbox for human approval).
|
|
108
|
+
- do_nothing if the organization is healthy.`;
|
|
109
|
+
|
|
110
|
+
export function createClaudePlanner(config: ChiefConfig) {
|
|
111
|
+
const model = config.model || process.env.MAESTRO_CHIEF_MODEL || "claude-sonnet-4-20250514";
|
|
112
|
+
const maxTurns = config.maxTurns ?? 3;
|
|
113
|
+
const client = config._client ?? new Anthropic({
|
|
114
|
+
apiKey: config.apiKey,
|
|
115
|
+
baseURL: config.baseURL || undefined,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return async (metrics: OrganizationalMetrics): Promise<ChiefDecision[]> => {
|
|
119
|
+
const decisions: ChiefDecision[] = [];
|
|
120
|
+
const messages: Anthropic.MessageParam[] = [
|
|
121
|
+
{ role: "user", content: `Organizational metrics:\n${JSON.stringify(metrics, null, 2)}\n\nAnalyze and take actions.` },
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
for (let turn = 0; turn < maxTurns; turn++) {
|
|
125
|
+
const response = await client.messages.create({
|
|
126
|
+
model,
|
|
127
|
+
max_tokens: 1024,
|
|
128
|
+
system: CHIEF_SYSTEM,
|
|
129
|
+
messages,
|
|
130
|
+
tools: chiefTools() as Anthropic.Tool[],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// 收集本轮 tool_use blocks
|
|
134
|
+
const toolUses = response.content.filter(
|
|
135
|
+
(block): block is Anthropic.ToolUseBlock => block.type === "tool_use"
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (toolUses.length === 0) break; // 没有工具调用,结束
|
|
139
|
+
|
|
140
|
+
// 记录决策
|
|
141
|
+
for (const tu of toolUses) {
|
|
142
|
+
decisions.push({ tool: tu.name, args: (tu.input as Record<string, any>) || {} });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 如果 stop_reason 不是 tool_use,结束
|
|
146
|
+
if (response.stop_reason !== "tool_use") break;
|
|
147
|
+
|
|
148
|
+
// 构造 tool_result 回复给模型(agentic loop)
|
|
149
|
+
messages.push({ role: "assistant", content: response.content });
|
|
150
|
+
messages.push({
|
|
151
|
+
role: "user",
|
|
152
|
+
content: toolUses.map((tu) => ({
|
|
153
|
+
type: "tool_result" as const,
|
|
154
|
+
tool_use_id: tu.id,
|
|
155
|
+
content: JSON.stringify({ ok: true, tool: tu.name }),
|
|
156
|
+
})),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return decisions;
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── Decision Executor ───────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
export function applyChiefDecision(db: Database, decision: ChiefDecision): { applied: boolean; message: string; ref?: any } {
|
|
167
|
+
switch (decision.tool) {
|
|
168
|
+
case "propose_project":
|
|
169
|
+
return proposeProject(db, decision.args);
|
|
170
|
+
case "archive_project":
|
|
171
|
+
return proposeArchiveProject(db, decision.args);
|
|
172
|
+
case "spawn_agent":
|
|
173
|
+
return spawnAgent(db, decision.args);
|
|
174
|
+
case "retire_agent":
|
|
175
|
+
return retireAgent(db, decision.args);
|
|
176
|
+
case "escalate_to_user":
|
|
177
|
+
return escalateToUser(db, decision.args);
|
|
178
|
+
case "recommend_roles":
|
|
179
|
+
return recommendRolesForWorkspace(db, decision.args);
|
|
180
|
+
case "update_task_flow":
|
|
181
|
+
return updateTaskFlow(db, decision.args);
|
|
182
|
+
case "create_role":
|
|
183
|
+
return createRole(db, decision.args);
|
|
184
|
+
case "do_nothing":
|
|
185
|
+
return { applied: false, message: "Chief did nothing" };
|
|
186
|
+
default:
|
|
187
|
+
return { applied: false, message: `Unknown chief tool: ${decision.tool}` };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── Metrics ─────────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
export function collectOrganizationalMetrics(db: Database): OrganizationalMetrics {
|
|
194
|
+
return {
|
|
195
|
+
workspaces: count(db, "workspace"),
|
|
196
|
+
projects: count(db, "project"),
|
|
197
|
+
open_tasks: countTasksByPredicate(db, (task) => isInitialTaskStatus(db, task)),
|
|
198
|
+
blocked_tasks: countTasksByPredicate(db, (task) => isBlockedTaskStatus(db, task)),
|
|
199
|
+
working_agents: countWhere(db, "agent", "status = 'working'"),
|
|
200
|
+
idle_agents: countWhere(db, "agent", "status = 'idle'"),
|
|
201
|
+
offline_agents: countWhere(db, "agent", "status = 'offline'"),
|
|
202
|
+
agent_runtimes: count(db, "agent_runtime"),
|
|
203
|
+
unread_inbox: countWhere(db, "inbox_message", "status = 'unread'"),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ─── Default Rule-based Planner (no LLM) ─────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
function defaultChiefPlanner(db: Database, metrics: OrganizationalMetrics): ChiefDecision[] {
|
|
210
|
+
const decisions: ChiefDecision[] = [];
|
|
211
|
+
if (metrics.blocked_tasks > 0) {
|
|
212
|
+
decisions.push({
|
|
213
|
+
tool: "escalate_to_user",
|
|
214
|
+
args: { kind: "escalation", body: `${metrics.blocked_tasks} task(s) are blocked.` },
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const activeWorkspaces = db.query("SELECT id FROM workspace WHERE status = 'active'").all() as Array<{ id: string }>;
|
|
219
|
+
if (metrics.unread_inbox === 0) {
|
|
220
|
+
decisions.push(...activeWorkspaces.map((workspace) => ({ tool: "recommend_roles", args: { workspace_id: workspace.id } })));
|
|
221
|
+
}
|
|
222
|
+
if (decisions.length === 0) decisions.push({ tool: "do_nothing", args: {} });
|
|
223
|
+
return decisions;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ─── Tool Implementations ────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
function proposeProject(db: Database, args: Record<string, any>) {
|
|
229
|
+
const workspace = db.query("SELECT * FROM workspace WHERE id = ?").get(args.workspace_id) as any;
|
|
230
|
+
if (!workspace) return { applied: false, message: `Workspace not found: ${args.workspace_id}` };
|
|
231
|
+
const ref = {
|
|
232
|
+
tool: "propose_project",
|
|
233
|
+
workspace_id: args.workspace_id,
|
|
234
|
+
name: args.name,
|
|
235
|
+
rationale: args.rationale,
|
|
236
|
+
charter: args.charter,
|
|
237
|
+
};
|
|
238
|
+
createInbox(db, "proposal", "chief", "user", `Project proposal for ${workspace.name}`, args.rationale || args.name || "", ref);
|
|
239
|
+
return { applied: false, message: "Project proposal sent to inbox", ref };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function proposeArchiveProject(db: Database, args: Record<string, any>) {
|
|
243
|
+
const project = db.query("SELECT * FROM project WHERE id = ?").get(args.project_id) as any;
|
|
244
|
+
if (!project) return { applied: false, message: `Project not found: ${args.project_id}` };
|
|
245
|
+
const ref = { tool: "archive_project", project_id: args.project_id, rationale: args.rationale };
|
|
246
|
+
createInbox(db, "proposal", "chief", "user", `Archive project: ${project.name}`, args.rationale || "", ref);
|
|
247
|
+
return { applied: false, message: "Archive proposal sent to inbox", ref };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function spawnAgent(db: Database, args: Record<string, any>) {
|
|
251
|
+
const role = db.query("SELECT * FROM role WHERE id = ?").get(args.role_id) as any;
|
|
252
|
+
const runtime = db.query("SELECT * FROM agent_runtime WHERE id = ?").get(args.runtime_id || args.agent_runtime) as any;
|
|
253
|
+
if (!role || !runtime) return { applied: false, message: "Role or agent runtime not found" };
|
|
254
|
+
const id = generateId("agent");
|
|
255
|
+
const ts = now();
|
|
256
|
+
db.run(
|
|
257
|
+
"INSERT INTO agent (id, role_id, runtime_id, name, status, metrics_json, created_at, last_active_at) VALUES (?, ?, ?, ?, 'idle', '{}', ?, ?)",
|
|
258
|
+
[id, role.id, runtime.id, `${role.name}-${id.slice(-4)}`, ts, ts],
|
|
259
|
+
);
|
|
260
|
+
audit(db, "chief", "chief.spawn_agent", id, args);
|
|
261
|
+
return { applied: true, message: `Spawned agent ${id}`, ref: { agent_id: id } };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function retireAgent(db: Database, args: Record<string, any>) {
|
|
265
|
+
const agent = db.query("SELECT * FROM agent WHERE id = ?").get(args.agent_id) as any;
|
|
266
|
+
if (!agent) return { applied: false, message: `Agent not found: ${args.agent_id}` };
|
|
267
|
+
const cooldownMs = args.cooldown_ms ?? 5 * 60_000;
|
|
268
|
+
if (cooldownMs > 0 && now() - Number(agent.last_active_at || 0) < cooldownMs) {
|
|
269
|
+
return { applied: false, message: "Retire cooldown has not elapsed" };
|
|
270
|
+
}
|
|
271
|
+
db.run("UPDATE agent SET status = 'offline' WHERE id = ?", [args.agent_id]);
|
|
272
|
+
audit(db, "chief", "chief.retire_agent", args.agent_id, args);
|
|
273
|
+
return { applied: true, message: `Retired agent ${args.agent_id}` };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function escalateToUser(db: Database, args: Record<string, any>) {
|
|
277
|
+
const kind = args.kind || "report";
|
|
278
|
+
const limit = Number(args.limit_per_hour ?? 10);
|
|
279
|
+
const since = now() - 60 * 60_000;
|
|
280
|
+
const c = (db.query(
|
|
281
|
+
"SELECT COUNT(*) AS count FROM inbox_message WHERE from_actor = 'chief' AND to_actor = 'user' AND kind = ? AND created_at >= ?",
|
|
282
|
+
).get(kind, since) as any).count;
|
|
283
|
+
if (c >= limit) return { applied: false, message: "Chief escalation rate limit reached" };
|
|
284
|
+
const message = createInbox(db, kind, "chief", "user", `Chief ${kind}`, args.body || "", args.refs || null);
|
|
285
|
+
return { applied: true, message: "Escalation sent", ref: message };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function recommendRolesForWorkspace(db: Database, args: Record<string, any>) {
|
|
289
|
+
const workspaceId = args.workspace_id;
|
|
290
|
+
if (!workspaceId) return { applied: false, message: "recommend_roles requires workspace_id" };
|
|
291
|
+
const workspace = db.query("SELECT * FROM workspace WHERE id = ?").get(workspaceId) as any;
|
|
292
|
+
if (!workspace) return { applied: false, message: `Workspace not found: ${workspaceId}` };
|
|
293
|
+
const proposal = proposeRoster(db, workspaceId);
|
|
294
|
+
return { applied: true, message: "Role recommendation sent", ref: proposal };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function updateTaskFlow(db: Database, args: Record<string, any>) {
|
|
298
|
+
const mode = String(args.mode || "");
|
|
299
|
+
const ts = now();
|
|
300
|
+
|
|
301
|
+
if (mode === "add_dependency") {
|
|
302
|
+
const task = db.query("SELECT * FROM task WHERE id = ?").get(args.task_id) as any;
|
|
303
|
+
const dependsOn = Array.isArray(args.depends_on) ? args.depends_on : [];
|
|
304
|
+
if (!task || dependsOn.length === 0) return { applied: false, message: "Task or dependencies not found" };
|
|
305
|
+
for (const dependencyId of dependsOn) {
|
|
306
|
+
db.run("INSERT OR IGNORE INTO task_dependency (task_id, depends_on) VALUES (?, ?)", [task.id, dependencyId]);
|
|
307
|
+
}
|
|
308
|
+
audit(db, "chief", "chief.update_task_flow", task.id, args);
|
|
309
|
+
return { applied: true, message: `Added ${dependsOn.length} dependency(s)`, ref: { task_id: task.id, depends_on: dependsOn } };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (mode === "reparent_task") {
|
|
313
|
+
const task = db.query("SELECT * FROM task WHERE id = ?").get(args.task_id) as any;
|
|
314
|
+
const parent = db.query("SELECT * FROM task WHERE id = ?").get(args.parent_task_id) as any;
|
|
315
|
+
if (!task || !parent) return { applied: false, message: "Task or parent not found" };
|
|
316
|
+
const lineageDepth = (parent.lineage_depth || 0) + 1;
|
|
317
|
+
db.run(
|
|
318
|
+
"UPDATE task SET parent_task_id = ?, project_id = ?, lineage_depth = ?, updated_at = ? WHERE id = ?",
|
|
319
|
+
[parent.id, parent.project_id, lineageDepth, ts, task.id],
|
|
320
|
+
);
|
|
321
|
+
audit(db, "chief", "chief.update_task_flow", task.id, args);
|
|
322
|
+
return { applied: true, message: `Reparented task ${task.id}`, ref: { task_id: task.id, parent_task_id: parent.id } };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (mode === "spawn_followup") {
|
|
326
|
+
const parent = db.query("SELECT * FROM task WHERE id = ?").get(args.parent_task_id) as any;
|
|
327
|
+
if (!parent || !args.title) return { applied: false, message: "Parent task or title not found" };
|
|
328
|
+
const id = generateId("task");
|
|
329
|
+
const lineageDepth = (parent.lineage_depth || 0) + 1;
|
|
330
|
+
db.run(
|
|
331
|
+
`INSERT INTO task (id, project_id, parent_task_id, title, description, status, required_capabilities_json, priority, lineage_depth, created_by, created_at, updated_at)
|
|
332
|
+
VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, 'chief', ?, ?)`,
|
|
333
|
+
[
|
|
334
|
+
id, parent.project_id, parent.id, args.title, args.description || "",
|
|
335
|
+
JSON.stringify(Array.isArray(args.required_capabilities) ? args.required_capabilities : []),
|
|
336
|
+
Number(args.priority || 0), lineageDepth, ts, ts,
|
|
337
|
+
],
|
|
338
|
+
);
|
|
339
|
+
audit(db, "chief", "chief.update_task_flow", id, args);
|
|
340
|
+
return { applied: true, message: `Created follow-up task ${id}`, ref: { task_id: id, parent_task_id: parent.id } };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return { applied: false, message: `Unknown task flow mode: ${mode}` };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function createRole(db: Database, args: Record<string, any>) {
|
|
347
|
+
const name = String(args.name || "").trim();
|
|
348
|
+
if (!name) return { applied: false, message: "Role name is required" };
|
|
349
|
+
if (!args.workspace_id) return { applied: false, message: "workspace_id is required" };
|
|
350
|
+
const created = createWorkspaceRole(db, {
|
|
351
|
+
workspaceId: args.workspace_id,
|
|
352
|
+
projectId: args.project_id || null,
|
|
353
|
+
id: args.id,
|
|
354
|
+
name,
|
|
355
|
+
capabilities: Array.isArray(args.capabilities) ? args.capabilities : [],
|
|
356
|
+
preferredRuntimes: Array.isArray(args.preferred_runtimes) ? args.preferred_runtimes : [],
|
|
357
|
+
headcount: Number(args.headcount || 1),
|
|
358
|
+
runtimeId: args.runtime_id || null,
|
|
359
|
+
});
|
|
360
|
+
audit(db, "chief", "chief.create_role", created.role.id, args);
|
|
361
|
+
return { applied: true, message: `Created role ${created.role.id}`, ref: { role: created.role, agents: created.agents } };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
function createInbox(db: Database, kind: string, fromActor: string, toActor: string, subject: string, body: string, ref: unknown) {
|
|
367
|
+
const id = generateId("msg");
|
|
368
|
+
const ts = now();
|
|
369
|
+
db.run(
|
|
370
|
+
`INSERT INTO inbox_message (id, kind, from_actor, to_actor, subject, body, ref_json, status, created_at)
|
|
371
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'unread', ?)`,
|
|
372
|
+
[id, kind, fromActor, toActor, subject, body, ref ? JSON.stringify(ref) : null, ts],
|
|
373
|
+
);
|
|
374
|
+
return db.query("SELECT * FROM inbox_message WHERE id = ?").get(id) as any;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function audit(db: Database, actor: string, action: string, target: string, payload: unknown) {
|
|
378
|
+
db.run(
|
|
379
|
+
"INSERT INTO audit_log (id, actor, action, target, payload_json, created_at) VALUES (?, ?, ?, ?, ?, ?)",
|
|
380
|
+
[generateId("audit"), actor, action, target, JSON.stringify(payload), now()],
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function count(db: Database, table: string): number {
|
|
385
|
+
return (db.query(`SELECT COUNT(*) AS count FROM ${table}`).get() as any).count;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function countWhere(db: Database, table: string, where: string): number {
|
|
389
|
+
return (db.query(`SELECT COUNT(*) AS count FROM ${table} WHERE ${where}`).get() as any).count;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ─── Tool Definitions ────────────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
function chiefTools(): Anthropic.Tool[] {
|
|
395
|
+
return [
|
|
396
|
+
{
|
|
397
|
+
name: "propose_project",
|
|
398
|
+
description: "Propose a new project for a workspace. This must go through Inbox approval.",
|
|
399
|
+
input_schema: { type: "object" as const, properties: { workspace_id: { type: "string" }, name: { type: "string" }, rationale: { type: "string" }, charter: { type: "string" } }, required: ["workspace_id", "name", "rationale"] },
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
name: "archive_project",
|
|
403
|
+
description: "Propose archiving a project. This must go through Inbox approval.",
|
|
404
|
+
input_schema: { type: "object" as const, properties: { project_id: { type: "string" }, rationale: { type: "string" } }, required: ["project_id", "rationale"] },
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
name: "spawn_agent",
|
|
408
|
+
description: "Spawn an agent for a role and agent runtime.",
|
|
409
|
+
input_schema: { type: "object" as const, properties: { role_id: { type: "string" }, runtime_id: { type: "string" }, rationale: { type: "string" } }, required: ["role_id", "runtime_id", "rationale"] },
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: "retire_agent",
|
|
413
|
+
description: "Retire an agent after cooldown.",
|
|
414
|
+
input_schema: { type: "object" as const, properties: { agent_id: { type: "string" }, rationale: { type: "string" } }, required: ["agent_id", "rationale"] },
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: "escalate_to_user",
|
|
418
|
+
description: "Send a rate-limited report, approval, or escalation to the user.",
|
|
419
|
+
input_schema: { type: "object" as const, properties: { kind: { type: "string" }, body: { type: "string" }, refs: { type: "object" } }, required: ["kind", "body"] },
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: "recommend_roles",
|
|
423
|
+
description: "Recommend workspace roles and send the proposal to Inbox.",
|
|
424
|
+
input_schema: { type: "object" as const, properties: { workspace_id: { type: "string" } }, required: ["workspace_id"] },
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
name: "update_task_flow",
|
|
428
|
+
description: "Update task flow by adding dependencies, changing parentage, or inserting a follow-up task.",
|
|
429
|
+
input_schema: {
|
|
430
|
+
type: "object" as const,
|
|
431
|
+
properties: {
|
|
432
|
+
task_id: { type: "string" }, project_id: { type: "string" }, mode: { type: "string" },
|
|
433
|
+
depends_on: { type: "array", items: { type: "string" } },
|
|
434
|
+
parent_task_id: { type: "string" }, title: { type: "string" },
|
|
435
|
+
description: { type: "string" }, required_capabilities: { type: "array", items: { type: "string" } },
|
|
436
|
+
priority: { type: "number" }, rationale: { type: "string" },
|
|
437
|
+
},
|
|
438
|
+
required: ["mode", "rationale"],
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: "create_role",
|
|
443
|
+
description: "Create a role directly from a chief decision.",
|
|
444
|
+
input_schema: {
|
|
445
|
+
type: "object" as const,
|
|
446
|
+
properties: {
|
|
447
|
+
id: { type: "string" }, name: { type: "string" }, workspace_id: { type: "string" },
|
|
448
|
+
project_id: { type: "string" }, capabilities: { type: "array", items: { type: "string" } },
|
|
449
|
+
preferred_runtimes: { type: "array", items: { type: "string" } },
|
|
450
|
+
headcount: { type: "number" }, rationale: { type: "string" },
|
|
451
|
+
},
|
|
452
|
+
required: ["name", "rationale"],
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
name: "do_nothing",
|
|
457
|
+
description: "No action is needed. Call this if the organization is healthy.",
|
|
458
|
+
input_schema: { type: "object" as const, properties: {} },
|
|
459
|
+
},
|
|
460
|
+
];
|
|
461
|
+
}
|