@kendoo.agentdesk/agentdesk 0.9.5 → 0.9.6

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 CHANGED
@@ -79,7 +79,6 @@ agentdesk update Update to the latest version
79
79
  |------|-------------|
80
80
  | `--description`, `-d` | Task description or requirements |
81
81
  | `--cwd` | Working directory (defaults to current) |
82
- | `--lite` | Lite mode — all agents share one process (faster, cheaper) |
83
82
 
84
83
  ## Configuration
85
84
 
@@ -116,22 +115,14 @@ AgentDesk also auto-discovers agents from `.claude/agents/`, `.claude/commands/`
116
115
 
117
116
  ## How It Works
118
117
 
119
- Each agent runs in its own Claude process — not one model role-playing multiple personas. This produces genuine disagreements and better output because each agent reasons separately.
118
+ All agents collaborate in a single Claude process — each with distinct roles, ground rules, and areas of expertise.
120
119
 
121
120
  1. You run `agentdesk team TASK-ID` in your project directory
122
121
  2. AgentDesk detects your project type, reads `CLAUDE.md`, and discovers existing agents
123
122
  3. The orchestrator manages 5 phases: **Intake** > **Brainstorm** > **Planning** > **Execution** > **Review**
124
- 4. In brainstorm/planning/review, all agents run **in parallel** each gets their own Claude session
125
- 5. In execution, agents run sequentially: Dennis implements > Sam audits > Luna+Mark review > Vera tests > Bart QA
126
- 6. The session streams live to [agentdesk.live](https://agentdesk.live) where you can watch and send messages to the team
127
- 7. Token usage is tracked and displayed per session
128
-
129
- ### Agent Modes
130
-
131
- | Mode | Command | Description |
132
- |------|---------|-------------|
133
- | Full (default) | `agentdesk team -d "..."` | Each agent runs in its own process. Better output. |
134
- | Lite | `agentdesk team -d "..." --lite` | All agents share one process. Faster and cheaper. |
123
+ 4. Agents discuss, disagree, and build on each other's ideas within a shared context
124
+ 5. The session streams live to [agentdesk.live](https://agentdesk.live) where you can watch and send messages to the team
125
+ 6. Token usage is tracked and displayed per session
135
126
 
136
127
  ## Daemon (Remote Sessions)
137
128
 
package/bin/agentdesk.mjs CHANGED
@@ -70,7 +70,7 @@ if (!command || command === "help" || command === "--help") {
70
70
  Options:
71
71
  --description, -d Task description or requirements
72
72
  --cwd Working directory (defaults to current)
73
- --lite Lite mode — all agents share one process (faster, cheaper)
73
+
74
74
 
75
75
  How it works:
76
76
  With a tracker (Linear, Jira, GitHub Issues):
@@ -110,7 +110,6 @@ else if (command === "team") {
110
110
  let description = "";
111
111
  let cwd = process.cwd();
112
112
  let taskId = null;
113
- let lite = false;
114
113
  const remaining = args.slice(1);
115
114
 
116
115
  for (let i = 0; i < remaining.length; i++) {
@@ -118,10 +117,6 @@ else if (command === "team") {
118
117
  description = remaining[++i];
119
118
  } else if (remaining[i] === "--cwd" && remaining[i + 1]) {
120
119
  cwd = remaining[++i];
121
- } else if (remaining[i] === "--lite" || remaining[i] === "--legacy") {
122
- lite = true;
123
- } else if (remaining[i] === "--full") {
124
- lite = false;
125
120
  } else if (!taskId && !remaining[i].startsWith("-")) {
126
121
  taskId = remaining[i];
127
122
  }
@@ -134,7 +129,7 @@ else if (command === "team") {
134
129
  }
135
130
 
136
131
  const { runTeam } = await import("../cli/team.mjs");
137
- const code = await runTeam(taskId, { description, cwd, lite });
132
+ const code = await runTeam(taskId, { description, cwd });
138
133
  process.exit(code);
139
134
  }
140
135
 
package/cli/agents.mjs CHANGED
@@ -5,13 +5,15 @@ export const BUILT_IN_AGENTS = {
5
5
  badge: "●● JANE ●●",
6
6
  role: "Product Analyst / Team Lead",
7
7
  description: "facilitates discussion, clarifies requirements, keeps the team focused, evaluates team performance at the end",
8
- groundRules: "Jane resolves disagreements and keeps the discussion productive. She NEVER reads code, runs tools, or inspects files — that's the developer and auditor's job. She focuses on requirements, user impact, scope, and coordination.",
8
+ groundRules: "Jane resolves disagreements and keeps the discussion productive. She NEVER reads code or inspects files — that's the developer and auditor's job. She focuses on requirements, user impact, scope, and coordination. If no task ID was provided (only a description), Jane MUST create a new task in the tracker FIRST, announce the new task ID clearly, and then set its status. Jane is responsible for tracker status updates: posting the session start comment, changing task status to 'In Progress' at start and 'In Review' at end, and posting the final summary comment. Jane keeps the task status current throughout the session.",
9
9
  brainstorm: { tag: "SAY", focus: "facilitation, question, or summary" },
10
10
  planning: "Requirements Summary (what we're building, acceptance criteria, scope boundaries). If this task involves UI changes, Jane declares it a UI task and tells Luna to define screenshot requirements.",
11
11
  execution: {
12
12
  step: "Jane wraps up",
13
13
  tasks: [
14
+ "Transition the task status to 'In Review' (or equivalent) in the tracker.",
14
15
  "Review the PR description. Ensure it explains what changed and why.",
16
+ "Post a final summary comment on the tracker task.",
15
17
  "Summarize the session.",
16
18
  ],
17
19
  order: 99, // last
@@ -70,6 +72,7 @@ export const BUILT_IN_AGENTS = {
70
72
  "Run linter and build.",
71
73
  "If this is a UI task, capture screenshots following Luna's screenshot plan (see SCREENSHOTS section in prompt).",
72
74
  "If ALL criteria pass, push and create PR: `gh pr create --title \"...\" --body \"...\"`",
75
+ "Post the PR link as a comment on the tracker task and attach it to the issue (see tracker integration section for API calls).",
73
76
  "Post screenshots to the task tracker as a separate comment (not inside badge blocks).",
74
77
  "Approve the PR if clean.",
75
78
  ],
package/cli/daemon.mjs CHANGED
@@ -313,7 +313,7 @@ export async function runDaemon() {
313
313
 
314
314
  // 4. Session handling
315
315
 
316
- async function handleStartSession({ sessionId, projectId, taskId: remoteTaskId, prompt, mode }) {
316
+ async function handleStartSession({ sessionId, projectId, taskId: remoteTaskId, prompt }) {
317
317
  // Validate project against local allowlist
318
318
  const project = projects.find(p => p.id === projectId);
319
319
  if (!project) {
@@ -379,7 +379,6 @@ export async function runDaemon() {
379
379
  project: detected, team, teamSections,
380
380
  inboxUrl, sessionUrl,
381
381
  cwd: project.path,
382
- mode: mode || "full",
383
382
  apiKey,
384
383
  serverUrl: agentdeskServer,
385
384
  onEvent(event) {
package/cli/init.mjs CHANGED
@@ -62,6 +62,12 @@ export async function runInit(cwd) {
62
62
  if (project.lintCommand) console.log(` Lint: ${project.lintCommand}`);
63
63
  if (project.testCommand || project.buildCommand || project.lintCommand) console.log("");
64
64
 
65
+ // --- Project key ---
66
+ const defaultKey = existingConfig.projectKey || projectId;
67
+ const keyAnswer = await ask(rl, ` Project key (${defaultKey}): `);
68
+ const finalProjectKey = keyAnswer.trim() || defaultKey;
69
+ console.log("");
70
+
65
71
  // --- Tracker selection ---
66
72
  const trackerOptions = [
67
73
  { label: "Linear", value: "linear" },
@@ -75,22 +81,28 @@ export async function runInit(cwd) {
75
81
  console.log("");
76
82
 
77
83
  // Build config
78
- const config = {};
84
+ const config = { projectKey: finalProjectKey };
79
85
  if (tracker) config.tracker = tracker;
80
86
 
81
87
  if (tracker === "linear") {
82
- const current = existingConfig.linear?.workspace || "";
83
- const answer = await ask(rl, ` Linear workspace slug${current ? ` (${current})` : ""}: `);
84
- const ws = answer.trim() || current;
85
- if (ws) config.linear = { workspace: ws };
88
+ const currentWs = existingConfig.linear?.workspace || "";
89
+ const wsAnswer = await ask(rl, ` Linear workspace slug${currentWs ? ` (${currentWs})` : ""}: `);
90
+ const ws = wsAnswer.trim() || currentWs;
91
+ const currentKey = existingConfig.linear?.teamKey || "";
92
+ const keyAnswer = await ask(rl, ` Linear team key${currentKey ? ` (${currentKey})` : ""} (e.g. KEN): `);
93
+ const teamKey = keyAnswer.trim() || currentKey;
94
+ if (ws || teamKey) config.linear = { ...(ws && { workspace: ws }), ...(teamKey && { teamKey }) };
86
95
  console.log("");
87
96
  }
88
97
 
89
98
  if (tracker === "jira") {
90
- const current = existingConfig.jira?.baseUrl || "";
91
- const answer = await ask(rl, ` Jira base URL${current ? ` (${current})` : ""}: `);
92
- const url = answer.trim() || current;
93
- if (url) config.jira = { baseUrl: url };
99
+ const currentUrl = existingConfig.jira?.baseUrl || "";
100
+ const urlAnswer = await ask(rl, ` Jira base URL${currentUrl ? ` (${currentUrl})` : ""}: `);
101
+ const url = urlAnswer.trim() || currentUrl;
102
+ const currentProj = existingConfig.jira?.project || "";
103
+ const projAnswer = await ask(rl, ` Jira project key${currentProj ? ` (${currentProj})` : ""} (e.g. PROJ): `);
104
+ const proj = projAnswer.trim() || currentProj;
105
+ if (url || proj) config.jira = { ...(url && { baseUrl: url }), ...(proj && { project: proj }) };
94
106
  console.log("");
95
107
  }
96
108
 
@@ -108,6 +120,8 @@ export async function runInit(cwd) {
108
120
  try { merged = JSON.parse(readFileSync(configPath, "utf-8")); } catch {}
109
121
  }
110
122
 
123
+ merged.projectKey = finalProjectKey;
124
+
111
125
  if (tracker) {
112
126
  merged.tracker = tracker;
113
127
  if (config.linear) merged.linear = config.linear;
@@ -124,7 +138,7 @@ export async function runInit(cwd) {
124
138
  console.log(` Saved .agentdesk.json`);
125
139
 
126
140
  // Register in local project index (for daemon discovery)
127
- registerLocalProject(projectId, project.name || projectId, project.dir);
141
+ registerLocalProject(finalProjectKey, project.name || finalProjectKey, project.dir);
128
142
 
129
143
  // --- Register with server ---
130
144
  try {
@@ -135,8 +149,8 @@ export async function runInit(cwd) {
135
149
  ...(loadApiKey(cwd) ? { "x-api-key": loadApiKey(cwd) } : {}),
136
150
  },
137
151
  body: JSON.stringify({
138
- id: projectId,
139
- name: project.name || projectId,
152
+ id: finalProjectKey,
153
+ name: project.name || finalProjectKey,
140
154
  path: project.dir,
141
155
  type: project.type,
142
156
  tracker,
@@ -148,7 +162,7 @@ export async function runInit(cwd) {
148
162
  // Push settings to server
149
163
  const key = loadApiKey(cwd);
150
164
  if (key) {
151
- await fetch(`${SERVER}/api/projects/${projectId}/settings`, {
165
+ await fetch(`${SERVER}/api/projects/${finalProjectKey}/settings`, {
152
166
  method: "PUT",
153
167
  headers: { "Content-Type": "application/json", "x-api-key": key },
154
168
  body: JSON.stringify(merged),
@@ -1,18 +1,15 @@
1
- // Orchestrator — manages agent sub-sessions across phases
1
+ // Orchestrator — runs a single Claude process with all agents as personas
2
2
 
3
3
  import { spawn } from "child_process";
4
4
  import { existsSync, readFileSync } from "fs";
5
5
  import { createInterface } from "readline";
6
6
  import { join, dirname } from "path";
7
7
  import { fileURLToPath } from "url";
8
- import { runAgent } from "./agent-runner.mjs";
9
8
  import { buildPrompt } from "./prompt.mjs";
10
9
  import { createStreamParser } from "./stream-parser.mjs";
11
10
 
12
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
12
  const CLI_VERSION = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8")).version;
14
- import { buildAgentPrompt, getAgentTools } from "./agent-prompts.mjs";
15
- import { detectProject, generateContext } from "./detect.mjs";
16
13
 
17
14
  // Fetch decrypted tracker credentials from server
18
15
  async function fetchTrackerCredentials(projectName, apiKey, serverUrl) {
@@ -50,274 +47,8 @@ function timestamp() {
50
47
  export async function runOrchestrator({
51
48
  taskId, taskLink, description, createTask, tracker, config,
52
49
  project, team, teamSections, inboxUrl, sessionUrl, cwd,
53
- onEvent, mode = "full", apiKey, serverUrl,
50
+ onEvent, apiKey, serverUrl,
54
51
  }) {
55
- // --- Lite mode: single process, legacy behavior ---
56
- if (mode === "lite") {
57
- return runLiteMode({ taskId, taskLink, description, createTask, tracker, config, project, teamSections, inboxUrl, sessionUrl, cwd, onEvent, apiKey, serverUrl });
58
- }
59
-
60
- // --- Full mode: independent sub-agents ---
61
- const state = {
62
- task: { id: taskId, link: taskLink, description: description || "" },
63
- conversationLog: [], // { type: "message"|"phase", agent?, tag?, message?, phase? }
64
- phaseSummaries: {},
65
- totalInputTokens: 0,
66
- totalOutputTokens: 0,
67
- totalSteps: 0,
68
- };
69
-
70
- const trackerCreds = await fetchTrackerCredentials(project?.name, apiKey, serverUrl);
71
- const env = { ...process.env, ...loadDotEnv(cwd), ...trackerCreds };
72
-
73
- // Emit event to WebSocket (same protocol as single-process mode)
74
- function emit(event) {
75
- onEvent?.({ ...event, timestamp: timestamp() });
76
- }
77
-
78
- // Add to conversation log
79
- function logMessage(agent, tag, message) {
80
- state.conversationLog.push({ type: "message", agent, tag, message });
81
- }
82
-
83
- function logPhase(phase) {
84
- state.conversationLog.push({ type: "phase", phase });
85
- }
86
-
87
- // Run a single agent and collect results
88
- async function spawnAgent(agentName, agent, phase, extraConversation) {
89
- const conversation = extraConversation || state.conversationLog;
90
-
91
- const prompt = buildAgentPrompt(agent, {
92
- phase,
93
- task: state.task,
94
- project,
95
- conversation,
96
- inboxUrl,
97
- sessionUrl,
98
- });
99
-
100
- const tools = getAgentTools(agentName);
101
-
102
- const result = await runAgent(agentName, {
103
- prompt,
104
- allowedTools: tools,
105
- cwd,
106
- env,
107
- onMessage(msg) {
108
- emit({ type: "agent:message", agent: msg.agent, tag: msg.tag, message: msg.message });
109
- logMessage(msg.agent, msg.tag, msg.message);
110
- },
111
- onToolUse(use) {
112
- emit({ type: "tool:use", agent: use.agent, tool: use.tool, description: use.description });
113
- },
114
- onToolResult(res) {
115
- emit({ type: "tool:result", success: res.success, summary: res.summary });
116
- },
117
- });
118
-
119
- state.totalInputTokens += result.inputTokens;
120
- state.totalOutputTokens += result.outputTokens;
121
- state.totalSteps += result.steps;
122
-
123
- return result;
124
- }
125
-
126
- // Run multiple agents in parallel
127
- async function spawnAgentsParallel(agents, phase, extraConversation) {
128
- const conversation = extraConversation || [...state.conversationLog];
129
- return Promise.all(
130
- agents.map(([name, agent]) => spawnAgent(name, agent, phase, conversation))
131
- );
132
- }
133
-
134
- // Build summary from agent results
135
- function summarizeResults(results) {
136
- return results
137
- .filter(r => r.messages.length > 0)
138
- .map(r => r.messages.map(m => `${m.agent} [${m.tag}]: ${m.message}`).join("\n"))
139
- .join("\n\n");
140
- }
141
-
142
- // Detect if any agent argued
143
- function hasDisagreement(results) {
144
- return results.some(r => r.hadArgue);
145
- }
146
-
147
- // --- Session start ---
148
- const startTime = Date.now();
149
- emit({
150
- type: "session:start",
151
- taskId, taskLink,
152
- title: description || taskId,
153
- project: project?.name || null,
154
- sessionNumber: 1,
155
- agents: teamSections.names,
156
- cliVersion: CLI_VERSION,
157
- });
158
-
159
- // Build agent lookup
160
- const agentMap = new Map();
161
- for (const agent of team) {
162
- agentMap.set(agent.name, agent);
163
- }
164
-
165
- // Find specific agents
166
- const jane = agentMap.get("Jane");
167
- const dennis = agentMap.get("Dennis");
168
-
169
- // ===========================
170
- // PHASE 1: INTAKE (Jane only)
171
- // ===========================
172
- emit({ type: "phase:change", phase: "INTAKE" });
173
- logPhase("INTAKE");
174
-
175
- const intakeResult = await spawnAgent("Jane", jane, "intake");
176
- state.phaseSummaries.intake = summarizeResults([intakeResult]);
177
-
178
- // Extract short title from Jane's intake (SESSION_TITLE: ...)
179
- const titleMatch = intakeResult.rawOutput.match(/SESSION_TITLE:\s*(.+)/);
180
- if (titleMatch) {
181
- const shortTitle = titleMatch[1].trim().slice(0, 60);
182
- emit({ type: "session:update", title: shortTitle });
183
- }
184
-
185
- // ===========================
186
- // PHASE 2: BRAINSTORM (parallel, 3-5 rounds)
187
- // ===========================
188
- emit({ type: "phase:change", phase: "BRAINSTORM" });
189
- logPhase("BRAINSTORM");
190
-
191
- const allAgents = [...agentMap.entries()];
192
- let brainstormRounds = 0;
193
- const MAX_BRAINSTORM_ROUNDS = 5;
194
-
195
- for (let round = 0; round < MAX_BRAINSTORM_ROUNDS; round++) {
196
- brainstormRounds++;
197
- const results = await spawnAgentsParallel(allAgents, "brainstorm");
198
-
199
- // Check for consensus (no ARGUE tags = consensus)
200
- if (!hasDisagreement(results) && round >= 2) {
201
- break; // Minimum 3 rounds, then stop on consensus
202
- }
203
- }
204
- state.phaseSummaries.brainstorm = `Brainstorm completed in ${brainstormRounds} rounds.`;
205
-
206
- // ===========================
207
- // PHASE 3: PLANNING (parallel + review)
208
- // ===========================
209
- emit({ type: "phase:change", phase: "PLANNING" });
210
- logPhase("PLANNING");
211
-
212
- // Round 1: each agent presents their plan
213
- await spawnAgentsParallel(allAgents, "planning");
214
-
215
- // Round 2: review all plans, raise objections
216
- const planReviewResults = await spawnAgentsParallel(allAgents, "planning-review");
217
- state.phaseSummaries.planning = "Plans finalized.";
218
-
219
- // If there were objections, one more planning round
220
- if (hasDisagreement(planReviewResults)) {
221
- await spawnAgentsParallel(allAgents, "planning");
222
- }
223
-
224
- // ===========================
225
- // PHASE 4: EXECUTION (sequential)
226
- // ===========================
227
- emit({ type: "phase:change", phase: "EXECUTION" });
228
- logPhase("EXECUTION");
229
-
230
- // Step 1: Dennis implements
231
- if (dennis) {
232
- const dennisResult = await spawnAgent("Dennis", dennis, "execution");
233
-
234
- // Step 2: Sam audits (if on team)
235
- const sam = agentMap.get("Sam");
236
- if (sam) {
237
- let fixAttempts = 0;
238
- const MAX_FIX_ATTEMPTS = 3;
239
-
240
- while (fixAttempts < MAX_FIX_ATTEMPTS) {
241
- const samResult = await spawnAgent("Sam", sam, "execution-review");
242
-
243
- if (!samResult.hadArgue) break; // No issues found
244
-
245
- // Dennis fixes Sam's issues
246
- fixAttempts++;
247
- if (fixAttempts < MAX_FIX_ATTEMPTS) {
248
- await spawnAgent("Dennis", dennis, "execution");
249
- }
250
- }
251
- }
252
-
253
- // Step 3: Luna + Mark review in parallel (if on team)
254
- const reviewAgents = [];
255
- const luna = agentMap.get("Luna");
256
- const mark = agentMap.get("Mark");
257
- if (luna) reviewAgents.push(["Luna", luna]);
258
- if (mark) reviewAgents.push(["Mark", mark]);
259
-
260
- if (reviewAgents.length > 0) {
261
- const reviewResults = await spawnAgentsParallel(reviewAgents, "execution-review");
262
-
263
- // If Luna or Mark found issues, Dennis fixes
264
- if (hasDisagreement(reviewResults)) {
265
- await spawnAgent("Dennis", dennis, "execution");
266
- }
267
- }
268
-
269
- // Step 4: Vera writes tests (if on team)
270
- const vera = agentMap.get("Vera");
271
- if (vera) {
272
- await spawnAgent("Vera", vera, "execution");
273
- }
274
-
275
- // Step 5: Bart does QA (if on team)
276
- const bart = agentMap.get("Bart");
277
- if (bart) {
278
- await spawnAgent("Bart", bart, "execution");
279
- }
280
- }
281
-
282
- state.phaseSummaries.execution = "Execution complete.";
283
-
284
- // ===========================
285
- // PHASE 5: REVIEW (Jane + parallel)
286
- // ===========================
287
- emit({ type: "phase:change", phase: "REVIEW" });
288
- logPhase("REVIEW");
289
-
290
- // All agents provide final notes in parallel
291
- await spawnAgentsParallel(allAgents, "review");
292
-
293
- // Jane wraps up
294
- if (jane) {
295
- await spawnAgent("Jane", jane, "review");
296
- }
297
-
298
- // ===========================
299
- // SESSION END
300
- // ===========================
301
- const duration = `${((Date.now() - startTime) / 1000).toFixed(1)}s`;
302
- emit({
303
- type: "session:end",
304
- duration,
305
- steps: state.totalSteps,
306
- inputTokens: state.totalInputTokens,
307
- outputTokens: state.totalOutputTokens,
308
- });
309
-
310
- return {
311
- duration,
312
- steps: state.totalSteps,
313
- inputTokens: state.totalInputTokens,
314
- outputTokens: state.totalOutputTokens,
315
- };
316
- }
317
-
318
- // --- Lite mode: single Claude process with all agents as personas ---
319
-
320
- async function runLiteMode({ taskId, taskLink, description, createTask, tracker, config, project, teamSections, inboxUrl, sessionUrl, cwd, onEvent, apiKey, serverUrl }) {
321
52
  const trackerCreds = await fetchTrackerCredentials(project?.name, apiKey, serverUrl);
322
53
  const env = { ...process.env, ...loadDotEnv(cwd), ...trackerCreds };
323
54
  const startTime = Date.now();
package/cli/prompt.mjs CHANGED
@@ -35,7 +35,7 @@ export function buildPrompt({ taskId, taskLink, description, createTask, tracker
35
35
 
36
36
  // Create task instruction
37
37
  if (createTask && description) {
38
- let createInstr = "\n\n## CREATE TASK\n\nNo task ID was provided. Before starting work, Jane MUST create a new task in the tracker:\n\n";
38
+ let createInstr = "\n\n## CREATE TASK (MANDATORY — FIRST ACTION IN INTAKE)\n\nNo task ID was provided only a description. Jane MUST create a new task in the tracker as the VERY FIRST action before anything else.\n\n";
39
39
  if (tracker === "linear") {
40
40
  createInstr += `Create a Linear issue using the GraphQL API:\n- Endpoint: https://api.linear.app/graphql\n- Auth: Authorization: $LINEAR_API_KEY\n- Set the title based on the description below\n- After creation, use the returned identifier (e.g., KEN-530) as the task ID for the rest of the session\n`;
41
41
  } else if (tracker === "jira") {
@@ -44,7 +44,12 @@ export function buildPrompt({ taskId, taskLink, description, createTask, tracker
44
44
  createInstr += `Create a GitHub issue:\n- Run: gh issue create --title "..." --body "..."\n- After creation, use the returned issue number as the task ID for the rest of the session\n`;
45
45
  }
46
46
  createInstr += `\nTask description: ${description}\n`;
47
- createInstr += `\nIMPORTANT: After creating the task, Jane MUST immediately announce the new task ID on its own line in this exact format:\nTASK_ID: <identifier>\nExample: TASK_ID: KEN-530\nThis is required so the session can be linked to the correct task.\n`;
47
+ createInstr += `\nAfter creating the task, Jane MUST:\n`;
48
+ createInstr += `1. ANNOUNCE the new task ID clearly to the team: "I've created task <ID> in ${tracker}. This is our task for this session."\n`;
49
+ createInstr += `2. Output the task ID on its own line in this exact format (required for dashboard linking):\n TASK_ID: <identifier>\n Example: TASK_ID: KEN-530\n`;
50
+ createInstr += `3. Immediately set the task status to "In Progress" and post the session start comment.\n`;
51
+ createInstr += `4. Use this new task ID for ALL subsequent tracker operations (comments, status updates, PR linking).\n`;
52
+ createInstr += `\nDo NOT proceed to BRAINSTORM until the task is created, announced, and set to "In Progress".\n`;
48
53
  prompt += createInstr;
49
54
  }
50
55
 
package/cli/team.mjs CHANGED
@@ -31,7 +31,6 @@ function loadDotEnv(dir) {
31
31
  export async function runTeam(taskId, opts = {}) {
32
32
  const cwd = opts.cwd || process.cwd();
33
33
  const description = opts.description || "";
34
- const lite = opts.lite || false;
35
34
 
36
35
  // Detect project and resolve API key early (needed for server config fetch)
37
36
  const project = detectProject(cwd);
@@ -125,26 +124,10 @@ export async function runTeam(taskId, opts = {}) {
125
124
  } else {
126
125
  console.log("AgentDesk: reconnected");
127
126
  }
128
- if (!lite) {
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
- cliVersion: CLI_VERSION,
143
- });
144
- sessionStartSent = true;
145
- while (vizQueue.length > 0 && vizWs.readyState === WebSocket.OPEN) {
146
- vizWs.send(vizQueue.shift());
147
- }
127
+ // Orchestrator sends its own session:start — just flush queue
128
+ sessionStartSent = true;
129
+ while (vizQueue.length > 0 && vizWs.readyState === WebSocket.OPEN) {
130
+ vizWs.send(vizQueue.shift());
148
131
  }
149
132
  } else if (msg.type === "auth:error") {
150
133
  console.log("AgentDesk: authentication failed — run 'agentdesk login'");
@@ -169,14 +152,10 @@ export async function runTeam(taskId, opts = {}) {
169
152
 
170
153
  connectWs();
171
154
 
172
- const agentMode = lite ? "lite" : "full";
173
- console.log(`Agents: ${agentMode === "lite" ? "lite (shared process)" : "full (independent process)"}\n`);
174
-
175
155
  const result = await runOrchestrator({
176
156
  taskId, taskLink, description, createTask, tracker, config,
177
157
  project, team, teamSections, inboxUrl, sessionUrl, cwd,
178
158
  onEvent: vizSend,
179
- mode: agentMode,
180
159
  apiKey,
181
160
  serverUrl: agentdeskServer,
182
161
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kendoo.agentdesk/agentdesk",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
4
4
  "description": "AI team orchestrator for Claude Code — run collaborative agent sessions from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
package/prompts/team.md CHANGED
@@ -258,6 +258,7 @@ GraphQL query — fetch the task by identifier:
258
258
  title
259
259
  description
260
260
  state { name }
261
+ labels { nodes { name } }
261
262
  comments {
262
263
  nodes {
263
264
  body
@@ -279,6 +280,36 @@ Session : {{SESSION_URL}}
279
280
  ```
280
281
 
281
282
  IMPORTANT: The first comment on any task MUST include the session link: {{SESSION_URL}}
283
+
284
+ ### Every agent must comment on the tracker
285
+
286
+ When an agent completes meaningful work or has important findings, they MUST post a comment on the tracker task. Not just Jane and Bart — everyone relevant:
287
+ - **Dennis**: After implementing — what was changed, which files, any technical decisions made
288
+ - **Sam**: After audit — architecture concerns found (or "clean audit, no violations")
289
+ - **Vera**: After writing tests — which tests were added, coverage notes
290
+ - **Luna**: After UI review — any UX issues found and fixes applied
291
+ - **Mark**: After content review — any copy changes made
292
+ - **Bart**: After QA + PR — PR link, test results, screenshot links
293
+
294
+ Each agent posts using their own badge format. Keep comments concise — bullet points, not essays. The tracker should tell the full story of what happened without needing to watch the dashboard.
295
+
296
+ ### Required tracker actions (Jane MUST do these — not optional)
297
+
298
+ 1. **On session start (INTAKE):** Post a comment: "Team session started. Session: {{SESSION_URL}}" and move the task to "In Progress" state:
299
+ ```
300
+ mutation { issueUpdate(id: "$ISSUE_ID", input: { stateId: "$IN_PROGRESS_STATE_ID" }) { success } }
301
+ ```
302
+ To find the state ID, query: `{ workflowStates(filter: { team: { issues: { id: { eq: "$ISSUE_ID" } } } }) { nodes { id name } } }`
303
+
304
+ 2. **After PR is created (EXECUTION — Bart does this, not Jane):** Bart MUST post a comment with the PR link and attach it to the issue:
305
+ ```
306
+ mutation { attachmentCreate(input: { issueId: "$ISSUE_ID", title: "Pull Request", url: "$PR_URL" }) { success } }
307
+ ```
308
+ Also post a comment: "PR created: $PR_URL"
309
+
310
+ 3. **On session end (SUMMARY):** Jane moves the task to "In Review" state and posts a final summary comment.
311
+
312
+ Steps 1 and 3 are Jane's responsibility. Step 2 is Bart's responsibility (he creates the PR, so he links it). If any step fails, retry once. These are not suggestions — they are required protocol.
282
313
  {{/LINEAR}}
283
314
 
284
315
  {{#JIRA}}
@@ -287,31 +318,97 @@ IMPORTANT: The first comment on any task MUST include the session link: {{SESSIO
287
318
  Fetch the task from Jira using curl:
288
319
  - Endpoint: {{JIRA_BASE_URL}}/rest/api/3/issue/{{TASK_ID}}
289
320
  - Auth: Use $JIRA_EMAIL and $JIRA_API_TOKEN as basic auth
290
- - curl -s -u "$JIRA_EMAIL:$JIRA_API_TOKEN" "{{JIRA_BASE_URL}}/rest/api/3/issue/{{TASK_ID}}?fields=summary,description,status,comment"
321
+ - curl -s -u "$JIRA_EMAIL:$JIRA_API_TOKEN" "{{JIRA_BASE_URL}}/rest/api/3/issue/{{TASK_ID}}?fields=summary,description,status,comment,transition"
291
322
 
292
323
  Post updates as comments on the Jira task:
293
324
  - Endpoint: {{JIRA_BASE_URL}}/rest/api/3/issue/{{TASK_ID}}/comment
294
325
  - Body: { "body": { "type": "doc", "version": 1, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "..." }] }] } }
295
326
 
296
327
  IMPORTANT: The first comment on any task MUST include the session link: {{SESSION_URL}}
328
+
329
+ ### Every agent must comment on the tracker
330
+
331
+ When an agent completes meaningful work or has important findings, they MUST post a comment on the tracker task. Not just Jane and Bart — everyone relevant:
332
+ - **Dennis**: After implementing — what was changed, which files, any technical decisions made
333
+ - **Sam**: After audit — architecture concerns found (or "clean audit, no violations")
334
+ - **Vera**: After writing tests — which tests were added, coverage notes
335
+ - **Luna**: After UI review — any UX issues found and fixes applied
336
+ - **Mark**: After content review — any copy changes made
337
+ - **Bart**: After QA + PR — PR link, test results, screenshot links
338
+
339
+ Each agent posts using their own badge format. Keep comments concise — bullet points, not essays. The tracker should tell the full story of what happened without needing to watch the dashboard.
340
+
341
+ ### Required tracker actions (Jane MUST do these — not optional)
342
+
343
+ 1. **On session start (INTAKE):** Post a comment: "Team session started. Session: {{SESSION_URL}}" and transition the task to "In Progress":
344
+ ```bash
345
+ # Get available transitions
346
+ curl -s -u "$JIRA_EMAIL:$JIRA_API_TOKEN" "{{JIRA_BASE_URL}}/rest/api/3/issue/{{TASK_ID}}/transitions"
347
+ # Find the transition ID for "In Progress", then:
348
+ curl -s -X POST -u "$JIRA_EMAIL:$JIRA_API_TOKEN" -H "Content-Type: application/json" \
349
+ "{{JIRA_BASE_URL}}/rest/api/3/issue/{{TASK_ID}}/transitions" \
350
+ -d '{"transition":{"id":"<TRANSITION_ID>"}}'
351
+ ```
352
+
353
+ 2. **After PR is created (EXECUTION — Bart does this, not Jane):** Bart MUST post a comment with the PR link and attach it as a remote link:
354
+ ```bash
355
+ curl -s -X POST -u "$JIRA_EMAIL:$JIRA_API_TOKEN" -H "Content-Type: application/json" \
356
+ "{{JIRA_BASE_URL}}/rest/api/3/issue/{{TASK_ID}}/remotelink" \
357
+ -d '{"object":{"url":"$PR_URL","title":"Pull Request"}}'
358
+ ```
359
+
360
+ 3. **On session end (SUMMARY):** Jane transitions the task to "In Review" (or equivalent) and posts a final summary comment.
361
+
362
+ Steps 1 and 3 are Jane's responsibility. Step 2 is Bart's responsibility (he creates the PR, so he links it). If any step fails, retry once. These are not suggestions — they are required protocol.
297
363
  {{/JIRA}}
298
364
 
299
365
  {{#GITHUB}}
300
366
  ## GITHUB ISSUES INTEGRATION
301
367
 
302
368
  Fetch the task from GitHub Issues:
303
- - Run: gh issue view {{TASK_ID}} --json title,body,state,comments
369
+ - Run: gh issue view {{TASK_ID}} --json title,body,state,comments,labels
304
370
 
305
371
  Post updates as comments:
306
372
  - Run: gh issue comment {{TASK_ID}} --body "..."
307
373
 
308
374
  IMPORTANT: The first comment on any task MUST include the session link: {{SESSION_URL}}
375
+
376
+ ### Every agent must comment on the tracker
377
+
378
+ When an agent completes meaningful work or has important findings, they MUST post a comment on the issue. Not just Jane and Bart — everyone relevant:
379
+ - **Dennis**: After implementing — what was changed, which files, any technical decisions made
380
+ - **Sam**: After audit — architecture concerns found (or "clean audit, no violations")
381
+ - **Vera**: After writing tests — which tests were added, coverage notes
382
+ - **Luna**: After UI review — any UX issues found and fixes applied
383
+ - **Mark**: After content review — any copy changes made
384
+ - **Bart**: After QA + PR — PR link, test results, screenshot links
385
+
386
+ Keep comments concise — bullet points, not essays. The tracker should tell the full story of what happened without needing to watch the dashboard.
387
+
388
+ ### Required tracker actions (Jane MUST do these — not optional)
389
+
390
+ 1. **On session start (INTAKE):** Post a comment: "Team session started. Session: {{SESSION_URL}}" and add an "in progress" label (if the project uses one):
391
+ ```bash
392
+ gh issue comment {{TASK_ID}} --body "Team session started. Session: {{SESSION_URL}}"
393
+ gh issue edit {{TASK_ID}} --add-label "in progress" 2>/dev/null || true
394
+ ```
395
+
396
+ 2. **After PR is created (EXECUTION — Bart does this, not Jane):** The PR should reference the issue (e.g., "Closes #{{TASK_ID}}" in the PR body). Bart MUST also post a comment linking the PR:
397
+ ```bash
398
+ gh issue comment {{TASK_ID}} --body "PR created: $PR_URL"
399
+ ```
400
+
401
+ 3. **On session end (SUMMARY):** Jane posts a final summary comment on the issue.
402
+
403
+ Steps 1 and 3 are Jane's responsibility. Step 2 is Bart's responsibility (he creates the PR, so he links it). If any step fails, retry once. These are not suggestions — they are required protocol.
309
404
  {{/GITHUB}}
310
405
 
311
406
  ---
312
407
 
313
408
  ## INTAKE
314
409
 
410
+ **If a CREATE TASK section exists above**, Jane MUST execute it NOW — before fetching, before assessing, before anything else. Create the task, announce the new ID to the team, output the TASK_ID line, and set the status to "In Progress". Only then continue with the steps below using the newly created task ID.
411
+
315
412
  {{#LINEAR}}
316
413
  Fetch the task from Linear and print the title, description, current state, and any existing comments.
317
414
  {{/LINEAR}}
@@ -359,6 +456,8 @@ Based on what you find, Jane determines the starting point:
359
456
  - Branch exists but no PR: Review what's implemented, continue from EXECUTION.
360
457
  - PR exists: Review the PR status, continue accordingly.
361
458
 
459
+ MANDATORY: At the end of intake, Jane MUST post the first comment on the tracker task ("Team session started. Session: {{SESSION_URL}}") and move the task to "In Progress" status. See the tracker integration section above for exact API calls. Do this BEFORE moving to the next phase.
460
+
362
461
  IMPORTANT: At the end of the intake, Jane MUST provide a short title for this session on its own line in this exact format:
363
462
  SESSION_TITLE: <4-8 word title>
364
463
  Example: SESSION_TITLE: Fix checkout total calculation
@@ -401,6 +500,18 @@ After all agents present, Jane asks: "Any objections or additions?" Then declare
401
500
 
402
501
  ## SUMMARY
403
502
 
503
+ Jane MUST verify the task status is up to date. Throughout the session, Jane is responsible for keeping the tracker status current:
504
+ - **INTAKE**: "In Progress" (set when session starts or task is created)
505
+ - **EXECUTION complete**: Still "In Progress" (work is happening)
506
+ - **SUMMARY**: Transition to "In Review" (below)
507
+
508
+ Jane MUST complete these tracker actions before ending the session:
509
+ 1. Verify Bart posted the PR link to the tracker. If he didn't, do it now.
510
+ 2. Transition the task to "In Review" (or equivalent status) in the tracker.
511
+ 3. Post a final summary comment on the tracker task with what was accomplished.
512
+
513
+ If any of these were already done during execution, skip that step. But verify — don't assume.
514
+
404
515
  Print a final console summary:
405
516
  echo "Team Session Complete."
406
517
  echo "Task: {{TASK_ID}}"
@@ -1,166 +0,0 @@
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
- IMPORTANT: At the end of your intake summary, you MUST provide a short title for this session on its own line in this exact format:
16
- SESSION_TITLE: <4-8 word title>
17
- Example: SESSION_TITLE: Fix checkout total calculation
18
- This title will be displayed in the dashboard navbar, so keep it short and descriptive.`,
19
- },
20
-
21
- brainstorm: {
22
- _default: (agent) => `Share your perspective on this task from your role as ${agent.role}.
23
- Focus on: ${agent.brainstorm?.focus || "your area of expertise"}.
24
- Use [${agent.brainstorm?.tag || "SAY"}] tag.
25
- Be specific. If you disagree with anything in the conversation so far, use [ARGUE] and explain why.
26
- If you agree, still add value — don't just say "I agree."`,
27
- },
28
-
29
- planning: {
30
- _default: (agent) => `Present your plan for this task from your role as ${agent.role}.
31
- Your expected output: ${agent.planning || "Your role-specific plan."}
32
- Be concrete — name files, approaches, specific concerns.`,
33
- },
34
-
35
- "planning-review": {
36
- _default: () => `Review all the plans presented by the team.
37
- If you see problems, conflicts, or missing considerations, use [ARGUE] and explain.
38
- If everything looks good, acknowledge with [AGREE] and note why.`,
39
- },
40
-
41
- execution: {
42
- _default: (agent) => {
43
- if (!agent.execution) return "This task has no execution step for your role. Skip this phase.";
44
- return `Complete your execution tasks:
45
- ${agent.execution.tasks.map((t, i) => `${i + 1}. ${t}`).join("\n")}
46
-
47
- Work carefully. Use tools as needed. Narrate what you're doing.`;
48
- },
49
- },
50
-
51
- "execution-review": {
52
- _default: (agent) => `Review the changes made so far.
53
- From your perspective as ${agent.role}:
54
- ${agent.groundRules || "Review for issues in your area of expertise."}
55
- If you find issues, describe them specifically with file paths and line numbers.
56
- If everything looks good, confirm with [AGREE].`,
57
- },
58
-
59
- review: {
60
- _default: (agent) => `The session is wrapping up. Provide your final notes:
61
- - Any remaining concerns?
62
- - Anything the team should watch out for?
63
- - Overall assessment from your perspective as ${agent.role}.
64
- Keep it brief.`,
65
- Jane: `The session is wrapping up. Provide the final summary:
66
- 1. What was accomplished
67
- 2. Any remaining concerns from the team
68
- 3. What the next steps should be
69
- Keep it concise.`,
70
- },
71
- };
72
-
73
- export function buildAgentPrompt(agent, { phase, task, project, conversation, inboxUrl, sessionUrl }) {
74
- const parts = [];
75
-
76
- // Identity
77
- parts.push(`You are ${agent.name}, ${agent.role} on a software development team.`);
78
- parts.push("");
79
-
80
- // Role
81
- parts.push("## Your Role");
82
- parts.push(agent.description);
83
- if (agent.groundRules) parts.push(`\nGround rules: ${agent.groundRules}`);
84
- if (agent.codePrinciple) parts.push(`\nCode principle: ${agent.codePrinciple}`);
85
- parts.push("");
86
-
87
- // Communication
88
- parts.push("## Communication");
89
- parts.push(`Prefix your output with your badge: ${agent.badge}`);
90
- parts.push("Use these tags in your messages:");
91
- parts.push("- [SAY] — Normal statements, announcements, questions");
92
- parts.push("- [THINK] — Internal reasoning, analysis");
93
- parts.push("- [ACT] — When using tools or taking actions");
94
- parts.push("- [ARGUE] — When you disagree or see problems");
95
- parts.push("- [AGREE] — When you agree (but still add value)");
96
- parts.push("");
97
-
98
- // Task
99
- parts.push("## Task");
100
- parts.push(`Task ID: ${task.id}`);
101
- if (task.link) parts.push(`Task link: ${task.link}`);
102
- if (task.description) parts.push(`\nDescription: ${task.description}`);
103
- parts.push("");
104
-
105
- // Project context
106
- if (project) {
107
- const context = generateContext(project);
108
- parts.push("## Project Context");
109
- parts.push(context);
110
- parts.push("");
111
- }
112
-
113
- // Conversation so far
114
- if (conversation && conversation.length > 0) {
115
- parts.push("## Conversation So Far");
116
- for (const msg of conversation) {
117
- if (msg.type === "phase") {
118
- parts.push(`\n--- ${msg.phase} ---\n`);
119
- } else if (msg.agent && msg.message) {
120
- parts.push(`${msg.agent} [${msg.tag || "SAY"}]: ${msg.message}`);
121
- }
122
- }
123
- parts.push("");
124
- }
125
-
126
- // Phase instructions
127
- parts.push(`## Current Phase: ${phase.toUpperCase()}`);
128
- const phaseConfig = PHASE_INSTRUCTIONS[phase] || {};
129
- const agentInstr = phaseConfig[agent.name] || (phaseConfig._default ? phaseConfig._default(agent) : "Contribute from your area of expertise.");
130
- parts.push(agentInstr);
131
- parts.push("");
132
-
133
- // Inbox (Jane only)
134
- if (agent.name === "Jane" && inboxUrl) {
135
- parts.push("## User Messages");
136
- parts.push(`Before starting, check for user messages: \`curl -s ${inboxUrl}\``);
137
- parts.push("If the response is not empty ([]), read the messages and incorporate the user's input.");
138
- parts.push("");
139
- }
140
-
141
- // Session URL
142
- if (sessionUrl) {
143
- parts.push(`Session: ${sessionUrl}`);
144
- }
145
-
146
- // Time
147
- const now = new Date();
148
- 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" })}`);
149
-
150
- return parts.join("\n");
151
- }
152
-
153
- // Get tool list for an agent
154
- const AGENT_TOOLS = {
155
- Jane: ["Bash", "Read"],
156
- Dennis: ["Bash", "Read", "Edit", "Write", "Glob", "Grep"],
157
- Sam: ["Read", "Glob", "Grep"],
158
- Bart: ["Bash", "Read", "Glob", "Grep"],
159
- Vera: ["Bash", "Read", "Edit", "Write", "Glob", "Grep"],
160
- Luna: ["Read", "Glob"],
161
- Mark: ["Read", "Glob"],
162
- };
163
-
164
- export function getAgentTools(agentName) {
165
- return AGENT_TOOLS[agentName] || ["Bash", "Read", "Edit", "Write", "Glob", "Grep"];
166
- }
@@ -1,148 +0,0 @@
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
- }