Learn Claude Code
Back to Autonomous Agents

Team ProtocolsAutonomous Agents

s10 (317 LOC) → s11 (413 LOC)

LOC Delta

+96lines

New Tools

2

idleclaim_task
New Classes

0

New Functions

4

sleepscanUnclaimedTasksclaimTaskmakeIdentityBlock

Team Protocols

Shared Communication Rules

317 LOC

12 tools: bash, read_file, write_file, edit_file, send_message, read_inbox, shutdown_response, plan_approval, spawn_teammate, list_teammates, broadcast, shutdown_request

collaboration

Autonomous Agents

Scan Board, Claim Tasks

413 LOC

14 tools: bash, read_file, write_file, edit_file, send_message, read_inbox, shutdown_response, plan_approval, idle, claim_task, spawn_teammate, list_teammates, broadcast, shutdown_request

collaboration

Source Code Diff

s10 (s10_team_protocols.ts) -> s11 (s11_autonomous_agents.ts)
11#!/usr/bin/env node
22/**
3- * s10_team_protocols.ts - Team Protocols
3+ * s11_autonomous_agents.ts - Autonomous Agents
44 *
5- * request_id based shutdown and plan approval protocols.
5+ * Idle polling + auto-claim task board + identity re-injection.
66 */
77
8-import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8+import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
99import { spawnSync } from "node:child_process";
1010import { resolve } from "node:path";
1111import process from "node:process";
1212import { randomUUID } from "node:crypto";
1313import { createInterface } from "node:readline/promises";
1414import type Anthropic from "@anthropic-ai/sdk";
1515import "dotenv/config";
1616import { buildSystemPrompt, createAnthropicClient, resolveModel, shellToolDescription } from "./shared";
1717
1818type MessageType = "message" | "broadcast" | "shutdown_request" | "shutdown_response" | "plan_approval_response";
1919type ToolName =
2020 | "bash" | "read_file" | "write_file" | "edit_file"
2121 | "spawn_teammate" | "list_teammates" | "send_message" | "read_inbox" | "broadcast"
22- | "shutdown_request" | "shutdown_response" | "plan_approval";
22+ | "shutdown_request" | "shutdown_response" | "plan_approval" | "idle" | "claim_task";
2323type ToolUseBlock = { id: string; type: "tool_use"; name: ToolName; input: Record<string, unknown> };
2424type TextBlock = { type: "text"; text: string };
2525type ToolResultBlock = { type: "tool_result"; tool_use_id: string; content: string };
2626type Message = { role: "user" | "assistant"; content: string | Array<ToolUseBlock | TextBlock | ToolResultBlock> };
2727type TeamMember = { name: string; role: string; status: "working" | "idle" | "shutdown" };
2828type TeamConfig = { team_name: string; members: TeamMember[] };
29+type TaskRecord = { id: number; subject: string; description?: string; status: string; owner?: string; blockedBy?: number[] };
2930
3031const WORKDIR = process.cwd();
3132const MODEL = resolveModel();
3233const TEAM_DIR = resolve(WORKDIR, ".team");
3334const INBOX_DIR = resolve(TEAM_DIR, "inbox");
35+const TASKS_DIR = resolve(WORKDIR, ".tasks");
36+const POLL_INTERVAL = 5_000;
37+const IDLE_TIMEOUT = 60_000;
3438const VALID_MSG_TYPES: MessageType[] = ["message", "broadcast", "shutdown_request", "shutdown_response", "plan_approval_response"];
3539const shutdownRequests: Record<string, { target: string; status: string }> = {};
3640const planRequests: Record<string, { from: string; plan: string; status: string }> = {};
3741const client = createAnthropicClient();
3842
39-const SYSTEM = buildSystemPrompt(`You are a team lead at ${WORKDIR}. Manage teammates with shutdown and plan approval protocols.`);
43+const SYSTEM = buildSystemPrompt(`You are a team lead at ${WORKDIR}. Teammates are autonomous -- they find work themselves.`);
4044
45+function sleep(ms: number) {
46+ return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
47+}
48+
4149function safePath(relativePath: string) {
4250 const filePath = resolve(WORKDIR, relativePath);
4351 const normalizedWorkdir = `${WORKDIR}${process.platform === "win32" ? "\\" : "/"}`;
4452 if (filePath !== WORKDIR && !filePath.startsWith(normalizedWorkdir)) throw new Error(`Path escapes workspace: ${relativePath}`);
4553 return filePath;
4654}
4755
4856function runBash(command: string): string {
4957 const dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"];
5058 if (dangerous.some((item) => command.includes(item))) return "Error: Dangerous command blocked";
5159 const shell = process.platform === "win32" ? "cmd.exe" : "/bin/sh";
5260 const args = process.platform === "win32" ? ["/d", "/s", "/c", command] : ["-lc", command];
5361 const result = spawnSync(shell, args, { cwd: WORKDIR, encoding: "utf8", timeout: 120_000 });
5462 if (result.error?.name === "TimeoutError") return "Error: Timeout (120s)";
5563 const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
5664 return output.slice(0, 50_000) || "(no output)";
5765}
5866
5967function runRead(path: string, limit?: number): string {
6068 try {
6169 let lines = readFileSync(safePath(path), "utf8").split(/\r?\n/);
6270 if (limit && limit < lines.length) lines = lines.slice(0, limit).concat(`... (${lines.length - limit} more)`);
6371 return lines.join("\n").slice(0, 50_000);
6472 } catch (error) {
6573 return `Error: ${error instanceof Error ? error.message : String(error)}`;
6674 }
6775}
6876
6977function runWrite(path: string, content: string): string {
7078 try {
7179 const filePath = safePath(path);
7280 mkdirSync(resolve(filePath, ".."), { recursive: true });
7381 writeFileSync(filePath, content, "utf8");
7482 return `Wrote ${content.length} bytes`;
7583 } catch (error) {
7684 return `Error: ${error instanceof Error ? error.message : String(error)}`;
7785 }
7886}
7987
8088function runEdit(path: string, oldText: string, newText: string): string {
8189 try {
8290 const filePath = safePath(path);
8391 const content = readFileSync(filePath, "utf8");
8492 if (!content.includes(oldText)) return `Error: Text not found in ${path}`;
8593 writeFileSync(filePath, content.replace(oldText, newText), "utf8");
8694 return `Edited ${path}`;
8795 } catch (error) {
8896 return `Error: ${error instanceof Error ? error.message : String(error)}`;
8997 }
9098}
9199
100+function scanUnclaimedTasks() {
101+ mkdirSync(TASKS_DIR, { recursive: true });
102+ const tasks: TaskRecord[] = [];
103+ for (const entry of readdirSync(TASKS_DIR, { withFileTypes: true })) {
104+ if (!entry.isFile() || !/^task_\d+\.json$/.test(entry.name)) continue;
105+ const task = JSON.parse(readFileSync(resolve(TASKS_DIR, entry.name), "utf8")) as TaskRecord;
106+ if (task.status === "pending" && !task.owner && !(task.blockedBy?.length)) tasks.push(task);
107+ }
108+ return tasks.sort((a, b) => a.id - b.id);
109+}
110+
111+function claimTask(taskId: number, owner: string) {
112+ const path = resolve(TASKS_DIR, `task_${taskId}.json`);
113+ if (!existsSync(path)) return `Error: Task ${taskId} not found`;
114+ const task = JSON.parse(readFileSync(path, "utf8")) as TaskRecord;
115+ task.owner = owner;
116+ task.status = "in_progress";
117+ writeFileSync(path, `${JSON.stringify(task, null, 2)}\n`, "utf8");
118+ return `Claimed task #${taskId} for ${owner}`;
119+}
120+
121+function makeIdentityBlock(name: string, role: string, teamName: string): Message {
122+ return { role: "user", content: `<identity>You are '${name}', role: ${role}, team: ${teamName}. Continue your work.</identity>` };
123+}
124+
92125class MessageBus {
93126 constructor(private inboxDir: string) {
94127 mkdirSync(inboxDir, { recursive: true });
95128 }
96129
97130 send(sender: string, to: string, content: string, msgType: MessageType = "message", extra?: Record<string, unknown>) {
98131 if (!VALID_MSG_TYPES.includes(msgType)) return `Error: Invalid type '${msgType}'.`;
99132 const payload = { type: msgType, from: sender, content, timestamp: Date.now() / 1000, ...(extra ?? {}) };
100133 appendFileSync(resolve(this.inboxDir, `${to}.jsonl`), `${JSON.stringify(payload)}\n`, "utf8");
101134 return `Sent ${msgType} to ${to}`;
102135 }
103136
104137 readInbox(name: string) {
105138 const inboxPath = resolve(this.inboxDir, `${name}.jsonl`);
106139 if (!existsSync(inboxPath)) return [];
107140 const lines = readFileSync(inboxPath, "utf8").split(/\r?\n/).filter(Boolean);
108141 writeFileSync(inboxPath, "", "utf8");
109142 return lines.map((line) => JSON.parse(line));
110143 }
111144
112145 broadcast(sender: string, content: string, teammates: string[]) {
113146 let count = 0;
114147 for (const name of teammates) {
115148 if (name === sender) continue;
116149 this.send(sender, name, content, "broadcast");
117150 count += 1;
118151 }
119152 return `Broadcast to ${count} teammates`;
120153 }
121154}
122155
123156const BUS = new MessageBus(INBOX_DIR);
124157
125158class TeammateManager {
126159 private configPath: string;
127160 private config: TeamConfig;
128161
129162 constructor(private teamDir: string) {
130163 mkdirSync(teamDir, { recursive: true });
131164 this.configPath = resolve(teamDir, "config.json");
132165 this.config = this.loadConfig();
133166 }
134167
135168 private loadConfig(): TeamConfig {
136169 if (existsSync(this.configPath)) return JSON.parse(readFileSync(this.configPath, "utf8")) as TeamConfig;
137170 return { team_name: "default", members: [] };
138171 }
139172
140173 private saveConfig() {
141174 writeFileSync(this.configPath, `${JSON.stringify(this.config, null, 2)}\n`, "utf8");
142175 }
143176
144177 private findMember(name: string) {
145178 return this.config.members.find((member) => member.name === name);
146179 }
147180
181+ private setStatus(name: string, status: TeamMember["status"]) {
182+ const member = this.findMember(name);
183+ if (member) {
184+ member.status = status;
185+ this.saveConfig();
186+ }
187+ }
188+
148189 spawn(name: string, role: string, prompt: string) {
149190 let member = this.findMember(name);
150191 if (member) {
151192 if (!["idle", "shutdown"].includes(member.status)) return `Error: '${name}' is currently ${member.status}`;
152193 member.status = "working";
153194 member.role = role;
154195 } else {
155196 member = { name, role, status: "working" };
156197 this.config.members.push(member);
157198 }
158199 this.saveConfig();
159- void this.teammateLoop(name, role, prompt);
200+ void this.loop(name, role, prompt);
160201 return `Spawned '${name}' (role: ${role})`;
161202 }
162203
163- private async teammateLoop(name: string, role: string, prompt: string) {
164- const sysPrompt = buildSystemPrompt(`You are '${name}', role: ${role}, at ${WORKDIR}. Submit plans via plan_approval before major work. Respond to shutdown_request with shutdown_response.`);
204+ private async loop(name: string, role: string, prompt: string) {
205+ const teamName = this.config.team_name;
206+ const sysPrompt = buildSystemPrompt(`You are '${name}', role: ${role}, team: ${teamName}, at ${WORKDIR}. Use idle when you have no more work. You will auto-claim new tasks.`);
165207 const messages: Message[] = [{ role: "user", content: prompt }];
166- let shouldExit = false;
167- for (let attempt = 0; attempt < 50; attempt += 1) {
168- for (const msg of BUS.readInbox(name)) messages.push({ role: "user", content: JSON.stringify(msg) });
169- if (shouldExit) break;
170- const response = await client.messages.create({
171- model: MODEL,
172- system: sysPrompt,
173- messages: messages as Anthropic.Messages.MessageParam[],
174- tools: this.tools() as Anthropic.Messages.Tool[],
175- max_tokens: 8000,
176- }).catch(() => null);
177- if (!response) break;
178- messages.push({ role: "assistant", content: response.content as Array<ToolUseBlock | TextBlock> });
179- if (response.stop_reason !== "tool_use") break;
180- const results: ToolResultBlock[] = [];
181- for (const block of response.content) {
182- if (block.type !== "tool_use") continue;
183- const output = this.exec(name, block.name, block.input as Record<string, unknown>);
184- if (block.name === "shutdown_response" && block.input.approve) shouldExit = true;
185- console.log(` [${name}] ${block.name}: ${output.slice(0, 120)}`);
186- results.push({ type: "tool_result", tool_use_id: block.id, content: output });
208+ while (true) {
209+ let idleRequested = false;
210+ for (let attempt = 0; attempt < 50; attempt += 1) {
211+ for (const msg of BUS.readInbox(name)) {
212+ if (msg.type === "shutdown_request") {
213+ this.setStatus(name, "shutdown");
214+ return;
215+ }
216+ messages.push({ role: "user", content: JSON.stringify(msg) });
217+ }
218+ const response = await client.messages.create({
219+ model: MODEL,
220+ system: sysPrompt,
221+ messages: messages as Anthropic.Messages.MessageParam[],
222+ tools: this.tools() as Anthropic.Messages.Tool[],
223+ max_tokens: 8000,
224+ }).catch(() => null);
225+ if (!response) {
226+ this.setStatus(name, "idle");
227+ return;
228+ }
229+ messages.push({ role: "assistant", content: response.content as Array<ToolUseBlock | TextBlock> });
230+ if (response.stop_reason !== "tool_use") break;
231+ const results: ToolResultBlock[] = [];
232+ for (const block of response.content) {
233+ if (block.type !== "tool_use") continue;
234+ let output = "";
235+ if (block.name === "idle") {
236+ idleRequested = true;
237+ output = "Entering idle phase. Will poll for new tasks.";
238+ } else {
239+ output = this.exec(name, block.name, block.input as Record<string, unknown>);
240+ }
241+ console.log(` [${name}] ${block.name}: ${output.slice(0, 120)}`);
242+ results.push({ type: "tool_result", tool_use_id: block.id, content: output });
243+ }
244+ messages.push({ role: "user", content: results });
245+ if (idleRequested) break;
187246 }
188- messages.push({ role: "user", content: results });
247+
248+ this.setStatus(name, "idle");
249+ let resume = false;
250+ const start = Date.now();
251+ while (Date.now() - start < IDLE_TIMEOUT) {
252+ await sleep(POLL_INTERVAL);
253+ const inbox = BUS.readInbox(name);
254+ if (inbox.length) {
255+ for (const msg of inbox) {
256+ if (msg.type === "shutdown_request") {
257+ this.setStatus(name, "shutdown");
258+ return;
259+ }
260+ messages.push({ role: "user", content: JSON.stringify(msg) });
261+ }
262+ resume = true;
263+ break;
264+ }
265+ const unclaimed = scanUnclaimedTasks();
266+ if (unclaimed.length) {
267+ const task = unclaimed[0];
268+ claimTask(task.id, name);
269+ if (messages.length <= 3) {
270+ messages.unshift({ role: "assistant", content: `I am ${name}. Continuing.` });
271+ messages.unshift(makeIdentityBlock(name, role, teamName));
272+ }
273+ messages.push({ role: "user", content: `<auto-claimed>Task #${task.id}: ${task.subject}\n${task.description ?? ""}</auto-claimed>` });
274+ messages.push({ role: "assistant", content: `Claimed task #${task.id}. Working on it.` });
275+ resume = true;
276+ break;
277+ }
278+ }
279+
280+ if (!resume) {
281+ this.setStatus(name, "shutdown");
282+ return;
283+ }
284+ this.setStatus(name, "working");
189285 }
190- const member = this.findMember(name);
191- if (member) {
192- member.status = shouldExit ? "shutdown" : "idle";
193- this.saveConfig();
194- }
195286 }
196287
197288 private tools() {
198289 return [
199290 { name: "bash", description: shellToolDescription(), input_schema: { type: "object", properties: { command: { type: "string" } }, required: ["command"] } },
200291 { name: "read_file", description: "Read file contents.", input_schema: { type: "object", properties: { path: { type: "string" }, limit: { type: "integer" } }, required: ["path"] } },
201292 { name: "write_file", description: "Write content to file.", input_schema: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] } },
202293 { name: "edit_file", description: "Replace exact text in file.", input_schema: { type: "object", properties: { path: { type: "string" }, old_text: { type: "string" }, new_text: { type: "string" } }, required: ["path", "old_text", "new_text"] } },
203294 { name: "send_message", description: "Send message to a teammate.", input_schema: { type: "object", properties: { to: { type: "string" }, content: { type: "string" }, msg_type: { type: "string", enum: VALID_MSG_TYPES } }, required: ["to", "content"] } },
204295 { name: "read_inbox", description: "Read and drain your inbox.", input_schema: { type: "object", properties: {} } },
205296 { name: "shutdown_response", description: "Respond to a shutdown request.", input_schema: { type: "object", properties: { request_id: { type: "string" }, approve: { type: "boolean" }, reason: { type: "string" } }, required: ["request_id", "approve"] } },
206297 { name: "plan_approval", description: "Submit a plan for lead approval.", input_schema: { type: "object", properties: { plan: { type: "string" } }, required: ["plan"] } },
298+ { name: "idle", description: "Signal that you have no more work.", input_schema: { type: "object", properties: {} } },
299+ { name: "claim_task", description: "Claim a task by ID.", input_schema: { type: "object", properties: { task_id: { type: "integer" } }, required: ["task_id"] } },
207300 ];
208301 }
209302
210303 private exec(sender: string, toolName: string, input: Record<string, unknown>) {
211304 if (toolName === "bash") return runBash(String(input.command ?? ""));
212305 if (toolName === "read_file") return runRead(String(input.path ?? ""), Number(input.limit ?? 0) || undefined);
213306 if (toolName === "write_file") return runWrite(String(input.path ?? ""), String(input.content ?? ""));
214307 if (toolName === "edit_file") return runEdit(String(input.path ?? ""), String(input.old_text ?? ""), String(input.new_text ?? ""));
215308 if (toolName === "send_message") return BUS.send(sender, String(input.to ?? ""), String(input.content ?? ""), (input.msg_type as MessageType | undefined) ?? "message");
216309 if (toolName === "read_inbox") return JSON.stringify(BUS.readInbox(sender), null, 2);
217310 if (toolName === "shutdown_response") {
218311 const requestId = String(input.request_id ?? "");
219312 shutdownRequests[requestId] = { ...(shutdownRequests[requestId] ?? { target: sender }), status: input.approve ? "approved" : "rejected" };
220313 BUS.send(sender, "lead", String(input.reason ?? ""), "shutdown_response", { request_id: requestId, approve: Boolean(input.approve) });
221314 return `Shutdown ${input.approve ? "approved" : "rejected"}`;
222315 }
223316 if (toolName === "plan_approval") {
224317 const requestId = randomUUID().slice(0, 8);
225318 planRequests[requestId] = { from: sender, plan: String(input.plan ?? ""), status: "pending" };
226319 BUS.send(sender, "lead", String(input.plan ?? ""), "plan_approval_response", { request_id: requestId, plan: String(input.plan ?? "") });
227320 return `Plan submitted (request_id=${requestId}). Waiting for lead approval.`;
228321 }
322+ if (toolName === "claim_task") return claimTask(Number(input.task_id ?? 0), sender);
229323 return `Unknown tool: ${toolName}`;
230324 }
231325 listAll() {
232326 if (!this.config.members.length) return "No teammates.";
233327 return [`Team: ${this.config.team_name}`, ...this.config.members.map((m) => ` ${m.name} (${m.role}): ${m.status}`)].join("\n");
234328 }
235329
236330 memberNames() {
237331 return this.config.members.map((m) => m.name);
238332 }
239333}
240334
241335const TEAM = new TeammateManager(TEAM_DIR);
242336
243337function handleShutdownRequest(teammate: string) {
244338 const requestId = randomUUID().slice(0, 8);
245339 shutdownRequests[requestId] = { target: teammate, status: "pending" };
246340 BUS.send("lead", teammate, "Please shut down gracefully.", "shutdown_request", { request_id: requestId });
247341 return `Shutdown request ${requestId} sent to '${teammate}' (status: pending)`;
248342}
249343
250344function handlePlanReview(requestId: string, approve: boolean, feedback = "") {
251345 const request = planRequests[requestId];
252346 if (!request) return `Error: Unknown plan request_id '${requestId}'`;
253347 request.status = approve ? "approved" : "rejected";
254348 BUS.send("lead", request.from, feedback, "plan_approval_response", { request_id: requestId, approve, feedback });
255349 return `Plan ${request.status} for '${request.from}'`;
256350}
257351
258352const TOOL_HANDLERS: Record<ToolName, (input: Record<string, unknown>) => string> = {
259353 bash: (input) => runBash(String(input.command ?? "")),
260354 read_file: (input) => runRead(String(input.path ?? ""), Number(input.limit ?? 0) || undefined),
261355 write_file: (input) => runWrite(String(input.path ?? ""), String(input.content ?? "")),
262356 edit_file: (input) => runEdit(String(input.path ?? ""), String(input.old_text ?? ""), String(input.new_text ?? "")),
263357 spawn_teammate: (input) => TEAM.spawn(String(input.name ?? ""), String(input.role ?? ""), String(input.prompt ?? "")),
264358 list_teammates: () => TEAM.listAll(),
265359 send_message: (input) => BUS.send("lead", String(input.to ?? ""), String(input.content ?? ""), (input.msg_type as MessageType | undefined) ?? "message"),
266360 read_inbox: () => JSON.stringify(BUS.readInbox("lead"), null, 2),
267361 broadcast: (input) => BUS.broadcast("lead", String(input.content ?? ""), TEAM.memberNames()),
268362 shutdown_request: (input) => handleShutdownRequest(String(input.teammate ?? "")),
269363 shutdown_response: (input) => JSON.stringify(shutdownRequests[String(input.request_id ?? "")] ?? { error: "not found" }),
270364 plan_approval: (input) => handlePlanReview(String(input.request_id ?? ""), Boolean(input.approve), String(input.feedback ?? "")),
365+ idle: () => "Lead does not idle.",
366+ claim_task: (input) => claimTask(Number(input.task_id ?? 0), "lead"),
271367};
272368
273369const TOOLS = [
274370 { name: "bash", description: shellToolDescription(), input_schema: { type: "object", properties: { command: { type: "string" } }, required: ["command"] } },
275371 { name: "read_file", description: "Read file contents.", input_schema: { type: "object", properties: { path: { type: "string" }, limit: { type: "integer" } }, required: ["path"] } },
276372 { name: "write_file", description: "Write content to file.", input_schema: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] } },
277373 { name: "edit_file", description: "Replace exact text in file.", input_schema: { type: "object", properties: { path: { type: "string" }, old_text: { type: "string" }, new_text: { type: "string" } }, required: ["path", "old_text", "new_text"] } },
278- { name: "spawn_teammate", description: "Spawn a persistent teammate.", input_schema: { type: "object", properties: { name: { type: "string" }, role: { type: "string" }, prompt: { type: "string" } }, required: ["name", "role", "prompt"] } },
374+ { name: "spawn_teammate", description: "Spawn an autonomous teammate.", input_schema: { type: "object", properties: { name: { type: "string" }, role: { type: "string" }, prompt: { type: "string" } }, required: ["name", "role", "prompt"] } },
279375 { name: "list_teammates", description: "List all teammates.", input_schema: { type: "object", properties: {} } },
280376 { name: "send_message", description: "Send a message to a teammate.", input_schema: { type: "object", properties: { to: { type: "string" }, content: { type: "string" }, msg_type: { type: "string", enum: VALID_MSG_TYPES } }, required: ["to", "content"] } },
281377 { name: "read_inbox", description: "Read and drain the lead inbox.", input_schema: { type: "object", properties: {} } },
282378 { name: "broadcast", description: "Send a message to all teammates.", input_schema: { type: "object", properties: { content: { type: "string" } }, required: ["content"] } },
283- { name: "shutdown_request", description: "Request a teammate to shut down gracefully.", input_schema: { type: "object", properties: { teammate: { type: "string" } }, required: ["teammate"] } },
284- { name: "shutdown_response", description: "Check shutdown request status by request_id.", input_schema: { type: "object", properties: { request_id: { type: "string" } }, required: ["request_id"] } },
379+ { name: "shutdown_request", description: "Request a teammate to shut down.", input_schema: { type: "object", properties: { teammate: { type: "string" } }, required: ["teammate"] } },
380+ { name: "shutdown_response", description: "Check shutdown request status.", input_schema: { type: "object", properties: { request_id: { type: "string" } }, required: ["request_id"] } },
285381 { name: "plan_approval", description: "Approve or reject a teammate plan.", input_schema: { type: "object", properties: { request_id: { type: "string" }, approve: { type: "boolean" }, feedback: { type: "string" } }, required: ["request_id", "approve"] } },
382+ { name: "idle", description: "Enter idle state.", input_schema: { type: "object", properties: {} } },
383+ { name: "claim_task", description: "Claim a task from the board by ID.", input_schema: { type: "object", properties: { task_id: { type: "integer" } }, required: ["task_id"] } },
286384];
287385
288386function assistantText(content: Array<ToolUseBlock | TextBlock>) {
289387 return content.filter((block): block is TextBlock => block.type === "text").map((block) => block.text).join("\n");
290388}
291389
292390export async function agentLoop(messages: Message[]) {
293391 while (true) {
294392 const inbox = BUS.readInbox("lead");
295393 if (inbox.length) {
296394 messages.push({ role: "user", content: `<inbox>${JSON.stringify(inbox, null, 2)}</inbox>` });
297395 messages.push({ role: "assistant", content: "Noted inbox messages." });
298396 }
299397 const response = await client.messages.create({
300398 model: MODEL,
301399 system: SYSTEM,
302400 messages: messages as Anthropic.Messages.MessageParam[],
303401 tools: TOOLS as Anthropic.Messages.Tool[],
304402 max_tokens: 8000,
305403 });
306404 messages.push({ role: "assistant", content: response.content as Array<ToolUseBlock | TextBlock> });
307405 if (response.stop_reason !== "tool_use") return;
308406 const results: ToolResultBlock[] = [];
309407 for (const block of response.content) {
310408 if (block.type !== "tool_use") continue;
311409 const handler = TOOL_HANDLERS[block.name as ToolName];
312410 const output = handler ? handler(block.input as Record<string, unknown>) : `Unknown tool: ${block.name}`;
313411 console.log(`> ${block.name}: ${output.slice(0, 200)}`);
314412 results.push({ type: "tool_result", tool_use_id: block.id, content: output });
315413 }
316414 messages.push({ role: "user", content: results });
317415 }
318416}
319417
320418async function main() {
321419 const rl = createInterface({ input: process.stdin, output: process.stdout });
322420 const history: Message[] = [];
323421 while (true) {
324422 let query = "";
325423 try {
326- query = await rl.question("\x1b[36ms10 >> \x1b[0m");
424+ query = await rl.question("\x1b[36ms11 >> \x1b[0m");
327425 } catch (error) {
328426 if (
329427 error instanceof Error &&
330428 (("code" in error && error.code === "ERR_USE_AFTER_CLOSE") || error.name === "AbortError")
331429 ) {
332430 break;
333431 }
334432 throw error;
335433 }
336434 if (!query.trim() || ["q", "exit"].includes(query.trim().toLowerCase())) break;
337435 if (query.trim() === "/team") { console.log(TEAM.listAll()); continue; }
338436 if (query.trim() === "/inbox") { console.log(JSON.stringify(BUS.readInbox("lead"), null, 2)); continue; }
437+ if (query.trim() === "/tasks") {
438+ mkdirSync(TASKS_DIR, { recursive: true });
439+ for (const task of scanUnclaimedTasks()) console.log(` [ ] #${task.id}: ${task.subject}`);
440+ continue;
441+ }
339442 history.push({ role: "user", content: query });
340443 await agentLoop(history);
341444 const last = history[history.length - 1]?.content;
342445 if (Array.isArray(last)) {
343446 const text = assistantText(last as Array<ToolUseBlock | TextBlock>);
344447 if (text) console.log(text);
345448 }
346449 console.log();
347450 }
348451 rl.close();
349452}
350453
351454void main();