@kendoo.agentdesk/agentdesk 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/agentdesk.mjs CHANGED
@@ -65,6 +65,7 @@ if (!command || command === "help" || command === "--help") {
65
65
  Options:
66
66
  --description, -d Task description or requirements
67
67
  --cwd Working directory (defaults to current)
68
+ --legacy Use single-process mode (cheaper, less independent)
68
69
 
69
70
  How it works:
70
71
  With a tracker (Linear, Jira, GitHub Issues):
@@ -100,10 +101,11 @@ else if (command === "init") {
100
101
  }
101
102
 
102
103
  else if (command === "team") {
103
- // Parse options first to find -d and --cwd
104
+ // Parse options first to find -d, --cwd, --legacy
104
105
  let description = "";
105
106
  let cwd = process.cwd();
106
107
  let taskId = null;
108
+ let legacy = false;
107
109
  const remaining = args.slice(1);
108
110
 
109
111
  for (let i = 0; i < remaining.length; i++) {
@@ -111,6 +113,8 @@ else if (command === "team") {
111
113
  description = remaining[++i];
112
114
  } else if (remaining[i] === "--cwd" && remaining[i + 1]) {
113
115
  cwd = remaining[++i];
116
+ } else if (remaining[i] === "--legacy") {
117
+ legacy = true;
114
118
  } else if (!taskId && !remaining[i].startsWith("-")) {
115
119
  taskId = remaining[i];
116
120
  }
@@ -123,7 +127,7 @@ else if (command === "team") {
123
127
  }
124
128
 
125
129
  const { runTeam } = await import("../cli/team.mjs");
126
- const code = await runTeam(taskId, { description, cwd });
130
+ const code = await runTeam(taskId, { description, cwd, legacy });
127
131
  process.exit(code);
128
132
  }
129
133
 
@@ -0,0 +1,161 @@
1
+ // Per-agent prompt generator for sub-agent architecture
2
+
3
+ import { generateContext } from "./detect.mjs";
4
+
5
+ const PHASE_INSTRUCTIONS = {
6
+ intake: {
7
+ Jane: `You are leading the intake for this task. Your job:
8
+ 1. Understand the task requirements from the description and/or tracker.
9
+ 2. Check for existing branches: \`git branch -a | grep <task-id>\`
10
+ 3. Check for existing PRs: \`gh pr list --search <task-id> --json number,title,state\`
11
+ 4. Explore the codebase briefly to understand what we're working with.
12
+ 5. Summarize: what needs to be done, what already exists, and what the starting point is.
13
+ 6. Recommend which phase to start from (BRAINSTORM for new work, EXECUTION if branch exists).`,
14
+ },
15
+
16
+ brainstorm: {
17
+ _default: (agent) => `Share your perspective on this task from your role as ${agent.role}.
18
+ Focus on: ${agent.brainstorm?.focus || "your area of expertise"}.
19
+ Use [${agent.brainstorm?.tag || "SAY"}] tag.
20
+ Be specific. If you disagree with anything in the conversation so far, use [ARGUE] and explain why.
21
+ If you agree, still add value — don't just say "I agree."`,
22
+ },
23
+
24
+ planning: {
25
+ _default: (agent) => `Present your plan for this task from your role as ${agent.role}.
26
+ Your expected output: ${agent.planning || "Your role-specific plan."}
27
+ Be concrete — name files, approaches, specific concerns.`,
28
+ },
29
+
30
+ "planning-review": {
31
+ _default: () => `Review all the plans presented by the team.
32
+ If you see problems, conflicts, or missing considerations, use [ARGUE] and explain.
33
+ If everything looks good, acknowledge with [AGREE] and note why.`,
34
+ },
35
+
36
+ execution: {
37
+ _default: (agent) => {
38
+ if (!agent.execution) return "This task has no execution step for your role. Skip this phase.";
39
+ return `Complete your execution tasks:
40
+ ${agent.execution.tasks.map((t, i) => `${i + 1}. ${t}`).join("\n")}
41
+
42
+ Work carefully. Use tools as needed. Narrate what you're doing.`;
43
+ },
44
+ },
45
+
46
+ "execution-review": {
47
+ _default: (agent) => `Review the changes made so far.
48
+ From your perspective as ${agent.role}:
49
+ ${agent.groundRules || "Review for issues in your area of expertise."}
50
+ If you find issues, describe them specifically with file paths and line numbers.
51
+ If everything looks good, confirm with [AGREE].`,
52
+ },
53
+
54
+ review: {
55
+ _default: (agent) => `The session is wrapping up. Provide your final notes:
56
+ - Any remaining concerns?
57
+ - Anything the team should watch out for?
58
+ - Overall assessment from your perspective as ${agent.role}.
59
+ Keep it brief.`,
60
+ Jane: `The session is wrapping up. Provide the final summary:
61
+ 1. What was accomplished
62
+ 2. Any remaining concerns from the team
63
+ 3. What the next steps should be
64
+ Keep it concise.`,
65
+ },
66
+ };
67
+
68
+ export function buildAgentPrompt(agent, { phase, task, project, conversation, inboxUrl, sessionUrl }) {
69
+ const parts = [];
70
+
71
+ // Identity
72
+ parts.push(`You are ${agent.name}, ${agent.role} on a software development team.`);
73
+ parts.push("");
74
+
75
+ // Role
76
+ parts.push("## Your Role");
77
+ parts.push(agent.description);
78
+ if (agent.groundRules) parts.push(`\nGround rules: ${agent.groundRules}`);
79
+ if (agent.codePrinciple) parts.push(`\nCode principle: ${agent.codePrinciple}`);
80
+ parts.push("");
81
+
82
+ // Communication
83
+ parts.push("## Communication");
84
+ parts.push(`Prefix your output with your badge: ${agent.badge}`);
85
+ parts.push("Use these tags in your messages:");
86
+ parts.push("- [SAY] — Normal statements, announcements, questions");
87
+ parts.push("- [THINK] — Internal reasoning, analysis");
88
+ parts.push("- [ACT] — When using tools or taking actions");
89
+ parts.push("- [ARGUE] — When you disagree or see problems");
90
+ parts.push("- [AGREE] — When you agree (but still add value)");
91
+ parts.push("");
92
+
93
+ // Task
94
+ parts.push("## Task");
95
+ parts.push(`Task ID: ${task.id}`);
96
+ if (task.link) parts.push(`Task link: ${task.link}`);
97
+ if (task.description) parts.push(`\nDescription: ${task.description}`);
98
+ parts.push("");
99
+
100
+ // Project context
101
+ if (project) {
102
+ const context = generateContext(project);
103
+ parts.push("## Project Context");
104
+ parts.push(context);
105
+ parts.push("");
106
+ }
107
+
108
+ // Conversation so far
109
+ if (conversation && conversation.length > 0) {
110
+ parts.push("## Conversation So Far");
111
+ for (const msg of conversation) {
112
+ if (msg.type === "phase") {
113
+ parts.push(`\n--- ${msg.phase} ---\n`);
114
+ } else if (msg.agent && msg.message) {
115
+ parts.push(`${msg.agent} [${msg.tag || "SAY"}]: ${msg.message}`);
116
+ }
117
+ }
118
+ parts.push("");
119
+ }
120
+
121
+ // Phase instructions
122
+ parts.push(`## Current Phase: ${phase.toUpperCase()}`);
123
+ const phaseConfig = PHASE_INSTRUCTIONS[phase] || {};
124
+ const agentInstr = phaseConfig[agent.name] || (phaseConfig._default ? phaseConfig._default(agent) : "Contribute from your area of expertise.");
125
+ parts.push(agentInstr);
126
+ parts.push("");
127
+
128
+ // Inbox (Jane only)
129
+ if (agent.name === "Jane" && inboxUrl) {
130
+ parts.push("## User Messages");
131
+ parts.push(`Before starting, check for user messages: \`curl -s ${inboxUrl}\``);
132
+ parts.push("If the response is not empty ([]), read the messages and incorporate the user's input.");
133
+ parts.push("");
134
+ }
135
+
136
+ // Session URL
137
+ if (sessionUrl) {
138
+ parts.push(`Session: ${sessionUrl}`);
139
+ }
140
+
141
+ // Time
142
+ const now = new Date();
143
+ parts.push(`\nCurrent date/time: ${now.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" })} ${now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}`);
144
+
145
+ return parts.join("\n");
146
+ }
147
+
148
+ // Get tool list for an agent
149
+ const AGENT_TOOLS = {
150
+ Jane: ["Bash", "Read"],
151
+ Dennis: ["Bash", "Read", "Edit", "Write", "Glob", "Grep"],
152
+ Sam: ["Read", "Glob", "Grep"],
153
+ Bart: ["Bash", "Read", "Glob", "Grep"],
154
+ Vera: ["Bash", "Read", "Edit", "Write", "Glob", "Grep"],
155
+ Luna: ["Read", "Glob"],
156
+ Mark: ["Read", "Glob"],
157
+ };
158
+
159
+ export function getAgentTools(agentName) {
160
+ return AGENT_TOOLS[agentName] || ["Bash", "Read", "Edit", "Write", "Glob", "Grep"];
161
+ }
@@ -0,0 +1,148 @@
1
+ // Spawns a single Claude process for one agent and parses its output
2
+
3
+ import { spawn } from "child_process";
4
+ import { createInterface } from "readline";
5
+
6
+ export async function runAgent(agentName, {
7
+ prompt,
8
+ allowedTools = ["Bash", "Read", "Edit", "Write", "Glob", "Grep"],
9
+ cwd,
10
+ env,
11
+ onMessage,
12
+ onToolUse,
13
+ onToolResult,
14
+ }) {
15
+ const messages = [];
16
+ const toolUses = [];
17
+ let rawOutput = "";
18
+ let inputTokens = 0;
19
+ let outputTokens = 0;
20
+ let steps = 0;
21
+ let hadArgue = false;
22
+
23
+ const child = spawn(
24
+ "claude",
25
+ [
26
+ "-p", prompt,
27
+ "--allowedTools", allowedTools.join(","),
28
+ "--verbose",
29
+ "--output-format", "stream-json",
30
+ ],
31
+ {
32
+ stdio: ["pipe", "pipe", "inherit"],
33
+ shell: false,
34
+ env: env || process.env,
35
+ cwd,
36
+ }
37
+ );
38
+
39
+ child.stdin.end();
40
+
41
+ const rl = createInterface({ input: child.stdout });
42
+
43
+ for await (const line of rl) {
44
+ try {
45
+ const event = JSON.parse(line);
46
+
47
+ // Track tokens
48
+ if (event.usage) {
49
+ if (event.usage.input_tokens) inputTokens = Math.max(inputTokens, event.usage.input_tokens);
50
+ if (event.usage.output_tokens) outputTokens += (event.usage.output_tokens_delta || 0);
51
+ }
52
+ if (event.message?.usage) {
53
+ if (event.message.usage.input_tokens) inputTokens = Math.max(inputTokens, event.message.usage.input_tokens);
54
+ if (event.message.usage.output_tokens) outputTokens = Math.max(outputTokens, event.message.usage.output_tokens);
55
+ }
56
+
57
+ // Text output
58
+ if (event.type === "assistant" && event.message?.content) {
59
+ for (const block of event.message.content) {
60
+ if (block.type === "text" && block.text.trim()) {
61
+ const text = block.text.trim();
62
+ rawOutput += text + "\n";
63
+
64
+ // Detect tags
65
+ let tag = "SAY";
66
+ if (/\[ARGUE\]/i.test(text)) { tag = "ARGUE"; hadArgue = true; }
67
+ else if (/\[AGREE\]/i.test(text)) tag = "AGREE";
68
+ else if (/\[THINK\]/i.test(text)) tag = "THINK";
69
+ else if (/\[ACT\]/i.test(text)) tag = "ACT";
70
+
71
+ const cleanMsg = text.replace(/\[(SAY|ACT|THINK|AGREE|ARGUE)\]\s*/gi, "").replace(/\*+/g, "");
72
+ const msg = { agent: agentName, tag, message: cleanMsg };
73
+ messages.push(msg);
74
+ onMessage?.(msg);
75
+ }
76
+ }
77
+ }
78
+
79
+ // Tool use
80
+ if (event.type === "tool_use") {
81
+ const name = event.name || event.tool_name;
82
+ steps++;
83
+
84
+ let description = "Running command...";
85
+ if (name === "Bash") {
86
+ const cmd = event.input?.command || "";
87
+ if (cmd.includes("curl") && cmd.includes("linear")) description = "Calling Linear API...";
88
+ else if (cmd.includes("curl")) description = "Making API request...";
89
+ else {
90
+ const shortCmd = cmd.length > 80 ? cmd.slice(0, 80) + "..." : cmd;
91
+ description = `$ ${shortCmd}`;
92
+ }
93
+ } else if (name === "Read") {
94
+ const shortPath = (event.input?.file_path || "").split("/").slice(-3).join("/");
95
+ description = `Reading ${shortPath}`;
96
+ } else if (name === "Edit" || name === "Write") {
97
+ const shortPath = (event.input?.file_path || "").split("/").slice(-3).join("/");
98
+ description = `${name === "Edit" ? "Editing" : "Writing"} ${shortPath}`;
99
+ } else if (name === "Glob" || name === "Grep") {
100
+ description = `Searching ${event.input?.pattern || ""}`;
101
+ }
102
+
103
+ const use = { agent: agentName, tool: name, description };
104
+ toolUses.push(use);
105
+ onToolUse?.(use);
106
+ }
107
+
108
+ // Tool result
109
+ if (event.type === "tool_result") {
110
+ const output = event.content || event.output;
111
+ let text = "";
112
+ if (typeof output === "string") text = output.trim();
113
+ else if (Array.isArray(output)) {
114
+ text = output.filter(b => b.type === "text").map(b => b.text.trim()).join("\n");
115
+ }
116
+ const hasError = text && (text.toLowerCase().includes("error") || text.toLowerCase().includes("failed"));
117
+ const summary = text?.length > 300 ? `Done (${text.length} chars)` : text || "Done";
118
+ onToolResult?.({ success: !hasError, summary });
119
+ }
120
+
121
+ // Final result
122
+ if (event.type === "result") {
123
+ if (event.usage) {
124
+ if (event.usage.input_tokens) inputTokens = Math.max(inputTokens, event.usage.input_tokens);
125
+ if (event.usage.output_tokens) outputTokens = Math.max(outputTokens, event.usage.output_tokens);
126
+ }
127
+ }
128
+ } catch {
129
+ // skip non-JSON
130
+ }
131
+ }
132
+
133
+ const exitCode = await new Promise(resolve => {
134
+ child.on("close", (code) => resolve(code || 0));
135
+ });
136
+
137
+ return {
138
+ agent: agentName,
139
+ messages,
140
+ toolUses,
141
+ rawOutput,
142
+ inputTokens,
143
+ outputTokens,
144
+ steps,
145
+ hadArgue,
146
+ exitCode,
147
+ };
148
+ }
package/cli/daemon.mjs CHANGED
@@ -12,6 +12,7 @@ import { getStoredApiKey } from "./login.mjs";
12
12
  import { resolveTeam, generateTeamPrompt } from "./agents.mjs";
13
13
  import { buildPrompt } from "./prompt.mjs";
14
14
  import { createStreamParser } from "./stream-parser.mjs";
15
+ import { runOrchestrator } from "./orchestrator.mjs";
15
16
  import { getRegisteredProjects, registerLocalProject } from "./projects.mjs";
16
17
 
17
18
  const CONFIG_DIR = join(process.env.HOME || process.env.USERPROFILE, ".agentdesk");
@@ -236,7 +237,8 @@ export async function runDaemon() {
236
237
  ws = new WebSocket(DAEMON_URL);
237
238
 
238
239
  ws.on("open", () => {
239
- send({ type: "auth", apiKey });
240
+ // Send auth directly — send() requires connected=true which isn't set yet
241
+ ws.send(JSON.stringify({ type: "auth", apiKey }));
240
242
  });
241
243
 
242
244
  ws.on("message", (raw) => {
@@ -358,113 +360,36 @@ export async function runDaemon() {
358
360
  const inboxUrl = `${agentdeskServer}/api/sessions/${sessionId}/inbox`;
359
361
  const sessionUrl = `${agentdeskServer}/sessions/${sessionId}`;
360
362
 
361
- // Build prompt
362
- const fullPrompt = buildPrompt({
363
+ // Run orchestrator (sub-agent mode)
364
+ const result = await runOrchestrator({
363
365
  taskId, taskLink,
364
366
  description: prompt || "",
365
367
  createTask: false,
366
368
  tracker, config,
367
- project: detected,
368
- teamSections, inboxUrl, sessionUrl,
369
- });
370
-
371
- // Send session:start
372
- sendBuffered(sessionId, {
373
- type: "session:start",
374
- taskId, taskLink,
375
- title: prompt || taskId,
376
- project: project.name,
377
- sessionNumber: 1,
378
- agents: teamSections.names,
379
- });
380
-
381
- sendBuffered(sessionId, { type: "phase:change", phase: "INTAKE" });
382
-
383
- // Spawn Claude — NEVER uses --dangerously-skip-permissions
384
- const child = spawn(
385
- "claude",
386
- [
387
- "-p", fullPrompt,
388
- "--allowedTools", "Bash,Read,Edit,Write,Glob,Grep",
389
- "--verbose",
390
- "--output-format", "stream-json",
391
- ],
392
- {
393
- stdio: ["pipe", "pipe", "inherit"],
394
- shell: false,
395
- env: { ...process.env, ...loadDotEnv(project.path) },
396
- cwd: project.path,
397
- }
398
- );
399
-
400
- // Close stdin so claude doesn't wait for input
401
- child.stdin.end();
402
-
403
- activeSession.child = child;
404
-
405
- // Parse stream
406
- const { parseLine } = createStreamParser({
407
- teamNames: teamSections.names,
408
- callbacks: {
409
- onPhaseChange({ phase }) {
410
- sendBuffered(sessionId, { type: "phase:change", phase });
411
- },
412
- onAgentMessage({ agent, tag, message }) {
413
- sendBuffered(sessionId, { type: "agent:message", agent, tag, message });
414
- },
415
- onToolUse({ agent, tool, description }) {
416
- sendBuffered(sessionId, { type: "tool:use", agent, tool, description });
417
- // Track file paths for metadata logging
418
- const pathMatch = description.match(/(?:Reading|Editing|Writing)\s+(.+)/);
369
+ project: detected, team, teamSections,
370
+ inboxUrl, sessionUrl,
371
+ cwd: project.path,
372
+ onEvent(event) {
373
+ sendBuffered(sessionId, event);
374
+ // Track file paths for metadata logging
375
+ if (event.type === "tool:use" && event.description) {
376
+ const pathMatch = event.description.match(/(?:Reading|Editing|Writing)\s+(.+)/);
419
377
  if (pathMatch) filePathsTouched.add(pathMatch[1]);
420
- },
421
- onToolResult({ success, summary }) {
422
- sendBuffered(sessionId, { type: "tool:result", success, summary });
423
- },
424
- onSessionUpdate({ taskId: newTaskId }) {
425
- sendBuffered(sessionId, { type: "session:update", taskId: newTaskId });
426
- },
427
- onSessionEnd({ duration, steps, inputTokens, outputTokens }) {
428
- sendBuffered(sessionId, { type: "session:end", duration, steps, inputTokens, outputTokens });
429
- console.log(` ${green}Session complete${reset} ${dim}${sessionId}${reset} (${duration}, ${steps} steps)`);
430
-
431
- // Log metadata
432
- logSessionMetadata(sessionId, {
433
- sessionId, projectId,
434
- startedAt, endedAt: Date.now(),
435
- duration, exitCode: 0, steps,
436
- filePathsTouched: [...filePathsTouched],
437
- });
438
-
439
- activeSession = null;
440
- },
378
+ }
441
379
  },
442
380
  });
443
381
 
444
- const rl = createInterface({ input: child.stdout });
445
- for await (const line of rl) {
446
- parseLine(line);
447
- }
382
+ console.log(` ${green}Session complete${reset} ${dim}${sessionId}${reset} (${result.duration}, ${result.steps} steps)`);
448
383
 
449
- // Handle unexpected exit (no "result" event)
450
- child.on("close", (code) => {
451
- if (activeSession?.sessionId === sessionId) {
452
- const duration = `${((Date.now() - startedAt) / 1000).toFixed(1)}s`;
453
- console.log(` ${code ? red : yellow}Session exited${reset} ${dim}${sessionId}${reset} (code ${code})`);
454
-
455
- sendBuffered(sessionId, { type: "session:end", duration, steps: 0, inputTokens: 0, outputTokens: 0 });
456
-
457
- logSessionMetadata(sessionId, {
458
- sessionId, projectId,
459
- startedAt, endedAt: Date.now(),
460
- duration, exitCode: code || 0, steps: 0,
461
- filePathsTouched: [...filePathsTouched],
462
- });
463
-
464
- activeSession = null;
465
- }
384
+ logSessionMetadata(sessionId, {
385
+ sessionId, projectId,
386
+ startedAt, endedAt: Date.now(),
387
+ duration: result.duration, exitCode: 0, steps: result.steps,
388
+ filePathsTouched: [...filePathsTouched],
466
389
  });
467
390
 
391
+ activeSession = null;
392
+
468
393
  } catch (err) {
469
394
  console.log(` ${red}Failed to start session:${reset} ${err.message}`);
470
395
  // Send generic error to server — don't leak internal details (paths, config, etc.)
@@ -0,0 +1,280 @@
1
+ // Orchestrator — manages agent sub-sessions across phases
2
+
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { join } from "path";
5
+ import { runAgent } from "./agent-runner.mjs";
6
+ import { buildAgentPrompt, getAgentTools } from "./agent-prompts.mjs";
7
+ import { detectProject, generateContext } from "./detect.mjs";
8
+
9
+ function loadDotEnv(dir) {
10
+ const envPath = join(dir, ".env");
11
+ if (!existsSync(envPath)) return {};
12
+ const vars = {};
13
+ for (const line of readFileSync(envPath, "utf-8").split("\n")) {
14
+ const trimmed = line.trim();
15
+ if (!trimmed || trimmed.startsWith("#")) continue;
16
+ const eq = trimmed.indexOf("=");
17
+ if (eq === -1) continue;
18
+ vars[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
19
+ }
20
+ return vars;
21
+ }
22
+
23
+ function timestamp() {
24
+ const d = new Date();
25
+ return [d.getHours(), d.getMinutes(), d.getSeconds()]
26
+ .map(n => String(n).padStart(2, "0")).join(":");
27
+ }
28
+
29
+ export async function runOrchestrator({
30
+ taskId, taskLink, description, createTask, tracker, config,
31
+ project, team, teamSections, inboxUrl, sessionUrl, cwd,
32
+ onEvent,
33
+ }) {
34
+ const state = {
35
+ task: { id: taskId, link: taskLink, description: description || "" },
36
+ conversationLog: [], // { type: "message"|"phase", agent?, tag?, message?, phase? }
37
+ phaseSummaries: {},
38
+ totalInputTokens: 0,
39
+ totalOutputTokens: 0,
40
+ totalSteps: 0,
41
+ };
42
+
43
+ const env = { ...process.env, ...loadDotEnv(cwd) };
44
+
45
+ // Emit event to WebSocket (same protocol as single-process mode)
46
+ function emit(event) {
47
+ onEvent?.({ ...event, timestamp: timestamp() });
48
+ }
49
+
50
+ // Add to conversation log
51
+ function logMessage(agent, tag, message) {
52
+ state.conversationLog.push({ type: "message", agent, tag, message });
53
+ }
54
+
55
+ function logPhase(phase) {
56
+ state.conversationLog.push({ type: "phase", phase });
57
+ }
58
+
59
+ // Run a single agent and collect results
60
+ async function spawnAgent(agentName, agent, phase, extraConversation) {
61
+ const conversation = extraConversation || state.conversationLog;
62
+
63
+ const prompt = buildAgentPrompt(agent, {
64
+ phase,
65
+ task: state.task,
66
+ project,
67
+ conversation,
68
+ inboxUrl,
69
+ sessionUrl,
70
+ });
71
+
72
+ const tools = getAgentTools(agentName);
73
+
74
+ const result = await runAgent(agentName, {
75
+ prompt,
76
+ allowedTools: tools,
77
+ cwd,
78
+ env,
79
+ onMessage(msg) {
80
+ emit({ type: "agent:message", agent: msg.agent, tag: msg.tag, message: msg.message });
81
+ logMessage(msg.agent, msg.tag, msg.message);
82
+ },
83
+ onToolUse(use) {
84
+ emit({ type: "tool:use", agent: use.agent, tool: use.tool, description: use.description });
85
+ },
86
+ onToolResult(res) {
87
+ emit({ type: "tool:result", success: res.success, summary: res.summary });
88
+ },
89
+ });
90
+
91
+ state.totalInputTokens += result.inputTokens;
92
+ state.totalOutputTokens += result.outputTokens;
93
+ state.totalSteps += result.steps;
94
+
95
+ return result;
96
+ }
97
+
98
+ // Run multiple agents in parallel
99
+ async function spawnAgentsParallel(agents, phase, extraConversation) {
100
+ const conversation = extraConversation || [...state.conversationLog];
101
+ return Promise.all(
102
+ agents.map(([name, agent]) => spawnAgent(name, agent, phase, conversation))
103
+ );
104
+ }
105
+
106
+ // Build summary from agent results
107
+ function summarizeResults(results) {
108
+ return results
109
+ .filter(r => r.messages.length > 0)
110
+ .map(r => r.messages.map(m => `${m.agent} [${m.tag}]: ${m.message}`).join("\n"))
111
+ .join("\n\n");
112
+ }
113
+
114
+ // Detect if any agent argued
115
+ function hasDisagreement(results) {
116
+ return results.some(r => r.hadArgue);
117
+ }
118
+
119
+ // --- Session start ---
120
+ const startTime = Date.now();
121
+ emit({
122
+ type: "session:start",
123
+ taskId, taskLink,
124
+ title: description || taskId,
125
+ project: project?.name || null,
126
+ sessionNumber: 1,
127
+ agents: teamSections.names,
128
+ });
129
+
130
+ // Build agent lookup
131
+ const agentMap = new Map();
132
+ for (const agent of team) {
133
+ agentMap.set(agent.name, agent);
134
+ }
135
+
136
+ // Find specific agents
137
+ const jane = agentMap.get("Jane");
138
+ const dennis = agentMap.get("Dennis");
139
+
140
+ // ===========================
141
+ // PHASE 1: INTAKE (Jane only)
142
+ // ===========================
143
+ emit({ type: "phase:change", phase: "INTAKE" });
144
+ logPhase("INTAKE");
145
+
146
+ const intakeResult = await spawnAgent("Jane", jane, "intake");
147
+ state.phaseSummaries.intake = summarizeResults([intakeResult]);
148
+
149
+ // ===========================
150
+ // PHASE 2: BRAINSTORM (parallel, 3-5 rounds)
151
+ // ===========================
152
+ emit({ type: "phase:change", phase: "BRAINSTORM" });
153
+ logPhase("BRAINSTORM");
154
+
155
+ const allAgents = [...agentMap.entries()];
156
+ let brainstormRounds = 0;
157
+ const MAX_BRAINSTORM_ROUNDS = 5;
158
+
159
+ for (let round = 0; round < MAX_BRAINSTORM_ROUNDS; round++) {
160
+ brainstormRounds++;
161
+ const results = await spawnAgentsParallel(allAgents, "brainstorm");
162
+
163
+ // Check for consensus (no ARGUE tags = consensus)
164
+ if (!hasDisagreement(results) && round >= 2) {
165
+ break; // Minimum 3 rounds, then stop on consensus
166
+ }
167
+ }
168
+ state.phaseSummaries.brainstorm = `Brainstorm completed in ${brainstormRounds} rounds.`;
169
+
170
+ // ===========================
171
+ // PHASE 3: PLANNING (parallel + review)
172
+ // ===========================
173
+ emit({ type: "phase:change", phase: "PLANNING" });
174
+ logPhase("PLANNING");
175
+
176
+ // Round 1: each agent presents their plan
177
+ await spawnAgentsParallel(allAgents, "planning");
178
+
179
+ // Round 2: review all plans, raise objections
180
+ const planReviewResults = await spawnAgentsParallel(allAgents, "planning-review");
181
+ state.phaseSummaries.planning = "Plans finalized.";
182
+
183
+ // If there were objections, one more planning round
184
+ if (hasDisagreement(planReviewResults)) {
185
+ await spawnAgentsParallel(allAgents, "planning");
186
+ }
187
+
188
+ // ===========================
189
+ // PHASE 4: EXECUTION (sequential)
190
+ // ===========================
191
+ emit({ type: "phase:change", phase: "EXECUTION" });
192
+ logPhase("EXECUTION");
193
+
194
+ // Step 1: Dennis implements
195
+ if (dennis) {
196
+ const dennisResult = await spawnAgent("Dennis", dennis, "execution");
197
+
198
+ // Step 2: Sam audits (if on team)
199
+ const sam = agentMap.get("Sam");
200
+ if (sam) {
201
+ let fixAttempts = 0;
202
+ const MAX_FIX_ATTEMPTS = 3;
203
+
204
+ while (fixAttempts < MAX_FIX_ATTEMPTS) {
205
+ const samResult = await spawnAgent("Sam", sam, "execution-review");
206
+
207
+ if (!samResult.hadArgue) break; // No issues found
208
+
209
+ // Dennis fixes Sam's issues
210
+ fixAttempts++;
211
+ if (fixAttempts < MAX_FIX_ATTEMPTS) {
212
+ await spawnAgent("Dennis", dennis, "execution");
213
+ }
214
+ }
215
+ }
216
+
217
+ // Step 3: Luna + Mark review in parallel (if on team)
218
+ const reviewAgents = [];
219
+ const luna = agentMap.get("Luna");
220
+ const mark = agentMap.get("Mark");
221
+ if (luna) reviewAgents.push(["Luna", luna]);
222
+ if (mark) reviewAgents.push(["Mark", mark]);
223
+
224
+ if (reviewAgents.length > 0) {
225
+ const reviewResults = await spawnAgentsParallel(reviewAgents, "execution-review");
226
+
227
+ // If Luna or Mark found issues, Dennis fixes
228
+ if (hasDisagreement(reviewResults)) {
229
+ await spawnAgent("Dennis", dennis, "execution");
230
+ }
231
+ }
232
+
233
+ // Step 4: Vera writes tests (if on team)
234
+ const vera = agentMap.get("Vera");
235
+ if (vera) {
236
+ await spawnAgent("Vera", vera, "execution");
237
+ }
238
+
239
+ // Step 5: Bart does QA (if on team)
240
+ const bart = agentMap.get("Bart");
241
+ if (bart) {
242
+ await spawnAgent("Bart", bart, "execution");
243
+ }
244
+ }
245
+
246
+ state.phaseSummaries.execution = "Execution complete.";
247
+
248
+ // ===========================
249
+ // PHASE 5: REVIEW (Jane + parallel)
250
+ // ===========================
251
+ emit({ type: "phase:change", phase: "REVIEW" });
252
+ logPhase("REVIEW");
253
+
254
+ // All agents provide final notes in parallel
255
+ await spawnAgentsParallel(allAgents, "review");
256
+
257
+ // Jane wraps up
258
+ if (jane) {
259
+ await spawnAgent("Jane", jane, "review");
260
+ }
261
+
262
+ // ===========================
263
+ // SESSION END
264
+ // ===========================
265
+ const duration = `${((Date.now() - startTime) / 1000).toFixed(1)}s`;
266
+ emit({
267
+ type: "session:end",
268
+ duration,
269
+ steps: state.totalSteps,
270
+ inputTokens: state.totalInputTokens,
271
+ outputTokens: state.totalOutputTokens,
272
+ });
273
+
274
+ return {
275
+ duration,
276
+ steps: state.totalSteps,
277
+ inputTokens: state.totalInputTokens,
278
+ outputTokens: state.totalOutputTokens,
279
+ };
280
+ }
package/cli/team.mjs CHANGED
@@ -12,6 +12,7 @@ import { getStoredApiKey } from "./login.mjs";
12
12
  import { resolveTeam, generateTeamPrompt } from "./agents.mjs";
13
13
  import { buildPrompt } from "./prompt.mjs";
14
14
  import { createStreamParser } from "./stream-parser.mjs";
15
+ import { runOrchestrator } from "./orchestrator.mjs";
15
16
 
16
17
  function loadDotEnv(dir) {
17
18
  const envPath = join(dir, ".env");
@@ -30,6 +31,7 @@ function loadDotEnv(dir) {
30
31
  export async function runTeam(taskId, opts = {}) {
31
32
  const cwd = opts.cwd || process.cwd();
32
33
  const description = opts.description || "";
34
+ const legacy = opts.legacy || false;
33
35
 
34
36
  // Detect project and resolve API key early (needed for server config fetch)
35
37
  const project = detectProject(cwd);
@@ -44,13 +46,11 @@ export async function runTeam(taskId, opts = {}) {
44
46
  let createTask = false;
45
47
  if (!taskId) {
46
48
  if (tracker) {
47
- // Tracker configured but no task ID — agents will create a task
48
49
  createTask = true;
49
50
  taskId = `new-${Date.now().toString(36)}`;
50
51
  console.log(`Project: ${project.name || "unknown"} (${project.label})`);
51
52
  console.log(`Mode: Create new ${tracker} task from description`);
52
53
  } else {
53
- // No tracker — generate a label from description
54
54
  taskId = description
55
55
  .toLowerCase()
56
56
  .replace(/[^a-z0-9\s-]/g, "")
@@ -90,12 +90,6 @@ export async function runTeam(taskId, opts = {}) {
90
90
  const inboxUrl = `${agentdeskServer}/api/sessions/${sessionId}/inbox`;
91
91
  const sessionUrl = `${agentdeskServer}/sessions/${sessionId}`;
92
92
 
93
- // Build prompt using shared builder
94
- const fullPrompt = buildPrompt({
95
- taskId, taskLink, description, createTask, tracker, config, project,
96
- teamSections, inboxUrl, sessionUrl,
97
- });
98
-
99
93
  let vizWs = null;
100
94
  let vizConnected = false;
101
95
  const vizQueue = [];
@@ -131,19 +125,25 @@ export async function runTeam(taskId, opts = {}) {
131
125
  } else {
132
126
  console.log("AgentDesk: reconnected");
133
127
  }
134
- // Always re-send session:start on (re)connect so server knows about the session
135
- vizSend({
136
- type: "session:start",
137
- taskId,
138
- taskLink,
139
- title: description || taskId,
140
- project: project.name || null,
141
- sessionNumber: 1,
142
- agents: teamSections.names,
143
- });
144
- sessionStartSent = true;
145
- while (vizQueue.length > 0 && vizWs.readyState === WebSocket.OPEN) {
146
- vizWs.send(vizQueue.shift());
128
+ if (!legacy) {
129
+ // Orchestrator sends its own session:start — just flush queue
130
+ sessionStartSent = true;
131
+ while (vizQueue.length > 0 && vizWs.readyState === WebSocket.OPEN) {
132
+ vizWs.send(vizQueue.shift());
133
+ }
134
+ } else {
135
+ vizSend({
136
+ type: "session:start",
137
+ taskId, taskLink,
138
+ title: description || taskId,
139
+ project: project.name || null,
140
+ sessionNumber: 1,
141
+ agents: teamSections.names,
142
+ });
143
+ sessionStartSent = true;
144
+ while (vizQueue.length > 0 && vizWs.readyState === WebSocket.OPEN) {
145
+ vizWs.send(vizQueue.shift());
146
+ }
147
147
  }
148
148
  } else if (msg.type === "auth:error") {
149
149
  console.log("AgentDesk: authentication failed — run 'agentdesk login'");
@@ -155,14 +155,12 @@ export async function runTeam(taskId, opts = {}) {
155
155
  vizConnected = false;
156
156
  if (code === 4001) {
157
157
  console.log("AgentDesk: authentication required — run 'agentdesk login'");
158
- return; // Don't reconnect on auth failure
158
+ return;
159
159
  }
160
- // Auto-reconnect after 5 seconds
161
160
  clearTimeout(reconnectTimer);
162
161
  reconnectTimer = setTimeout(connectWs, 5000);
163
162
  });
164
163
  } catch {
165
- // AgentDesk not running — retry in 10s
166
164
  clearTimeout(reconnectTimer);
167
165
  reconnectTimer = setTimeout(connectWs, 10000);
168
166
  }
@@ -170,17 +168,37 @@ export async function runTeam(taskId, opts = {}) {
170
168
 
171
169
  connectWs();
172
170
 
173
- // --- Spawn Claude ---
171
+ console.log(`Mode: ${legacy ? "legacy (single process)" : "sub-agents"}\n`);
172
+
173
+ // --- Sub-agent mode (default) ---
174
+ if (!legacy) {
175
+ const result = await runOrchestrator({
176
+ taskId, taskLink, description, createTask, tracker, config,
177
+ project, team, teamSections, inboxUrl, sessionUrl, cwd,
178
+ onEvent: vizSend,
179
+ });
180
+
181
+ console.log(`\n━━━ DONE ━━━`);
182
+ const totalTokens = result.inputTokens + result.outputTokens;
183
+ console.log(` ${result.duration} | ${result.steps} steps${totalTokens ? ` | ${totalTokens.toLocaleString()} tokens` : ""}\n`);
184
+
185
+ setTimeout(() => { try { vizWs?.close(); } catch {} }, 500);
186
+ return 0;
187
+ }
188
+
189
+ // --- Legacy mode (single process) ---
190
+ const fullPrompt = buildPrompt({
191
+ taskId, taskLink, description, createTask, tracker, config, project,
192
+ teamSections, inboxUrl, sessionUrl,
193
+ });
194
+
174
195
  const child = spawn(
175
196
  "claude",
176
197
  [
177
- "-p",
178
- fullPrompt,
179
- "--allowedTools",
180
- "Bash,Read,Edit,Write,Glob,Grep",
198
+ "-p", fullPrompt,
199
+ "--allowedTools", "Bash,Read,Edit,Write,Glob,Grep",
181
200
  "--verbose",
182
- "--output-format",
183
- "stream-json",
201
+ "--output-format", "stream-json",
184
202
  ],
185
203
  {
186
204
  stdio: ["inherit", "pipe", "inherit"],
@@ -193,7 +211,6 @@ export async function runTeam(taskId, opts = {}) {
193
211
  process.on("SIGINT", () => { try { child.kill(); } catch {} process.exit(1); });
194
212
  process.on("SIGTERM", () => { try { child.kill(); } catch {} process.exit(1); });
195
213
 
196
- // --- Stream parsing via shared parser ---
197
214
  console.log("\n━━━ INTAKE ━━━\n");
198
215
  vizSend({ type: "phase:change", phase: "INTAKE" });
199
216
 
@@ -215,11 +232,7 @@ export async function runTeam(taskId, opts = {}) {
215
232
  vizSend({ type: "agent:message", agent, tag, message });
216
233
  },
217
234
  onToolUse({ agent, tool, description }) {
218
- if (tool === "Bash") {
219
- console.log(` ${agent} [ACT] ${description}`);
220
- } else {
221
- console.log(` ${agent} [ACT] ${description}`);
222
- }
235
+ console.log(` ${agent} [ACT] ${description}`);
223
236
  vizSend({ type: "tool:use", agent, tool, description });
224
237
  },
225
238
  onToolResult({ success, summary }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kendoo.agentdesk/agentdesk",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "description": "AI team orchestrator for Claude Code — run collaborative agent sessions from your terminal",
5
5
  "type": "module",
6
6
  "bin": {