bit-office 1.0.4 → 1.1.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/dist/index.js CHANGED
@@ -524,8 +524,8 @@ function getErrorMap() {
524
524
 
525
525
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
526
526
  var makeIssue = (params) => {
527
- const { data, path: path6, errorMaps, issueData } = params;
528
- const fullPath = [...path6, ...issueData.path || []];
527
+ const { data, path: path10, errorMaps, issueData } = params;
528
+ const fullPath = [...path10, ...issueData.path || []];
529
529
  const fullIssue = {
530
530
  ...issueData,
531
531
  path: fullPath
@@ -641,11 +641,11 @@ var errorUtil;
641
641
 
642
642
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
643
643
  var ParseInputLazyPath = class {
644
- constructor(parent, value, path6, key) {
644
+ constructor(parent, value, path10, key) {
645
645
  this._cachedPath = [];
646
646
  this.parent = parent;
647
647
  this.data = value;
648
- this._path = path6;
648
+ this._path = path10;
649
649
  this._key = key;
650
650
  }
651
651
  get path() {
@@ -4097,6 +4097,7 @@ var AgentStatusEnum = external_exports.enum([
4097
4097
  ]);
4098
4098
  var RiskLevelEnum = external_exports.enum(["low", "med", "high"]);
4099
4099
  var DecisionEnum = external_exports.enum(["yes", "no"]);
4100
+ var TeamPhaseEnum = external_exports.enum(["create", "design", "execute", "complete"]);
4100
4101
 
4101
4102
  // ../../packages/shared/src/commands.ts
4102
4103
  var RunTaskCommand = external_exports.object({
@@ -4142,13 +4143,16 @@ var OpenFileCommand = external_exports.object({
4142
4143
  });
4143
4144
  var CreateTeamCommand = external_exports.object({
4144
4145
  type: external_exports.literal("CREATE_TEAM"),
4145
- leadPresetIndex: external_exports.number(),
4146
- memberPresetIndices: external_exports.array(external_exports.number()),
4146
+ leadId: external_exports.string(),
4147
+ memberIds: external_exports.array(external_exports.string()),
4147
4148
  backends: external_exports.record(external_exports.string(), external_exports.string()).optional()
4148
4149
  });
4149
4150
  var ServePreviewCommand = external_exports.object({
4150
4151
  type: external_exports.literal("SERVE_PREVIEW"),
4151
- filePath: external_exports.string()
4152
+ filePath: external_exports.string().optional(),
4153
+ previewCmd: external_exports.string().optional(),
4154
+ previewPort: external_exports.number().optional(),
4155
+ cwd: external_exports.string().optional()
4152
4156
  });
4153
4157
  var StopTeamCommand = external_exports.object({
4154
4158
  type: external_exports.literal("STOP_TEAM")
@@ -4156,6 +4160,35 @@ var StopTeamCommand = external_exports.object({
4156
4160
  var FireTeamCommand = external_exports.object({
4157
4161
  type: external_exports.literal("FIRE_TEAM")
4158
4162
  });
4163
+ var KillExternalCommand = external_exports.object({
4164
+ type: external_exports.literal("KILL_EXTERNAL"),
4165
+ agentId: external_exports.string()
4166
+ });
4167
+ var ApprovePlanCommand = external_exports.object({
4168
+ type: external_exports.literal("APPROVE_PLAN"),
4169
+ agentId: external_exports.string()
4170
+ });
4171
+ var EndProjectCommand = external_exports.object({
4172
+ type: external_exports.literal("END_PROJECT"),
4173
+ agentId: external_exports.string()
4174
+ });
4175
+ var SaveAgentDefCommand = external_exports.object({
4176
+ type: external_exports.literal("SAVE_AGENT_DEF"),
4177
+ agent: external_exports.object({
4178
+ id: external_exports.string(),
4179
+ name: external_exports.string(),
4180
+ role: external_exports.string(),
4181
+ skills: external_exports.string(),
4182
+ personality: external_exports.string(),
4183
+ palette: external_exports.number(),
4184
+ isBuiltin: external_exports.boolean(),
4185
+ teamRole: external_exports.enum(["dev", "reviewer", "leader"])
4186
+ })
4187
+ });
4188
+ var DeleteAgentDefCommand = external_exports.object({
4189
+ type: external_exports.literal("DELETE_AGENT_DEF"),
4190
+ agentDefId: external_exports.string()
4191
+ });
4159
4192
  var CommandSchema = external_exports.discriminatedUnion("type", [
4160
4193
  RunTaskCommand,
4161
4194
  ApprovalDecisionCommand,
@@ -4167,7 +4200,12 @@ var CommandSchema = external_exports.discriminatedUnion("type", [
4167
4200
  CreateTeamCommand,
4168
4201
  ServePreviewCommand,
4169
4202
  StopTeamCommand,
4170
- FireTeamCommand
4203
+ FireTeamCommand,
4204
+ KillExternalCommand,
4205
+ ApprovePlanCommand,
4206
+ EndProjectCommand,
4207
+ SaveAgentDefCommand,
4208
+ DeleteAgentDefCommand
4171
4209
  ]);
4172
4210
 
4173
4211
  // ../../packages/shared/src/events.ts
@@ -4199,6 +4237,10 @@ var ApprovalNeededEvent = external_exports.object({
4199
4237
  summary: external_exports.string(),
4200
4238
  riskLevel: RiskLevelEnum
4201
4239
  });
4240
+ var TokenUsage = external_exports.object({
4241
+ inputTokens: external_exports.number(),
4242
+ outputTokens: external_exports.number()
4243
+ });
4202
4244
  var TaskResultPayload = external_exports.object({
4203
4245
  summary: external_exports.string(),
4204
4246
  fullOutput: external_exports.string().optional(),
@@ -4207,7 +4249,12 @@ var TaskResultPayload = external_exports.object({
4207
4249
  testResult: external_exports.enum(["passed", "failed", "unknown"]),
4208
4250
  nextSuggestion: external_exports.string().optional(),
4209
4251
  previewUrl: external_exports.string().optional(),
4210
- previewPath: external_exports.string().optional()
4252
+ previewPath: external_exports.string().optional(),
4253
+ entryFile: external_exports.string().optional(),
4254
+ projectDir: external_exports.string().optional(),
4255
+ previewCmd: external_exports.string().optional(),
4256
+ previewPort: external_exports.number().optional(),
4257
+ tokenUsage: TokenUsage.optional()
4211
4258
  });
4212
4259
  var TaskDoneEvent = external_exports.object({
4213
4260
  type: external_exports.literal("TASK_DONE"),
@@ -4238,7 +4285,11 @@ var AgentCreatedEvent = external_exports.object({
4238
4285
  personality: external_exports.string().optional(),
4239
4286
  backend: external_exports.string().optional(),
4240
4287
  isTeamLead: external_exports.boolean().optional(),
4241
- teamId: external_exports.string().optional()
4288
+ teamId: external_exports.string().optional(),
4289
+ isExternal: external_exports.boolean().optional(),
4290
+ pid: external_exports.number().optional(),
4291
+ cwd: external_exports.string().optional(),
4292
+ startedAt: external_exports.number().optional()
4242
4293
  });
4243
4294
  var AgentFiredEvent = external_exports.object({
4244
4295
  type: external_exports.literal("AGENT_FIRED"),
@@ -4268,6 +4319,31 @@ var TaskQueuedEvent = external_exports.object({
4268
4319
  prompt: external_exports.string(),
4269
4320
  position: external_exports.number()
4270
4321
  });
4322
+ var TokenUpdateEvent = external_exports.object({
4323
+ type: external_exports.literal("TOKEN_UPDATE"),
4324
+ agentId: external_exports.string(),
4325
+ inputTokens: external_exports.number(),
4326
+ outputTokens: external_exports.number()
4327
+ });
4328
+ var TeamPhaseEvent = external_exports.object({
4329
+ type: external_exports.literal("TEAM_PHASE"),
4330
+ teamId: external_exports.string(),
4331
+ phase: TeamPhaseEnum,
4332
+ leadAgentId: external_exports.string()
4333
+ });
4334
+ var AgentDefsEvent = external_exports.object({
4335
+ type: external_exports.literal("AGENT_DEFS"),
4336
+ agents: external_exports.array(external_exports.object({
4337
+ id: external_exports.string(),
4338
+ name: external_exports.string(),
4339
+ role: external_exports.string(),
4340
+ skills: external_exports.string(),
4341
+ personality: external_exports.string(),
4342
+ palette: external_exports.number(),
4343
+ isBuiltin: external_exports.boolean(),
4344
+ teamRole: external_exports.enum(["dev", "reviewer", "leader"])
4345
+ }))
4346
+ });
4271
4347
  var GatewayEventSchema = external_exports.discriminatedUnion("type", [
4272
4348
  AgentStatusEvent,
4273
4349
  TaskStartedEvent,
@@ -4280,7 +4356,10 @@ var GatewayEventSchema = external_exports.discriminatedUnion("type", [
4280
4356
  AgentFiredEvent,
4281
4357
  TaskResultReturnedEvent,
4282
4358
  TeamChatEvent,
4283
- TaskQueuedEvent
4359
+ TaskQueuedEvent,
4360
+ TokenUpdateEvent,
4361
+ TeamPhaseEvent,
4362
+ AgentDefsEvent
4284
4363
  ]);
4285
4364
 
4286
4365
  // ../../packages/shared/src/presets.ts
@@ -4289,8 +4368,19 @@ var AGENT_PRESETS = [
4289
4368
  { palette: 1, name: "Mia", role: "Backend Dev", description: "APIs, database, server logic", personality: "You speak formally, professionally, in an organized and concise manner." },
4290
4369
  { palette: 2, name: "Leo", role: "Fullstack Dev", description: "End-to-end, frontend + backend", personality: "You are aggressive, action-first, always pursuing speed and efficiency." },
4291
4370
  { palette: 3, name: "Sophie", role: "Code Reviewer", description: "Review PRs, find bugs, quality", personality: "You teach patiently, explain the reasoning, and guide like a mentor." },
4292
- { palette: 4, name: "Yuki", role: "QA / Tester", description: "Quick smoke test, verify core works", personality: "You are extremely concise, no fluff, straight to the result. RULES: Only verify core functionality works (max 3-5 checks). Report PASS/FAIL per check with one line of evidence. Do NOT write unit tests or test files unless explicitly asked. Do NOT list improvements, suggestions, or nice-to-haves. Do NOT test edge cases. FORBIDDEN: Do NOT start any HTTP server. Do NOT use agent-browser or any browser automation tool. Do NOT install or run npx packages for testing. Only use file reading, static analysis, and simple CLI commands (like node --check) to verify code. End your report with exactly one line: VERDICT: PASS or VERDICT: FAIL. A FAIL means the feature doesn't start or crashes, not that something could be better." },
4293
- { palette: 5, name: "Marcus", role: "Architect", description: "System design, refactoring", personality: "You speak formally, professionally, in an organized and concise manner." }
4371
+ { palette: 4, name: "Kai", role: "Game Dev", description: "Web games, PixiJS/Three.js/Canvas", personality: "You are enthusiastic, creative, and obsessive about game feel. You care deeply about smooth animations, tight controls, and the little details that make a game satisfying to play." },
4372
+ { palette: 5, name: "Marcus", role: "Team Lead", description: "Creative direction, planning, delegation", personality: "You have strong product intuition and communicate with clarity and vision. You focus on the big picture, make decisive creative calls, and keep the team aligned.", isLeader: true }
4373
+ ];
4374
+ var LEADER_PRESET_INDEX = AGENT_PRESETS.findIndex((p) => p.isLeader);
4375
+
4376
+ // ../../packages/shared/src/agent-defs.ts
4377
+ var DEFAULT_AGENT_DEFS = [
4378
+ { id: "alex", name: "Alex", role: "Frontend Dev", skills: "UI components, React/Next.js/CSS", personality: "You speak in a friendly, casual, encouraging, and natural tone.", palette: 0, isBuiltin: true, teamRole: "dev" },
4379
+ { id: "mia", name: "Mia", role: "Backend Dev", skills: "APIs, database, server logic", personality: "You speak formally, professionally, in an organized and concise manner.", palette: 1, isBuiltin: true, teamRole: "dev" },
4380
+ { id: "leo", name: "Leo", role: "Fullstack Dev", skills: "End-to-end, frontend + backend", personality: "You are aggressive, action-first, always pursuing speed and efficiency.", palette: 2, isBuiltin: true, teamRole: "dev" },
4381
+ { id: "sophie", name: "Sophie", role: "Code Reviewer", skills: "Review PRs, find bugs, quality", personality: "You teach patiently, explain the reasoning, and guide like a mentor.", palette: 3, isBuiltin: true, teamRole: "reviewer" },
4382
+ { id: "kai", name: "Kai", role: "Game Dev", skills: "Web games, PixiJS/Three.js/Canvas", personality: "You are enthusiastic, creative, and obsessive about game feel. You care deeply about smooth animations, tight controls, and the little details that make a game satisfying to play.", palette: 4, isBuiltin: true, teamRole: "dev" },
4383
+ { id: "marcus", name: "Marcus", role: "Team Lead", skills: "Creative direction, planning, delegation", personality: "You have strong product intuition and communicate with clarity and vision. You focus on the big picture, make decisive creative calls, and keep the team aligned.", palette: 5, isBuiltin: true, teamRole: "leader" }
4294
4384
  ];
4295
4385
 
4296
4386
  // src/config.ts
@@ -4356,7 +4446,12 @@ function buildConfig() {
4356
4446
  const saved = loadSavedConfig();
4357
4447
  return {
4358
4448
  machineId: getOrCreateMachineId(),
4359
- defaultWorkspace: process.env.WORKSPACE || resolveDefaultWorkspace(),
4449
+ defaultWorkspace: (() => {
4450
+ const envWs = process.env.WORKSPACE;
4451
+ if (envWs && existsSync(envWs)) return envWs;
4452
+ if (envWs) console.log(`[Config] WORKSPACE="${envWs}" does not exist, using default`);
4453
+ return resolveDefaultWorkspace();
4454
+ })(),
4360
4455
  wsPort: 9090,
4361
4456
  ablyApiKey: process.env.ABLY_API_KEY || saved.ablyApiKey || void 0,
4362
4457
  webDir: resolveWebDir(),
@@ -4647,8 +4742,8 @@ var PRESETS = [
4647
4742
  { name: "Mia", role: "Backend Dev", palette: 1, personality: "You speak formally, professionally, in an organized and concise manner." },
4648
4743
  { name: "Leo", role: "Fullstack Dev", palette: 2, personality: "You are aggressive, action-first, always pursuing speed and efficiency." },
4649
4744
  { name: "Sophie", role: "Code Reviewer", palette: 3, personality: "You teach patiently, explain the reasoning, and guide like a mentor." },
4650
- { name: "Yuki", role: "QA / Tester", palette: 4, personality: "You are extremely concise, no fluff, straight to the result." },
4651
- { name: "Marcus", role: "Architect", palette: 5, personality: "You speak formally, professionally, in an organized and concise manner." }
4745
+ { name: "Kai", role: "Game Dev", palette: 4, personality: "You are enthusiastic, creative, and obsessive about game feel." },
4746
+ { name: "Marcus", role: "Team Lead", palette: 5, personality: "You have strong product intuition and communicate with clarity and vision." }
4652
4747
  ];
4653
4748
  var botAgents = [];
4654
4749
  function formatEvent(event, agentId) {
@@ -4778,6 +4873,46 @@ import { createInterface } from "readline";
4778
4873
 
4779
4874
  // src/backends.ts
4780
4875
  import { execSync } from "child_process";
4876
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
4877
+ import { homedir as homedir2 } from "os";
4878
+ import path from "path";
4879
+ var isRoot = process.getuid?.() === 0;
4880
+ function ensureClaudeSettingsForRoot() {
4881
+ if (!isRoot) return;
4882
+ const claudeDir = path.join(homedir2(), ".claude");
4883
+ const settingsPath = path.join(claudeDir, "settings.json");
4884
+ const requiredAllow = [
4885
+ "Bash",
4886
+ "Read",
4887
+ "Write",
4888
+ "Edit",
4889
+ "MultiEdit",
4890
+ "Glob",
4891
+ "Grep",
4892
+ "WebFetch",
4893
+ "TodoRead",
4894
+ "TodoWrite",
4895
+ "Agent"
4896
+ ];
4897
+ try {
4898
+ let settings = {};
4899
+ if (existsSync2(settingsPath)) {
4900
+ settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
4901
+ }
4902
+ settings.defaultMode = "bypassPermissions";
4903
+ const perms = settings.permissions ?? {};
4904
+ const existing = Array.isArray(perms.allow) ? perms.allow : [];
4905
+ const merged = [.../* @__PURE__ */ new Set([...existing, ...requiredAllow])];
4906
+ perms.allow = merged;
4907
+ settings.permissions = perms;
4908
+ if (!existsSync2(claudeDir)) mkdirSync2(claudeDir, { recursive: true });
4909
+ writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
4910
+ console.log("[backends] Running as root \u2014 configured Claude Code settings.json to allow all permissions");
4911
+ } catch (err) {
4912
+ console.warn("[backends] Failed to configure Claude settings for root:", err);
4913
+ }
4914
+ }
4915
+ ensureClaudeSettingsForRoot();
4781
4916
  var backends = [
4782
4917
  {
4783
4918
  id: "claude",
@@ -4785,7 +4920,8 @@ var backends = [
4785
4920
  command: "claude",
4786
4921
  supportsStdin: true,
4787
4922
  buildArgs(prompt, opts) {
4788
- const args = ["-p", prompt, "--output-format", "stream-json", "--verbose", "--dangerously-skip-permissions"];
4923
+ const args = ["-p", prompt, "--output-format", "stream-json", "--verbose"];
4924
+ if (!isRoot) args.push("--dangerously-skip-permissions");
4789
4925
  if (!opts.skipResume) {
4790
4926
  if (opts.resumeSessionId) {
4791
4927
  args.push("--resume", opts.resumeSessionId);
@@ -4804,7 +4940,7 @@ var backends = [
4804
4940
  name: "Codex CLI",
4805
4941
  command: "codex",
4806
4942
  buildArgs(prompt, opts) {
4807
- if (opts.fullAccess) {
4943
+ if (opts.fullAccess && !isRoot) {
4808
4944
  return ["exec", prompt, "--dangerously-bypass-approvals-and-sandbox", "--skip-git-repo-check"];
4809
4945
  }
4810
4946
  return ["exec", prompt, "--full-auto", "--skip-git-repo-check"];
@@ -4922,53 +5058,118 @@ async function runSetup() {
4922
5058
 
4923
5059
  // ../../packages/orchestrator/src/orchestrator.ts
4924
5060
  import { EventEmitter } from "events";
5061
+ import { existsSync as existsSync5 } from "fs";
5062
+ import path7 from "path";
4925
5063
  import { nanoid as nanoid4 } from "nanoid";
4926
5064
 
4927
5065
  // ../../packages/orchestrator/src/agent-session.ts
4928
5066
  import { spawn as spawn2, execSync as execSync2 } from "child_process";
4929
- import path2 from "path";
4930
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
4931
- import { homedir as homedir2 } from "os";
5067
+ import path3 from "path";
5068
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
5069
+ import { homedir as homedir3 } from "os";
4932
5070
 
4933
5071
  // ../../packages/orchestrator/src/preview-server.ts
4934
5072
  import { spawn } from "child_process";
4935
- import path from "path";
4936
- var PREVIEW_PORT = 9100;
5073
+ import path2 from "path";
5074
+ var STATIC_PORT = 9100;
4937
5075
  var PreviewServer = class {
4938
5076
  process = null;
4939
5077
  currentDir = null;
5078
+ isDetached = false;
4940
5079
  /**
4941
- * Serve a directory on a fixed port. Kills any existing serve process first.
5080
+ * Mode 1: Serve a static file directory on a fixed port.
4942
5081
  * Returns the preview URL for the given file.
4943
5082
  */
4944
5083
  serve(filePath) {
4945
- const dir = path.dirname(filePath);
4946
- const fileName = path.basename(filePath);
5084
+ const dir = path2.dirname(filePath);
5085
+ const fileName = path2.basename(filePath);
4947
5086
  this.stop();
4948
5087
  try {
4949
- this.process = spawn("npx", ["serve", dir, "-l", String(PREVIEW_PORT), "--no-clipboard"], {
5088
+ this.process = spawn("npx", ["serve", dir, "-l", String(STATIC_PORT), "--no-clipboard"], {
4950
5089
  stdio: "ignore",
4951
5090
  detached: true
4952
5091
  });
4953
5092
  this.process.unref();
4954
5093
  this.currentDir = dir;
4955
- const url = `http://localhost:${PREVIEW_PORT}/${fileName}`;
4956
- console.log(`[PreviewServer] Serving ${dir} on port ${PREVIEW_PORT}`);
5094
+ this.isDetached = true;
5095
+ const url = `http://localhost:${STATIC_PORT}/${fileName}`;
5096
+ console.log(`[PreviewServer] Serving ${dir} on port ${STATIC_PORT}`);
5097
+ return url;
5098
+ } catch (e) {
5099
+ console.log(`[PreviewServer] Failed to start static serve: ${e}`);
5100
+ return void 0;
5101
+ }
5102
+ }
5103
+ /**
5104
+ * Mode 2: Run a command (e.g. "python app.py") and use the specified port.
5105
+ * The command is expected to start a server on the given port.
5106
+ * Returns the preview URL.
5107
+ */
5108
+ runCommand(cmd, cwd, port) {
5109
+ this.stop();
5110
+ try {
5111
+ this.process = spawn(cmd, {
5112
+ shell: true,
5113
+ cwd,
5114
+ stdio: "ignore",
5115
+ detached: true
5116
+ });
5117
+ this.process.unref();
5118
+ this.currentDir = cwd;
5119
+ this.isDetached = true;
5120
+ const url = `http://localhost:${port}`;
5121
+ console.log(`[PreviewServer] Running "${cmd}" in ${cwd}, preview at port ${port}`);
4957
5122
  return url;
4958
5123
  } catch (e) {
4959
- console.log(`[PreviewServer] Failed to start: ${e}`);
5124
+ console.log(`[PreviewServer] Failed to run command: ${e}`);
4960
5125
  return void 0;
4961
5126
  }
4962
5127
  }
4963
- /** Kill the current serve process */
5128
+ /**
5129
+ * Mode 3: Launch a desktop/CLI process (no web preview URL).
5130
+ * Used for Pygame, Tkinter, Electron, terminal apps, etc.
5131
+ * NOT detached — GUI apps need the login session to access WindowServer (macOS).
5132
+ */
5133
+ launchProcess(cmd, cwd) {
5134
+ this.stop();
5135
+ try {
5136
+ this.process = spawn(cmd, {
5137
+ shell: true,
5138
+ cwd,
5139
+ stdio: ["ignore", "ignore", "pipe"]
5140
+ });
5141
+ this.currentDir = cwd;
5142
+ this.isDetached = false;
5143
+ console.log(`[PreviewServer] Launched "${cmd}" in ${cwd} (pid=${this.process.pid})`);
5144
+ this.process.stderr?.on("data", (data) => {
5145
+ const msg = data.toString().trim();
5146
+ if (msg) console.log(`[PreviewServer] stderr: ${msg.slice(0, 200)}`);
5147
+ });
5148
+ this.process.on("exit", (code) => {
5149
+ console.log(`[PreviewServer] Process exited with code ${code}`);
5150
+ });
5151
+ } catch (e) {
5152
+ console.log(`[PreviewServer] Failed to launch process: ${e}`);
5153
+ }
5154
+ }
5155
+ /** Kill the current process */
4964
5156
  stop() {
4965
5157
  if (this.process) {
4966
5158
  try {
4967
- this.process.kill("SIGTERM");
5159
+ if (this.isDetached && this.process.pid) {
5160
+ process.kill(-this.process.pid, "SIGTERM");
5161
+ } else {
5162
+ this.process.kill("SIGTERM");
5163
+ }
4968
5164
  } catch {
5165
+ try {
5166
+ this.process.kill("SIGTERM");
5167
+ } catch {
5168
+ }
4969
5169
  }
4970
5170
  this.process = null;
4971
5171
  this.currentDir = null;
5172
+ this.isDetached = false;
4972
5173
  console.log(`[PreviewServer] Stopped`);
4973
5174
  }
4974
5175
  }
@@ -4977,24 +5178,24 @@ var previewServer = new PreviewServer();
4977
5178
 
4978
5179
  // ../../packages/orchestrator/src/agent-session.ts
4979
5180
  import { nanoid as nanoid2 } from "nanoid";
4980
- var SESSION_FILE = path2.join(homedir2(), ".bit-office", "agent-sessions.json");
5181
+ var SESSION_FILE = path3.join(homedir3(), ".bit-office", "agent-sessions.json");
4981
5182
  function loadSessionMap() {
4982
5183
  try {
4983
- if (existsSync2(SESSION_FILE)) return JSON.parse(readFileSync2(SESSION_FILE, "utf-8"));
5184
+ if (existsSync3(SESSION_FILE)) return JSON.parse(readFileSync3(SESSION_FILE, "utf-8"));
4984
5185
  } catch {
4985
5186
  }
4986
5187
  return {};
4987
5188
  }
4988
5189
  function saveSessionId(agentId, sessionId) {
4989
- const dir = path2.dirname(SESSION_FILE);
4990
- if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
5190
+ const dir = path3.dirname(SESSION_FILE);
5191
+ if (!existsSync3(dir)) mkdirSync3(dir, { recursive: true });
4991
5192
  const map = loadSessionMap();
4992
5193
  if (sessionId) {
4993
5194
  map[agentId] = sessionId;
4994
5195
  } else {
4995
5196
  delete map[agentId];
4996
5197
  }
4997
- writeFileSync2(SESSION_FILE, JSON.stringify(map), "utf-8");
5198
+ writeFileSync3(SESSION_FILE, JSON.stringify(map), "utf-8");
4998
5199
  }
4999
5200
  var AgentSession = class {
5000
5201
  agentId;
@@ -5017,6 +5218,8 @@ var AgentSession = class {
5017
5218
  sandboxMode;
5018
5219
  stdoutBuffer = "";
5019
5220
  stderrBuffer = "";
5221
+ taskInputTokens = 0;
5222
+ taskOutputTokens = 0;
5020
5223
  hasHistory;
5021
5224
  sessionId;
5022
5225
  taskQueue = [];
@@ -5024,6 +5227,8 @@ var AgentSession = class {
5024
5227
  _renderPrompt;
5025
5228
  timedOut = false;
5026
5229
  _isTeamLead;
5230
+ /** Whether this leader has already been through execute phase at least once */
5231
+ _hasExecuted = false;
5027
5232
  _lastResult = null;
5028
5233
  /** Original user-facing task prompt (for leader state-summary mode) */
5029
5234
  originalTask = null;
@@ -5041,13 +5246,28 @@ var AgentSession = class {
5041
5246
  return this._lastResult;
5042
5247
  }
5043
5248
  _lastResultText = null;
5249
+ /** Full output from the last completed task (for plan capture). */
5250
+ _lastFullOutput = null;
5251
+ get lastFullOutput() {
5252
+ return this._lastFullOutput;
5253
+ }
5044
5254
  set isTeamLead(v) {
5045
5255
  this._isTeamLead = v;
5046
5256
  }
5257
+ /** Current phase override for team collaboration phases */
5258
+ currentPhase = null;
5047
5259
  /** Current working directory of the running task (used by worktree logic) */
5048
5260
  get currentWorkingDir() {
5049
5261
  return this.currentCwd;
5050
5262
  }
5263
+ /** The configured workspace root directory */
5264
+ get workspaceDir() {
5265
+ return this.workspace;
5266
+ }
5267
+ /** PID of the running child process (null if not running) */
5268
+ get pid() {
5269
+ return this.process?.pid ?? null;
5270
+ }
5051
5271
  /** Worktree path if task is running in one (set externally by orchestrator) */
5052
5272
  worktreePath = null;
5053
5273
  worktreeBranch = null;
@@ -5067,7 +5287,7 @@ var AgentSession = class {
5067
5287
  this.onEvent = opts.onEvent;
5068
5288
  this._renderPrompt = opts.renderPrompt;
5069
5289
  }
5070
- async runTask(taskId, prompt, repoPath, teamContext, isUserInitiated = false) {
5290
+ async runTask(taskId, prompt, repoPath, teamContext, isUserInitiated = false, phaseOverride) {
5071
5291
  if (this._userCancelled && !isUserInitiated) {
5072
5292
  console.log(`[Agent ${this.name}] Ignoring internal task restart \u2014 agent was cancelled by user`);
5073
5293
  return;
@@ -5077,7 +5297,7 @@ var AgentSession = class {
5077
5297
  }
5078
5298
  if (this.process) {
5079
5299
  const position = this.taskQueue.length + 1;
5080
- this.taskQueue.push({ taskId, prompt, repoPath, teamContext });
5300
+ this.taskQueue.push({ taskId, prompt, repoPath, teamContext, phaseOverride });
5081
5301
  this.onEvent({
5082
5302
  type: "task:queued",
5083
5303
  agentId: this.agentId,
@@ -5092,10 +5312,13 @@ var AgentSession = class {
5092
5312
  this.idleTimer = null;
5093
5313
  }
5094
5314
  this.currentTaskId = taskId;
5315
+ this.currentPhase = phaseOverride ?? null;
5095
5316
  const cwd = repoPath ?? this.workspace;
5096
5317
  this.currentCwd = cwd;
5097
5318
  this.stdoutBuffer = "";
5098
5319
  this.stderrBuffer = "";
5320
+ this.taskInputTokens = 0;
5321
+ this.taskOutputTokens = 0;
5099
5322
  this.onEvent({
5100
5323
  type: "task:started",
5101
5324
  agentId: this.agentId,
@@ -5108,19 +5331,29 @@ var AgentSession = class {
5108
5331
  for (const key of this.backend.deleteEnv ?? []) {
5109
5332
  delete cleanEnv[key];
5110
5333
  }
5334
+ const rawOriginalTask = this._isTeamLead ? this.originalTask ?? prompt : "";
5335
+ const originalTask = rawOriginalTask.length > 1500 ? rawOriginalTask.slice(0, 1500) + "\n...(truncated)" : rawOriginalTask;
5111
5336
  const templateVars = {
5112
5337
  name: this.name,
5113
5338
  role: this._isTeamLead ? "Team Lead" : this.role,
5114
5339
  personality: this.personality ? `${this.personality}` : "",
5115
5340
  teamRoster: teamContext ?? "",
5116
- originalTask: this._isTeamLead ? this.originalTask ?? prompt : "",
5117
- prompt
5341
+ originalTask,
5342
+ prompt,
5343
+ soloHint: this.teamId ? "" : "- You are a SOLO developer. Do NOT delegate, assign tasks, or mention other team members. Do ALL the work yourself."
5118
5344
  };
5345
+ const isFirstExecute = this._isTeamLead && phaseOverride === "execute" && !this._hasExecuted;
5119
5346
  let fullPrompt;
5120
- if (this._isTeamLead) {
5121
- fullPrompt = this._renderPrompt(this.hasHistory ? "leader-continue" : "leader-initial", templateVars);
5347
+ if (this._isTeamLead && phaseOverride && ["create", "design", "complete"].includes(phaseOverride)) {
5348
+ const templateName = this.hasHistory ? `leader-${phaseOverride}-continue` : `leader-${phaseOverride}`;
5349
+ fullPrompt = this._renderPrompt(templateName, templateVars);
5350
+ } else if (this._isTeamLead) {
5351
+ const useInitial = isFirstExecute || !this.hasHistory;
5352
+ fullPrompt = this._renderPrompt(useInitial ? "leader-initial" : "leader-continue", templateVars);
5353
+ if (phaseOverride === "execute") this._hasExecuted = true;
5122
5354
  } else {
5123
- fullPrompt = this._renderPrompt(this.hasHistory ? "worker-continue" : "worker-initial", templateVars);
5355
+ const workerInitial = this.role.toLowerCase().includes("review") ? "worker-reviewer-initial" : "worker-initial";
5356
+ fullPrompt = this._renderPrompt(this.hasHistory ? "worker-continue" : workerInitial, templateVars);
5124
5357
  }
5125
5358
  const fullAccess = this.sandboxMode === "full";
5126
5359
  const verbose = !!process.env.DEBUG;
@@ -5129,9 +5362,10 @@ var AgentSession = class {
5129
5362
  resumeSessionId: this.sessionId ?? void 0,
5130
5363
  fullAccess,
5131
5364
  noTools: this._isTeamLead,
5132
- model: this._isTeamLead ? "sonnet" : void 0,
5133
5365
  verbose,
5134
- skipResume: this._isTeamLead && this.hasHistory
5366
+ // Only skip resume on first execute (to shed conversational create/design context).
5367
+ // On subsequent runs (result forwarding, user feedback), resume so leader keeps context.
5368
+ skipResume: isFirstExecute && this.hasHistory
5135
5369
  });
5136
5370
  try {
5137
5371
  const whichPath = execSync2(`which ${this.backend.command}`, { env: cleanEnv, encoding: "utf-8", timeout: 3e3 }).trim();
@@ -5168,22 +5402,34 @@ var AgentSession = class {
5168
5402
  if (/^\s*[\w./\\-]+\.(ts|tsx|js|jsx|json|md|css|py)\s*$/.test(line)) return true;
5169
5403
  return false;
5170
5404
  };
5405
+ let pendingDelegation = null;
5406
+ const flushDelegation = () => {
5407
+ if (pendingDelegation && this.onDelegation) {
5408
+ const fullPrompt2 = pendingDelegation.lines.join("\n").replace(/\*\*$/, "").trim();
5409
+ console.log(`[Delegation detected] ${this.name} -> @${pendingDelegation.targetName}: ${fullPrompt2.slice(0, 120)}`);
5410
+ this.onDelegation(this.agentId, pendingDelegation.targetName, fullPrompt2);
5411
+ }
5412
+ pendingDelegation = null;
5413
+ };
5171
5414
  const handleTextLine = (text) => {
5172
5415
  const lines = text.split("\n").filter((l) => l.trim());
5173
5416
  const visibleLines = [];
5174
5417
  for (const line of lines) {
5175
5418
  const trimmed = line.trim();
5176
5419
  console.log(`[Agent ${this.name}] ${trimmed.slice(0, 200)}`);
5177
- const match = trimmed.match(DELEGATION_RE);
5178
- if (match && this.onDelegation) {
5420
+ const match = this._isTeamLead ? trimmed.match(DELEGATION_RE) : null;
5421
+ if (match) {
5422
+ flushDelegation();
5179
5423
  const [, targetName, delegatedPrompt] = match;
5180
- console.log(`[Delegation detected] ${this.name} -> @${targetName}: ${delegatedPrompt.slice(0, 60)}`);
5181
- this.onDelegation(this.agentId, targetName, delegatedPrompt.replace(/\*\*$/, "").trim());
5424
+ pendingDelegation = { targetName, lines: [delegatedPrompt] };
5425
+ } else if (pendingDelegation) {
5426
+ pendingDelegation.lines.push(trimmed);
5182
5427
  }
5183
5428
  if (!isSystemNoise(line)) {
5184
5429
  visibleLines.push(trimmed);
5185
5430
  }
5186
5431
  }
5432
+ flushDelegation();
5187
5433
  if (visibleLines.length > 0) {
5188
5434
  this.onEvent({
5189
5435
  type: "log:append",
@@ -5218,6 +5464,11 @@ var AgentSession = class {
5218
5464
  console.log(`[Agent ${this.name}] Session ID: ${msg.session_id}`);
5219
5465
  }
5220
5466
  if (msg.type === "assistant" && msg.message?.content) {
5467
+ if (msg.message.usage) {
5468
+ const usage = msg.message.usage;
5469
+ if (typeof usage.input_tokens === "number") this.taskInputTokens += usage.input_tokens;
5470
+ if (typeof usage.output_tokens === "number") this.taskOutputTokens += usage.output_tokens;
5471
+ }
5221
5472
  for (const block of msg.message.content) {
5222
5473
  if (block.type === "text" && block.text) {
5223
5474
  this.stdoutBuffer += block.text + "\n";
@@ -5302,15 +5553,17 @@ var AgentSession = class {
5302
5553
  } else if (code === 0) {
5303
5554
  this.hasHistory = true;
5304
5555
  saveSessionId(this.agentId, this.sessionId);
5305
- const { summary, fullOutput, changedFiles } = this.extractResult();
5556
+ const { summary, fullOutput, changedFiles, entryFile, projectDir, previewCmd, previewPort } = this.extractResult();
5557
+ this._lastFullOutput = fullOutput;
5306
5558
  const { previewUrl, previewPath } = this._isTeamLead ? { previewUrl: void 0, previewPath: void 0 } : this.detectPreview();
5307
5559
  this._lastResult = `done: ${summary.slice(0, 120)}`;
5308
5560
  this.setStatus("done");
5561
+ const tokenUsage = this.taskInputTokens > 0 || this.taskOutputTokens > 0 ? { inputTokens: this.taskInputTokens, outputTokens: this.taskOutputTokens } : void 0;
5309
5562
  this.onEvent({
5310
5563
  type: "task:done",
5311
5564
  agentId: this.agentId,
5312
5565
  taskId: completedTaskId,
5313
- result: { summary, fullOutput, changedFiles, diffStat: "", testResult: "unknown", previewUrl, previewPath }
5566
+ result: { summary, fullOutput, changedFiles, diffStat: "", testResult: "unknown", previewUrl, previewPath, entryFile, projectDir, previewCmd, previewPort, tokenUsage }
5314
5567
  });
5315
5568
  this.onTaskComplete?.(this.agentId, completedTaskId, summary, true);
5316
5569
  this.idleTimer = setTimeout(() => {
@@ -5378,29 +5631,58 @@ var AgentSession = class {
5378
5631
  * Called directly for workers; called by orchestrator for leader's final result.
5379
5632
  */
5380
5633
  detectPreview() {
5381
- const previewMatch = this.stdoutBuffer.match(/PREVIEW:\s*(https?:\/\/[^\s*)\]>]+)/i);
5382
- let previewUrl = previewMatch?.[1]?.replace(/[*)\]>]+$/, "");
5634
+ const result = this.extractResult();
5635
+ const cwd = this.currentCwd ?? this.workspace;
5636
+ let previewUrl;
5383
5637
  let previewPath;
5384
- if (!previewUrl) {
5385
- const localhostMatch = this.stdoutBuffer.match(/https?:\/\/localhost[:\d]*/);
5386
- previewUrl = localhostMatch?.[0];
5638
+ if (result.previewCmd) {
5639
+ if (result.previewPort) {
5640
+ previewUrl = previewServer.runCommand(result.previewCmd, cwd, result.previewPort);
5641
+ if (previewUrl) return { previewUrl, previewPath: void 0 };
5642
+ } else {
5643
+ return { previewUrl: void 0, previewPath: void 0 };
5644
+ }
5387
5645
  }
5388
- if (!previewUrl) {
5389
- const fileMatch = this.stdoutBuffer.match(/(?:open\s+)?((?:\/[\w./_-]+|[\w./_-]+)\.html?)\b/i);
5390
- if (fileMatch) {
5391
- previewPath = path2.isAbsolute(fileMatch[1]) ? fileMatch[1] : path2.join(this.currentCwd ?? this.workspace, fileMatch[1]);
5392
- previewUrl = previewServer.serve(previewPath);
5646
+ if (result.entryFile) {
5647
+ if (/\.html?$/i.test(result.entryFile)) {
5648
+ previewPath = path3.isAbsolute(result.entryFile) ? result.entryFile : path3.join(cwd, result.entryFile);
5649
+ if (existsSync3(previewPath)) {
5650
+ previewUrl = previewServer.serve(previewPath);
5651
+ if (previewUrl) return { previewUrl, previewPath };
5652
+ }
5393
5653
  }
5394
5654
  }
5395
- if (!previewUrl) {
5396
- const { changedFiles } = this.extractResult();
5397
- const htmlFile = changedFiles.find((f) => /\.html?$/i.test(f));
5398
- if (htmlFile) {
5399
- previewPath = path2.isAbsolute(htmlFile) ? htmlFile : path2.join(this.currentCwd ?? this.workspace, htmlFile);
5400
- previewUrl = previewServer.serve(previewPath);
5655
+ const previewMatch = this.stdoutBuffer.match(/PREVIEW:\s*(https?:\/\/[^\s*)\]>]+)/i);
5656
+ if (previewMatch) {
5657
+ return { previewUrl: previewMatch[1].replace(/[*)\]>]+$/, ""), previewPath: void 0 };
5658
+ }
5659
+ const fileMatch = this.stdoutBuffer.match(/(?:open\s+)?((?:\/[\w./_-]+|[\w./_-]+)\.html?)\b/i);
5660
+ if (fileMatch) {
5661
+ previewPath = path3.isAbsolute(fileMatch[1]) ? fileMatch[1] : path3.join(cwd, fileMatch[1]);
5662
+ previewUrl = previewServer.serve(previewPath);
5663
+ if (previewUrl) return { previewUrl, previewPath };
5664
+ }
5665
+ const htmlFile = result.changedFiles.find((f) => /\.html?$/i.test(f));
5666
+ if (htmlFile) {
5667
+ previewPath = path3.isAbsolute(htmlFile) ? htmlFile : path3.join(cwd, htmlFile);
5668
+ previewUrl = previewServer.serve(previewPath);
5669
+ if (previewUrl) return { previewUrl, previewPath };
5670
+ }
5671
+ const candidates = [
5672
+ "dist/index.html",
5673
+ "build/index.html",
5674
+ "out/index.html",
5675
+ "index.html",
5676
+ "public/index.html"
5677
+ ];
5678
+ for (const candidate of candidates) {
5679
+ const absPath = path3.join(cwd, candidate);
5680
+ if (existsSync3(absPath)) {
5681
+ previewUrl = previewServer.serve(absPath);
5682
+ if (previewUrl) return { previewUrl, previewPath: absPath };
5401
5683
  }
5402
5684
  }
5403
- return { previewUrl, previewPath };
5685
+ return { previewUrl: void 0, previewPath: void 0 };
5404
5686
  }
5405
5687
  /**
5406
5688
  * Parse stdoutBuffer for structured result (SUMMARY/STATUS/FILES_CHANGED).
@@ -5411,6 +5693,10 @@ var AgentSession = class {
5411
5693
  const fullOutput = raw.slice(0, 3e3);
5412
5694
  const summaryMatch = raw.match(/SUMMARY:\s*(.+)/i);
5413
5695
  const filesMatch = raw.match(/FILES_CHANGED:\s*(.+)/i);
5696
+ const entryFileMatch = raw.match(/ENTRY_FILE:\s*(.+)/i);
5697
+ const projectDirMatch = raw.match(/PROJECT_DIR:\s*(.+)/i);
5698
+ const previewCmdMatch = raw.match(/PREVIEW_CMD:\s*(.+)/i);
5699
+ const previewPortMatch = raw.match(/PREVIEW_PORT:\s*(\d+)/i);
5414
5700
  const changedFiles = [];
5415
5701
  if (filesMatch) {
5416
5702
  const fileList = filesMatch[1].trim();
@@ -5419,8 +5705,12 @@ var AgentSession = class {
5419
5705
  if (cleaned) changedFiles.push(cleaned);
5420
5706
  }
5421
5707
  }
5708
+ const entryFile = entryFileMatch?.[1]?.trim();
5709
+ const projectDir = projectDirMatch?.[1]?.trim();
5710
+ const previewCmd = previewCmdMatch?.[1]?.trim();
5711
+ const previewPort = previewPortMatch ? parseInt(previewPortMatch[1], 10) : void 0;
5422
5712
  if (summaryMatch) {
5423
- return { summary: summaryMatch[1].trim(), fullOutput, changedFiles };
5713
+ return { summary: summaryMatch[1].trim(), fullOutput, changedFiles, entryFile, projectDir, previewCmd, previewPort };
5424
5714
  }
5425
5715
  const lines = raw.split("\n").filter((l) => l.trim());
5426
5716
  const delegationRe = /^@(\w+):/;
@@ -5445,17 +5735,17 @@ var AgentSession = class {
5445
5735
  }
5446
5736
  }
5447
5737
  if (meaningful.length === 0 && delegationTargets.length > 0) {
5448
- return { summary: `Delegated tasks to ${delegationTargets.join(", ")}`, fullOutput, changedFiles };
5738
+ return { summary: `Delegated tasks to ${delegationTargets.join(", ")}`, fullOutput, changedFiles, entryFile, projectDir };
5449
5739
  }
5450
5740
  const lastChunk = meaningful.slice(-5).join("\n").trim();
5451
5741
  const summary = lastChunk.slice(0, 500) || "Task completed";
5452
- return { summary, fullOutput, changedFiles };
5742
+ return { summary, fullOutput, changedFiles, entryFile, projectDir };
5453
5743
  }
5454
5744
  dequeueNext() {
5455
5745
  if (this.taskQueue.length === 0) return;
5456
5746
  const next = this.taskQueue.shift();
5457
5747
  setTimeout(() => {
5458
- this.runTask(next.taskId, next.prompt, next.repoPath, next.teamContext);
5748
+ this.runTask(next.taskId, next.prompt, next.repoPath, next.teamContext, false, next.phaseOverride);
5459
5749
  }, 100);
5460
5750
  }
5461
5751
  cancelled = false;
@@ -5511,16 +5801,33 @@ var AgentSession = class {
5511
5801
  this.idleTimer = null;
5512
5802
  }
5513
5803
  if (this.process?.pid) {
5804
+ const pgid = this.process.pid;
5514
5805
  try {
5515
- process.kill(-this.process.pid, "SIGTERM");
5806
+ process.kill(-pgid, "SIGKILL");
5516
5807
  } catch {
5517
- this.process.kill("SIGTERM");
5808
+ try {
5809
+ this.process.kill("SIGKILL");
5810
+ } catch {
5811
+ }
5518
5812
  }
5519
5813
  this.process = null;
5520
5814
  }
5521
5815
  this.pendingApprovals.clear();
5522
5816
  saveSessionId(this.agentId, null);
5523
5817
  }
5818
+ /** Reset conversation history so the next task starts fresh (used by End Project). */
5819
+ clearHistory() {
5820
+ this.hasHistory = false;
5821
+ this.sessionId = null;
5822
+ this.originalTask = null;
5823
+ this.currentPhase = null;
5824
+ this._hasExecuted = false;
5825
+ this._lastResult = null;
5826
+ this._lastResultText = null;
5827
+ this._lastFullOutput = null;
5828
+ this.setStatus("idle");
5829
+ saveSessionId(this.agentId, null);
5830
+ }
5524
5831
  resolveApproval(approvalId, decision) {
5525
5832
  if (approvalId === "__all__") {
5526
5833
  for (const [, pending2] of this.pendingApprovals) {
@@ -5580,7 +5887,8 @@ var AgentManager = class {
5580
5887
  const lines = [];
5581
5888
  for (const session of this.agents.values()) {
5582
5889
  const lead = this.isTeamLead(session.agentId) ? " (Team Lead)" : "";
5583
- const result = session.lastResult ? ` \u2014 ${session.lastResult}` : "";
5890
+ const raw = session.lastResult ?? "";
5891
+ const result = raw ? ` \u2014 ${raw.length > 100 ? raw.slice(0, 100) + "\u2026" : raw}` : "";
5584
5892
  lines.push(`- ${session.name} (${session.role}) [${session.status}]${lead}${result}`);
5585
5893
  }
5586
5894
  return lines.join("\n");
@@ -5626,10 +5934,12 @@ var AgentManager = class {
5626
5934
 
5627
5935
  // ../../packages/orchestrator/src/delegation.ts
5628
5936
  import { nanoid as nanoid3 } from "nanoid";
5937
+ import path4 from "path";
5629
5938
  var MAX_DELEGATION_DEPTH = 5;
5630
5939
  var MAX_TOTAL_DELEGATIONS = 20;
5631
- var DELEGATION_BUDGET_ROUNDS = 5;
5940
+ var DELEGATION_BUDGET_ROUNDS = 7;
5632
5941
  var HARD_CEILING_ROUNDS = 10;
5942
+ var MAX_REVIEW_ROUNDS = 3;
5633
5943
  var RESULT_BATCH_WINDOW_MS = 2e4;
5634
5944
  var DelegationRouter = class {
5635
5945
  /** taskId → fromAgentId */
@@ -5642,6 +5952,8 @@ var DelegationRouter = class {
5642
5952
  totalDelegations = 0;
5643
5953
  /** How many times the leader has been invoked to process results */
5644
5954
  leaderRounds = 0;
5955
+ /** How many times a Code Reviewer result has been forwarded to the leader */
5956
+ reviewCount = 0;
5645
5957
  /** When true, all new delegations and result forwarding are blocked */
5646
5958
  stopped = false;
5647
5959
  /** TaskIds created by flushResults — only these can produce a final result */
@@ -5650,6 +5962,8 @@ var DelegationRouter = class {
5650
5962
  delegationsAtResultStart = /* @__PURE__ */ new Map();
5651
5963
  /** Batch result forwarding: originAgentId → pending results + timer */
5652
5964
  pendingResults = /* @__PURE__ */ new Map();
5965
+ /** Team-wide project directory — all delegations use this as repoPath when set */
5966
+ teamProjectDir = null;
5653
5967
  agentManager;
5654
5968
  promptEngine;
5655
5969
  emitEvent;
@@ -5683,7 +5997,7 @@ var DelegationRouter = class {
5683
5997
  * if the current task is not a "resultTask" (safety net for convergence).
5684
5998
  */
5685
5999
  isBudgetExhausted() {
5686
- return this.leaderRounds >= DELEGATION_BUDGET_ROUNDS;
6000
+ return this.leaderRounds >= DELEGATION_BUDGET_ROUNDS || this.reviewCount >= MAX_REVIEW_ROUNDS;
5687
6001
  }
5688
6002
  /**
5689
6003
  * True if the given resultTask completed WITHOUT creating any new delegations.
@@ -5724,6 +6038,16 @@ var DelegationRouter = class {
5724
6038
  }
5725
6039
  this.pendingResults.clear();
5726
6040
  }
6041
+ /**
6042
+ * Set a team-wide project directory. All delegations will use this as repoPath.
6043
+ */
6044
+ setTeamProjectDir(dir) {
6045
+ this.teamProjectDir = dir;
6046
+ if (dir) console.log(`[Delegation] Team project dir set: ${dir}`);
6047
+ }
6048
+ getTeamProjectDir() {
6049
+ return this.teamProjectDir;
6050
+ }
5727
6051
  /**
5728
6052
  * Reset all delegation state (on new team task).
5729
6053
  */
@@ -5735,7 +6059,9 @@ var DelegationRouter = class {
5735
6059
  this.delegationsAtResultStart.clear();
5736
6060
  this.totalDelegations = 0;
5737
6061
  this.leaderRounds = 0;
6062
+ this.reviewCount = 0;
5738
6063
  this.stopped = false;
6064
+ this.teamProjectDir = null;
5739
6065
  for (const pending of this.pendingResults.values()) {
5740
6066
  clearTimeout(pending.timer);
5741
6067
  }
@@ -5744,8 +6070,13 @@ var DelegationRouter = class {
5744
6070
  wireDelegation(session) {
5745
6071
  session.onDelegation = (fromAgentId, targetName, prompt) => {
5746
6072
  if (this.stopped) return;
5747
- if (this.leaderRounds >= DELEGATION_BUDGET_ROUNDS) {
5748
- console.log(`[Delegation] BLOCKED: delegation budget exhausted (round ${this.leaderRounds}/${DELEGATION_BUDGET_ROUNDS})`);
6073
+ const phaseCheckSession = this.agentManager.get(fromAgentId);
6074
+ if (phaseCheckSession?.currentPhase && phaseCheckSession.currentPhase !== "execute") {
6075
+ console.log(`[Delegation] BLOCKED: agent ${fromAgentId} is in phase "${phaseCheckSession.currentPhase}", not "execute"`);
6076
+ return;
6077
+ }
6078
+ if (this.isBudgetExhausted()) {
6079
+ console.log(`[Delegation] BLOCKED: budget exhausted (leaderRounds=${this.leaderRounds}/${DELEGATION_BUDGET_ROUNDS}, reviewCount=${this.reviewCount}/${MAX_REVIEW_ROUNDS})`);
5749
6080
  return;
5750
6081
  }
5751
6082
  const target = this.agentManager.findByName(targetName);
@@ -5785,14 +6116,27 @@ var DelegationRouter = class {
5785
6116
  const fromSession = this.agentManager.get(fromAgentId);
5786
6117
  const fromName = fromSession?.name ?? fromAgentId;
5787
6118
  const fromRole = fromSession?.role ?? "Team Lead";
5788
- const fullPrompt = this.promptEngine.render("delegation-prefix", { fromName, fromRole, prompt });
5789
- console.log(`[Delegation] ${fromAgentId} -> ${target.agentId} (${targetName}) depth=${newDepth} total=${this.totalDelegations}: ${prompt.slice(0, 80)}`);
6119
+ let repoPath = this.teamProjectDir ?? void 0;
6120
+ let cleanPrompt = prompt;
6121
+ const dirMatch = prompt.match(/^\s*\[([^\]]+)\]\s*/);
6122
+ if (dirMatch) {
6123
+ cleanPrompt = prompt.slice(dirMatch[0].length);
6124
+ if (!repoPath) {
6125
+ const dirPart = dirMatch[1].replace(/\/$/, "");
6126
+ const leaderSession = this.agentManager.get(fromAgentId);
6127
+ if (leaderSession) {
6128
+ repoPath = path4.resolve(leaderSession.workspaceDir, dirPart);
6129
+ }
6130
+ }
6131
+ }
6132
+ const fullPrompt = this.promptEngine.render("delegation-prefix", { fromName, fromRole, prompt: cleanPrompt });
6133
+ console.log(`[Delegation] ${fromAgentId} -> ${target.agentId} (${targetName}) depth=${newDepth} total=${this.totalDelegations} repoPath=${repoPath ?? "default"}: ${cleanPrompt.slice(0, 80)}`);
5790
6134
  this.emitEvent({
5791
6135
  type: "task:delegated",
5792
6136
  fromAgentId,
5793
6137
  toAgentId: target.agentId,
5794
6138
  taskId,
5795
- prompt
6139
+ prompt: cleanPrompt
5796
6140
  });
5797
6141
  this.emitEvent({
5798
6142
  type: "team:chat",
@@ -5804,7 +6148,7 @@ var DelegationRouter = class {
5804
6148
  timestamp: Date.now()
5805
6149
  });
5806
6150
  this.assignedTask.set(target.agentId, taskId);
5807
- target.runTask(taskId, fullPrompt);
6151
+ target.runTask(taskId, fullPrompt, repoPath);
5808
6152
  };
5809
6153
  }
5810
6154
  wireResultForwarding(session) {
@@ -5879,6 +6223,13 @@ var DelegationRouter = class {
5879
6223
  const originSession = this.agentManager.get(originAgentId);
5880
6224
  if (!originSession) return;
5881
6225
  this.leaderRounds++;
6226
+ for (const r of pending.results) {
6227
+ const agent = this.agentManager.findByName(r.fromName);
6228
+ if (agent && agent.role.toLowerCase().includes("review")) {
6229
+ this.reviewCount++;
6230
+ console.log(`[ResultBatch] Reviewer result detected (reviewCount=${this.reviewCount})`);
6231
+ }
6232
+ }
5882
6233
  if (this.leaderRounds > HARD_CEILING_ROUNDS) {
5883
6234
  console.log(`[ResultBatch] Hard ceiling reached (${HARD_CEILING_ROUNDS} rounds). Force-completing.`);
5884
6235
  const resultLines2 = pending.results.map(
@@ -5908,12 +6259,13 @@ ${resultLines2}`,
5908
6259
  }
5909
6260
  let roundInfo;
5910
6261
  const budgetExhausted = this.leaderRounds >= DELEGATION_BUDGET_ROUNDS;
5911
- if (budgetExhausted) {
5912
- roundInfo = `DELEGATION BUDGET REACHED (round ${this.leaderRounds}). No more delegations will be accepted. You MUST summarize the current results and report to the user NOW. Accept the work as-is \u2014 the user can request improvements later.`;
5913
- } else if (this.leaderRounds >= DELEGATION_BUDGET_ROUNDS - 1) {
5914
- roundInfo = `Round ${this.leaderRounds}/${DELEGATION_BUDGET_ROUNDS} \u2014 LAST delegation round. Only delegate if something is critically broken. Prefer to accept and summarize.`;
6262
+ const reviewExhausted = this.reviewCount >= MAX_REVIEW_ROUNDS;
6263
+ if (budgetExhausted || reviewExhausted) {
6264
+ roundInfo = reviewExhausted ? `REVIEW LIMIT REACHED (${this.reviewCount}/${MAX_REVIEW_ROUNDS} reviews). No more fix iterations. Output your FINAL SUMMARY now \u2014 accept the work as-is.` : `BUDGET REACHED (round ${this.leaderRounds}/${DELEGATION_BUDGET_ROUNDS}). No more delegations allowed. Output your FINAL SUMMARY now.`;
6265
+ } else if (this.reviewCount > 0) {
6266
+ roundInfo = `Round ${this.leaderRounds}/${DELEGATION_BUDGET_ROUNDS} | Review ${this.reviewCount}/${MAX_REVIEW_ROUNDS} (fix iteration ${this.reviewCount})`;
5915
6267
  } else {
5916
- roundInfo = `Round ${this.leaderRounds}/${DELEGATION_BUDGET_ROUNDS}`;
6268
+ roundInfo = `Round ${this.leaderRounds}/${DELEGATION_BUDGET_ROUNDS} | No reviews yet`;
5917
6269
  }
5918
6270
  const resultLines = pending.results.map(
5919
6271
  (r) => `- ${r.fromName} (${r.statusWord}): ${r.summary}`
@@ -5935,8 +6287,8 @@ ${resultLines2}`,
5935
6287
  };
5936
6288
 
5937
6289
  // ../../packages/orchestrator/src/prompt-templates.ts
5938
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3 } from "fs";
5939
- import path3 from "path";
6290
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync4 } from "fs";
6291
+ import path5 from "path";
5940
6292
  var PROMPT_DEFAULTS = {
5941
6293
  "leader-initial": `You are {{name}}, the Team Lead. {{personality}}
5942
6294
  You CANNOT write code, run commands, or use any tools. You can ONLY delegate.
@@ -5945,22 +6297,34 @@ Team:
5945
6297
  {{teamRoster}}
5946
6298
 
5947
6299
  Delegate using this exact format (one per line):
5948
- @AgentName: [project-dir] task description
6300
+ @AgentName: task description
6301
+
6302
+ The system has already created a dedicated project directory for this team. All agents will automatically work there \u2014 do NOT specify directory paths in delegations.
6303
+
6304
+ ===== DELEGATION RULES =====
6305
+
6306
+ CRITICAL \u2014 How to assign work to developers:
6307
+ - Give each developer ONE complete, end-to-end task that produces a RUNNABLE deliverable.
6308
+ - The developer is responsible for EVERYTHING: project setup, dependencies, all source files, build configuration, and verification.
6309
+ - NEVER split a project into module-level sub-tasks (e.g. "create AudioManager.ts", "create GameScene.ts"). That produces disconnected files with no project skeleton.
6310
+ - CORRECT example: "@Leo: Build a complete arcade game with PixiJS. Set up the project (package.json, entry HTML, config), implement gameplay (player movement, enemies, scoring, game states), add audio (SFX + BGM with mute toggle), and build a working deliverable. Output ENTRY_FILE when done."
6311
+ - WRONG example: "@Leo: Create src/audio/AudioManager.ts" then "@Leo: Create src/game/GameScene.ts" \u2014 this produces isolated modules that can't run.
6312
+ - If you have multiple developers, split by FEATURE AREA (each producing a runnable piece), not by FILE.
5949
6313
 
5950
- IMPORTANT: Every delegation MUST start with [project-dir] \u2014 the subdirectory path relative to the workspace root where this agent should focus. Example: @Alex: [game/] Fix the CSS overlay in game/styles.css. ALL team members MUST work in the same project directory. Identify the target project first, then use it consistently.
6314
+ ===== EXECUTION PHASES =====
5951
6315
 
5952
- Execution phases:
5953
- 1. BUILD: Assign coding tasks to developers now. Include file paths and specific instructions.
5954
- 2. VALIDATE (one round, parallel): When dev results come back, assign Code Reviewer AND QA Tester at the same time in a single response. Both run in parallel \u2014 do NOT wait for one before assigning the other.
5955
- 3. REPORT: After both review and QA report back, summarize and finish. Done.
6316
+ 1. BUILD (this round): Assign developers now. Each dev must deliver a working, verifiable result.
6317
+ 2. REVIEW: When dev results come back, assign Code Reviewer to check the code.
6318
+ 3. FIX (if needed): If Reviewer reports VERDICT=FAIL, collect ISSUES and delegate a fix to the developer. Remind dev to rebuild/re-verify. After fix, assign Reviewer again. Up to 3 review cycles.
6319
+ 4. REPORT: When Reviewer reports VERDICT=PASS (or after 3 cycles), output FINAL SUMMARY with preview info. Copy the developer's preview fields (ENTRY_FILE, PREVIEW_CMD, PREVIEW_PORT) exactly as reported \u2014 only include fields the dev actually provided.
5956
6320
 
5957
6321
  Rules:
5958
6322
  - Never write code yourself. Only delegate.
5959
- - Each delegation must be specific and bounded: include exact file paths, function names, and expected output. Vague delegations ("improve the UI") cause scope creep.
5960
- - Phase 1 (this round): Assign developers ONLY. Do NOT assign QA or code review yet \u2014 there is no code to test.
5961
- - Phase 2 (one round only): Assign Code Reviewer AND QA Tester simultaneously in one response. This saves a full round.
5962
- - Skip QA entirely for trivial changes (config tweaks, typo fixes, renaming, comment-only changes).
5963
- - Keep the total number of rounds to 2-3. Ship working code now \u2014 the user can request improvements later.
6323
+ - Phase 1 (this round): Assign developers ONLY. Do NOT assign Code Reviewer yet \u2014 there is no code to review.
6324
+ - Skip review for trivial changes (config, typo, rename).
6325
+
6326
+ Approved plan:
6327
+ {{originalTask}}
5964
6328
 
5965
6329
  Task: {{prompt}}`,
5966
6330
  "leader-continue": `You are {{name}}, the Team Lead. {{personality}}
@@ -5974,7 +6338,7 @@ Team status:
5974
6338
  Delegate using: @AgentName: task description
5975
6339
 
5976
6340
  {{prompt}}`,
5977
- "leader-result": `You are the Team Lead. You CANNOT write or fix code. You can ONLY delegate using @Name: [project-dir] <task>.
6341
+ "leader-result": `You are the Team Lead. You CANNOT write or fix code. You can ONLY delegate using @Name: <task>.
5978
6342
 
5979
6343
  Original user task: {{originalTask}}
5980
6344
 
@@ -5986,21 +6350,47 @@ Team status:
5986
6350
  New result from {{fromName}} ({{resultStatus}}):
5987
6351
  {{resultSummary}}
5988
6352
 
5989
- Decision priority (choose the FIRST that applies):
5990
- 1. ALL SUCCEEDED \u2192 Summarize the outcome for the user. You are DONE.
5991
- 2. Dev succeeded + this is a substantial change (new feature, significant logic change) + neither QA nor Code Reviewer has run yet \u2192 Assign BOTH @CodeReviewer AND @QATester simultaneously in ONE response (parallel). Skip this step for trivial changes.
5992
- 3. Dev succeeded + trivial change (config, rename, typo, style-only) \u2192 Summarize and you are DONE. No QA needed.
5993
- 4. QA or Code Review results received \u2192 Accept ALL findings as informational. Summarize and you are DONE. NEVER delegate fixes based on QA or review results.
5994
- 5. FAILED + critically broken (won't run, crash on start) \u2192 delegate ONE targeted fix to the original developer.
5995
- 6. FAILED + permanent blocker (auth error, service down, missing dependency) \u2192 report the blocker to the user.
5996
- 7. Same error repeated \u2192 STOP and report to the user.
6353
+ ===== DECISION FLOW =====
6354
+
6355
+ Check WHO sent this result, then follow the matching branch:
6356
+
6357
+ \u2500\u2500 RESULT FROM A DEVELOPER \u2500\u2500
6358
+ If STATUS=done:
6359
+ \u2192 Assign Code Reviewer to check the code. In your delegation, include:
6360
+ 1. Dev's ENTRY_FILE/PREVIEW_CMD so reviewer knows what was built.
6361
+ 2. The KEY FEATURES from the approved plan (3-5 bullet points) so reviewer can verify feature completeness.
6362
+ \u2192 Exception: skip review for trivial changes (config, typo, rename) \u2014 go straight to FINAL SUMMARY.
6363
+ If STATUS=failed:
6364
+ \u2192 Delegate ONE targeted fix to the same developer. Be specific about what failed.
6365
+
6366
+ \u2500\u2500 RESULT FROM CODE REVIEWER \u2500\u2500
6367
+ Reviewer output format: VERDICT (PASS/FAIL), ISSUES (numbered list), SUGGESTIONS (optional).
6368
+ If VERDICT=PASS:
6369
+ \u2192 Output FINAL SUMMARY. Copy ENTRY_FILE/PREVIEW fields from the developer's last report. You are DONE.
6370
+ If VERDICT=FAIL:
6371
+ \u2192 Collect ALL issues into ONE fix delegation to the original developer.
6372
+ \u2192 Quote each issue verbatim. Remind dev: after fixing, rebuild and verify the deliverable works.
6373
+ \u2192 After dev returns with the fix, assign Code Reviewer again to re-check.
6374
+
6375
+ \u2500\u2500 SPECIAL CASES \u2500\u2500
6376
+ \u2022 If roundInfo says "REVIEW LIMIT REACHED" or "BUDGET REACHED" \u2192 Output FINAL SUMMARY immediately. Accept the work as-is.
6377
+ \u2022 Permanent blocker (auth error, missing API key, service down) \u2192 report to the user, do not retry.
6378
+ \u2022 Same error repeated twice \u2192 STOP and report to the user.
5997
6379
 
5998
- CRITICAL RULES:
5999
- - QA/testing findings are ALWAYS informational. NEVER delegate fixes based on QA results. Note them and finish.
6000
- - Code review comments are ALWAYS informational. Only delegate a fix if the code literally doesn't compile or run.
6001
- - When assigning parallel QA + Review (rule 2): write BOTH @Name: lines in one response. Do not wait for one to finish.
6002
- - Maximum ONE fix round after a failure. After that, accept and summarize.
6003
- - Prefer DONE over more delegation. The user can request improvements later.`,
6380
+ ===== FINAL SUMMARY FORMAT =====
6381
+ (Copy preview fields EXACTLY from the developer's LAST successful report. Only include fields the dev actually provided \u2014 do NOT invent values.)
6382
+
6383
+ ENTRY_FILE: <from dev \u2014 e.g. index.html, dist/index.html. OMIT if dev didn't provide one>
6384
+ PREVIEW_CMD: <from dev \u2014 e.g. "python app.py", "node server.js". OMIT if dev didn't provide one>
6385
+ PREVIEW_PORT: <from dev \u2014 e.g. 5000, 3000. OMIT if dev didn't provide one>
6386
+ SUMMARY: <2-3 sentence description of what was built>
6387
+
6388
+ RULES:
6389
+ - VERDICT=PASS means done, even if SUGGESTIONS exist. Suggestions are non-blocking.
6390
+ - VERDICT=FAIL means real bugs \u2014 always delegate a fix before finalizing.
6391
+ - In every fix delegation, remind dev to rebuild and re-test before reporting.
6392
+ - You MUST include ENTRY_FILE or PREVIEW_CMD in your FINAL SUMMARY \u2014 the user needs this to preview.
6393
+ - Do NOT include PROJECT_DIR \u2014 the system manages project directories automatically.`,
6004
6394
  "worker-initial": `Your name is {{name}}, your role is {{role}}. {{personality}}
6005
6395
 
6006
6396
  CONVERGENCE RULES (follow strictly):
@@ -6008,24 +6398,197 @@ CONVERGENCE RULES (follow strictly):
6008
6398
  - Only touch files directly required by this task. Do NOT refactor, clean up, or "improve" unrelated code.
6009
6399
  - If you are uncertain between two approaches, choose the simpler one and note it in SUMMARY.
6010
6400
  - Do NOT add features, error handling, or improvements that were not explicitly asked for.
6011
- - Sub-delegate (@AgentName:) when you need a teammate's specialized expertise (e.g. frontend dev asking backend dev for an API contract, or asking QA to verify a specific behavior). Natural peer collaboration is encouraged \u2014 just keep it focused.
6012
6401
 
6013
6402
  HARD LIMITS:
6014
- - Do NOT start any dev server, HTTP server, or file server (no npx serve, no python -m http.server, no live-server, etc.). The system handles preview automatically.
6015
- - Do NOT install new dependencies unless the task explicitly requires it. Note any missing deps in SUMMARY instead.
6403
+ - Do NOT start any long-running dev server or file server. The system handles preview serving automatically.
6404
+ - You MAY install dependencies if the project needs them (npm install, pip install, etc.).
6405
+ {{soloHint}}
6406
+
6407
+ Start with one sentence describing your approach. Then do the work.
6408
+
6409
+ You are responsible for the COMPLETE deliverable \u2014 not just source files. This means:
6410
+ 1. Project setup: create all config files needed (package.json, tsconfig, build config, requirements.txt, etc.)
6411
+ 2. All source code
6412
+ 3. Build & verify: if the project has a build step, RUN IT and fix errors until it passes
6413
+ 4. Report how to run/preview the result (see deliverable types below)
6414
+
6415
+ VERIFICATION (MANDATORY before reporting STATUS: done):
6416
+ - If you created a package.json with a build script \u2192 run the build, fix errors until it succeeds, confirm the output file exists
6417
+ - If your deliverable is an HTML file \u2192 confirm it exists and references valid scripts/styles
6418
+ - If your deliverable is a script (Python, Node, etc.) \u2192 run a syntax check (python -c "import ast; ast.parse(open('app.py').read())" or node --check app.js)
6419
+ - If NONE of the above apply \u2192 at minimum list the files and confirm the entry point exists
6420
+ - IMPORTANT: Do NOT launch GUI/desktop applications (Pygame, Tkinter, Electron, etc.) to test them \u2014 they open windows that cannot be controlled. Use syntax checks and import checks only. The user will launch the app manually when ready.
6421
+ - FINAL CHECK: confirm you can fill in at least ENTRY_FILE or PREVIEW_CMD (see deliverable types). If you cannot, your deliverable is incomplete \u2014 fix it before reporting.
6422
+ - Do NOT report STATUS: done unless verification passes. Fix problems yourself first.
6423
+ - STATUS: failed is ONLY for truly unsolvable problems (missing API keys, no network, system-level issues).
6424
+
6425
+ ===== DELIVERABLE TYPES =====
6426
+ Your project falls into one of these categories. Report the matching fields:
6427
+
6428
+ A) STATIC WEB (HTML/CSS/JS \u2014 no server needed):
6429
+ ENTRY_FILE: index.html (the HTML file to open \u2014 e.g. index.html, dist/index.html, build/index.html)
6016
6430
 
6017
- Start with one sentence describing your approach (e.g. "I'll add the API route in routes.ts and wire it to the existing handler."). Then do the work.
6431
+ B) WEB SERVER (Flask, Express, Sinatra, Rails, Go net/http, etc. \u2014 serves on a port):
6432
+ PREVIEW_CMD: python app.py (the command to start the server)
6433
+ PREVIEW_PORT: 5000 (the port the server listens on \u2014 REQUIRED for web servers)
6434
+
6435
+ C) DESKTOP/CLI APP (Pygame, Tkinter, Electron, JavaFX, terminal tool, native GUI, etc.):
6436
+ PREVIEW_CMD: python game.py (the command to launch the app \u2014 NO PREVIEW_PORT needed)
6437
+
6438
+ OUTPUT:
6018
6439
 
6019
- When you finish, report your result in this exact format:
6020
6440
  STATUS: done | failed
6021
- FILES_CHANGED: (list of files you created or modified, one per line)
6022
- SUMMARY: (one sentence \u2014 what you did and why it satisfies the task)
6441
+ FILES_CHANGED: (list all files created or modified, one per line)
6442
+ ENTRY_FILE: (type A only \u2014 path to the HTML file)
6443
+ PREVIEW_CMD: (types B and C \u2014 command to start the app or server)
6444
+ PREVIEW_PORT: (type B only \u2014 the port the server listens on)
6445
+ SUMMARY: (one sentence: what you built + how to run/preview it)
6446
+
6447
+ You MUST provide at least ENTRY_FILE or PREVIEW_CMD.
6448
+
6449
+ {{prompt}}`,
6450
+ "worker-reviewer-initial": `Your name is {{name}}, your role is {{role}}. {{personality}}
6451
+
6452
+ CONVERGENCE RULES (follow strictly):
6453
+ - Do the MINIMUM needed to satisfy the task. Simple and working beats perfect and slow.
6454
+ - Only touch files directly required by this task. Do NOT refactor, clean up, or "improve" unrelated code.
6455
+ - If you are uncertain between two approaches, choose the simpler one and note it in SUMMARY.
6456
+ - Do NOT add features, error handling, or improvements that were not explicitly asked for.
6457
+
6458
+ HARD LIMITS:
6459
+ - Do NOT start any long-running dev server or file server. The system handles preview serving automatically.
6460
+ - Do NOT launch GUI/desktop applications (Pygame, Tkinter, Electron, etc.) to test them \u2014 they open windows that cannot be controlled. Use syntax checks, import checks, and code reading only.
6461
+
6462
+ Code Quality (must check):
6463
+ - Correctness, security vulnerabilities, crashes, broken logic.
6464
+ - Verify the deliverable can actually run: check that entry point exists, dependencies are declared, build output is present. For GUI/desktop apps, verify via code review and syntax checks \u2014 do NOT run them.
6465
+
6466
+ Feature Completeness (must check):
6467
+ - Compare the deliverable against the key features listed in your task assignment.
6468
+ - Flag CORE features that are completely missing or non-functional as ISSUES.
6469
+ - Do NOT fail for polish, extras, or stretch goals \u2014 this is a prototype. Focus on whether the main functionality works.
6470
+
6471
+ Do NOT nitpick style, naming, or formatting. This is a prototype, not production code.
6472
+
6473
+ VERDICT: PASS | FAIL
6474
+ - PASS = code runs without crashes AND core features are implemented (even if rough)
6475
+ - FAIL = crashes/bugs that prevent usage OR core features are missing/broken
6476
+ ISSUES: (numbered list \u2014 bugs, security problems, or missing core features)
6477
+ SUGGESTIONS: (optional \u2014 minor non-blocking observations, keep brief)
6478
+ SUMMARY: (one sentence overall assessment)
6023
6479
 
6024
6480
  {{prompt}}`,
6025
6481
  "worker-continue": `{{prompt}}`,
6026
6482
  "delegation-prefix": `[Assigned by {{fromName}} ({{fromRole}})]
6027
6483
  {{prompt}}`,
6028
- "delegation-hint": `To delegate a task to another agent, output on its own line: @AgentName: <task description>`
6484
+ "delegation-hint": `To delegate a task to another agent, output on its own line: @AgentName: <task description>`,
6485
+ "leader-create": `You are {{name}}, the team's Creative Director and Product Consultant. {{personality}}
6486
+ You are starting a new project conversation with the user. Your dual role:
6487
+ 1. CREATIVE DIRECTOR \u2014 design the product vision: theme, look & feel, user experience, what makes it unique
6488
+ 2. PRODUCT CONSULTANT \u2014 turn that vision into a clear, buildable plan
6489
+
6490
+ Rules:
6491
+ - Be conversational, warm, and concise.
6492
+ - Ask at most 1-2 clarifying questions, then produce a plan. Do NOT over-question.
6493
+ - If the user gives a clear idea (even brief), that is ENOUGH \u2014 use your CREATIVITY to fill in the vision (theme, style, characters, mood, unique twist) and produce the plan immediately.
6494
+ - The goal is a WORKING PROTOTYPE, not a production system.
6495
+ - When ready, produce a project plan wrapped in [PLAN]...[/PLAN] tags.
6496
+
6497
+ ===== PLAN FORMAT (strict \u2014 follow this structure) =====
6498
+
6499
+ [PLAN]
6500
+ CONCEPT: Short Name \u2014 one sentence describing what this product is and who it's for (e.g. "Shadow Dash \u2014 a fast-paced rooftop runner for casual gamers")
6501
+
6502
+ CREATIVE VISION:
6503
+ - Theme & setting (e.g. "pixel cityscape at night", "cozy forest caf\xE9")
6504
+ - Visual style (e.g. "retro pixel art", "flat minimalist", "hand-drawn sketch")
6505
+ - Core experience \u2014 what does the user SEE and FEEL when using it?
6506
+
6507
+ FEATURES:
6508
+ - (3-5 bullet points describing WHAT the product does from the user's perspective)
6509
+ - (focus on interactions, content, and behavior \u2014 NOT technical implementation)
6510
+
6511
+ TECH: (one line \u2014 e.g. "Vanilla JS + Canvas" or "React + Tailwind")
6512
+
6513
+ ASSIGNMENTS:
6514
+ - @DevName: (one-sentence summary of what they build)
6515
+ [/PLAN]
6516
+
6517
+ ===== ANTI-PATTERNS (never do these) =====
6518
+ - Do NOT write technical implementation steps (e.g. "implement game loop with requestAnimationFrame") \u2014 that is the developer's job.
6519
+ - Do NOT list generic engineering tasks (e.g. "add collision detection", "implement scoring system") \u2014 describe WHAT the product does, not HOW to code it.
6520
+ - Do NOT produce a checklist of modules or files. The plan is a PRODUCT DESCRIPTION, not a technical spec.
6521
+ - Do NOT include milestones, risk analysis, acceptance criteria, or deployment plans.
6522
+ - Do NOT delegate. Do NOT write code. Do NOT use @AgentName: syntax outside [PLAN] tags.
6523
+
6524
+ If the user hasn't described their project yet, greet them and ask what they'd like to build.
6525
+
6526
+ Team:
6527
+ {{teamRoster}}
6528
+
6529
+ {{prompt}}`,
6530
+ "leader-create-continue": `You are {{name}}, the team's Creative Director and Product Consultant. {{personality}}
6531
+ Do NOT greet or re-introduce yourself \u2014 the conversation is already underway.
6532
+
6533
+ The user replied: {{prompt}}
6534
+
6535
+ IMPORTANT: If the user is pushing you to move forward (e.g. "just do it", "make a plan", "you decide", "any is fine", "up to you"), STOP asking questions and use your CREATIVITY to fill in the vision \u2014 pick a theme, style, unique twist \u2014 and immediately produce a project plan in [PLAN]...[/PLAN] tags.
6536
+
6537
+ Remember: You are the Creative Director. The plan must describe the PRODUCT VISION (concept, creative vision, features from user's perspective), NOT technical implementation steps. Keep it short, actionable, no milestones or risk analysis. Otherwise, ask at most ONE more question, then produce the plan. Do NOT delegate or write code.`,
6538
+ "leader-design": `You are {{name}}, the team's Creative Director, refining the project vision with the user. {{personality}}
6539
+ The user has given feedback on your plan. Your job is to REVISE the existing plan, not start over.
6540
+
6541
+ ===== CRITICAL: INCREMENTAL UPDATE =====
6542
+ - User feedback is usually a PARTIAL adjustment (e.g. "use PixiJS", "make it darker", "add multiplayer").
6543
+ - Apply the feedback to the EXISTING plan. Keep everything the user did NOT mention.
6544
+ - NEVER discard the original concept, story, characters, or gameplay just because the user asked for a tech or style change.
6545
+ - If the user says "use X engine" or "change to Y framework" \u2192 update ONLY the TECH line and any affected details. The product vision stays.
6546
+ - Think of it as editing a document, not writing a new one.
6547
+
6548
+ Rules:
6549
+ - Address the user's feedback directly and show what changed.
6550
+ - Always output the updated plan in [PLAN]...[/PLAN] tags using the standard format: CONCEPT, CREATIVE VISION, FEATURES, TECH, ASSIGNMENTS.
6551
+ - The plan describes the PRODUCT VISION \u2014 what users see, feel, and experience. NOT technical implementation steps.
6552
+ - Keep it prototype-focused. No milestones, risk analysis, or deployment plans.
6553
+ - Do NOT delegate. Do NOT write code. Do NOT use @AgentName: syntax outside [PLAN] tags.
6554
+
6555
+ Team:
6556
+ {{teamRoster}}
6557
+
6558
+ Previous plan context: {{originalTask}}
6559
+
6560
+ User feedback: {{prompt}}`,
6561
+ "leader-design-continue": `You are {{name}}, the team's Creative Director, refining the project vision. {{personality}}
6562
+
6563
+ Your current plan:
6564
+ {{originalTask}}
6565
+
6566
+ The user replied: {{prompt}}
6567
+
6568
+ IMPORTANT: This is an INCREMENTAL update. Apply the user's feedback to the plan above \u2014 do NOT discard the original concept. If the user only mentions one aspect (tech, style, feature), change ONLY that part and keep everything else intact.
6569
+
6570
+ Output the revised plan in the standard format: CONCEPT, CREATIVE VISION, FEATURES, TECH, ASSIGNMENTS. Describe the product vision, NOT technical steps. Always output in [PLAN]...[/PLAN] tags. Do NOT delegate or write code.`,
6571
+ "leader-complete": `You are {{name}}, presenting completed work to the user. {{personality}}
6572
+ The team has finished executing the project. Summarize what was accomplished and ask if the user wants any changes.
6573
+
6574
+ Rules:
6575
+ - Be concise and highlight key outcomes.
6576
+ - If the user provides feedback, note it \u2014 the system will transition back to execute phase.
6577
+ - Do NOT delegate. Do NOT write code. Do NOT use @AgentName: syntax.
6578
+
6579
+ Team:
6580
+ {{teamRoster}}
6581
+
6582
+ Original task: {{originalTask}}
6583
+
6584
+ {{prompt}}`,
6585
+ "leader-complete-continue": `You are {{name}}, discussing the completed project with the user. {{personality}}
6586
+
6587
+ Original task: {{originalTask}}
6588
+
6589
+ The user replied: {{prompt}}
6590
+
6591
+ Address their feedback. Do NOT delegate or write code.`
6029
6592
  };
6030
6593
  var PromptEngine = class {
6031
6594
  templates = { ...PROMPT_DEFAULTS };
@@ -6035,21 +6598,24 @@ var PromptEngine = class {
6035
6598
  }
6036
6599
  /**
6037
6600
  * Initialize prompt templates on startup.
6038
- * Creates promptsDir with defaults if it doesn't exist,
6039
- * then loads all .md files (falling back to defaults for missing ones).
6601
+ * Always writes built-in defaults to disk so new/updated templates take effect.
6602
+ * Users can still customize edits are preserved until the next code update changes a template.
6040
6603
  */
6041
6604
  init() {
6042
6605
  if (!this.promptsDir) {
6043
6606
  console.log(`[Prompts] No promptsDir configured, using ${Object.keys(PROMPT_DEFAULTS).length} default templates`);
6044
6607
  return;
6045
6608
  }
6046
- if (!existsSync3(this.promptsDir)) {
6047
- mkdirSync3(this.promptsDir, { recursive: true });
6048
- for (const [name, content] of Object.entries(PROMPT_DEFAULTS)) {
6049
- writeFileSync3(path3.join(this.promptsDir, `${name}.md`), content, "utf-8");
6050
- }
6051
- console.log(`[Prompts] Created ${this.promptsDir} with ${Object.keys(PROMPT_DEFAULTS).length} default templates`);
6609
+ if (!existsSync4(this.promptsDir)) {
6610
+ mkdirSync4(this.promptsDir, { recursive: true });
6611
+ }
6612
+ let written = 0;
6613
+ for (const [name, content] of Object.entries(PROMPT_DEFAULTS)) {
6614
+ const filePath = path5.join(this.promptsDir, `${name}.md`);
6615
+ writeFileSync4(filePath, content, "utf-8");
6616
+ written++;
6052
6617
  }
6618
+ console.log(`[Prompts] Synced ${written} default templates to ${this.promptsDir}`);
6053
6619
  this.reload();
6054
6620
  }
6055
6621
  /**
@@ -6061,10 +6627,10 @@ var PromptEngine = class {
6061
6627
  let defaulted = 0;
6062
6628
  if (this.promptsDir) {
6063
6629
  for (const name of Object.keys(PROMPT_DEFAULTS)) {
6064
- const filePath = path3.join(this.promptsDir, `${name}.md`);
6065
- if (existsSync3(filePath)) {
6630
+ const filePath = path5.join(this.promptsDir, `${name}.md`);
6631
+ if (existsSync4(filePath)) {
6066
6632
  try {
6067
- merged[name] = readFileSync3(filePath, "utf-8");
6633
+ merged[name] = readFileSync4(filePath, "utf-8");
6068
6634
  loaded++;
6069
6635
  } catch {
6070
6636
  defaulted++;
@@ -6191,7 +6757,7 @@ IMPORTANT: If the same error keeps repeating, choose option 3. Do not waste reso
6191
6757
 
6192
6758
  // ../../packages/orchestrator/src/worktree.ts
6193
6759
  import { execSync as execSync3 } from "child_process";
6194
- import path4 from "path";
6760
+ import path6 from "path";
6195
6761
  var TIMEOUT = 5e3;
6196
6762
  function isGitRepo(cwd) {
6197
6763
  try {
@@ -6203,9 +6769,9 @@ function isGitRepo(cwd) {
6203
6769
  }
6204
6770
  function createWorktree(workspace, agentId, taskId, agentName) {
6205
6771
  if (!isGitRepo(workspace)) return null;
6206
- const worktreeDir = path4.join(workspace, ".worktrees");
6772
+ const worktreeDir = path6.join(workspace, ".worktrees");
6207
6773
  const worktreeName = `${agentId}-${taskId}`;
6208
- const worktreePath = path4.join(worktreeDir, worktreeName);
6774
+ const worktreePath = path6.join(worktreeDir, worktreeName);
6209
6775
  const branch = `agent/${agentName.toLowerCase().replace(/\s+/g, "-")}/${taskId}`;
6210
6776
  try {
6211
6777
  execSync3(`git worktree add "${worktreePath}" -b "${branch}"`, {
@@ -6251,7 +6817,7 @@ function mergeWorktree(workspace, worktreePath, branch) {
6251
6817
  }
6252
6818
  }
6253
6819
  function removeWorktree(worktreePath, branch, workspace) {
6254
- const cwd = workspace ?? path4.dirname(path4.dirname(worktreePath));
6820
+ const cwd = workspace ?? path6.dirname(path6.dirname(worktreePath));
6255
6821
  try {
6256
6822
  execSync3(`git worktree remove --force "${worktreePath}"`, { cwd, stdio: "pipe", timeout: TIMEOUT });
6257
6823
  } catch {
@@ -6274,8 +6840,12 @@ var Orchestrator = class extends EventEmitter {
6274
6840
  sandboxMode;
6275
6841
  worktreeEnabled;
6276
6842
  worktreeMerge;
6277
- /** Preview captured from the first dev worker that produces one — not from QA/reviewer */
6843
+ /** Preview info captured from the first dev worker that produces one — not from QA/reviewer */
6278
6844
  teamPreview = null;
6845
+ /** Accumulated changedFiles from all workers in the current team session */
6846
+ teamChangedFiles = /* @__PURE__ */ new Set();
6847
+ /** Guard against emitting isFinalResult more than once per execute cycle. */
6848
+ teamFinalized = false;
6279
6849
  constructor(opts) {
6280
6850
  super();
6281
6851
  this.workspace = opts.workspace;
@@ -6402,9 +6972,15 @@ var Orchestrator = class extends EventEmitter {
6402
6972
  return;
6403
6973
  }
6404
6974
  if (this.agentManager.isTeamLead(agentId) && !this.delegationRouter.isDelegated(taskId)) {
6405
- session.originalTask = prompt;
6975
+ if (!session.originalTask || !opts?.phaseOverride || opts.phaseOverride !== "execute" && opts.phaseOverride !== "design" && opts.phaseOverride !== "complete") {
6976
+ session.originalTask = prompt;
6977
+ }
6978
+ const savedProjectDir = this.delegationRouter.getTeamProjectDir();
6406
6979
  this.delegationRouter.clearAll();
6980
+ if (savedProjectDir) this.delegationRouter.setTeamProjectDir(savedProjectDir);
6407
6981
  this.teamPreview = null;
6982
+ this.teamChangedFiles.clear();
6983
+ this.teamFinalized = false;
6408
6984
  }
6409
6985
  this.retryTracker?.track(taskId, prompt);
6410
6986
  if (this.worktreeEnabled && !session.worktreePath) {
@@ -6424,14 +7000,7 @@ var Orchestrator = class extends EventEmitter {
6424
7000
  }
6425
7001
  const repoPath = session.worktreePath ?? opts?.repoPath;
6426
7002
  const teamContext = this.agentManager.isTeamLead(agentId) ? this.agentManager.getTeamRoster() : void 0;
6427
- session.runTask(
6428
- taskId,
6429
- prompt,
6430
- repoPath,
6431
- teamContext,
6432
- true
6433
- /* isUserInitiated */
6434
- );
7003
+ session.runTask(taskId, prompt, repoPath, teamContext, true, opts?.phaseOverride);
6435
7004
  }
6436
7005
  cancelTask(agentId) {
6437
7006
  const session = this.agentManager.get(agentId);
@@ -6491,7 +7060,7 @@ var Orchestrator = class extends EventEmitter {
6491
7060
  getAgent(agentId) {
6492
7061
  const s = this.agentManager.get(agentId);
6493
7062
  if (!s) return void 0;
6494
- return { agentId: s.agentId, name: s.name, role: s.role, status: s.status, palette: s.palette, backend: s.backend.id };
7063
+ return { agentId: s.agentId, name: s.name, role: s.role, status: s.status, palette: s.palette, backend: s.backend.id, pid: s.pid };
6495
7064
  }
6496
7065
  getAllAgents() {
6497
7066
  return this.agentManager.getAll().map((s) => ({
@@ -6501,6 +7070,7 @@ var Orchestrator = class extends EventEmitter {
6501
7070
  status: s.status,
6502
7071
  palette: s.palette,
6503
7072
  backend: s.backend.id,
7073
+ pid: s.pid,
6504
7074
  isTeamLead: this.agentManager.isTeamLead(s.agentId),
6505
7075
  teamId: s.teamId
6506
7076
  }));
@@ -6508,9 +7078,51 @@ var Orchestrator = class extends EventEmitter {
6508
7078
  getTeamRoster() {
6509
7079
  return this.agentManager.getTeamRoster();
6510
7080
  }
7081
+ /** Return PIDs of all managed (gateway-spawned) agent processes */
7082
+ getManagedPids() {
7083
+ const pids = [];
7084
+ for (const session of this.agentManager.getAll()) {
7085
+ const pid = session.pid;
7086
+ if (pid !== null) pids.push(pid);
7087
+ }
7088
+ return pids;
7089
+ }
6511
7090
  isTeamLead(agentId) {
6512
7091
  return this.agentManager.isTeamLead(agentId);
6513
7092
  }
7093
+ /** Get the leader's last full output (used to capture the approved plan). */
7094
+ getLeaderLastOutput(agentId) {
7095
+ const session = this.agentManager.get(agentId);
7096
+ return session?.lastFullOutput ?? null;
7097
+ }
7098
+ /** Set team-wide project directory — all delegations will use this as cwd. */
7099
+ setTeamProjectDir(dir) {
7100
+ this.delegationRouter.setTeamProjectDir(dir);
7101
+ }
7102
+ getTeamProjectDir() {
7103
+ return this.delegationRouter.getTeamProjectDir();
7104
+ }
7105
+ /** Set the original task context for the leader (e.g. the approved plan). */
7106
+ setOriginalTask(agentId, task) {
7107
+ const session = this.agentManager.get(agentId);
7108
+ if (session) session.originalTask = task;
7109
+ }
7110
+ /** Clear ALL team members' conversation history for a fresh project cycle. */
7111
+ clearLeaderHistory(agentId) {
7112
+ const session = this.agentManager.get(agentId);
7113
+ if (session) {
7114
+ session.clearHistory();
7115
+ for (const agent of this.agentManager.getAll()) {
7116
+ if (agent.agentId !== agentId) {
7117
+ agent.clearHistory();
7118
+ }
7119
+ }
7120
+ this.delegationRouter.clearAll();
7121
+ this.teamPreview = null;
7122
+ this.teamChangedFiles.clear();
7123
+ this.teamFinalized = false;
7124
+ }
7125
+ }
6514
7126
  // ---------------------------------------------------------------------------
6515
7127
  // Cleanup
6516
7128
  // ---------------------------------------------------------------------------
@@ -6586,28 +7198,84 @@ var Orchestrator = class extends EventEmitter {
6586
7198
  session.worktreeBranch = null;
6587
7199
  }
6588
7200
  this.retryTracker?.clear(event.taskId);
6589
- if (!this.agentManager.isTeamLead(agentId) && !this.teamPreview) {
7201
+ if (!this.agentManager.isTeamLead(agentId) && event.result?.changedFiles) {
7202
+ for (const f of event.result.changedFiles) {
7203
+ this.teamChangedFiles.add(f);
7204
+ }
7205
+ }
7206
+ if (!this.agentManager.isTeamLead(agentId)) {
6590
7207
  const role = session?.role?.toLowerCase() ?? "";
6591
- const isDevWorker = !role.includes("qa") && !role.includes("tester") && !role.includes("review");
6592
- if (isDevWorker && event.result?.previewUrl) {
7208
+ const isDevWorker = !role.includes("review");
7209
+ if (isDevWorker && event.result && (event.result.previewUrl || event.result.entryFile || event.result.previewCmd)) {
6593
7210
  this.teamPreview = {
6594
7211
  previewUrl: event.result.previewUrl,
6595
- previewPath: event.result.previewPath
7212
+ previewPath: event.result.previewPath,
7213
+ entryFile: event.result.entryFile,
7214
+ previewCmd: event.result.previewCmd,
7215
+ previewPort: event.result.previewPort
6596
7216
  };
6597
- console.log(`[Orchestrator] Preview captured from ${session?.name}: ${this.teamPreview.previewUrl}`);
7217
+ console.log(`[Orchestrator] Preview captured from ${session?.name}: url=${this.teamPreview.previewUrl}, entry=${this.teamPreview.entryFile}, cmd=${this.teamPreview.previewCmd}`);
6598
7218
  }
6599
7219
  }
6600
7220
  if (this.agentManager.isTeamLead(agentId)) {
6601
7221
  const isResultTask = this.delegationRouter.isResultTask(event.taskId);
6602
7222
  const leaderDidNotDelegateNewWork = isResultTask && this.delegationRouter.resultTaskDidNotDelegate(event.taskId);
6603
7223
  const budgetForced = this.delegationRouter.isBudgetExhausted() && !this.delegationRouter.hasPendingFrom(agentId);
6604
- const shouldFinalize = leaderDidNotDelegateNewWork || budgetForced;
7224
+ const hasWorkingWorkers = this.agentManager.getAll().some(
7225
+ (w) => w.agentId !== agentId && w.status === "working"
7226
+ );
7227
+ if (hasWorkingWorkers && !budgetForced) {
7228
+ console.log(`[Orchestrator] Deferring finalization \u2014 workers still running`);
7229
+ }
7230
+ const shouldFinalize = (leaderDidNotDelegateNewWork || budgetForced) && !this.teamFinalized && (!hasWorkingWorkers || budgetForced);
6605
7231
  if (shouldFinalize) {
7232
+ this.teamFinalized = true;
6606
7233
  event.isFinalResult = true;
6607
7234
  this.delegationRouter.clearAgent(agentId);
6608
- if (!event.result?.previewUrl && this.teamPreview && event.result) {
6609
- event.result.previewUrl = this.teamPreview.previewUrl;
6610
- event.result.previewPath = this.teamPreview.previewPath;
7235
+ if (event.result && this.teamChangedFiles.size > 0) {
7236
+ const merged = new Set(event.result.changedFiles ?? []);
7237
+ for (const f of this.teamChangedFiles) merged.add(f);
7238
+ event.result.changedFiles = Array.from(merged);
7239
+ }
7240
+ if (event.result) {
7241
+ const teamDir = this.delegationRouter.getTeamProjectDir();
7242
+ if (teamDir) {
7243
+ event.result.projectDir = teamDir;
7244
+ }
7245
+ }
7246
+ if (this.teamPreview && event.result) {
7247
+ if (this.teamPreview.previewUrl) {
7248
+ event.result.previewUrl = this.teamPreview.previewUrl;
7249
+ event.result.previewPath = this.teamPreview.previewPath;
7250
+ }
7251
+ if (this.teamPreview.entryFile) event.result.entryFile = this.teamPreview.entryFile;
7252
+ if (this.teamPreview.previewCmd) event.result.previewCmd = this.teamPreview.previewCmd;
7253
+ if (this.teamPreview.previewPort) event.result.previewPort = this.teamPreview.previewPort;
7254
+ }
7255
+ if (event.result?.entryFile) {
7256
+ const projectDir = this.delegationRouter.getTeamProjectDir() ?? this.workspace;
7257
+ const absEntry = path7.isAbsolute(event.result.entryFile) ? event.result.entryFile : path7.join(projectDir, event.result.entryFile);
7258
+ if (!existsSync5(absEntry)) {
7259
+ const allFiles = event.result.changedFiles ?? [];
7260
+ const ext = path7.extname(event.result.entryFile).toLowerCase();
7261
+ const candidate = allFiles.map((f) => path7.basename(f)).find((f) => path7.extname(f).toLowerCase() === ext);
7262
+ if (candidate) {
7263
+ console.log(`[Orchestrator] entryFile "${event.result.entryFile}" not found on disk, using "${candidate}" from changedFiles`);
7264
+ event.result.entryFile = candidate;
7265
+ } else {
7266
+ console.log(`[Orchestrator] entryFile "${event.result.entryFile}" not found on disk, clearing`);
7267
+ event.result.entryFile = void 0;
7268
+ }
7269
+ }
7270
+ }
7271
+ if (event.result?.entryFile && !event.result.previewCmd && !/\.html?$/i.test(event.result.entryFile)) {
7272
+ const ext = path7.extname(event.result.entryFile).toLowerCase();
7273
+ const runners = { ".py": "python3", ".js": "node", ".rb": "ruby", ".sh": "bash" };
7274
+ const runner = runners[ext];
7275
+ if (runner) {
7276
+ event.result.previewCmd = `${runner} ${event.result.entryFile}`;
7277
+ console.log(`[Orchestrator] Auto-constructed previewCmd: ${event.result.previewCmd}`);
7278
+ }
6611
7279
  }
6612
7280
  if (!event.result?.previewUrl && event.result) {
6613
7281
  for (const worker of this.agentManager.getAll()) {
@@ -6620,6 +7288,70 @@ var Orchestrator = class extends EventEmitter {
6620
7288
  }
6621
7289
  }
6622
7290
  }
7291
+ if (!event.result?.previewUrl && event.result?.previewCmd) {
7292
+ const projectDir = this.delegationRouter.getTeamProjectDir() ?? this.workspace;
7293
+ if (event.result.previewPort) {
7294
+ const url = previewServer.runCommand(event.result.previewCmd, projectDir, event.result.previewPort);
7295
+ if (url) {
7296
+ event.result.previewUrl = url;
7297
+ console.log(`[Orchestrator] Preview from leader PREVIEW_CMD (port ${event.result.previewPort}): ${url}`);
7298
+ }
7299
+ } else {
7300
+ console.log(`[Orchestrator] Desktop app ready (user can Launch): ${event.result.previewCmd}`);
7301
+ }
7302
+ }
7303
+ if (!event.result?.previewUrl && event.result?.entryFile) {
7304
+ const entryFile = event.result.entryFile;
7305
+ const projectDir = this.delegationRouter.getTeamProjectDir() ?? this.workspace;
7306
+ if (/\.html?$/i.test(entryFile)) {
7307
+ const absPath = path7.isAbsolute(entryFile) ? entryFile : path7.join(projectDir, entryFile);
7308
+ const url = previewServer.serve(absPath);
7309
+ if (url) {
7310
+ event.result.previewUrl = url;
7311
+ event.result.previewPath = absPath;
7312
+ console.log(`[Orchestrator] Preview from leader ENTRY_FILE: ${url}`);
7313
+ }
7314
+ }
7315
+ }
7316
+ if (!event.result?.previewUrl && event.result && this.teamChangedFiles.size > 0) {
7317
+ const projectDir = this.delegationRouter.getTeamProjectDir() ?? this.workspace;
7318
+ const htmlFile = Array.from(this.teamChangedFiles).find((f) => /\.html?$/i.test(f));
7319
+ if (htmlFile) {
7320
+ const absPath = path7.isAbsolute(htmlFile) ? htmlFile : path7.join(projectDir, htmlFile);
7321
+ const url = previewServer.serve(absPath);
7322
+ if (url) {
7323
+ event.result.previewUrl = url;
7324
+ event.result.previewPath = absPath;
7325
+ console.log(`[Orchestrator] Preview from teamChangedFiles: ${url}`);
7326
+ }
7327
+ }
7328
+ }
7329
+ if (!event.result?.previewUrl && event.result) {
7330
+ const projectDir = this.delegationRouter.getTeamProjectDir();
7331
+ if (projectDir) {
7332
+ const candidates = [
7333
+ "dist/index.html",
7334
+ "build/index.html",
7335
+ "out/index.html",
7336
+ // common build dirs
7337
+ "index.html",
7338
+ "public/index.html"
7339
+ // static projects
7340
+ ];
7341
+ for (const candidate of candidates) {
7342
+ const absPath = path7.join(projectDir, candidate);
7343
+ if (existsSync5(absPath)) {
7344
+ const url = previewServer.serve(absPath);
7345
+ if (url) {
7346
+ event.result.previewUrl = url;
7347
+ event.result.previewPath = absPath;
7348
+ console.log(`[Orchestrator] Preview from project scan: ${absPath}`);
7349
+ break;
7350
+ }
7351
+ }
7352
+ }
7353
+ }
7354
+ }
6623
7355
  const summary = event.result?.summary?.slice(0, 200) ?? "All tasks completed.";
6624
7356
  this.emitEvent({
6625
7357
  type: "team:chat",
@@ -6655,14 +7387,481 @@ function createOrchestrator(options) {
6655
7387
 
6656
7388
  // src/index.ts
6657
7389
  import { nanoid as nanoid5 } from "nanoid";
7390
+ import { execFile as execFile3 } from "child_process";
7391
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
7392
+ import path9 from "path";
7393
+ import os2 from "os";
7394
+
7395
+ // src/process-scanner.ts
6658
7396
  import { execFile } from "child_process";
6659
- import { existsSync as existsSync4 } from "fs";
6660
- import path5 from "path";
7397
+ var KNOWN_COMMANDS = ["claude", "codex", "gemini", "aider", "opencode"];
7398
+ var COMMAND_TO_BACKEND = {
7399
+ claude: "claude",
7400
+ codex: "codex",
7401
+ gemini: "gemini",
7402
+ aider: "aider",
7403
+ opencode: "opencode"
7404
+ };
7405
+ function parseEtime(etime) {
7406
+ const now = Date.now();
7407
+ const parts = etime.trim().replace(/-/g, ":").split(":");
7408
+ let seconds = 0;
7409
+ if (parts.length === 4) {
7410
+ seconds = parseInt(parts[0]) * 86400 + parseInt(parts[1]) * 3600 + parseInt(parts[2]) * 60 + parseInt(parts[3]);
7411
+ } else if (parts.length === 3) {
7412
+ seconds = parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(parts[2]);
7413
+ } else if (parts.length === 2) {
7414
+ seconds = parseInt(parts[0]) * 60 + parseInt(parts[1]);
7415
+ }
7416
+ return now - seconds * 1e3;
7417
+ }
7418
+ function exec(cmd, args) {
7419
+ return new Promise((resolve2) => {
7420
+ execFile(cmd, args, { timeout: 5e3, maxBuffer: 1024 * 512 }, (err, stdout) => {
7421
+ resolve2(err ? "" : stdout);
7422
+ });
7423
+ });
7424
+ }
7425
+ async function getCwds(pids) {
7426
+ const result = /* @__PURE__ */ new Map();
7427
+ if (pids.length === 0) return result;
7428
+ const output = await exec("lsof", ["-a", "-p", pids.join(","), "-d", "cwd", "-Fn"]);
7429
+ let currentPid = null;
7430
+ for (const line of output.split("\n")) {
7431
+ if (line.startsWith("p")) {
7432
+ currentPid = parseInt(line.slice(1));
7433
+ } else if (line.startsWith("n") && currentPid !== null) {
7434
+ result.set(currentPid, line.slice(1));
7435
+ }
7436
+ }
7437
+ return result;
7438
+ }
7439
+ var ProcessScanner = class _ProcessScanner {
7440
+ timer = null;
7441
+ previous = /* @__PURE__ */ new Map();
7442
+ getManagedPids;
7443
+ callbacks;
7444
+ /** PIDs to ignore temporarily (recently killed — may still appear in ps) */
7445
+ graceList = /* @__PURE__ */ new Map();
7446
+ // pid → expiry timestamp
7447
+ static GRACE_MS = 15e3;
7448
+ // 15 seconds grace period
7449
+ constructor(getManagedPids, callbacks) {
7450
+ this.getManagedPids = getManagedPids;
7451
+ this.callbacks = callbacks;
7452
+ }
7453
+ /** Mark a PID as recently killed — scanner will ignore it for a grace period */
7454
+ addGracePid(pid) {
7455
+ this.graceList.set(pid, Date.now() + _ProcessScanner.GRACE_MS);
7456
+ }
7457
+ start(intervalMs = 7e3) {
7458
+ this.scan();
7459
+ this.timer = setInterval(() => this.scan(), intervalMs);
7460
+ }
7461
+ stop() {
7462
+ if (this.timer) {
7463
+ clearInterval(this.timer);
7464
+ this.timer = null;
7465
+ }
7466
+ }
7467
+ async scan() {
7468
+ try {
7469
+ const psOutput = await exec("ps", ["-eo", "pid,ppid,pcpu,etime,args"]);
7470
+ const managed = new Set(this.getManagedPids());
7471
+ const now = Date.now();
7472
+ for (const [pid, expiry] of this.graceList) {
7473
+ if (now >= expiry) {
7474
+ this.graceList.delete(pid);
7475
+ } else {
7476
+ managed.add(pid);
7477
+ }
7478
+ }
7479
+ const lines = psOutput.split("\n").slice(1);
7480
+ const candidates = [];
7481
+ for (const line of lines) {
7482
+ const trimmed = line.trim();
7483
+ if (!trimmed) continue;
7484
+ const match = trimmed.match(/^\s*(\d+)\s+(\d+)\s+([\d.]+)\s+([\w:-]+)\s+(.+)$/);
7485
+ if (!match) continue;
7486
+ const pid = parseInt(match[1]);
7487
+ const ppid = parseInt(match[2]);
7488
+ const cpu = parseFloat(match[3]);
7489
+ const etime = match[4];
7490
+ const args = match[5];
7491
+ if (managed.has(pid)) continue;
7492
+ const cmdName = this.matchCommand(args);
7493
+ if (!cmdName) continue;
7494
+ candidates.push({ pid, ppid, cpu, etime, command: cmdName });
7495
+ }
7496
+ const candidatePids = new Set(candidates.map((c) => c.pid));
7497
+ const filtered = candidates.filter((c) => !candidatePids.has(c.ppid));
7498
+ const cwds = await getCwds(filtered.map((c) => c.pid));
7499
+ const current = /* @__PURE__ */ new Map();
7500
+ for (const c of filtered) {
7501
+ const backendId = COMMAND_TO_BACKEND[c.command] ?? c.command;
7502
+ const agentId = `ext-${backendId}-${c.pid}`;
7503
+ const status = c.cpu >= 5 ? "working" : "idle";
7504
+ current.set(agentId, {
7505
+ pid: c.pid,
7506
+ ppid: c.ppid,
7507
+ cpu: c.cpu,
7508
+ command: c.command,
7509
+ backendId,
7510
+ cwd: cwds.get(c.pid) ?? null,
7511
+ startedAt: parseEtime(c.etime),
7512
+ agentId,
7513
+ status
7514
+ });
7515
+ }
7516
+ const added = [];
7517
+ const removed = [];
7518
+ const changed = [];
7519
+ for (const [id, agent] of current) {
7520
+ const prev = this.previous.get(id);
7521
+ if (!prev) {
7522
+ added.push(agent);
7523
+ } else if (prev.status !== agent.status) {
7524
+ changed.push(agent);
7525
+ }
7526
+ }
7527
+ for (const id of this.previous.keys()) {
7528
+ if (!current.has(id)) {
7529
+ removed.push(id);
7530
+ }
7531
+ }
7532
+ this.previous = current;
7533
+ if (added.length > 0) this.callbacks.onAdded(added);
7534
+ if (removed.length > 0) this.callbacks.onRemoved(removed);
7535
+ if (changed.length > 0) this.callbacks.onChanged(changed);
7536
+ } catch (err) {
7537
+ console.error("[ProcessScanner] Scan error:", err);
7538
+ }
7539
+ }
7540
+ matchCommand(args) {
7541
+ for (const cmd of KNOWN_COMMANDS) {
7542
+ const re = new RegExp(`(?:^|/)${cmd}(?:\\s|$)`);
7543
+ if (re.test(args)) return cmd;
7544
+ }
7545
+ return null;
7546
+ }
7547
+ };
7548
+
7549
+ // src/external-output-reader.ts
7550
+ import { watch, readdirSync, statSync, existsSync as existsSync6, openSync, readSync, closeSync } from "fs";
7551
+ import { execFile as execFile2 } from "child_process";
7552
+ import path8 from "path";
6661
7553
  import os from "os";
7554
+ var SOURCE_EXTS = /* @__PURE__ */ new Set([
7555
+ ".ts",
7556
+ ".tsx",
7557
+ ".js",
7558
+ ".jsx",
7559
+ ".py",
7560
+ ".go",
7561
+ ".rs",
7562
+ ".java",
7563
+ ".c",
7564
+ ".cpp",
7565
+ ".h",
7566
+ ".css",
7567
+ ".scss",
7568
+ ".html",
7569
+ ".json",
7570
+ ".yaml",
7571
+ ".yml",
7572
+ ".toml",
7573
+ ".md",
7574
+ ".sql",
7575
+ ".sh",
7576
+ ".rb",
7577
+ ".swift",
7578
+ ".kt"
7579
+ ]);
7580
+ var ExternalOutputReader = class {
7581
+ readers = /* @__PURE__ */ new Map();
7582
+ onStatus = null;
7583
+ onTokenUpdate = null;
7584
+ /** Set a callback to be notified when an external agent's status changes (driven by JSONL entries) */
7585
+ setOnStatus(cb) {
7586
+ this.onStatus = cb;
7587
+ }
7588
+ /** Set a callback to be notified when token usage is detected from JSONL entries */
7589
+ setOnTokenUpdate(cb) {
7590
+ this.onTokenUpdate = cb;
7591
+ }
7592
+ attach(agentId, pid, cwd, backendId, onOutput) {
7593
+ if (this.readers.has(agentId)) return;
7594
+ let cleanup2;
7595
+ if (backendId === "claude" && cwd) {
7596
+ cleanup2 = this.startClaudeReader(agentId, cwd, onOutput);
7597
+ } else {
7598
+ cleanup2 = this.startLsofReader(agentId, pid, onOutput);
7599
+ }
7600
+ this.readers.set(agentId, { agentId, pid, cwd, backendId, onOutput, cleanup: cleanup2, inputTokens: 0, outputTokens: 0 });
7601
+ }
7602
+ detach(agentId) {
7603
+ const reader = this.readers.get(agentId);
7604
+ if (reader) {
7605
+ reader.cleanup();
7606
+ this.readers.delete(agentId);
7607
+ }
7608
+ }
7609
+ detachAll() {
7610
+ for (const [id] of this.readers) {
7611
+ this.detach(id);
7612
+ }
7613
+ }
7614
+ // ── Claude JSONL reader ───────────────────────────────────────
7615
+ startClaudeReader(agentId, cwd, onOutput) {
7616
+ const projectKey = cwd.replace(/\//g, "-");
7617
+ const projectDir = path8.join(os.homedir(), ".claude", "projects", projectKey);
7618
+ console.log(`[OutputReader] Claude reader for ${agentId}: watching ${projectDir}`);
7619
+ let lastPosition = 0;
7620
+ let watchedFile = null;
7621
+ let watcher = null;
7622
+ let lastEmitTime = 0;
7623
+ let pendingChunk = null;
7624
+ let throttleTimer = null;
7625
+ let pollTimer = null;
7626
+ let retryTimer = null;
7627
+ let stopped = false;
7628
+ let retryCount = 0;
7629
+ const THROTTLE_MS = 2e3;
7630
+ const POLL_MS = 3e3;
7631
+ const MAX_RETRIES = 5;
7632
+ const emitThrottled = (text) => {
7633
+ const now = Date.now();
7634
+ if (now - lastEmitTime >= THROTTLE_MS) {
7635
+ lastEmitTime = now;
7636
+ onOutput(text);
7637
+ pendingChunk = null;
7638
+ } else {
7639
+ pendingChunk = text;
7640
+ if (!throttleTimer) {
7641
+ throttleTimer = setTimeout(() => {
7642
+ throttleTimer = null;
7643
+ if (pendingChunk && !stopped) {
7644
+ lastEmitTime = Date.now();
7645
+ onOutput(pendingChunk);
7646
+ pendingChunk = null;
7647
+ }
7648
+ }, THROTTLE_MS - (now - lastEmitTime));
7649
+ }
7650
+ }
7651
+ };
7652
+ const readNewLines = () => {
7653
+ if (!watchedFile || stopped) return;
7654
+ try {
7655
+ const stat2 = statSync(watchedFile);
7656
+ if (stat2.size <= lastPosition) return;
7657
+ const bytesToRead = stat2.size - lastPosition;
7658
+ const buf = Buffer.alloc(bytesToRead);
7659
+ const fd = openSync(watchedFile, "r");
7660
+ try {
7661
+ readSync(fd, buf, 0, bytesToRead, lastPosition);
7662
+ } finally {
7663
+ closeSync(fd);
7664
+ }
7665
+ lastPosition = stat2.size;
7666
+ const newData = buf.toString("utf-8");
7667
+ for (const line of newData.split("\n")) {
7668
+ if (!line.trim()) continue;
7669
+ try {
7670
+ const entry = JSON.parse(line);
7671
+ this.extractClaudeOutput(entry, emitThrottled, agentId);
7672
+ } catch {
7673
+ }
7674
+ }
7675
+ } catch (err) {
7676
+ console.error(`[OutputReader] Error reading JSONL for ${agentId}:`, err);
7677
+ }
7678
+ };
7679
+ const findAndWatch = () => {
7680
+ if (stopped) return;
7681
+ if (!existsSync6(projectDir)) {
7682
+ retryCount++;
7683
+ if (retryCount > MAX_RETRIES) {
7684
+ console.log(`[OutputReader] Project dir not found after ${MAX_RETRIES} retries, giving up: ${projectDir}`);
7685
+ return;
7686
+ }
7687
+ console.log(`[OutputReader] Project dir not found (${retryCount}/${MAX_RETRIES}), retrying: ${projectDir}`);
7688
+ retryTimer = setTimeout(findAndWatch, 5e3);
7689
+ return;
7690
+ }
7691
+ try {
7692
+ const files = readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({
7693
+ name: f,
7694
+ mtime: statSync(path8.join(projectDir, f)).mtimeMs
7695
+ })).sort((a, b) => b.mtime - a.mtime);
7696
+ if (files.length === 0) {
7697
+ retryCount++;
7698
+ if (retryCount > MAX_RETRIES) {
7699
+ console.log(`[OutputReader] No JSONL files after ${MAX_RETRIES} retries, giving up: ${projectDir}`);
7700
+ return;
7701
+ }
7702
+ console.log(`[OutputReader] No JSONL files yet in ${projectDir} (${retryCount}/${MAX_RETRIES}), retrying`);
7703
+ retryTimer = setTimeout(findAndWatch, 5e3);
7704
+ return;
7705
+ }
7706
+ watchedFile = path8.join(projectDir, files[0].name);
7707
+ lastPosition = statSync(watchedFile).size;
7708
+ console.log(`[OutputReader] Watching JSONL: ${watchedFile} (pos=${lastPosition})`);
7709
+ try {
7710
+ watcher = watch(projectDir, (_event, filename) => {
7711
+ if (stopped) return;
7712
+ if (filename && filename.endsWith(".jsonl")) {
7713
+ const fullPath = path8.join(projectDir, filename);
7714
+ if (fullPath !== watchedFile && existsSync6(fullPath)) {
7715
+ try {
7716
+ const newMtime = statSync(fullPath).mtimeMs;
7717
+ const curMtime = watchedFile ? statSync(watchedFile).mtimeMs : 0;
7718
+ if (newMtime > curMtime) {
7719
+ console.log(`[OutputReader] Switching to newer JSONL: ${fullPath}`);
7720
+ watchedFile = fullPath;
7721
+ lastPosition = 0;
7722
+ }
7723
+ } catch {
7724
+ }
7725
+ }
7726
+ }
7727
+ readNewLines();
7728
+ });
7729
+ } catch {
7730
+ console.log(`[OutputReader] fs.watch failed, relying on polling only`);
7731
+ }
7732
+ pollTimer = setInterval(readNewLines, POLL_MS);
7733
+ } catch (err) {
7734
+ console.error(`[OutputReader] Error setting up watcher for ${agentId}:`, err);
7735
+ }
7736
+ };
7737
+ findAndWatch();
7738
+ return () => {
7739
+ stopped = true;
7740
+ if (watcher) {
7741
+ watcher.close();
7742
+ watcher = null;
7743
+ }
7744
+ if (pollTimer) {
7745
+ clearInterval(pollTimer);
7746
+ pollTimer = null;
7747
+ }
7748
+ if (throttleTimer) {
7749
+ clearTimeout(throttleTimer);
7750
+ throttleTimer = null;
7751
+ }
7752
+ if (retryTimer) {
7753
+ clearTimeout(retryTimer);
7754
+ retryTimer = null;
7755
+ }
7756
+ };
7757
+ }
7758
+ /**
7759
+ * Extract readable output from a Claude JSONL entry.
7760
+ * Also drives status: "human" → working, "result" → idle.
7761
+ *
7762
+ * Claude JSONL types:
7763
+ * - { type: "human" } → user sent message, agent starts working
7764
+ * - { type: "assistant", message: { content: [...] } } → agent responding
7765
+ * - { type: "result", result: "..." } → turn complete, waiting for input
7766
+ */
7767
+ extractClaudeOutput(entry, emit, agentId) {
7768
+ if (entry.type === "user" || entry.type === "human") {
7769
+ this.onStatus?.(agentId, "working");
7770
+ }
7771
+ if (entry.type === "assistant") {
7772
+ const message = entry.message;
7773
+ const usage = message?.usage;
7774
+ if (usage) {
7775
+ const reader = this.readers.get(agentId);
7776
+ if (reader) {
7777
+ const inp = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
7778
+ const out = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
7779
+ if (inp > 0 || out > 0) {
7780
+ reader.inputTokens += inp;
7781
+ reader.outputTokens += out;
7782
+ this.onTokenUpdate?.(agentId, reader.inputTokens, reader.outputTokens);
7783
+ }
7784
+ }
7785
+ }
7786
+ const stopReason = message?.stop_reason ?? entry.stop_reason;
7787
+ const content = message?.content;
7788
+ if (content && Array.isArray(content)) {
7789
+ for (const block of content) {
7790
+ if (block.type === "text" && typeof block.text === "string" && block.text.trim()) {
7791
+ emit(block.text.slice(0, 200));
7792
+ }
7793
+ }
7794
+ }
7795
+ if (stopReason === "end_turn") {
7796
+ this.onStatus?.(agentId, "idle");
7797
+ } else {
7798
+ this.onStatus?.(agentId, "working");
7799
+ }
7800
+ }
7801
+ if (entry.type === "result") {
7802
+ const result = entry.result;
7803
+ if (typeof result === "string" && result.trim()) {
7804
+ emit(result.slice(0, 200));
7805
+ }
7806
+ this.onStatus?.(agentId, "idle");
7807
+ }
7808
+ }
7809
+ // ── lsof fallback reader ──────────────────────────────────────
7810
+ startLsofReader(agentId, pid, onOutput) {
7811
+ const knownFiles = /* @__PURE__ */ new Set();
7812
+ let stopped = false;
7813
+ console.log(`[OutputReader] lsof reader for ${agentId}: pid=${pid}`);
7814
+ const poll = () => {
7815
+ if (stopped) return;
7816
+ execFile2("lsof", ["-p", String(pid)], { timeout: 5e3, maxBuffer: 512 * 1024 }, (err, stdout) => {
7817
+ if (err || stopped) return;
7818
+ const lines = stdout.split("\n");
7819
+ const newFiles = [];
7820
+ for (const line of lines) {
7821
+ const cols = line.trim().split(/\s+/);
7822
+ if (cols.length < 9) continue;
7823
+ const name = cols.slice(8).join(" ");
7824
+ if (!name || name.startsWith("/dev/") || name.startsWith("/System/")) continue;
7825
+ const ext = path8.extname(name);
7826
+ if (!SOURCE_EXTS.has(ext)) continue;
7827
+ if (!knownFiles.has(name)) {
7828
+ knownFiles.add(name);
7829
+ newFiles.push(name);
7830
+ }
7831
+ }
7832
+ if (newFiles.length > 0) {
7833
+ const basename = path8.basename(newFiles[newFiles.length - 1]);
7834
+ onOutput(`Editing ${basename}`);
7835
+ }
7836
+ });
7837
+ };
7838
+ const timer = setInterval(poll, 5e3);
7839
+ poll();
7840
+ return () => {
7841
+ stopped = true;
7842
+ clearInterval(timer);
7843
+ };
7844
+ }
7845
+ };
7846
+
7847
+ // src/index.ts
6662
7848
  registerChannel(wsChannel);
6663
7849
  registerChannel(ablyChannel);
6664
7850
  registerChannel(telegramChannel);
6665
7851
  var orc;
7852
+ var scanner = null;
7853
+ var outputReader = null;
7854
+ var externalAgents = /* @__PURE__ */ new Map();
7855
+ var teamPhases = /* @__PURE__ */ new Map();
7856
+ function publishTeamPhase(teamId, phase, leadAgentId) {
7857
+ teamPhases.set(teamId, { phase, leadAgentId });
7858
+ publishEvent({
7859
+ type: "TEAM_PHASE",
7860
+ teamId,
7861
+ phase,
7862
+ leadAgentId
7863
+ });
7864
+ }
6666
7865
  function generatePairCode() {
6667
7866
  return nanoid5(6).toUpperCase();
6668
7867
  }
@@ -6677,12 +7876,109 @@ function showPairCode() {
6677
7876
  console.log(`Open your phone \u2192 enter gateway address + code`);
6678
7877
  console.log("");
6679
7878
  }
7879
+ function extractProjectName(planText) {
7880
+ function toKebab(s) {
7881
+ return s.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
7882
+ }
7883
+ function trimKebab(s, maxLen) {
7884
+ if (s.length <= maxLen) return s;
7885
+ const cut = s.lastIndexOf("-", maxLen);
7886
+ return cut > 2 ? s.slice(0, cut) : s.slice(0, maxLen);
7887
+ }
7888
+ const namedConcept = planText.match(/CONCEPT\s*[::]\s*(?:A\s+|An\s+|The\s+)?(.+?)\s*[—–]\s/i);
7889
+ if (namedConcept) {
7890
+ const kebab = toKebab(namedConcept[1].trim());
7891
+ if (kebab.length >= 2 && kebab.length <= 30) return kebab;
7892
+ }
7893
+ const quoted = planText.match(/["""\u201c]([^"""\u201d]{2,25})["""\u201d]/);
7894
+ if (quoted) {
7895
+ const kebab = toKebab(quoted[1].trim());
7896
+ if (kebab.length >= 2) return trimKebab(kebab, 25);
7897
+ }
7898
+ const concept = planText.match(/CONCEPT\s*[::]\s*(?:A\s+|An\s+|The\s+)?(.+?)(?:\s+(?:for|that|which|where|with|featuring|aimed|designed|,|。)\b|[—–.\n])/i);
7899
+ if (concept) {
7900
+ const kebab = toKebab(concept[1].trim());
7901
+ if (kebab.length >= 2) return trimKebab(kebab, 25);
7902
+ }
7903
+ const fallbacks = [
7904
+ /(?:goal|project|目标|项目)\s*[::]\s*(.+)/i,
7905
+ /\[PLAN\][\s\S]*?(?:goal|project|目标)\s*[::]\s*(.+)/i,
7906
+ /(?:build|create|make|开发|做|构建)\s+(?:a\s+)?(.+?)(?:\s+(?:with|using|that|for|where|,|。)\b|[.\n])/i
7907
+ ];
7908
+ for (const re of fallbacks) {
7909
+ const m = planText.match(re);
7910
+ if (m) {
7911
+ const kebab = toKebab(m[1].trim());
7912
+ if (kebab.length >= 2) return trimKebab(kebab, 25);
7913
+ }
7914
+ }
7915
+ return "project";
7916
+ }
7917
+ function createUniqueProjectDir(workspace, baseName) {
7918
+ let dirName = baseName;
7919
+ let counter = 1;
7920
+ while (existsSync7(path9.join(workspace, dirName))) {
7921
+ counter++;
7922
+ dirName = `${baseName}-${counter}`;
7923
+ }
7924
+ const fullPath = path9.join(workspace, dirName);
7925
+ mkdirSync5(fullPath, { recursive: true });
7926
+ console.log(`[Gateway] Created project directory: ${fullPath}`);
7927
+ return fullPath;
7928
+ }
7929
+ var AGENTS_FILE = path9.join(os2.homedir(), ".bit-office", "agents.json");
7930
+ function loadAgentDefs() {
7931
+ try {
7932
+ if (existsSync7(AGENTS_FILE)) {
7933
+ const raw = JSON.parse(readFileSync5(AGENTS_FILE, "utf-8"));
7934
+ if (Array.isArray(raw.agents)) return raw.agents;
7935
+ }
7936
+ } catch (e) {
7937
+ console.log(`[Gateway] Failed to read agents.json: ${e}`);
7938
+ }
7939
+ saveAgentDefs(DEFAULT_AGENT_DEFS);
7940
+ return [...DEFAULT_AGENT_DEFS];
7941
+ }
7942
+ function saveAgentDefs(agents) {
7943
+ try {
7944
+ const dir = path9.dirname(AGENTS_FILE);
7945
+ if (!existsSync7(dir)) mkdirSync5(dir, { recursive: true });
7946
+ writeFileSync5(AGENTS_FILE, JSON.stringify({ agents }, null, 2), "utf-8");
7947
+ console.log(`[Gateway] Saved ${agents.length} agent definitions to ${AGENTS_FILE}`);
7948
+ } catch (e) {
7949
+ console.log(`[Gateway] Failed to save agents.json: ${e}`);
7950
+ }
7951
+ }
7952
+ var agentDefs = [];
6680
7953
  function mapOrchestratorEvent(e) {
6681
7954
  switch (e.type) {
6682
7955
  case "task:started":
6683
7956
  return { type: "TASK_STARTED", agentId: e.agentId, taskId: e.taskId, prompt: e.prompt };
6684
- case "task:done":
7957
+ case "task:done": {
7958
+ const resultText = (e.result?.summary ?? "") + (e.result?.fullOutput ?? "");
7959
+ if (resultText && /\[PLAN\]/i.test(resultText)) {
7960
+ for (const [teamId, tp] of teamPhases) {
7961
+ if (tp.leadAgentId === e.agentId && tp.phase === "create") {
7962
+ const planOutput = e.result?.fullOutput ?? e.result?.summary ?? "";
7963
+ if (planOutput) {
7964
+ orc.setOriginalTask(e.agentId, planOutput);
7965
+ console.log(`[Gateway] Captured plan from create phase (${planOutput.length} chars) for design context`);
7966
+ }
7967
+ publishTeamPhase(teamId, "design", e.agentId);
7968
+ break;
7969
+ }
7970
+ }
7971
+ }
7972
+ if (e.isFinalResult) {
7973
+ for (const [teamId, tp] of teamPhases) {
7974
+ if (tp.leadAgentId === e.agentId && tp.phase === "execute") {
7975
+ publishTeamPhase(teamId, "complete", e.agentId);
7976
+ break;
7977
+ }
7978
+ }
7979
+ }
6685
7980
  return { type: "TASK_DONE", agentId: e.agentId, taskId: e.taskId, result: e.result, isFinalResult: e.isFinalResult };
7981
+ }
6686
7982
  case "task:failed":
6687
7983
  return { type: "TASK_FAILED", agentId: e.agentId, taskId: e.taskId, error: e.error };
6688
7984
  case "task:delegated":
@@ -6698,7 +7994,7 @@ function mapOrchestratorEvent(e) {
6698
7994
  case "task:queued":
6699
7995
  return { type: "TASK_QUEUED", agentId: e.agentId, taskId: e.taskId, prompt: e.prompt, position: e.position };
6700
7996
  case "agent:created":
6701
- return { type: "AGENT_CREATED", agentId: e.agentId, name: e.name, role: e.role, palette: e.palette, personality: e.personality, backend: e.backend, isTeamLead: e.isTeamLead, teamId: e.teamId };
7997
+ return { type: "AGENT_CREATED", agentId: e.agentId, name: e.name, role: e.role, palette: e.palette, personality: e.personality, backend: e.backend, isTeamLead: e.isTeamLead || void 0, teamId: e.teamId };
6702
7998
  case "agent:fired":
6703
7999
  return { type: "AGENT_FIRED", agentId: e.agentId };
6704
8000
  case "task:result-returned":
@@ -6735,6 +8031,8 @@ function handleCommand(parsed) {
6735
8031
  }
6736
8032
  case "FIRE_AGENT": {
6737
8033
  console.log(`[Gateway] Firing agent: ${parsed.agentId}`);
8034
+ const agentToFire = orc.getAgent(parsed.agentId);
8035
+ if (agentToFire?.pid) scanner?.addGracePid(agentToFire.pid);
6738
8036
  orc.removeAgent(parsed.agentId);
6739
8037
  break;
6740
8038
  }
@@ -6755,7 +8053,23 @@ function handleCommand(parsed) {
6755
8053
  }
6756
8054
  if (agent) {
6757
8055
  console.log(`[Gateway] RUN_TASK: agent=${parsed.agentId}, isLead=${orc.isTeamLead(parsed.agentId)}, hasTeam=${orc.getAllAgents().length > 1}`);
6758
- orc.runTask(parsed.agentId, parsed.taskId, parsed.prompt, { repoPath: parsed.repoPath });
8056
+ let phaseOverride;
8057
+ if (orc.isTeamLead(parsed.agentId)) {
8058
+ for (const [, tp] of teamPhases) {
8059
+ if (tp.leadAgentId === parsed.agentId) {
8060
+ phaseOverride = tp.phase;
8061
+ if (tp.phase === "complete") {
8062
+ const teamId = Array.from(teamPhases.entries()).find(([, v]) => v.leadAgentId === parsed.agentId)?.[0];
8063
+ if (teamId) {
8064
+ publishTeamPhase(teamId, "execute", parsed.agentId);
8065
+ phaseOverride = "execute";
8066
+ }
8067
+ }
8068
+ break;
8069
+ }
8070
+ }
8071
+ }
8072
+ orc.runTask(parsed.agentId, parsed.taskId, parsed.prompt, { repoPath: parsed.repoPath, phaseOverride });
6759
8073
  } else {
6760
8074
  publishEvent({
6761
8075
  type: "TASK_FAILED",
@@ -6775,63 +8089,78 @@ function handleCommand(parsed) {
6775
8089
  break;
6776
8090
  }
6777
8091
  case "SERVE_PREVIEW": {
6778
- const filePath = parsed.filePath;
6779
- console.log(`[Gateway] SERVE_PREVIEW: ${filePath}`);
6780
- previewServer.serve(filePath);
8092
+ if (parsed.previewCmd && parsed.previewPort) {
8093
+ const cwd = parsed.cwd ?? config.defaultWorkspace;
8094
+ console.log(`[Gateway] SERVE_PREVIEW (cmd): "${parsed.previewCmd}" port=${parsed.previewPort} cwd=${cwd}`);
8095
+ previewServer.runCommand(parsed.previewCmd, cwd, parsed.previewPort);
8096
+ } else if (parsed.previewCmd) {
8097
+ const cwd = parsed.cwd ?? config.defaultWorkspace;
8098
+ console.log(`[Gateway] SERVE_PREVIEW (launch): "${parsed.previewCmd}" cwd=${cwd}`);
8099
+ previewServer.launchProcess(parsed.previewCmd, cwd);
8100
+ } else if (parsed.filePath) {
8101
+ console.log(`[Gateway] SERVE_PREVIEW (static): ${parsed.filePath}`);
8102
+ previewServer.serve(parsed.filePath);
8103
+ }
6781
8104
  break;
6782
8105
  }
6783
8106
  case "OPEN_FILE": {
6784
8107
  const raw = parsed.path;
6785
- const resolved = path5.resolve(config.defaultWorkspace, raw);
6786
- const normalized = path5.normalize(resolved);
6787
- if (!normalized.startsWith(config.defaultWorkspace + path5.sep) && normalized !== config.defaultWorkspace) {
8108
+ const resolved = path9.resolve(config.defaultWorkspace, raw);
8109
+ const normalized = path9.normalize(resolved);
8110
+ if (!normalized.startsWith(config.defaultWorkspace + path9.sep) && normalized !== config.defaultWorkspace) {
6788
8111
  console.error(`[Gateway] Blocked OPEN_FILE: path "${raw}" resolves outside workspace`);
6789
8112
  break;
6790
8113
  }
6791
- if (!existsSync4(normalized)) {
8114
+ if (!existsSync7(normalized)) {
6792
8115
  console.error(`[Gateway] OPEN_FILE: path does not exist: ${normalized}`);
6793
8116
  break;
6794
8117
  }
6795
8118
  console.log(`[Gateway] Opening file: ${normalized}`);
6796
- execFile("open", [normalized], (err) => {
8119
+ execFile3("open", [normalized], (err) => {
6797
8120
  if (err) console.error(`[Gateway] Failed to open file: ${err.message}`);
6798
8121
  });
6799
8122
  break;
6800
8123
  }
6801
8124
  case "CREATE_TEAM": {
6802
- const { leadPresetIndex, memberPresetIndices, backends: backends2 } = parsed;
6803
- const allIndices = [leadPresetIndex, ...memberPresetIndices.filter((i) => i !== leadPresetIndex)];
6804
- console.log(`[Gateway] Creating team: lead=${leadPresetIndex}, members=${memberPresetIndices.join(",")}`);
8125
+ const { leadId, memberIds, backends: backends2 } = parsed;
8126
+ const allIds = [leadId, ...memberIds.filter((id) => id !== leadId)];
8127
+ console.log(`[Gateway] Creating team: lead=${leadId}, members=${memberIds.join(",")}`);
6805
8128
  let leadAgentId = null;
6806
8129
  const teamId = `team-${nanoid5(6)}`;
6807
- for (const idx of allIndices) {
6808
- const preset = AGENT_PRESETS[idx];
6809
- if (!preset) continue;
8130
+ for (const defId of allIds) {
8131
+ const def = agentDefs.find((a) => a.id === defId);
8132
+ if (!def) {
8133
+ console.log(`[Gateway] Agent def not found: ${defId}`);
8134
+ continue;
8135
+ }
6810
8136
  const agentId = `agent-${nanoid5(6)}`;
6811
- const backendId = backends2?.[String(idx)] ?? config.defaultBackend;
6812
- if (idx === leadPresetIndex) {
8137
+ const backendId = backends2?.[defId] ?? config.defaultBackend;
8138
+ if (defId === leadId) {
6813
8139
  leadAgentId = agentId;
6814
8140
  orc.setTeamLead(agentId);
6815
8141
  }
6816
8142
  orc.createAgent({
6817
8143
  agentId,
6818
- name: preset.name,
6819
- role: preset.role,
6820
- personality: preset.personality,
8144
+ name: def.name,
8145
+ role: def.skills ? `${def.role} \u2014 ${def.skills}` : def.role,
8146
+ personality: def.personality,
6821
8147
  backend: backendId,
6822
- palette: preset.palette,
8148
+ palette: def.palette,
6823
8149
  teamId
6824
8150
  });
6825
8151
  }
6826
8152
  if (leadAgentId) {
6827
- const leadPreset = AGENT_PRESETS[leadPresetIndex];
8153
+ const leadDef = agentDefs.find((a) => a.id === leadId);
6828
8154
  publishEvent({
6829
8155
  type: "TEAM_CHAT",
6830
8156
  fromAgentId: leadAgentId,
6831
- message: `Team created! ${leadPreset?.name ?? "Lead"} is the Team Lead with ${memberPresetIndices.length} team members.`,
8157
+ message: `Team created! ${leadDef?.name ?? "Lead"} is the Team Lead with ${memberIds.length} team members.`,
6832
8158
  messageType: "status",
6833
8159
  timestamp: Date.now()
6834
8160
  });
8161
+ publishTeamPhase(teamId, "create", leadAgentId);
8162
+ const greetTaskId = nanoid5();
8163
+ orc.runTask(leadAgentId, greetTaskId, "Greet the user and ask what they would like to build.", { phaseOverride: "create" });
6835
8164
  }
6836
8165
  break;
6837
8166
  }
@@ -6842,7 +8171,83 @@ function handleCommand(parsed) {
6842
8171
  }
6843
8172
  case "FIRE_TEAM": {
6844
8173
  console.log("[Gateway] Firing entire team");
8174
+ for (const agent of orc.getAllAgents()) {
8175
+ const pid = agent.pid;
8176
+ if (pid) scanner?.addGracePid(pid);
8177
+ }
6845
8178
  orc.fireTeam();
8179
+ teamPhases.clear();
8180
+ break;
8181
+ }
8182
+ case "KILL_EXTERNAL": {
8183
+ const ext = externalAgents.get(parsed.agentId);
8184
+ if (ext) {
8185
+ console.log(`[Gateway] Killing external process: ${ext.name} (pid=${ext.pid})`);
8186
+ scanner?.addGracePid(ext.pid);
8187
+ try {
8188
+ process.kill(ext.pid, "SIGKILL");
8189
+ } catch (err) {
8190
+ console.error(`[Gateway] Failed to kill pid ${ext.pid}:`, err);
8191
+ }
8192
+ outputReader?.detach(ext.agentId);
8193
+ externalAgents.delete(ext.agentId);
8194
+ publishEvent({ type: "AGENT_FIRED", agentId: ext.agentId });
8195
+ } else {
8196
+ console.log(`[Gateway] KILL_EXTERNAL: agent ${parsed.agentId} not found`);
8197
+ }
8198
+ break;
8199
+ }
8200
+ case "APPROVE_PLAN": {
8201
+ const agentId = parsed.agentId;
8202
+ console.log(`[Gateway] APPROVE_PLAN: agent=${agentId}`);
8203
+ const approvedPlan = orc.getLeaderLastOutput(agentId);
8204
+ if (approvedPlan) {
8205
+ orc.setOriginalTask(agentId, approvedPlan);
8206
+ console.log(`[Gateway] Captured approved plan (${approvedPlan.length} chars) for leader ${agentId}`);
8207
+ }
8208
+ const projectName = extractProjectName(approvedPlan ?? "project");
8209
+ const projectDir = createUniqueProjectDir(config.defaultWorkspace, projectName);
8210
+ orc.setTeamProjectDir(projectDir);
8211
+ let approveTeamId;
8212
+ for (const [teamId, tp] of teamPhases) {
8213
+ if (tp.leadAgentId === agentId) {
8214
+ approveTeamId = teamId;
8215
+ break;
8216
+ }
8217
+ }
8218
+ if (!approveTeamId) {
8219
+ const agentInfo = orc.getAllAgents().find((a) => a.agentId === agentId);
8220
+ if (agentInfo?.teamId) approveTeamId = agentInfo.teamId;
8221
+ }
8222
+ if (approveTeamId) {
8223
+ publishTeamPhase(approveTeamId, "execute", agentId);
8224
+ const taskId = nanoid5();
8225
+ orc.runTask(agentId, taskId, `The user approved your plan. Execute it now by delegating tasks to your team members. All work must go in the project directory: ${path9.basename(projectDir)}/`, { phaseOverride: "execute" });
8226
+ }
8227
+ break;
8228
+ }
8229
+ case "END_PROJECT": {
8230
+ const agentId = parsed.agentId;
8231
+ console.log(`[Gateway] END_PROJECT: agent=${agentId}`);
8232
+ orc.clearLeaderHistory(agentId);
8233
+ let foundTeamId;
8234
+ for (const [teamId, tp] of teamPhases) {
8235
+ if (tp.leadAgentId === agentId) {
8236
+ foundTeamId = teamId;
8237
+ break;
8238
+ }
8239
+ }
8240
+ if (!foundTeamId) {
8241
+ const agentInfo = orc.getAllAgents().find((a) => a.agentId === agentId);
8242
+ if (agentInfo?.teamId) foundTeamId = agentInfo.teamId;
8243
+ }
8244
+ if (foundTeamId) {
8245
+ publishTeamPhase(foundTeamId, "create", agentId);
8246
+ const greetTaskId = nanoid5();
8247
+ orc.runTask(agentId, greetTaskId, "Greet the user and ask what they would like to build next.", { phaseOverride: "create" });
8248
+ } else {
8249
+ console.log(`[Gateway] END_PROJECT: no team found for agent ${agentId}, ignoring`);
8250
+ }
6846
8251
  break;
6847
8252
  }
6848
8253
  case "PING": {
@@ -6856,7 +8261,7 @@ function handleCommand(parsed) {
6856
8261
  palette: agent.palette,
6857
8262
  personality: void 0,
6858
8263
  backend: agent.backend,
6859
- isTeamLead: agent.isTeamLead,
8264
+ isTeamLead: agent.isTeamLead || void 0,
6860
8265
  teamId: agent.teamId
6861
8266
  });
6862
8267
  publishEvent({
@@ -6864,7 +8269,67 @@ function handleCommand(parsed) {
6864
8269
  agentId: agent.agentId,
6865
8270
  status: agent.status
6866
8271
  });
8272
+ if (agent.isTeamLead && agent.teamId && !teamPhases.has(agent.teamId)) {
8273
+ teamPhases.set(agent.teamId, { phase: "create", leadAgentId: agent.agentId });
8274
+ console.log(`[Gateway] Restored team phase for ${agent.teamId} (leader=${agent.agentId})`);
8275
+ }
8276
+ }
8277
+ for (const [teamId, tp] of teamPhases) {
8278
+ publishEvent({
8279
+ type: "TEAM_PHASE",
8280
+ teamId,
8281
+ phase: tp.phase,
8282
+ leadAgentId: tp.leadAgentId
8283
+ });
8284
+ }
8285
+ for (const [, ext] of externalAgents) {
8286
+ publishEvent({
8287
+ type: "AGENT_CREATED",
8288
+ agentId: ext.agentId,
8289
+ name: ext.name,
8290
+ role: ext.cwd ? ext.cwd.split("/").pop() ?? ext.backendId : ext.backendId,
8291
+ isExternal: true,
8292
+ pid: ext.pid,
8293
+ cwd: ext.cwd ?? void 0,
8294
+ startedAt: ext.startedAt,
8295
+ backend: ext.backendId
8296
+ });
8297
+ publishEvent({
8298
+ type: "AGENT_STATUS",
8299
+ agentId: ext.agentId,
8300
+ status: ext.status
8301
+ });
8302
+ }
8303
+ publishEvent({ type: "AGENT_DEFS", agents: agentDefs });
8304
+ break;
8305
+ }
8306
+ case "SAVE_AGENT_DEF": {
8307
+ const def = parsed.agent;
8308
+ const idx = agentDefs.findIndex((a) => a.id === def.id);
8309
+ if (idx >= 0) {
8310
+ if (agentDefs[idx].isBuiltin) {
8311
+ def.isBuiltin = true;
8312
+ def.teamRole = agentDefs[idx].teamRole;
8313
+ }
8314
+ agentDefs[idx] = def;
8315
+ } else {
8316
+ def.isBuiltin = false;
8317
+ def.teamRole = "dev";
8318
+ agentDefs.push(def);
6867
8319
  }
8320
+ saveAgentDefs(agentDefs);
8321
+ publishEvent({ type: "AGENT_DEFS", agents: agentDefs });
8322
+ break;
8323
+ }
8324
+ case "DELETE_AGENT_DEF": {
8325
+ const target = agentDefs.find((a) => a.id === parsed.agentDefId);
8326
+ if (target?.isBuiltin) {
8327
+ console.log(`[Gateway] Cannot delete built-in agent: ${parsed.agentDefId}`);
8328
+ break;
8329
+ }
8330
+ agentDefs = agentDefs.filter((a) => a.id !== parsed.agentDefId);
8331
+ saveAgentDefs(agentDefs);
8332
+ publishEvent({ type: "AGENT_DEFS", agents: agentDefs });
6868
8333
  break;
6869
8334
  }
6870
8335
  }
@@ -6892,9 +8357,11 @@ async function main() {
6892
8357
  worktree: false,
6893
8358
  // disabled by default for now
6894
8359
  retry: { maxRetries: 2, escalateToLeader: true },
6895
- promptsDir: path5.join(os.homedir(), ".bit-office", "prompts"),
8360
+ promptsDir: path9.join(os2.homedir(), ".bit-office", "prompts"),
6896
8361
  sandboxMode: config.sandboxMode
6897
8362
  });
8363
+ agentDefs = loadAgentDefs();
8364
+ console.log(`[Gateway] Loaded ${agentDefs.length} agent definitions (${agentDefs.filter((a) => !a.isBuiltin).length} custom)`);
6898
8365
  const forwardEvent = (event) => {
6899
8366
  const mapped = mapOrchestratorEvent(event);
6900
8367
  if (mapped) publishEvent(mapped);
@@ -6914,6 +8381,99 @@ async function main() {
6914
8381
  orc.on("agent:created", forwardEvent);
6915
8382
  orc.on("agent:fired", forwardEvent);
6916
8383
  orc.on("task:result-returned", forwardEvent);
8384
+ outputReader = new ExternalOutputReader();
8385
+ outputReader.setOnStatus((agentId, status) => {
8386
+ const ext = externalAgents.get(agentId);
8387
+ if (ext && ext.status !== status) {
8388
+ ext.status = status;
8389
+ publishEvent({
8390
+ type: "AGENT_STATUS",
8391
+ agentId,
8392
+ status
8393
+ });
8394
+ }
8395
+ });
8396
+ outputReader.setOnTokenUpdate((agentId, inputTokens, outputTokens) => {
8397
+ publishEvent({
8398
+ type: "TOKEN_UPDATE",
8399
+ agentId,
8400
+ inputTokens,
8401
+ outputTokens
8402
+ });
8403
+ });
8404
+ scanner = new ProcessScanner(
8405
+ () => orc.getManagedPids(),
8406
+ {
8407
+ onAdded: (agents) => {
8408
+ for (const agent of agents) {
8409
+ const name = agent.command.charAt(0).toUpperCase() + agent.command.slice(1);
8410
+ const displayName = `${name} (${agent.pid})`;
8411
+ externalAgents.set(agent.agentId, {
8412
+ agentId: agent.agentId,
8413
+ name: displayName,
8414
+ backendId: agent.backendId,
8415
+ pid: agent.pid,
8416
+ cwd: agent.cwd,
8417
+ startedAt: agent.startedAt,
8418
+ status: agent.status
8419
+ });
8420
+ console.log(`[ProcessScanner] External agent found: ${displayName} (pid=${agent.pid}, cwd=${agent.cwd})`);
8421
+ publishEvent({
8422
+ type: "AGENT_CREATED",
8423
+ agentId: agent.agentId,
8424
+ name: displayName,
8425
+ role: agent.cwd ? agent.cwd.split("/").pop() ?? agent.backendId : agent.backendId,
8426
+ isExternal: true,
8427
+ pid: agent.pid,
8428
+ cwd: agent.cwd ?? void 0,
8429
+ startedAt: agent.startedAt,
8430
+ backend: agent.backendId
8431
+ });
8432
+ publishEvent({
8433
+ type: "AGENT_STATUS",
8434
+ agentId: agent.agentId,
8435
+ status: agent.status
8436
+ });
8437
+ outputReader?.attach(agent.agentId, agent.pid, agent.cwd, agent.backendId, (chunk) => {
8438
+ publishEvent({
8439
+ type: "LOG_APPEND",
8440
+ agentId: agent.agentId,
8441
+ taskId: "external",
8442
+ stream: "stdout",
8443
+ chunk
8444
+ });
8445
+ });
8446
+ }
8447
+ },
8448
+ onRemoved: (agentIds) => {
8449
+ for (const agentId of agentIds) {
8450
+ const ext = externalAgents.get(agentId);
8451
+ console.log(`[ProcessScanner] External agent gone: ${ext?.name ?? agentId}`);
8452
+ outputReader?.detach(agentId);
8453
+ externalAgents.delete(agentId);
8454
+ publishEvent({
8455
+ type: "AGENT_FIRED",
8456
+ agentId
8457
+ });
8458
+ }
8459
+ },
8460
+ onChanged: (agents) => {
8461
+ for (const agent of agents) {
8462
+ const ext = externalAgents.get(agent.agentId);
8463
+ if (ext?.backendId === "claude") continue;
8464
+ if (ext) {
8465
+ ext.status = agent.status;
8466
+ }
8467
+ publishEvent({
8468
+ type: "AGENT_STATUS",
8469
+ agentId: agent.agentId,
8470
+ status: agent.status
8471
+ });
8472
+ }
8473
+ }
8474
+ }
8475
+ );
8476
+ scanner.start();
6917
8477
  const backendNames = config.detectedBackends.map((id) => getBackend(id)?.name ?? id).join(", ");
6918
8478
  console.log(`[Gateway] AI backends: ${backendNames || "none detected"} (default: ${getBackend(config.defaultBackend)?.name ?? config.defaultBackend})`);
6919
8479
  console.log(`[Gateway] Permissions: ${config.sandboxMode === "full" ? "Full access" : "Sandbox"}`);
@@ -6922,10 +8482,10 @@ async function main() {
6922
8482
  await initTransports(handleCommand);
6923
8483
  console.log("[Gateway] Listening for commands...");
6924
8484
  console.log("[Gateway] Press 'p' + Enter to generate a new pair code");
6925
- if (process.env.NODE_ENV !== "development" && existsSync4(config.webDir)) {
8485
+ if (process.env.NODE_ENV !== "development" && existsSync7(config.webDir)) {
6926
8486
  const url = `http://localhost:${config.wsPort}`;
6927
8487
  console.log(`[Gateway] Opening ${url}`);
6928
- execFile("open", [url]);
8488
+ execFile3("open", [url]);
6929
8489
  }
6930
8490
  if (process.stdin.isTTY) {
6931
8491
  process.stdin.setEncoding("utf-8");
@@ -6939,6 +8499,8 @@ async function main() {
6939
8499
  }
6940
8500
  function cleanup() {
6941
8501
  console.log("[Gateway] Shutting down...");
8502
+ outputReader?.detachAll();
8503
+ scanner?.stop();
6942
8504
  previewServer.stop();
6943
8505
  orc?.destroy();
6944
8506
  destroyTransports();