bit-office 1.0.4 → 1.1.1

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,30 @@ 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.
5344
+ - PROJECT DIRECTORY: When creating files, first create a dedicated project directory (short kebab-case name, e.g. "snake-game"). Do ALL work inside it. Report it as PROJECT_DIR: <directory-name> in your output. If the user is just chatting (no code needed), skip this.`
5118
5345
  };
5346
+ const isFirstExecute = this._isTeamLead && phaseOverride === "execute" && !this._hasExecuted;
5119
5347
  let fullPrompt;
5120
- if (this._isTeamLead) {
5121
- fullPrompt = this._renderPrompt(this.hasHistory ? "leader-continue" : "leader-initial", templateVars);
5348
+ if (this._isTeamLead && phaseOverride && ["create", "design", "complete"].includes(phaseOverride)) {
5349
+ const templateName = this.hasHistory ? `leader-${phaseOverride}-continue` : `leader-${phaseOverride}`;
5350
+ fullPrompt = this._renderPrompt(templateName, templateVars);
5351
+ } else if (this._isTeamLead) {
5352
+ const useInitial = isFirstExecute || !this.hasHistory;
5353
+ fullPrompt = this._renderPrompt(useInitial ? "leader-initial" : "leader-continue", templateVars);
5354
+ if (phaseOverride === "execute") this._hasExecuted = true;
5122
5355
  } else {
5123
- fullPrompt = this._renderPrompt(this.hasHistory ? "worker-continue" : "worker-initial", templateVars);
5356
+ const workerInitial = this.role.toLowerCase().includes("review") ? "worker-reviewer-initial" : "worker-initial";
5357
+ fullPrompt = this._renderPrompt(this.hasHistory ? "worker-continue" : workerInitial, templateVars);
5124
5358
  }
5125
5359
  const fullAccess = this.sandboxMode === "full";
5126
5360
  const verbose = !!process.env.DEBUG;
@@ -5129,9 +5363,10 @@ var AgentSession = class {
5129
5363
  resumeSessionId: this.sessionId ?? void 0,
5130
5364
  fullAccess,
5131
5365
  noTools: this._isTeamLead,
5132
- model: this._isTeamLead ? "sonnet" : void 0,
5133
5366
  verbose,
5134
- skipResume: this._isTeamLead && this.hasHistory
5367
+ // Only skip resume on first execute (to shed conversational create/design context).
5368
+ // On subsequent runs (result forwarding, user feedback), resume so leader keeps context.
5369
+ skipResume: isFirstExecute && this.hasHistory
5135
5370
  });
5136
5371
  try {
5137
5372
  const whichPath = execSync2(`which ${this.backend.command}`, { env: cleanEnv, encoding: "utf-8", timeout: 3e3 }).trim();
@@ -5168,22 +5403,34 @@ var AgentSession = class {
5168
5403
  if (/^\s*[\w./\\-]+\.(ts|tsx|js|jsx|json|md|css|py)\s*$/.test(line)) return true;
5169
5404
  return false;
5170
5405
  };
5406
+ let pendingDelegation = null;
5407
+ const flushDelegation = () => {
5408
+ if (pendingDelegation && this.onDelegation) {
5409
+ const fullPrompt2 = pendingDelegation.lines.join("\n").replace(/\*\*$/, "").trim();
5410
+ console.log(`[Delegation detected] ${this.name} -> @${pendingDelegation.targetName}: ${fullPrompt2.slice(0, 120)}`);
5411
+ this.onDelegation(this.agentId, pendingDelegation.targetName, fullPrompt2);
5412
+ }
5413
+ pendingDelegation = null;
5414
+ };
5171
5415
  const handleTextLine = (text) => {
5172
5416
  const lines = text.split("\n").filter((l) => l.trim());
5173
5417
  const visibleLines = [];
5174
5418
  for (const line of lines) {
5175
5419
  const trimmed = line.trim();
5176
5420
  console.log(`[Agent ${this.name}] ${trimmed.slice(0, 200)}`);
5177
- const match = trimmed.match(DELEGATION_RE);
5178
- if (match && this.onDelegation) {
5421
+ const match = this._isTeamLead ? trimmed.match(DELEGATION_RE) : null;
5422
+ if (match) {
5423
+ flushDelegation();
5179
5424
  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());
5425
+ pendingDelegation = { targetName, lines: [delegatedPrompt] };
5426
+ } else if (pendingDelegation) {
5427
+ pendingDelegation.lines.push(trimmed);
5182
5428
  }
5183
5429
  if (!isSystemNoise(line)) {
5184
5430
  visibleLines.push(trimmed);
5185
5431
  }
5186
5432
  }
5433
+ flushDelegation();
5187
5434
  if (visibleLines.length > 0) {
5188
5435
  this.onEvent({
5189
5436
  type: "log:append",
@@ -5218,6 +5465,11 @@ var AgentSession = class {
5218
5465
  console.log(`[Agent ${this.name}] Session ID: ${msg.session_id}`);
5219
5466
  }
5220
5467
  if (msg.type === "assistant" && msg.message?.content) {
5468
+ if (msg.message.usage) {
5469
+ const usage = msg.message.usage;
5470
+ if (typeof usage.input_tokens === "number") this.taskInputTokens += usage.input_tokens;
5471
+ if (typeof usage.output_tokens === "number") this.taskOutputTokens += usage.output_tokens;
5472
+ }
5221
5473
  for (const block of msg.message.content) {
5222
5474
  if (block.type === "text" && block.text) {
5223
5475
  this.stdoutBuffer += block.text + "\n";
@@ -5302,15 +5554,18 @@ var AgentSession = class {
5302
5554
  } else if (code === 0) {
5303
5555
  this.hasHistory = true;
5304
5556
  saveSessionId(this.agentId, this.sessionId);
5305
- const { summary, fullOutput, changedFiles } = this.extractResult();
5306
- const { previewUrl, previewPath } = this._isTeamLead ? { previewUrl: void 0, previewPath: void 0 } : this.detectPreview();
5557
+ const { summary, fullOutput, changedFiles, entryFile, projectDir, previewCmd, previewPort } = this.extractResult();
5558
+ this._lastFullOutput = fullOutput;
5559
+ const hasWorkOutput = changedFiles.length > 0 || entryFile || previewCmd || projectDir;
5560
+ const { previewUrl, previewPath } = this._isTeamLead || !hasWorkOutput ? { previewUrl: void 0, previewPath: void 0 } : this.detectPreview();
5307
5561
  this._lastResult = `done: ${summary.slice(0, 120)}`;
5308
5562
  this.setStatus("done");
5563
+ const tokenUsage = this.taskInputTokens > 0 || this.taskOutputTokens > 0 ? { inputTokens: this.taskInputTokens, outputTokens: this.taskOutputTokens } : void 0;
5309
5564
  this.onEvent({
5310
5565
  type: "task:done",
5311
5566
  agentId: this.agentId,
5312
5567
  taskId: completedTaskId,
5313
- result: { summary, fullOutput, changedFiles, diffStat: "", testResult: "unknown", previewUrl, previewPath }
5568
+ result: { summary, fullOutput, changedFiles, diffStat: "", testResult: "unknown", previewUrl, previewPath, entryFile, projectDir, previewCmd, previewPort, tokenUsage }
5314
5569
  });
5315
5570
  this.onTaskComplete?.(this.agentId, completedTaskId, summary, true);
5316
5571
  this.idleTimer = setTimeout(() => {
@@ -5378,29 +5633,59 @@ var AgentSession = class {
5378
5633
  * Called directly for workers; called by orchestrator for leader's final result.
5379
5634
  */
5380
5635
  detectPreview() {
5381
- const previewMatch = this.stdoutBuffer.match(/PREVIEW:\s*(https?:\/\/[^\s*)\]>]+)/i);
5382
- let previewUrl = previewMatch?.[1]?.replace(/[*)\]>]+$/, "");
5636
+ const result = this.extractResult();
5637
+ const baseCwd = this.currentCwd ?? this.workspace;
5638
+ const cwd = result.projectDir ? path3.isAbsolute(result.projectDir) ? result.projectDir : path3.join(baseCwd, result.projectDir) : baseCwd;
5639
+ let previewUrl;
5383
5640
  let previewPath;
5384
- if (!previewUrl) {
5385
- const localhostMatch = this.stdoutBuffer.match(/https?:\/\/localhost[:\d]*/);
5386
- previewUrl = localhostMatch?.[0];
5641
+ if (result.previewCmd) {
5642
+ if (result.previewPort) {
5643
+ previewUrl = previewServer.runCommand(result.previewCmd, cwd, result.previewPort);
5644
+ if (previewUrl) return { previewUrl, previewPath: void 0 };
5645
+ } else {
5646
+ return { previewUrl: void 0, previewPath: void 0 };
5647
+ }
5387
5648
  }
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);
5649
+ if (result.entryFile) {
5650
+ if (/\.html?$/i.test(result.entryFile)) {
5651
+ previewPath = path3.isAbsolute(result.entryFile) ? result.entryFile : path3.join(cwd, result.entryFile);
5652
+ if (existsSync3(previewPath)) {
5653
+ previewUrl = previewServer.serve(previewPath);
5654
+ if (previewUrl) return { previewUrl, previewPath };
5655
+ }
5393
5656
  }
5394
5657
  }
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);
5658
+ const previewMatch = this.stdoutBuffer.match(/PREVIEW:\s*(https?:\/\/[^\s*)\]>]+)/i);
5659
+ if (previewMatch) {
5660
+ return { previewUrl: previewMatch[1].replace(/[*)\]>]+$/, ""), previewPath: void 0 };
5661
+ }
5662
+ const fileMatch = this.stdoutBuffer.match(/(?:open\s+)?((?:\/[\w./_-]+|[\w./_-]+)\.html?)\b/i);
5663
+ if (fileMatch) {
5664
+ previewPath = path3.isAbsolute(fileMatch[1]) ? fileMatch[1] : path3.join(cwd, fileMatch[1]);
5665
+ previewUrl = previewServer.serve(previewPath);
5666
+ if (previewUrl) return { previewUrl, previewPath };
5667
+ }
5668
+ const htmlFile = result.changedFiles.find((f) => /\.html?$/i.test(f));
5669
+ if (htmlFile) {
5670
+ previewPath = path3.isAbsolute(htmlFile) ? htmlFile : path3.join(cwd, htmlFile);
5671
+ previewUrl = previewServer.serve(previewPath);
5672
+ if (previewUrl) return { previewUrl, previewPath };
5673
+ }
5674
+ const candidates = [
5675
+ "dist/index.html",
5676
+ "build/index.html",
5677
+ "out/index.html",
5678
+ "index.html",
5679
+ "public/index.html"
5680
+ ];
5681
+ for (const candidate of candidates) {
5682
+ const absPath = path3.join(cwd, candidate);
5683
+ if (existsSync3(absPath)) {
5684
+ previewUrl = previewServer.serve(absPath);
5685
+ if (previewUrl) return { previewUrl, previewPath: absPath };
5401
5686
  }
5402
5687
  }
5403
- return { previewUrl, previewPath };
5688
+ return { previewUrl: void 0, previewPath: void 0 };
5404
5689
  }
5405
5690
  /**
5406
5691
  * Parse stdoutBuffer for structured result (SUMMARY/STATUS/FILES_CHANGED).
@@ -5411,6 +5696,10 @@ var AgentSession = class {
5411
5696
  const fullOutput = raw.slice(0, 3e3);
5412
5697
  const summaryMatch = raw.match(/SUMMARY:\s*(.+)/i);
5413
5698
  const filesMatch = raw.match(/FILES_CHANGED:\s*(.+)/i);
5699
+ const entryFileMatch = raw.match(/ENTRY_FILE:\s*(.+)/i);
5700
+ const projectDirMatch = raw.match(/PROJECT_DIR:\s*(.+)/i);
5701
+ const previewCmdMatch = raw.match(/PREVIEW_CMD:\s*(.+)/i);
5702
+ const previewPortMatch = raw.match(/PREVIEW_PORT:\s*(\d+)/i);
5414
5703
  const changedFiles = [];
5415
5704
  if (filesMatch) {
5416
5705
  const fileList = filesMatch[1].trim();
@@ -5419,8 +5708,12 @@ var AgentSession = class {
5419
5708
  if (cleaned) changedFiles.push(cleaned);
5420
5709
  }
5421
5710
  }
5711
+ const entryFile = entryFileMatch?.[1]?.trim();
5712
+ const projectDir = projectDirMatch?.[1]?.trim();
5713
+ const previewCmd = previewCmdMatch?.[1]?.trim();
5714
+ const previewPort = previewPortMatch ? parseInt(previewPortMatch[1], 10) : void 0;
5422
5715
  if (summaryMatch) {
5423
- return { summary: summaryMatch[1].trim(), fullOutput, changedFiles };
5716
+ return { summary: summaryMatch[1].trim(), fullOutput, changedFiles, entryFile, projectDir, previewCmd, previewPort };
5424
5717
  }
5425
5718
  const lines = raw.split("\n").filter((l) => l.trim());
5426
5719
  const delegationRe = /^@(\w+):/;
@@ -5445,17 +5738,17 @@ var AgentSession = class {
5445
5738
  }
5446
5739
  }
5447
5740
  if (meaningful.length === 0 && delegationTargets.length > 0) {
5448
- return { summary: `Delegated tasks to ${delegationTargets.join(", ")}`, fullOutput, changedFiles };
5741
+ return { summary: `Delegated tasks to ${delegationTargets.join(", ")}`, fullOutput, changedFiles, entryFile, projectDir };
5449
5742
  }
5450
5743
  const lastChunk = meaningful.slice(-5).join("\n").trim();
5451
5744
  const summary = lastChunk.slice(0, 500) || "Task completed";
5452
- return { summary, fullOutput, changedFiles };
5745
+ return { summary, fullOutput, changedFiles, entryFile, projectDir };
5453
5746
  }
5454
5747
  dequeueNext() {
5455
5748
  if (this.taskQueue.length === 0) return;
5456
5749
  const next = this.taskQueue.shift();
5457
5750
  setTimeout(() => {
5458
- this.runTask(next.taskId, next.prompt, next.repoPath, next.teamContext);
5751
+ this.runTask(next.taskId, next.prompt, next.repoPath, next.teamContext, false, next.phaseOverride);
5459
5752
  }, 100);
5460
5753
  }
5461
5754
  cancelled = false;
@@ -5511,16 +5804,33 @@ var AgentSession = class {
5511
5804
  this.idleTimer = null;
5512
5805
  }
5513
5806
  if (this.process?.pid) {
5807
+ const pgid = this.process.pid;
5514
5808
  try {
5515
- process.kill(-this.process.pid, "SIGTERM");
5809
+ process.kill(-pgid, "SIGKILL");
5516
5810
  } catch {
5517
- this.process.kill("SIGTERM");
5811
+ try {
5812
+ this.process.kill("SIGKILL");
5813
+ } catch {
5814
+ }
5518
5815
  }
5519
5816
  this.process = null;
5520
5817
  }
5521
5818
  this.pendingApprovals.clear();
5522
5819
  saveSessionId(this.agentId, null);
5523
5820
  }
5821
+ /** Reset conversation history so the next task starts fresh (used by End Project). */
5822
+ clearHistory() {
5823
+ this.hasHistory = false;
5824
+ this.sessionId = null;
5825
+ this.originalTask = null;
5826
+ this.currentPhase = null;
5827
+ this._hasExecuted = false;
5828
+ this._lastResult = null;
5829
+ this._lastResultText = null;
5830
+ this._lastFullOutput = null;
5831
+ this.setStatus("idle");
5832
+ saveSessionId(this.agentId, null);
5833
+ }
5524
5834
  resolveApproval(approvalId, decision) {
5525
5835
  if (approvalId === "__all__") {
5526
5836
  for (const [, pending2] of this.pendingApprovals) {
@@ -5580,7 +5890,8 @@ var AgentManager = class {
5580
5890
  const lines = [];
5581
5891
  for (const session of this.agents.values()) {
5582
5892
  const lead = this.isTeamLead(session.agentId) ? " (Team Lead)" : "";
5583
- const result = session.lastResult ? ` \u2014 ${session.lastResult}` : "";
5893
+ const raw = session.lastResult ?? "";
5894
+ const result = raw ? ` \u2014 ${raw.length > 100 ? raw.slice(0, 100) + "\u2026" : raw}` : "";
5584
5895
  lines.push(`- ${session.name} (${session.role}) [${session.status}]${lead}${result}`);
5585
5896
  }
5586
5897
  return lines.join("\n");
@@ -5626,10 +5937,12 @@ var AgentManager = class {
5626
5937
 
5627
5938
  // ../../packages/orchestrator/src/delegation.ts
5628
5939
  import { nanoid as nanoid3 } from "nanoid";
5940
+ import path4 from "path";
5629
5941
  var MAX_DELEGATION_DEPTH = 5;
5630
5942
  var MAX_TOTAL_DELEGATIONS = 20;
5631
- var DELEGATION_BUDGET_ROUNDS = 5;
5943
+ var DELEGATION_BUDGET_ROUNDS = 7;
5632
5944
  var HARD_CEILING_ROUNDS = 10;
5945
+ var MAX_REVIEW_ROUNDS = 3;
5633
5946
  var RESULT_BATCH_WINDOW_MS = 2e4;
5634
5947
  var DelegationRouter = class {
5635
5948
  /** taskId → fromAgentId */
@@ -5642,6 +5955,8 @@ var DelegationRouter = class {
5642
5955
  totalDelegations = 0;
5643
5956
  /** How many times the leader has been invoked to process results */
5644
5957
  leaderRounds = 0;
5958
+ /** How many times a Code Reviewer result has been forwarded to the leader */
5959
+ reviewCount = 0;
5645
5960
  /** When true, all new delegations and result forwarding are blocked */
5646
5961
  stopped = false;
5647
5962
  /** TaskIds created by flushResults — only these can produce a final result */
@@ -5650,6 +5965,8 @@ var DelegationRouter = class {
5650
5965
  delegationsAtResultStart = /* @__PURE__ */ new Map();
5651
5966
  /** Batch result forwarding: originAgentId → pending results + timer */
5652
5967
  pendingResults = /* @__PURE__ */ new Map();
5968
+ /** Team-wide project directory — all delegations use this as repoPath when set */
5969
+ teamProjectDir = null;
5653
5970
  agentManager;
5654
5971
  promptEngine;
5655
5972
  emitEvent;
@@ -5683,7 +6000,7 @@ var DelegationRouter = class {
5683
6000
  * if the current task is not a "resultTask" (safety net for convergence).
5684
6001
  */
5685
6002
  isBudgetExhausted() {
5686
- return this.leaderRounds >= DELEGATION_BUDGET_ROUNDS;
6003
+ return this.leaderRounds >= DELEGATION_BUDGET_ROUNDS || this.reviewCount >= MAX_REVIEW_ROUNDS;
5687
6004
  }
5688
6005
  /**
5689
6006
  * True if the given resultTask completed WITHOUT creating any new delegations.
@@ -5724,6 +6041,16 @@ var DelegationRouter = class {
5724
6041
  }
5725
6042
  this.pendingResults.clear();
5726
6043
  }
6044
+ /**
6045
+ * Set a team-wide project directory. All delegations will use this as repoPath.
6046
+ */
6047
+ setTeamProjectDir(dir) {
6048
+ this.teamProjectDir = dir;
6049
+ if (dir) console.log(`[Delegation] Team project dir set: ${dir}`);
6050
+ }
6051
+ getTeamProjectDir() {
6052
+ return this.teamProjectDir;
6053
+ }
5727
6054
  /**
5728
6055
  * Reset all delegation state (on new team task).
5729
6056
  */
@@ -5735,7 +6062,9 @@ var DelegationRouter = class {
5735
6062
  this.delegationsAtResultStart.clear();
5736
6063
  this.totalDelegations = 0;
5737
6064
  this.leaderRounds = 0;
6065
+ this.reviewCount = 0;
5738
6066
  this.stopped = false;
6067
+ this.teamProjectDir = null;
5739
6068
  for (const pending of this.pendingResults.values()) {
5740
6069
  clearTimeout(pending.timer);
5741
6070
  }
@@ -5744,8 +6073,13 @@ var DelegationRouter = class {
5744
6073
  wireDelegation(session) {
5745
6074
  session.onDelegation = (fromAgentId, targetName, prompt) => {
5746
6075
  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})`);
6076
+ const phaseCheckSession = this.agentManager.get(fromAgentId);
6077
+ if (phaseCheckSession?.currentPhase && phaseCheckSession.currentPhase !== "execute") {
6078
+ console.log(`[Delegation] BLOCKED: agent ${fromAgentId} is in phase "${phaseCheckSession.currentPhase}", not "execute"`);
6079
+ return;
6080
+ }
6081
+ if (this.isBudgetExhausted()) {
6082
+ console.log(`[Delegation] BLOCKED: budget exhausted (leaderRounds=${this.leaderRounds}/${DELEGATION_BUDGET_ROUNDS}, reviewCount=${this.reviewCount}/${MAX_REVIEW_ROUNDS})`);
5749
6083
  return;
5750
6084
  }
5751
6085
  const target = this.agentManager.findByName(targetName);
@@ -5785,14 +6119,27 @@ var DelegationRouter = class {
5785
6119
  const fromSession = this.agentManager.get(fromAgentId);
5786
6120
  const fromName = fromSession?.name ?? fromAgentId;
5787
6121
  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)}`);
6122
+ let repoPath = this.teamProjectDir ?? void 0;
6123
+ let cleanPrompt = prompt;
6124
+ const dirMatch = prompt.match(/^\s*\[([^\]]+)\]\s*/);
6125
+ if (dirMatch) {
6126
+ cleanPrompt = prompt.slice(dirMatch[0].length);
6127
+ if (!repoPath) {
6128
+ const dirPart = dirMatch[1].replace(/\/$/, "");
6129
+ const leaderSession = this.agentManager.get(fromAgentId);
6130
+ if (leaderSession) {
6131
+ repoPath = path4.resolve(leaderSession.workspaceDir, dirPart);
6132
+ }
6133
+ }
6134
+ }
6135
+ const fullPrompt = this.promptEngine.render("delegation-prefix", { fromName, fromRole, prompt: cleanPrompt });
6136
+ console.log(`[Delegation] ${fromAgentId} -> ${target.agentId} (${targetName}) depth=${newDepth} total=${this.totalDelegations} repoPath=${repoPath ?? "default"}: ${cleanPrompt.slice(0, 80)}`);
5790
6137
  this.emitEvent({
5791
6138
  type: "task:delegated",
5792
6139
  fromAgentId,
5793
6140
  toAgentId: target.agentId,
5794
6141
  taskId,
5795
- prompt
6142
+ prompt: cleanPrompt
5796
6143
  });
5797
6144
  this.emitEvent({
5798
6145
  type: "team:chat",
@@ -5804,7 +6151,7 @@ var DelegationRouter = class {
5804
6151
  timestamp: Date.now()
5805
6152
  });
5806
6153
  this.assignedTask.set(target.agentId, taskId);
5807
- target.runTask(taskId, fullPrompt);
6154
+ target.runTask(taskId, fullPrompt, repoPath);
5808
6155
  };
5809
6156
  }
5810
6157
  wireResultForwarding(session) {
@@ -5879,6 +6226,13 @@ var DelegationRouter = class {
5879
6226
  const originSession = this.agentManager.get(originAgentId);
5880
6227
  if (!originSession) return;
5881
6228
  this.leaderRounds++;
6229
+ for (const r of pending.results) {
6230
+ const agent = this.agentManager.findByName(r.fromName);
6231
+ if (agent && agent.role.toLowerCase().includes("review")) {
6232
+ this.reviewCount++;
6233
+ console.log(`[ResultBatch] Reviewer result detected (reviewCount=${this.reviewCount})`);
6234
+ }
6235
+ }
5882
6236
  if (this.leaderRounds > HARD_CEILING_ROUNDS) {
5883
6237
  console.log(`[ResultBatch] Hard ceiling reached (${HARD_CEILING_ROUNDS} rounds). Force-completing.`);
5884
6238
  const resultLines2 = pending.results.map(
@@ -5908,12 +6262,13 @@ ${resultLines2}`,
5908
6262
  }
5909
6263
  let roundInfo;
5910
6264
  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.`;
6265
+ const reviewExhausted = this.reviewCount >= MAX_REVIEW_ROUNDS;
6266
+ if (budgetExhausted || reviewExhausted) {
6267
+ 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.`;
6268
+ } else if (this.reviewCount > 0) {
6269
+ roundInfo = `Round ${this.leaderRounds}/${DELEGATION_BUDGET_ROUNDS} | Review ${this.reviewCount}/${MAX_REVIEW_ROUNDS} (fix iteration ${this.reviewCount})`;
5915
6270
  } else {
5916
- roundInfo = `Round ${this.leaderRounds}/${DELEGATION_BUDGET_ROUNDS}`;
6271
+ roundInfo = `Round ${this.leaderRounds}/${DELEGATION_BUDGET_ROUNDS} | No reviews yet`;
5917
6272
  }
5918
6273
  const resultLines = pending.results.map(
5919
6274
  (r) => `- ${r.fromName} (${r.statusWord}): ${r.summary}`
@@ -5935,8 +6290,8 @@ ${resultLines2}`,
5935
6290
  };
5936
6291
 
5937
6292
  // ../../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";
6293
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync4 } from "fs";
6294
+ import path5 from "path";
5940
6295
  var PROMPT_DEFAULTS = {
5941
6296
  "leader-initial": `You are {{name}}, the Team Lead. {{personality}}
5942
6297
  You CANNOT write code, run commands, or use any tools. You can ONLY delegate.
@@ -5945,22 +6300,34 @@ Team:
5945
6300
  {{teamRoster}}
5946
6301
 
5947
6302
  Delegate using this exact format (one per line):
5948
- @AgentName: [project-dir] task description
6303
+ @AgentName: task description
6304
+
6305
+ 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.
6306
+
6307
+ ===== DELEGATION RULES =====
6308
+
6309
+ CRITICAL \u2014 How to assign work to developers:
6310
+ - Give each developer ONE complete, end-to-end task that produces a RUNNABLE deliverable.
6311
+ - The developer is responsible for EVERYTHING: project setup, dependencies, all source files, build configuration, and verification.
6312
+ - 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.
6313
+ - 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."
6314
+ - WRONG example: "@Leo: Create src/audio/AudioManager.ts" then "@Leo: Create src/game/GameScene.ts" \u2014 this produces isolated modules that can't run.
6315
+ - If you have multiple developers, split by FEATURE AREA (each producing a runnable piece), not by FILE.
5949
6316
 
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.
6317
+ ===== EXECUTION PHASES =====
5951
6318
 
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.
6319
+ 1. BUILD (this round): Assign developers now. Each dev must deliver a working, verifiable result.
6320
+ 2. REVIEW: When dev results come back, assign Code Reviewer to check the code.
6321
+ 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.
6322
+ 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
6323
 
5957
6324
  Rules:
5958
6325
  - 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.
6326
+ - Phase 1 (this round): Assign developers ONLY. Do NOT assign Code Reviewer yet \u2014 there is no code to review.
6327
+ - Skip review for trivial changes (config, typo, rename).
6328
+
6329
+ Approved plan:
6330
+ {{originalTask}}
5964
6331
 
5965
6332
  Task: {{prompt}}`,
5966
6333
  "leader-continue": `You are {{name}}, the Team Lead. {{personality}}
@@ -5974,7 +6341,7 @@ Team status:
5974
6341
  Delegate using: @AgentName: task description
5975
6342
 
5976
6343
  {{prompt}}`,
5977
- "leader-result": `You are the Team Lead. You CANNOT write or fix code. You can ONLY delegate using @Name: [project-dir] <task>.
6344
+ "leader-result": `You are the Team Lead. You CANNOT write or fix code. You can ONLY delegate using @Name: <task>.
5978
6345
 
5979
6346
  Original user task: {{originalTask}}
5980
6347
 
@@ -5986,21 +6353,47 @@ Team status:
5986
6353
  New result from {{fromName}} ({{resultStatus}}):
5987
6354
  {{resultSummary}}
5988
6355
 
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.
6356
+ ===== DECISION FLOW =====
6357
+
6358
+ Check WHO sent this result, then follow the matching branch:
6359
+
6360
+ \u2500\u2500 RESULT FROM A DEVELOPER \u2500\u2500
6361
+ If STATUS=done:
6362
+ \u2192 Assign Code Reviewer to check the code. In your delegation, include:
6363
+ 1. Dev's ENTRY_FILE/PREVIEW_CMD so reviewer knows what was built.
6364
+ 2. The KEY FEATURES from the approved plan (3-5 bullet points) so reviewer can verify feature completeness.
6365
+ \u2192 Exception: skip review for trivial changes (config, typo, rename) \u2014 go straight to FINAL SUMMARY.
6366
+ If STATUS=failed:
6367
+ \u2192 Delegate ONE targeted fix to the same developer. Be specific about what failed.
6368
+
6369
+ \u2500\u2500 RESULT FROM CODE REVIEWER \u2500\u2500
6370
+ Reviewer output format: VERDICT (PASS/FAIL), ISSUES (numbered list), SUGGESTIONS (optional).
6371
+ If VERDICT=PASS:
6372
+ \u2192 Output FINAL SUMMARY. Copy ENTRY_FILE/PREVIEW fields from the developer's last report. You are DONE.
6373
+ If VERDICT=FAIL:
6374
+ \u2192 Collect ALL issues into ONE fix delegation to the original developer.
6375
+ \u2192 Quote each issue verbatim. Remind dev: after fixing, rebuild and verify the deliverable works.
6376
+ \u2192 After dev returns with the fix, assign Code Reviewer again to re-check.
6377
+
6378
+ \u2500\u2500 SPECIAL CASES \u2500\u2500
6379
+ \u2022 If roundInfo says "REVIEW LIMIT REACHED" or "BUDGET REACHED" \u2192 Output FINAL SUMMARY immediately. Accept the work as-is.
6380
+ \u2022 Permanent blocker (auth error, missing API key, service down) \u2192 report to the user, do not retry.
6381
+ \u2022 Same error repeated twice \u2192 STOP and report to the user.
5997
6382
 
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.`,
6383
+ ===== FINAL SUMMARY FORMAT =====
6384
+ (Copy preview fields EXACTLY from the developer's LAST successful report. Only include fields the dev actually provided \u2014 do NOT invent values.)
6385
+
6386
+ ENTRY_FILE: <from dev \u2014 e.g. index.html, dist/index.html. OMIT if dev didn't provide one>
6387
+ PREVIEW_CMD: <from dev \u2014 e.g. "python app.py", "node server.js". OMIT if dev didn't provide one>
6388
+ PREVIEW_PORT: <from dev \u2014 e.g. 5000, 3000. OMIT if dev didn't provide one>
6389
+ SUMMARY: <2-3 sentence description of what was built>
6390
+
6391
+ RULES:
6392
+ - VERDICT=PASS means done, even if SUGGESTIONS exist. Suggestions are non-blocking.
6393
+ - VERDICT=FAIL means real bugs \u2014 always delegate a fix before finalizing.
6394
+ - In every fix delegation, remind dev to rebuild and re-test before reporting.
6395
+ - You MUST include ENTRY_FILE or PREVIEW_CMD in your FINAL SUMMARY \u2014 the user needs this to preview.
6396
+ - Do NOT include PROJECT_DIR \u2014 the system manages project directories automatically.`,
6004
6397
  "worker-initial": `Your name is {{name}}, your role is {{role}}. {{personality}}
6005
6398
 
6006
6399
  CONVERGENCE RULES (follow strictly):
@@ -6008,24 +6401,197 @@ CONVERGENCE RULES (follow strictly):
6008
6401
  - Only touch files directly required by this task. Do NOT refactor, clean up, or "improve" unrelated code.
6009
6402
  - If you are uncertain between two approaches, choose the simpler one and note it in SUMMARY.
6010
6403
  - 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
6404
 
6013
6405
  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.
6406
+ - Do NOT start any long-running dev server or file server. The system handles preview serving automatically.
6407
+ - You MAY install dependencies if the project needs them (npm install, pip install, etc.).
6408
+ {{soloHint}}
6409
+
6410
+ Start with one sentence describing your approach. Then do the work.
6411
+
6412
+ You are responsible for the COMPLETE deliverable \u2014 not just source files. This means:
6413
+ 1. Project setup: create all config files needed (package.json, tsconfig, build config, requirements.txt, etc.)
6414
+ 2. All source code
6415
+ 3. Build & verify: if the project has a build step, RUN IT and fix errors until it passes
6416
+ 4. Report how to run/preview the result (see deliverable types below)
6417
+
6418
+ VERIFICATION (MANDATORY before reporting STATUS: done):
6419
+ - If you created a package.json with a build script \u2192 run the build, fix errors until it succeeds, confirm the output file exists
6420
+ - If your deliverable is an HTML file \u2192 confirm it exists and references valid scripts/styles
6421
+ - 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)
6422
+ - If NONE of the above apply \u2192 at minimum list the files and confirm the entry point exists
6423
+ - 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.
6424
+ - 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.
6425
+ - Do NOT report STATUS: done unless verification passes. Fix problems yourself first.
6426
+ - STATUS: failed is ONLY for truly unsolvable problems (missing API keys, no network, system-level issues).
6427
+
6428
+ ===== DELIVERABLE TYPES =====
6429
+ Your project falls into one of these categories. Report the matching fields:
6430
+
6431
+ A) STATIC WEB (HTML/CSS/JS \u2014 no server needed):
6432
+ ENTRY_FILE: index.html (the HTML file to open \u2014 e.g. index.html, dist/index.html, build/index.html)
6016
6433
 
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.
6434
+ B) WEB SERVER (Flask, Express, Sinatra, Rails, Go net/http, etc. \u2014 serves on a port):
6435
+ PREVIEW_CMD: python app.py (the command to start the server)
6436
+ PREVIEW_PORT: 5000 (the port the server listens on \u2014 REQUIRED for web servers)
6437
+
6438
+ C) DESKTOP/CLI APP (Pygame, Tkinter, Electron, JavaFX, terminal tool, native GUI, etc.):
6439
+ PREVIEW_CMD: python game.py (the command to launch the app \u2014 NO PREVIEW_PORT needed)
6440
+
6441
+ OUTPUT:
6018
6442
 
6019
- When you finish, report your result in this exact format:
6020
6443
  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)
6444
+ FILES_CHANGED: (list all files created or modified, one per line)
6445
+ ENTRY_FILE: (type A only \u2014 path to the HTML file)
6446
+ PREVIEW_CMD: (types B and C \u2014 command to start the app or server)
6447
+ PREVIEW_PORT: (type B only \u2014 the port the server listens on)
6448
+ SUMMARY: (one sentence: what you built + how to run/preview it)
6449
+
6450
+ You MUST provide at least ENTRY_FILE or PREVIEW_CMD.
6451
+
6452
+ {{prompt}}`,
6453
+ "worker-reviewer-initial": `Your name is {{name}}, your role is {{role}}. {{personality}}
6454
+
6455
+ CONVERGENCE RULES (follow strictly):
6456
+ - Do the MINIMUM needed to satisfy the task. Simple and working beats perfect and slow.
6457
+ - Only touch files directly required by this task. Do NOT refactor, clean up, or "improve" unrelated code.
6458
+ - If you are uncertain between two approaches, choose the simpler one and note it in SUMMARY.
6459
+ - Do NOT add features, error handling, or improvements that were not explicitly asked for.
6460
+
6461
+ HARD LIMITS:
6462
+ - Do NOT start any long-running dev server or file server. The system handles preview serving automatically.
6463
+ - 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.
6464
+
6465
+ Code Quality (must check):
6466
+ - Correctness, security vulnerabilities, crashes, broken logic.
6467
+ - 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.
6468
+
6469
+ Feature Completeness (must check):
6470
+ - Compare the deliverable against the key features listed in your task assignment.
6471
+ - Flag CORE features that are completely missing or non-functional as ISSUES.
6472
+ - Do NOT fail for polish, extras, or stretch goals \u2014 this is a prototype. Focus on whether the main functionality works.
6473
+
6474
+ Do NOT nitpick style, naming, or formatting. This is a prototype, not production code.
6475
+
6476
+ VERDICT: PASS | FAIL
6477
+ - PASS = code runs without crashes AND core features are implemented (even if rough)
6478
+ - FAIL = crashes/bugs that prevent usage OR core features are missing/broken
6479
+ ISSUES: (numbered list \u2014 bugs, security problems, or missing core features)
6480
+ SUGGESTIONS: (optional \u2014 minor non-blocking observations, keep brief)
6481
+ SUMMARY: (one sentence overall assessment)
6023
6482
 
6024
6483
  {{prompt}}`,
6025
6484
  "worker-continue": `{{prompt}}`,
6026
6485
  "delegation-prefix": `[Assigned by {{fromName}} ({{fromRole}})]
6027
6486
  {{prompt}}`,
6028
- "delegation-hint": `To delegate a task to another agent, output on its own line: @AgentName: <task description>`
6487
+ "delegation-hint": `To delegate a task to another agent, output on its own line: @AgentName: <task description>`,
6488
+ "leader-create": `You are {{name}}, the team's Creative Director and Product Consultant. {{personality}}
6489
+ You are starting a new project conversation with the user. Your dual role:
6490
+ 1. CREATIVE DIRECTOR \u2014 design the product vision: theme, look & feel, user experience, what makes it unique
6491
+ 2. PRODUCT CONSULTANT \u2014 turn that vision into a clear, buildable plan
6492
+
6493
+ Rules:
6494
+ - Be conversational, warm, and concise.
6495
+ - Ask at most 1-2 clarifying questions, then produce a plan. Do NOT over-question.
6496
+ - 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.
6497
+ - The goal is a WORKING PROTOTYPE, not a production system.
6498
+ - When ready, produce a project plan wrapped in [PLAN]...[/PLAN] tags.
6499
+
6500
+ ===== PLAN FORMAT (strict \u2014 follow this structure) =====
6501
+
6502
+ [PLAN]
6503
+ 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")
6504
+
6505
+ CREATIVE VISION:
6506
+ - Theme & setting (e.g. "pixel cityscape at night", "cozy forest caf\xE9")
6507
+ - Visual style (e.g. "retro pixel art", "flat minimalist", "hand-drawn sketch")
6508
+ - Core experience \u2014 what does the user SEE and FEEL when using it?
6509
+
6510
+ FEATURES:
6511
+ - (3-5 bullet points describing WHAT the product does from the user's perspective)
6512
+ - (focus on interactions, content, and behavior \u2014 NOT technical implementation)
6513
+
6514
+ TECH: (one line \u2014 e.g. "Vanilla JS + Canvas" or "React + Tailwind")
6515
+
6516
+ ASSIGNMENTS:
6517
+ - @DevName: (one-sentence summary of what they build)
6518
+ [/PLAN]
6519
+
6520
+ ===== ANTI-PATTERNS (never do these) =====
6521
+ - Do NOT write technical implementation steps (e.g. "implement game loop with requestAnimationFrame") \u2014 that is the developer's job.
6522
+ - 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.
6523
+ - Do NOT produce a checklist of modules or files. The plan is a PRODUCT DESCRIPTION, not a technical spec.
6524
+ - Do NOT include milestones, risk analysis, acceptance criteria, or deployment plans.
6525
+ - Do NOT delegate. Do NOT write code. Do NOT use @AgentName: syntax outside [PLAN] tags.
6526
+
6527
+ If the user hasn't described their project yet, greet them and ask what they'd like to build.
6528
+
6529
+ Team:
6530
+ {{teamRoster}}
6531
+
6532
+ {{prompt}}`,
6533
+ "leader-create-continue": `You are {{name}}, the team's Creative Director and Product Consultant. {{personality}}
6534
+ Do NOT greet or re-introduce yourself \u2014 the conversation is already underway.
6535
+
6536
+ The user replied: {{prompt}}
6537
+
6538
+ 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.
6539
+
6540
+ 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.`,
6541
+ "leader-design": `You are {{name}}, the team's Creative Director, refining the project vision with the user. {{personality}}
6542
+ The user has given feedback on your plan. Your job is to REVISE the existing plan, not start over.
6543
+
6544
+ ===== CRITICAL: INCREMENTAL UPDATE =====
6545
+ - User feedback is usually a PARTIAL adjustment (e.g. "use PixiJS", "make it darker", "add multiplayer").
6546
+ - Apply the feedback to the EXISTING plan. Keep everything the user did NOT mention.
6547
+ - NEVER discard the original concept, story, characters, or gameplay just because the user asked for a tech or style change.
6548
+ - 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.
6549
+ - Think of it as editing a document, not writing a new one.
6550
+
6551
+ Rules:
6552
+ - Address the user's feedback directly and show what changed.
6553
+ - Always output the updated plan in [PLAN]...[/PLAN] tags using the standard format: CONCEPT, CREATIVE VISION, FEATURES, TECH, ASSIGNMENTS.
6554
+ - The plan describes the PRODUCT VISION \u2014 what users see, feel, and experience. NOT technical implementation steps.
6555
+ - Keep it prototype-focused. No milestones, risk analysis, or deployment plans.
6556
+ - Do NOT delegate. Do NOT write code. Do NOT use @AgentName: syntax outside [PLAN] tags.
6557
+
6558
+ Team:
6559
+ {{teamRoster}}
6560
+
6561
+ Previous plan context: {{originalTask}}
6562
+
6563
+ User feedback: {{prompt}}`,
6564
+ "leader-design-continue": `You are {{name}}, the team's Creative Director, refining the project vision. {{personality}}
6565
+
6566
+ Your current plan:
6567
+ {{originalTask}}
6568
+
6569
+ The user replied: {{prompt}}
6570
+
6571
+ 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.
6572
+
6573
+ 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.`,
6574
+ "leader-complete": `You are {{name}}, presenting completed work to the user. {{personality}}
6575
+ The team has finished executing the project. Summarize what was accomplished and ask if the user wants any changes.
6576
+
6577
+ Rules:
6578
+ - Be concise and highlight key outcomes.
6579
+ - If the user provides feedback, note it \u2014 the system will transition back to execute phase.
6580
+ - Do NOT delegate. Do NOT write code. Do NOT use @AgentName: syntax.
6581
+
6582
+ Team:
6583
+ {{teamRoster}}
6584
+
6585
+ Original task: {{originalTask}}
6586
+
6587
+ {{prompt}}`,
6588
+ "leader-complete-continue": `You are {{name}}, discussing the completed project with the user. {{personality}}
6589
+
6590
+ Original task: {{originalTask}}
6591
+
6592
+ The user replied: {{prompt}}
6593
+
6594
+ Address their feedback. Do NOT delegate or write code.`
6029
6595
  };
6030
6596
  var PromptEngine = class {
6031
6597
  templates = { ...PROMPT_DEFAULTS };
@@ -6035,21 +6601,24 @@ var PromptEngine = class {
6035
6601
  }
6036
6602
  /**
6037
6603
  * 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).
6604
+ * Always writes built-in defaults to disk so new/updated templates take effect.
6605
+ * Users can still customize edits are preserved until the next code update changes a template.
6040
6606
  */
6041
6607
  init() {
6042
6608
  if (!this.promptsDir) {
6043
6609
  console.log(`[Prompts] No promptsDir configured, using ${Object.keys(PROMPT_DEFAULTS).length} default templates`);
6044
6610
  return;
6045
6611
  }
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`);
6612
+ if (!existsSync4(this.promptsDir)) {
6613
+ mkdirSync4(this.promptsDir, { recursive: true });
6614
+ }
6615
+ let written = 0;
6616
+ for (const [name, content] of Object.entries(PROMPT_DEFAULTS)) {
6617
+ const filePath = path5.join(this.promptsDir, `${name}.md`);
6618
+ writeFileSync4(filePath, content, "utf-8");
6619
+ written++;
6052
6620
  }
6621
+ console.log(`[Prompts] Synced ${written} default templates to ${this.promptsDir}`);
6053
6622
  this.reload();
6054
6623
  }
6055
6624
  /**
@@ -6061,10 +6630,10 @@ var PromptEngine = class {
6061
6630
  let defaulted = 0;
6062
6631
  if (this.promptsDir) {
6063
6632
  for (const name of Object.keys(PROMPT_DEFAULTS)) {
6064
- const filePath = path3.join(this.promptsDir, `${name}.md`);
6065
- if (existsSync3(filePath)) {
6633
+ const filePath = path5.join(this.promptsDir, `${name}.md`);
6634
+ if (existsSync4(filePath)) {
6066
6635
  try {
6067
- merged[name] = readFileSync3(filePath, "utf-8");
6636
+ merged[name] = readFileSync4(filePath, "utf-8");
6068
6637
  loaded++;
6069
6638
  } catch {
6070
6639
  defaulted++;
@@ -6191,7 +6760,7 @@ IMPORTANT: If the same error keeps repeating, choose option 3. Do not waste reso
6191
6760
 
6192
6761
  // ../../packages/orchestrator/src/worktree.ts
6193
6762
  import { execSync as execSync3 } from "child_process";
6194
- import path4 from "path";
6763
+ import path6 from "path";
6195
6764
  var TIMEOUT = 5e3;
6196
6765
  function isGitRepo(cwd) {
6197
6766
  try {
@@ -6203,9 +6772,9 @@ function isGitRepo(cwd) {
6203
6772
  }
6204
6773
  function createWorktree(workspace, agentId, taskId, agentName) {
6205
6774
  if (!isGitRepo(workspace)) return null;
6206
- const worktreeDir = path4.join(workspace, ".worktrees");
6775
+ const worktreeDir = path6.join(workspace, ".worktrees");
6207
6776
  const worktreeName = `${agentId}-${taskId}`;
6208
- const worktreePath = path4.join(worktreeDir, worktreeName);
6777
+ const worktreePath = path6.join(worktreeDir, worktreeName);
6209
6778
  const branch = `agent/${agentName.toLowerCase().replace(/\s+/g, "-")}/${taskId}`;
6210
6779
  try {
6211
6780
  execSync3(`git worktree add "${worktreePath}" -b "${branch}"`, {
@@ -6251,7 +6820,7 @@ function mergeWorktree(workspace, worktreePath, branch) {
6251
6820
  }
6252
6821
  }
6253
6822
  function removeWorktree(worktreePath, branch, workspace) {
6254
- const cwd = workspace ?? path4.dirname(path4.dirname(worktreePath));
6823
+ const cwd = workspace ?? path6.dirname(path6.dirname(worktreePath));
6255
6824
  try {
6256
6825
  execSync3(`git worktree remove --force "${worktreePath}"`, { cwd, stdio: "pipe", timeout: TIMEOUT });
6257
6826
  } catch {
@@ -6274,8 +6843,12 @@ var Orchestrator = class extends EventEmitter {
6274
6843
  sandboxMode;
6275
6844
  worktreeEnabled;
6276
6845
  worktreeMerge;
6277
- /** Preview captured from the first dev worker that produces one — not from QA/reviewer */
6846
+ /** Preview info captured from the first dev worker that produces one — not from QA/reviewer */
6278
6847
  teamPreview = null;
6848
+ /** Accumulated changedFiles from all workers in the current team session */
6849
+ teamChangedFiles = /* @__PURE__ */ new Set();
6850
+ /** Guard against emitting isFinalResult more than once per execute cycle. */
6851
+ teamFinalized = false;
6279
6852
  constructor(opts) {
6280
6853
  super();
6281
6854
  this.workspace = opts.workspace;
@@ -6402,9 +6975,15 @@ var Orchestrator = class extends EventEmitter {
6402
6975
  return;
6403
6976
  }
6404
6977
  if (this.agentManager.isTeamLead(agentId) && !this.delegationRouter.isDelegated(taskId)) {
6405
- session.originalTask = prompt;
6978
+ if (!session.originalTask || !opts?.phaseOverride || opts.phaseOverride !== "execute" && opts.phaseOverride !== "design" && opts.phaseOverride !== "complete") {
6979
+ session.originalTask = prompt;
6980
+ }
6981
+ const savedProjectDir = this.delegationRouter.getTeamProjectDir();
6406
6982
  this.delegationRouter.clearAll();
6983
+ if (savedProjectDir) this.delegationRouter.setTeamProjectDir(savedProjectDir);
6407
6984
  this.teamPreview = null;
6985
+ this.teamChangedFiles.clear();
6986
+ this.teamFinalized = false;
6408
6987
  }
6409
6988
  this.retryTracker?.track(taskId, prompt);
6410
6989
  if (this.worktreeEnabled && !session.worktreePath) {
@@ -6424,14 +7003,7 @@ var Orchestrator = class extends EventEmitter {
6424
7003
  }
6425
7004
  const repoPath = session.worktreePath ?? opts?.repoPath;
6426
7005
  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
- );
7006
+ session.runTask(taskId, prompt, repoPath, teamContext, true, opts?.phaseOverride);
6435
7007
  }
6436
7008
  cancelTask(agentId) {
6437
7009
  const session = this.agentManager.get(agentId);
@@ -6491,7 +7063,7 @@ var Orchestrator = class extends EventEmitter {
6491
7063
  getAgent(agentId) {
6492
7064
  const s = this.agentManager.get(agentId);
6493
7065
  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 };
7066
+ return { agentId: s.agentId, name: s.name, role: s.role, status: s.status, palette: s.palette, backend: s.backend.id, pid: s.pid };
6495
7067
  }
6496
7068
  getAllAgents() {
6497
7069
  return this.agentManager.getAll().map((s) => ({
@@ -6501,6 +7073,7 @@ var Orchestrator = class extends EventEmitter {
6501
7073
  status: s.status,
6502
7074
  palette: s.palette,
6503
7075
  backend: s.backend.id,
7076
+ pid: s.pid,
6504
7077
  isTeamLead: this.agentManager.isTeamLead(s.agentId),
6505
7078
  teamId: s.teamId
6506
7079
  }));
@@ -6508,9 +7081,51 @@ var Orchestrator = class extends EventEmitter {
6508
7081
  getTeamRoster() {
6509
7082
  return this.agentManager.getTeamRoster();
6510
7083
  }
7084
+ /** Return PIDs of all managed (gateway-spawned) agent processes */
7085
+ getManagedPids() {
7086
+ const pids = [];
7087
+ for (const session of this.agentManager.getAll()) {
7088
+ const pid = session.pid;
7089
+ if (pid !== null) pids.push(pid);
7090
+ }
7091
+ return pids;
7092
+ }
6511
7093
  isTeamLead(agentId) {
6512
7094
  return this.agentManager.isTeamLead(agentId);
6513
7095
  }
7096
+ /** Get the leader's last full output (used to capture the approved plan). */
7097
+ getLeaderLastOutput(agentId) {
7098
+ const session = this.agentManager.get(agentId);
7099
+ return session?.lastFullOutput ?? null;
7100
+ }
7101
+ /** Set team-wide project directory — all delegations will use this as cwd. */
7102
+ setTeamProjectDir(dir) {
7103
+ this.delegationRouter.setTeamProjectDir(dir);
7104
+ }
7105
+ getTeamProjectDir() {
7106
+ return this.delegationRouter.getTeamProjectDir();
7107
+ }
7108
+ /** Set the original task context for the leader (e.g. the approved plan). */
7109
+ setOriginalTask(agentId, task) {
7110
+ const session = this.agentManager.get(agentId);
7111
+ if (session) session.originalTask = task;
7112
+ }
7113
+ /** Clear ALL team members' conversation history for a fresh project cycle. */
7114
+ clearLeaderHistory(agentId) {
7115
+ const session = this.agentManager.get(agentId);
7116
+ if (session) {
7117
+ session.clearHistory();
7118
+ for (const agent of this.agentManager.getAll()) {
7119
+ if (agent.agentId !== agentId) {
7120
+ agent.clearHistory();
7121
+ }
7122
+ }
7123
+ this.delegationRouter.clearAll();
7124
+ this.teamPreview = null;
7125
+ this.teamChangedFiles.clear();
7126
+ this.teamFinalized = false;
7127
+ }
7128
+ }
6514
7129
  // ---------------------------------------------------------------------------
6515
7130
  // Cleanup
6516
7131
  // ---------------------------------------------------------------------------
@@ -6586,28 +7201,84 @@ var Orchestrator = class extends EventEmitter {
6586
7201
  session.worktreeBranch = null;
6587
7202
  }
6588
7203
  this.retryTracker?.clear(event.taskId);
6589
- if (!this.agentManager.isTeamLead(agentId) && !this.teamPreview) {
7204
+ if (!this.agentManager.isTeamLead(agentId) && event.result?.changedFiles) {
7205
+ for (const f of event.result.changedFiles) {
7206
+ this.teamChangedFiles.add(f);
7207
+ }
7208
+ }
7209
+ if (!this.agentManager.isTeamLead(agentId)) {
6590
7210
  const role = session?.role?.toLowerCase() ?? "";
6591
- const isDevWorker = !role.includes("qa") && !role.includes("tester") && !role.includes("review");
6592
- if (isDevWorker && event.result?.previewUrl) {
7211
+ const isDevWorker = !role.includes("review");
7212
+ if (isDevWorker && event.result && (event.result.previewUrl || event.result.entryFile || event.result.previewCmd)) {
6593
7213
  this.teamPreview = {
6594
7214
  previewUrl: event.result.previewUrl,
6595
- previewPath: event.result.previewPath
7215
+ previewPath: event.result.previewPath,
7216
+ entryFile: event.result.entryFile,
7217
+ previewCmd: event.result.previewCmd,
7218
+ previewPort: event.result.previewPort
6596
7219
  };
6597
- console.log(`[Orchestrator] Preview captured from ${session?.name}: ${this.teamPreview.previewUrl}`);
7220
+ console.log(`[Orchestrator] Preview captured from ${session?.name}: url=${this.teamPreview.previewUrl}, entry=${this.teamPreview.entryFile}, cmd=${this.teamPreview.previewCmd}`);
6598
7221
  }
6599
7222
  }
6600
7223
  if (this.agentManager.isTeamLead(agentId)) {
6601
7224
  const isResultTask = this.delegationRouter.isResultTask(event.taskId);
6602
7225
  const leaderDidNotDelegateNewWork = isResultTask && this.delegationRouter.resultTaskDidNotDelegate(event.taskId);
6603
7226
  const budgetForced = this.delegationRouter.isBudgetExhausted() && !this.delegationRouter.hasPendingFrom(agentId);
6604
- const shouldFinalize = leaderDidNotDelegateNewWork || budgetForced;
7227
+ const hasWorkingWorkers = this.agentManager.getAll().some(
7228
+ (w) => w.agentId !== agentId && w.status === "working"
7229
+ );
7230
+ if (hasWorkingWorkers && !budgetForced) {
7231
+ console.log(`[Orchestrator] Deferring finalization \u2014 workers still running`);
7232
+ }
7233
+ const shouldFinalize = (leaderDidNotDelegateNewWork || budgetForced) && !this.teamFinalized && (!hasWorkingWorkers || budgetForced);
6605
7234
  if (shouldFinalize) {
7235
+ this.teamFinalized = true;
6606
7236
  event.isFinalResult = true;
6607
7237
  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;
7238
+ if (event.result && this.teamChangedFiles.size > 0) {
7239
+ const merged = new Set(event.result.changedFiles ?? []);
7240
+ for (const f of this.teamChangedFiles) merged.add(f);
7241
+ event.result.changedFiles = Array.from(merged);
7242
+ }
7243
+ if (event.result) {
7244
+ const teamDir = this.delegationRouter.getTeamProjectDir();
7245
+ if (teamDir) {
7246
+ event.result.projectDir = teamDir;
7247
+ }
7248
+ }
7249
+ if (this.teamPreview && event.result) {
7250
+ if (this.teamPreview.previewUrl) {
7251
+ event.result.previewUrl = this.teamPreview.previewUrl;
7252
+ event.result.previewPath = this.teamPreview.previewPath;
7253
+ }
7254
+ if (this.teamPreview.entryFile) event.result.entryFile = this.teamPreview.entryFile;
7255
+ if (this.teamPreview.previewCmd) event.result.previewCmd = this.teamPreview.previewCmd;
7256
+ if (this.teamPreview.previewPort) event.result.previewPort = this.teamPreview.previewPort;
7257
+ }
7258
+ if (event.result?.entryFile) {
7259
+ const projectDir = this.delegationRouter.getTeamProjectDir() ?? this.workspace;
7260
+ const absEntry = path7.isAbsolute(event.result.entryFile) ? event.result.entryFile : path7.join(projectDir, event.result.entryFile);
7261
+ if (!existsSync5(absEntry)) {
7262
+ const allFiles = event.result.changedFiles ?? [];
7263
+ const ext = path7.extname(event.result.entryFile).toLowerCase();
7264
+ const candidate = allFiles.map((f) => path7.basename(f)).find((f) => path7.extname(f).toLowerCase() === ext);
7265
+ if (candidate) {
7266
+ console.log(`[Orchestrator] entryFile "${event.result.entryFile}" not found on disk, using "${candidate}" from changedFiles`);
7267
+ event.result.entryFile = candidate;
7268
+ } else {
7269
+ console.log(`[Orchestrator] entryFile "${event.result.entryFile}" not found on disk, clearing`);
7270
+ event.result.entryFile = void 0;
7271
+ }
7272
+ }
7273
+ }
7274
+ if (event.result?.entryFile && !event.result.previewCmd && !/\.html?$/i.test(event.result.entryFile)) {
7275
+ const ext = path7.extname(event.result.entryFile).toLowerCase();
7276
+ const runners = { ".py": "python3", ".js": "node", ".rb": "ruby", ".sh": "bash" };
7277
+ const runner = runners[ext];
7278
+ if (runner) {
7279
+ event.result.previewCmd = `${runner} ${event.result.entryFile}`;
7280
+ console.log(`[Orchestrator] Auto-constructed previewCmd: ${event.result.previewCmd}`);
7281
+ }
6611
7282
  }
6612
7283
  if (!event.result?.previewUrl && event.result) {
6613
7284
  for (const worker of this.agentManager.getAll()) {
@@ -6620,6 +7291,70 @@ var Orchestrator = class extends EventEmitter {
6620
7291
  }
6621
7292
  }
6622
7293
  }
7294
+ if (!event.result?.previewUrl && event.result?.previewCmd) {
7295
+ const projectDir = this.delegationRouter.getTeamProjectDir() ?? this.workspace;
7296
+ if (event.result.previewPort) {
7297
+ const url = previewServer.runCommand(event.result.previewCmd, projectDir, event.result.previewPort);
7298
+ if (url) {
7299
+ event.result.previewUrl = url;
7300
+ console.log(`[Orchestrator] Preview from leader PREVIEW_CMD (port ${event.result.previewPort}): ${url}`);
7301
+ }
7302
+ } else {
7303
+ console.log(`[Orchestrator] Desktop app ready (user can Launch): ${event.result.previewCmd}`);
7304
+ }
7305
+ }
7306
+ if (!event.result?.previewUrl && event.result?.entryFile) {
7307
+ const entryFile = event.result.entryFile;
7308
+ const projectDir = this.delegationRouter.getTeamProjectDir() ?? this.workspace;
7309
+ if (/\.html?$/i.test(entryFile)) {
7310
+ const absPath = path7.isAbsolute(entryFile) ? entryFile : path7.join(projectDir, entryFile);
7311
+ const url = previewServer.serve(absPath);
7312
+ if (url) {
7313
+ event.result.previewUrl = url;
7314
+ event.result.previewPath = absPath;
7315
+ console.log(`[Orchestrator] Preview from leader ENTRY_FILE: ${url}`);
7316
+ }
7317
+ }
7318
+ }
7319
+ if (!event.result?.previewUrl && event.result && this.teamChangedFiles.size > 0) {
7320
+ const projectDir = this.delegationRouter.getTeamProjectDir() ?? this.workspace;
7321
+ const htmlFile = Array.from(this.teamChangedFiles).find((f) => /\.html?$/i.test(f));
7322
+ if (htmlFile) {
7323
+ const absPath = path7.isAbsolute(htmlFile) ? htmlFile : path7.join(projectDir, htmlFile);
7324
+ const url = previewServer.serve(absPath);
7325
+ if (url) {
7326
+ event.result.previewUrl = url;
7327
+ event.result.previewPath = absPath;
7328
+ console.log(`[Orchestrator] Preview from teamChangedFiles: ${url}`);
7329
+ }
7330
+ }
7331
+ }
7332
+ if (!event.result?.previewUrl && event.result) {
7333
+ const projectDir = this.delegationRouter.getTeamProjectDir();
7334
+ if (projectDir) {
7335
+ const candidates = [
7336
+ "dist/index.html",
7337
+ "build/index.html",
7338
+ "out/index.html",
7339
+ // common build dirs
7340
+ "index.html",
7341
+ "public/index.html"
7342
+ // static projects
7343
+ ];
7344
+ for (const candidate of candidates) {
7345
+ const absPath = path7.join(projectDir, candidate);
7346
+ if (existsSync5(absPath)) {
7347
+ const url = previewServer.serve(absPath);
7348
+ if (url) {
7349
+ event.result.previewUrl = url;
7350
+ event.result.previewPath = absPath;
7351
+ console.log(`[Orchestrator] Preview from project scan: ${absPath}`);
7352
+ break;
7353
+ }
7354
+ }
7355
+ }
7356
+ }
7357
+ }
6623
7358
  const summary = event.result?.summary?.slice(0, 200) ?? "All tasks completed.";
6624
7359
  this.emitEvent({
6625
7360
  type: "team:chat",
@@ -6655,14 +7390,481 @@ function createOrchestrator(options) {
6655
7390
 
6656
7391
  // src/index.ts
6657
7392
  import { nanoid as nanoid5 } from "nanoid";
7393
+ import { execFile as execFile3 } from "child_process";
7394
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
7395
+ import path9 from "path";
7396
+ import os2 from "os";
7397
+
7398
+ // src/process-scanner.ts
6658
7399
  import { execFile } from "child_process";
6659
- import { existsSync as existsSync4 } from "fs";
6660
- import path5 from "path";
7400
+ var KNOWN_COMMANDS = ["claude", "codex", "gemini", "aider", "opencode"];
7401
+ var COMMAND_TO_BACKEND = {
7402
+ claude: "claude",
7403
+ codex: "codex",
7404
+ gemini: "gemini",
7405
+ aider: "aider",
7406
+ opencode: "opencode"
7407
+ };
7408
+ function parseEtime(etime) {
7409
+ const now = Date.now();
7410
+ const parts = etime.trim().replace(/-/g, ":").split(":");
7411
+ let seconds = 0;
7412
+ if (parts.length === 4) {
7413
+ seconds = parseInt(parts[0]) * 86400 + parseInt(parts[1]) * 3600 + parseInt(parts[2]) * 60 + parseInt(parts[3]);
7414
+ } else if (parts.length === 3) {
7415
+ seconds = parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(parts[2]);
7416
+ } else if (parts.length === 2) {
7417
+ seconds = parseInt(parts[0]) * 60 + parseInt(parts[1]);
7418
+ }
7419
+ return now - seconds * 1e3;
7420
+ }
7421
+ function exec(cmd, args) {
7422
+ return new Promise((resolve2) => {
7423
+ execFile(cmd, args, { timeout: 5e3, maxBuffer: 1024 * 512 }, (err, stdout) => {
7424
+ resolve2(err ? "" : stdout);
7425
+ });
7426
+ });
7427
+ }
7428
+ async function getCwds(pids) {
7429
+ const result = /* @__PURE__ */ new Map();
7430
+ if (pids.length === 0) return result;
7431
+ const output = await exec("lsof", ["-a", "-p", pids.join(","), "-d", "cwd", "-Fn"]);
7432
+ let currentPid = null;
7433
+ for (const line of output.split("\n")) {
7434
+ if (line.startsWith("p")) {
7435
+ currentPid = parseInt(line.slice(1));
7436
+ } else if (line.startsWith("n") && currentPid !== null) {
7437
+ result.set(currentPid, line.slice(1));
7438
+ }
7439
+ }
7440
+ return result;
7441
+ }
7442
+ var ProcessScanner = class _ProcessScanner {
7443
+ timer = null;
7444
+ previous = /* @__PURE__ */ new Map();
7445
+ getManagedPids;
7446
+ callbacks;
7447
+ /** PIDs to ignore temporarily (recently killed — may still appear in ps) */
7448
+ graceList = /* @__PURE__ */ new Map();
7449
+ // pid → expiry timestamp
7450
+ static GRACE_MS = 15e3;
7451
+ // 15 seconds grace period
7452
+ constructor(getManagedPids, callbacks) {
7453
+ this.getManagedPids = getManagedPids;
7454
+ this.callbacks = callbacks;
7455
+ }
7456
+ /** Mark a PID as recently killed — scanner will ignore it for a grace period */
7457
+ addGracePid(pid) {
7458
+ this.graceList.set(pid, Date.now() + _ProcessScanner.GRACE_MS);
7459
+ }
7460
+ start(intervalMs = 7e3) {
7461
+ this.scan();
7462
+ this.timer = setInterval(() => this.scan(), intervalMs);
7463
+ }
7464
+ stop() {
7465
+ if (this.timer) {
7466
+ clearInterval(this.timer);
7467
+ this.timer = null;
7468
+ }
7469
+ }
7470
+ async scan() {
7471
+ try {
7472
+ const psOutput = await exec("ps", ["-eo", "pid,ppid,pcpu,etime,args"]);
7473
+ const managed = new Set(this.getManagedPids());
7474
+ const now = Date.now();
7475
+ for (const [pid, expiry] of this.graceList) {
7476
+ if (now >= expiry) {
7477
+ this.graceList.delete(pid);
7478
+ } else {
7479
+ managed.add(pid);
7480
+ }
7481
+ }
7482
+ const lines = psOutput.split("\n").slice(1);
7483
+ const candidates = [];
7484
+ for (const line of lines) {
7485
+ const trimmed = line.trim();
7486
+ if (!trimmed) continue;
7487
+ const match = trimmed.match(/^\s*(\d+)\s+(\d+)\s+([\d.]+)\s+([\w:-]+)\s+(.+)$/);
7488
+ if (!match) continue;
7489
+ const pid = parseInt(match[1]);
7490
+ const ppid = parseInt(match[2]);
7491
+ const cpu = parseFloat(match[3]);
7492
+ const etime = match[4];
7493
+ const args = match[5];
7494
+ if (managed.has(pid)) continue;
7495
+ const cmdName = this.matchCommand(args);
7496
+ if (!cmdName) continue;
7497
+ candidates.push({ pid, ppid, cpu, etime, command: cmdName });
7498
+ }
7499
+ const candidatePids = new Set(candidates.map((c) => c.pid));
7500
+ const filtered = candidates.filter((c) => !candidatePids.has(c.ppid));
7501
+ const cwds = await getCwds(filtered.map((c) => c.pid));
7502
+ const current = /* @__PURE__ */ new Map();
7503
+ for (const c of filtered) {
7504
+ const backendId = COMMAND_TO_BACKEND[c.command] ?? c.command;
7505
+ const agentId = `ext-${backendId}-${c.pid}`;
7506
+ const status = c.cpu >= 5 ? "working" : "idle";
7507
+ current.set(agentId, {
7508
+ pid: c.pid,
7509
+ ppid: c.ppid,
7510
+ cpu: c.cpu,
7511
+ command: c.command,
7512
+ backendId,
7513
+ cwd: cwds.get(c.pid) ?? null,
7514
+ startedAt: parseEtime(c.etime),
7515
+ agentId,
7516
+ status
7517
+ });
7518
+ }
7519
+ const added = [];
7520
+ const removed = [];
7521
+ const changed = [];
7522
+ for (const [id, agent] of current) {
7523
+ const prev = this.previous.get(id);
7524
+ if (!prev) {
7525
+ added.push(agent);
7526
+ } else if (prev.status !== agent.status) {
7527
+ changed.push(agent);
7528
+ }
7529
+ }
7530
+ for (const id of this.previous.keys()) {
7531
+ if (!current.has(id)) {
7532
+ removed.push(id);
7533
+ }
7534
+ }
7535
+ this.previous = current;
7536
+ if (added.length > 0) this.callbacks.onAdded(added);
7537
+ if (removed.length > 0) this.callbacks.onRemoved(removed);
7538
+ if (changed.length > 0) this.callbacks.onChanged(changed);
7539
+ } catch (err) {
7540
+ console.error("[ProcessScanner] Scan error:", err);
7541
+ }
7542
+ }
7543
+ matchCommand(args) {
7544
+ for (const cmd of KNOWN_COMMANDS) {
7545
+ const re = new RegExp(`(?:^|/)${cmd}(?:\\s|$)`);
7546
+ if (re.test(args)) return cmd;
7547
+ }
7548
+ return null;
7549
+ }
7550
+ };
7551
+
7552
+ // src/external-output-reader.ts
7553
+ import { watch, readdirSync, statSync, existsSync as existsSync6, openSync, readSync, closeSync } from "fs";
7554
+ import { execFile as execFile2 } from "child_process";
7555
+ import path8 from "path";
6661
7556
  import os from "os";
7557
+ var SOURCE_EXTS = /* @__PURE__ */ new Set([
7558
+ ".ts",
7559
+ ".tsx",
7560
+ ".js",
7561
+ ".jsx",
7562
+ ".py",
7563
+ ".go",
7564
+ ".rs",
7565
+ ".java",
7566
+ ".c",
7567
+ ".cpp",
7568
+ ".h",
7569
+ ".css",
7570
+ ".scss",
7571
+ ".html",
7572
+ ".json",
7573
+ ".yaml",
7574
+ ".yml",
7575
+ ".toml",
7576
+ ".md",
7577
+ ".sql",
7578
+ ".sh",
7579
+ ".rb",
7580
+ ".swift",
7581
+ ".kt"
7582
+ ]);
7583
+ var ExternalOutputReader = class {
7584
+ readers = /* @__PURE__ */ new Map();
7585
+ onStatus = null;
7586
+ onTokenUpdate = null;
7587
+ /** Set a callback to be notified when an external agent's status changes (driven by JSONL entries) */
7588
+ setOnStatus(cb) {
7589
+ this.onStatus = cb;
7590
+ }
7591
+ /** Set a callback to be notified when token usage is detected from JSONL entries */
7592
+ setOnTokenUpdate(cb) {
7593
+ this.onTokenUpdate = cb;
7594
+ }
7595
+ attach(agentId, pid, cwd, backendId, onOutput) {
7596
+ if (this.readers.has(agentId)) return;
7597
+ let cleanup2;
7598
+ if (backendId === "claude" && cwd) {
7599
+ cleanup2 = this.startClaudeReader(agentId, cwd, onOutput);
7600
+ } else {
7601
+ cleanup2 = this.startLsofReader(agentId, pid, onOutput);
7602
+ }
7603
+ this.readers.set(agentId, { agentId, pid, cwd, backendId, onOutput, cleanup: cleanup2, inputTokens: 0, outputTokens: 0 });
7604
+ }
7605
+ detach(agentId) {
7606
+ const reader = this.readers.get(agentId);
7607
+ if (reader) {
7608
+ reader.cleanup();
7609
+ this.readers.delete(agentId);
7610
+ }
7611
+ }
7612
+ detachAll() {
7613
+ for (const [id] of this.readers) {
7614
+ this.detach(id);
7615
+ }
7616
+ }
7617
+ // ── Claude JSONL reader ───────────────────────────────────────
7618
+ startClaudeReader(agentId, cwd, onOutput) {
7619
+ const projectKey = cwd.replace(/\//g, "-");
7620
+ const projectDir = path8.join(os.homedir(), ".claude", "projects", projectKey);
7621
+ console.log(`[OutputReader] Claude reader for ${agentId}: watching ${projectDir}`);
7622
+ let lastPosition = 0;
7623
+ let watchedFile = null;
7624
+ let watcher = null;
7625
+ let lastEmitTime = 0;
7626
+ let pendingChunk = null;
7627
+ let throttleTimer = null;
7628
+ let pollTimer = null;
7629
+ let retryTimer = null;
7630
+ let stopped = false;
7631
+ let retryCount = 0;
7632
+ const THROTTLE_MS = 2e3;
7633
+ const POLL_MS = 3e3;
7634
+ const MAX_RETRIES = 5;
7635
+ const emitThrottled = (text) => {
7636
+ const now = Date.now();
7637
+ if (now - lastEmitTime >= THROTTLE_MS) {
7638
+ lastEmitTime = now;
7639
+ onOutput(text);
7640
+ pendingChunk = null;
7641
+ } else {
7642
+ pendingChunk = text;
7643
+ if (!throttleTimer) {
7644
+ throttleTimer = setTimeout(() => {
7645
+ throttleTimer = null;
7646
+ if (pendingChunk && !stopped) {
7647
+ lastEmitTime = Date.now();
7648
+ onOutput(pendingChunk);
7649
+ pendingChunk = null;
7650
+ }
7651
+ }, THROTTLE_MS - (now - lastEmitTime));
7652
+ }
7653
+ }
7654
+ };
7655
+ const readNewLines = () => {
7656
+ if (!watchedFile || stopped) return;
7657
+ try {
7658
+ const stat2 = statSync(watchedFile);
7659
+ if (stat2.size <= lastPosition) return;
7660
+ const bytesToRead = stat2.size - lastPosition;
7661
+ const buf = Buffer.alloc(bytesToRead);
7662
+ const fd = openSync(watchedFile, "r");
7663
+ try {
7664
+ readSync(fd, buf, 0, bytesToRead, lastPosition);
7665
+ } finally {
7666
+ closeSync(fd);
7667
+ }
7668
+ lastPosition = stat2.size;
7669
+ const newData = buf.toString("utf-8");
7670
+ for (const line of newData.split("\n")) {
7671
+ if (!line.trim()) continue;
7672
+ try {
7673
+ const entry = JSON.parse(line);
7674
+ this.extractClaudeOutput(entry, emitThrottled, agentId);
7675
+ } catch {
7676
+ }
7677
+ }
7678
+ } catch (err) {
7679
+ console.error(`[OutputReader] Error reading JSONL for ${agentId}:`, err);
7680
+ }
7681
+ };
7682
+ const findAndWatch = () => {
7683
+ if (stopped) return;
7684
+ if (!existsSync6(projectDir)) {
7685
+ retryCount++;
7686
+ if (retryCount > MAX_RETRIES) {
7687
+ console.log(`[OutputReader] Project dir not found after ${MAX_RETRIES} retries, giving up: ${projectDir}`);
7688
+ return;
7689
+ }
7690
+ console.log(`[OutputReader] Project dir not found (${retryCount}/${MAX_RETRIES}), retrying: ${projectDir}`);
7691
+ retryTimer = setTimeout(findAndWatch, 5e3);
7692
+ return;
7693
+ }
7694
+ try {
7695
+ const files = readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({
7696
+ name: f,
7697
+ mtime: statSync(path8.join(projectDir, f)).mtimeMs
7698
+ })).sort((a, b) => b.mtime - a.mtime);
7699
+ if (files.length === 0) {
7700
+ retryCount++;
7701
+ if (retryCount > MAX_RETRIES) {
7702
+ console.log(`[OutputReader] No JSONL files after ${MAX_RETRIES} retries, giving up: ${projectDir}`);
7703
+ return;
7704
+ }
7705
+ console.log(`[OutputReader] No JSONL files yet in ${projectDir} (${retryCount}/${MAX_RETRIES}), retrying`);
7706
+ retryTimer = setTimeout(findAndWatch, 5e3);
7707
+ return;
7708
+ }
7709
+ watchedFile = path8.join(projectDir, files[0].name);
7710
+ lastPosition = statSync(watchedFile).size;
7711
+ console.log(`[OutputReader] Watching JSONL: ${watchedFile} (pos=${lastPosition})`);
7712
+ try {
7713
+ watcher = watch(projectDir, (_event, filename) => {
7714
+ if (stopped) return;
7715
+ if (filename && filename.endsWith(".jsonl")) {
7716
+ const fullPath = path8.join(projectDir, filename);
7717
+ if (fullPath !== watchedFile && existsSync6(fullPath)) {
7718
+ try {
7719
+ const newMtime = statSync(fullPath).mtimeMs;
7720
+ const curMtime = watchedFile ? statSync(watchedFile).mtimeMs : 0;
7721
+ if (newMtime > curMtime) {
7722
+ console.log(`[OutputReader] Switching to newer JSONL: ${fullPath}`);
7723
+ watchedFile = fullPath;
7724
+ lastPosition = 0;
7725
+ }
7726
+ } catch {
7727
+ }
7728
+ }
7729
+ }
7730
+ readNewLines();
7731
+ });
7732
+ } catch {
7733
+ console.log(`[OutputReader] fs.watch failed, relying on polling only`);
7734
+ }
7735
+ pollTimer = setInterval(readNewLines, POLL_MS);
7736
+ } catch (err) {
7737
+ console.error(`[OutputReader] Error setting up watcher for ${agentId}:`, err);
7738
+ }
7739
+ };
7740
+ findAndWatch();
7741
+ return () => {
7742
+ stopped = true;
7743
+ if (watcher) {
7744
+ watcher.close();
7745
+ watcher = null;
7746
+ }
7747
+ if (pollTimer) {
7748
+ clearInterval(pollTimer);
7749
+ pollTimer = null;
7750
+ }
7751
+ if (throttleTimer) {
7752
+ clearTimeout(throttleTimer);
7753
+ throttleTimer = null;
7754
+ }
7755
+ if (retryTimer) {
7756
+ clearTimeout(retryTimer);
7757
+ retryTimer = null;
7758
+ }
7759
+ };
7760
+ }
7761
+ /**
7762
+ * Extract readable output from a Claude JSONL entry.
7763
+ * Also drives status: "human" → working, "result" → idle.
7764
+ *
7765
+ * Claude JSONL types:
7766
+ * - { type: "human" } → user sent message, agent starts working
7767
+ * - { type: "assistant", message: { content: [...] } } → agent responding
7768
+ * - { type: "result", result: "..." } → turn complete, waiting for input
7769
+ */
7770
+ extractClaudeOutput(entry, emit, agentId) {
7771
+ if (entry.type === "user" || entry.type === "human") {
7772
+ this.onStatus?.(agentId, "working");
7773
+ }
7774
+ if (entry.type === "assistant") {
7775
+ const message = entry.message;
7776
+ const usage = message?.usage;
7777
+ if (usage) {
7778
+ const reader = this.readers.get(agentId);
7779
+ if (reader) {
7780
+ const inp = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
7781
+ const out = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
7782
+ if (inp > 0 || out > 0) {
7783
+ reader.inputTokens += inp;
7784
+ reader.outputTokens += out;
7785
+ this.onTokenUpdate?.(agentId, reader.inputTokens, reader.outputTokens);
7786
+ }
7787
+ }
7788
+ }
7789
+ const stopReason = message?.stop_reason ?? entry.stop_reason;
7790
+ const content = message?.content;
7791
+ if (content && Array.isArray(content)) {
7792
+ for (const block of content) {
7793
+ if (block.type === "text" && typeof block.text === "string" && block.text.trim()) {
7794
+ emit(block.text.slice(0, 200));
7795
+ }
7796
+ }
7797
+ }
7798
+ if (stopReason === "end_turn") {
7799
+ this.onStatus?.(agentId, "idle");
7800
+ } else {
7801
+ this.onStatus?.(agentId, "working");
7802
+ }
7803
+ }
7804
+ if (entry.type === "result") {
7805
+ const result = entry.result;
7806
+ if (typeof result === "string" && result.trim()) {
7807
+ emit(result.slice(0, 200));
7808
+ }
7809
+ this.onStatus?.(agentId, "idle");
7810
+ }
7811
+ }
7812
+ // ── lsof fallback reader ──────────────────────────────────────
7813
+ startLsofReader(agentId, pid, onOutput) {
7814
+ const knownFiles = /* @__PURE__ */ new Set();
7815
+ let stopped = false;
7816
+ console.log(`[OutputReader] lsof reader for ${agentId}: pid=${pid}`);
7817
+ const poll = () => {
7818
+ if (stopped) return;
7819
+ execFile2("lsof", ["-p", String(pid)], { timeout: 5e3, maxBuffer: 512 * 1024 }, (err, stdout) => {
7820
+ if (err || stopped) return;
7821
+ const lines = stdout.split("\n");
7822
+ const newFiles = [];
7823
+ for (const line of lines) {
7824
+ const cols = line.trim().split(/\s+/);
7825
+ if (cols.length < 9) continue;
7826
+ const name = cols.slice(8).join(" ");
7827
+ if (!name || name.startsWith("/dev/") || name.startsWith("/System/")) continue;
7828
+ const ext = path8.extname(name);
7829
+ if (!SOURCE_EXTS.has(ext)) continue;
7830
+ if (!knownFiles.has(name)) {
7831
+ knownFiles.add(name);
7832
+ newFiles.push(name);
7833
+ }
7834
+ }
7835
+ if (newFiles.length > 0) {
7836
+ const basename = path8.basename(newFiles[newFiles.length - 1]);
7837
+ onOutput(`Editing ${basename}`);
7838
+ }
7839
+ });
7840
+ };
7841
+ const timer = setInterval(poll, 5e3);
7842
+ poll();
7843
+ return () => {
7844
+ stopped = true;
7845
+ clearInterval(timer);
7846
+ };
7847
+ }
7848
+ };
7849
+
7850
+ // src/index.ts
6662
7851
  registerChannel(wsChannel);
6663
7852
  registerChannel(ablyChannel);
6664
7853
  registerChannel(telegramChannel);
6665
7854
  var orc;
7855
+ var scanner = null;
7856
+ var outputReader = null;
7857
+ var externalAgents = /* @__PURE__ */ new Map();
7858
+ var teamPhases = /* @__PURE__ */ new Map();
7859
+ function publishTeamPhase(teamId, phase, leadAgentId) {
7860
+ teamPhases.set(teamId, { phase, leadAgentId });
7861
+ publishEvent({
7862
+ type: "TEAM_PHASE",
7863
+ teamId,
7864
+ phase,
7865
+ leadAgentId
7866
+ });
7867
+ }
6666
7868
  function generatePairCode() {
6667
7869
  return nanoid5(6).toUpperCase();
6668
7870
  }
@@ -6677,12 +7879,109 @@ function showPairCode() {
6677
7879
  console.log(`Open your phone \u2192 enter gateway address + code`);
6678
7880
  console.log("");
6679
7881
  }
7882
+ function extractProjectName(planText) {
7883
+ function toKebab(s) {
7884
+ return s.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
7885
+ }
7886
+ function trimKebab(s, maxLen) {
7887
+ if (s.length <= maxLen) return s;
7888
+ const cut = s.lastIndexOf("-", maxLen);
7889
+ return cut > 2 ? s.slice(0, cut) : s.slice(0, maxLen);
7890
+ }
7891
+ const namedConcept = planText.match(/CONCEPT\s*[::]\s*(?:A\s+|An\s+|The\s+)?(.+?)\s*[—–]\s/i);
7892
+ if (namedConcept) {
7893
+ const kebab = toKebab(namedConcept[1].trim());
7894
+ if (kebab.length >= 2 && kebab.length <= 30) return kebab;
7895
+ }
7896
+ const quoted = planText.match(/["""\u201c]([^"""\u201d]{2,25})["""\u201d]/);
7897
+ if (quoted) {
7898
+ const kebab = toKebab(quoted[1].trim());
7899
+ if (kebab.length >= 2) return trimKebab(kebab, 25);
7900
+ }
7901
+ 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);
7902
+ if (concept) {
7903
+ const kebab = toKebab(concept[1].trim());
7904
+ if (kebab.length >= 2) return trimKebab(kebab, 25);
7905
+ }
7906
+ const fallbacks = [
7907
+ /(?:goal|project|目标|项目)\s*[::]\s*(.+)/i,
7908
+ /\[PLAN\][\s\S]*?(?:goal|project|目标)\s*[::]\s*(.+)/i,
7909
+ /(?:build|create|make|开发|做|构建)\s+(?:a\s+)?(.+?)(?:\s+(?:with|using|that|for|where|,|。)\b|[.\n])/i
7910
+ ];
7911
+ for (const re of fallbacks) {
7912
+ const m = planText.match(re);
7913
+ if (m) {
7914
+ const kebab = toKebab(m[1].trim());
7915
+ if (kebab.length >= 2) return trimKebab(kebab, 25);
7916
+ }
7917
+ }
7918
+ return "project";
7919
+ }
7920
+ function createUniqueProjectDir(workspace, baseName) {
7921
+ let dirName = baseName;
7922
+ let counter = 1;
7923
+ while (existsSync7(path9.join(workspace, dirName))) {
7924
+ counter++;
7925
+ dirName = `${baseName}-${counter}`;
7926
+ }
7927
+ const fullPath = path9.join(workspace, dirName);
7928
+ mkdirSync5(fullPath, { recursive: true });
7929
+ console.log(`[Gateway] Created project directory: ${fullPath}`);
7930
+ return fullPath;
7931
+ }
7932
+ var AGENTS_FILE = path9.join(os2.homedir(), ".bit-office", "agents.json");
7933
+ function loadAgentDefs() {
7934
+ try {
7935
+ if (existsSync7(AGENTS_FILE)) {
7936
+ const raw = JSON.parse(readFileSync5(AGENTS_FILE, "utf-8"));
7937
+ if (Array.isArray(raw.agents)) return raw.agents;
7938
+ }
7939
+ } catch (e) {
7940
+ console.log(`[Gateway] Failed to read agents.json: ${e}`);
7941
+ }
7942
+ saveAgentDefs(DEFAULT_AGENT_DEFS);
7943
+ return [...DEFAULT_AGENT_DEFS];
7944
+ }
7945
+ function saveAgentDefs(agents) {
7946
+ try {
7947
+ const dir = path9.dirname(AGENTS_FILE);
7948
+ if (!existsSync7(dir)) mkdirSync5(dir, { recursive: true });
7949
+ writeFileSync5(AGENTS_FILE, JSON.stringify({ agents }, null, 2), "utf-8");
7950
+ console.log(`[Gateway] Saved ${agents.length} agent definitions to ${AGENTS_FILE}`);
7951
+ } catch (e) {
7952
+ console.log(`[Gateway] Failed to save agents.json: ${e}`);
7953
+ }
7954
+ }
7955
+ var agentDefs = [];
6680
7956
  function mapOrchestratorEvent(e) {
6681
7957
  switch (e.type) {
6682
7958
  case "task:started":
6683
7959
  return { type: "TASK_STARTED", agentId: e.agentId, taskId: e.taskId, prompt: e.prompt };
6684
- case "task:done":
7960
+ case "task:done": {
7961
+ const resultText = (e.result?.summary ?? "") + (e.result?.fullOutput ?? "");
7962
+ if (resultText && /\[PLAN\]/i.test(resultText)) {
7963
+ for (const [teamId, tp] of teamPhases) {
7964
+ if (tp.leadAgentId === e.agentId && tp.phase === "create") {
7965
+ const planOutput = e.result?.fullOutput ?? e.result?.summary ?? "";
7966
+ if (planOutput) {
7967
+ orc.setOriginalTask(e.agentId, planOutput);
7968
+ console.log(`[Gateway] Captured plan from create phase (${planOutput.length} chars) for design context`);
7969
+ }
7970
+ publishTeamPhase(teamId, "design", e.agentId);
7971
+ break;
7972
+ }
7973
+ }
7974
+ }
7975
+ if (e.isFinalResult) {
7976
+ for (const [teamId, tp] of teamPhases) {
7977
+ if (tp.leadAgentId === e.agentId && tp.phase === "execute") {
7978
+ publishTeamPhase(teamId, "complete", e.agentId);
7979
+ break;
7980
+ }
7981
+ }
7982
+ }
6685
7983
  return { type: "TASK_DONE", agentId: e.agentId, taskId: e.taskId, result: e.result, isFinalResult: e.isFinalResult };
7984
+ }
6686
7985
  case "task:failed":
6687
7986
  return { type: "TASK_FAILED", agentId: e.agentId, taskId: e.taskId, error: e.error };
6688
7987
  case "task:delegated":
@@ -6698,7 +7997,7 @@ function mapOrchestratorEvent(e) {
6698
7997
  case "task:queued":
6699
7998
  return { type: "TASK_QUEUED", agentId: e.agentId, taskId: e.taskId, prompt: e.prompt, position: e.position };
6700
7999
  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 };
8000
+ 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
8001
  case "agent:fired":
6703
8002
  return { type: "AGENT_FIRED", agentId: e.agentId };
6704
8003
  case "task:result-returned":
@@ -6735,6 +8034,8 @@ function handleCommand(parsed) {
6735
8034
  }
6736
8035
  case "FIRE_AGENT": {
6737
8036
  console.log(`[Gateway] Firing agent: ${parsed.agentId}`);
8037
+ const agentToFire = orc.getAgent(parsed.agentId);
8038
+ if (agentToFire?.pid) scanner?.addGracePid(agentToFire.pid);
6738
8039
  orc.removeAgent(parsed.agentId);
6739
8040
  break;
6740
8041
  }
@@ -6755,7 +8056,23 @@ function handleCommand(parsed) {
6755
8056
  }
6756
8057
  if (agent) {
6757
8058
  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 });
8059
+ let phaseOverride;
8060
+ if (orc.isTeamLead(parsed.agentId)) {
8061
+ for (const [, tp] of teamPhases) {
8062
+ if (tp.leadAgentId === parsed.agentId) {
8063
+ phaseOverride = tp.phase;
8064
+ if (tp.phase === "complete") {
8065
+ const teamId = Array.from(teamPhases.entries()).find(([, v]) => v.leadAgentId === parsed.agentId)?.[0];
8066
+ if (teamId) {
8067
+ publishTeamPhase(teamId, "execute", parsed.agentId);
8068
+ phaseOverride = "execute";
8069
+ }
8070
+ }
8071
+ break;
8072
+ }
8073
+ }
8074
+ }
8075
+ orc.runTask(parsed.agentId, parsed.taskId, parsed.prompt, { repoPath: parsed.repoPath, phaseOverride });
6759
8076
  } else {
6760
8077
  publishEvent({
6761
8078
  type: "TASK_FAILED",
@@ -6775,63 +8092,78 @@ function handleCommand(parsed) {
6775
8092
  break;
6776
8093
  }
6777
8094
  case "SERVE_PREVIEW": {
6778
- const filePath = parsed.filePath;
6779
- console.log(`[Gateway] SERVE_PREVIEW: ${filePath}`);
6780
- previewServer.serve(filePath);
8095
+ if (parsed.previewCmd && parsed.previewPort) {
8096
+ const cwd = parsed.cwd ?? config.defaultWorkspace;
8097
+ console.log(`[Gateway] SERVE_PREVIEW (cmd): "${parsed.previewCmd}" port=${parsed.previewPort} cwd=${cwd}`);
8098
+ previewServer.runCommand(parsed.previewCmd, cwd, parsed.previewPort);
8099
+ } else if (parsed.previewCmd) {
8100
+ const cwd = parsed.cwd ?? config.defaultWorkspace;
8101
+ console.log(`[Gateway] SERVE_PREVIEW (launch): "${parsed.previewCmd}" cwd=${cwd}`);
8102
+ previewServer.launchProcess(parsed.previewCmd, cwd);
8103
+ } else if (parsed.filePath) {
8104
+ console.log(`[Gateway] SERVE_PREVIEW (static): ${parsed.filePath}`);
8105
+ previewServer.serve(parsed.filePath);
8106
+ }
6781
8107
  break;
6782
8108
  }
6783
8109
  case "OPEN_FILE": {
6784
8110
  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) {
8111
+ const resolved = path9.resolve(config.defaultWorkspace, raw);
8112
+ const normalized = path9.normalize(resolved);
8113
+ if (!normalized.startsWith(config.defaultWorkspace + path9.sep) && normalized !== config.defaultWorkspace) {
6788
8114
  console.error(`[Gateway] Blocked OPEN_FILE: path "${raw}" resolves outside workspace`);
6789
8115
  break;
6790
8116
  }
6791
- if (!existsSync4(normalized)) {
8117
+ if (!existsSync7(normalized)) {
6792
8118
  console.error(`[Gateway] OPEN_FILE: path does not exist: ${normalized}`);
6793
8119
  break;
6794
8120
  }
6795
8121
  console.log(`[Gateway] Opening file: ${normalized}`);
6796
- execFile("open", [normalized], (err) => {
8122
+ execFile3("open", [normalized], (err) => {
6797
8123
  if (err) console.error(`[Gateway] Failed to open file: ${err.message}`);
6798
8124
  });
6799
8125
  break;
6800
8126
  }
6801
8127
  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(",")}`);
8128
+ const { leadId, memberIds, backends: backends2 } = parsed;
8129
+ const allIds = [leadId, ...memberIds.filter((id) => id !== leadId)];
8130
+ console.log(`[Gateway] Creating team: lead=${leadId}, members=${memberIds.join(",")}`);
6805
8131
  let leadAgentId = null;
6806
8132
  const teamId = `team-${nanoid5(6)}`;
6807
- for (const idx of allIndices) {
6808
- const preset = AGENT_PRESETS[idx];
6809
- if (!preset) continue;
8133
+ for (const defId of allIds) {
8134
+ const def = agentDefs.find((a) => a.id === defId);
8135
+ if (!def) {
8136
+ console.log(`[Gateway] Agent def not found: ${defId}`);
8137
+ continue;
8138
+ }
6810
8139
  const agentId = `agent-${nanoid5(6)}`;
6811
- const backendId = backends2?.[String(idx)] ?? config.defaultBackend;
6812
- if (idx === leadPresetIndex) {
8140
+ const backendId = backends2?.[defId] ?? config.defaultBackend;
8141
+ if (defId === leadId) {
6813
8142
  leadAgentId = agentId;
6814
8143
  orc.setTeamLead(agentId);
6815
8144
  }
6816
8145
  orc.createAgent({
6817
8146
  agentId,
6818
- name: preset.name,
6819
- role: preset.role,
6820
- personality: preset.personality,
8147
+ name: def.name,
8148
+ role: def.skills ? `${def.role} \u2014 ${def.skills}` : def.role,
8149
+ personality: def.personality,
6821
8150
  backend: backendId,
6822
- palette: preset.palette,
8151
+ palette: def.palette,
6823
8152
  teamId
6824
8153
  });
6825
8154
  }
6826
8155
  if (leadAgentId) {
6827
- const leadPreset = AGENT_PRESETS[leadPresetIndex];
8156
+ const leadDef = agentDefs.find((a) => a.id === leadId);
6828
8157
  publishEvent({
6829
8158
  type: "TEAM_CHAT",
6830
8159
  fromAgentId: leadAgentId,
6831
- message: `Team created! ${leadPreset?.name ?? "Lead"} is the Team Lead with ${memberPresetIndices.length} team members.`,
8160
+ message: `Team created! ${leadDef?.name ?? "Lead"} is the Team Lead with ${memberIds.length} team members.`,
6832
8161
  messageType: "status",
6833
8162
  timestamp: Date.now()
6834
8163
  });
8164
+ publishTeamPhase(teamId, "create", leadAgentId);
8165
+ const greetTaskId = nanoid5();
8166
+ orc.runTask(leadAgentId, greetTaskId, "Greet the user and ask what they would like to build.", { phaseOverride: "create" });
6835
8167
  }
6836
8168
  break;
6837
8169
  }
@@ -6842,7 +8174,83 @@ function handleCommand(parsed) {
6842
8174
  }
6843
8175
  case "FIRE_TEAM": {
6844
8176
  console.log("[Gateway] Firing entire team");
8177
+ for (const agent of orc.getAllAgents()) {
8178
+ const pid = agent.pid;
8179
+ if (pid) scanner?.addGracePid(pid);
8180
+ }
6845
8181
  orc.fireTeam();
8182
+ teamPhases.clear();
8183
+ break;
8184
+ }
8185
+ case "KILL_EXTERNAL": {
8186
+ const ext = externalAgents.get(parsed.agentId);
8187
+ if (ext) {
8188
+ console.log(`[Gateway] Killing external process: ${ext.name} (pid=${ext.pid})`);
8189
+ scanner?.addGracePid(ext.pid);
8190
+ try {
8191
+ process.kill(ext.pid, "SIGKILL");
8192
+ } catch (err) {
8193
+ console.error(`[Gateway] Failed to kill pid ${ext.pid}:`, err);
8194
+ }
8195
+ outputReader?.detach(ext.agentId);
8196
+ externalAgents.delete(ext.agentId);
8197
+ publishEvent({ type: "AGENT_FIRED", agentId: ext.agentId });
8198
+ } else {
8199
+ console.log(`[Gateway] KILL_EXTERNAL: agent ${parsed.agentId} not found`);
8200
+ }
8201
+ break;
8202
+ }
8203
+ case "APPROVE_PLAN": {
8204
+ const agentId = parsed.agentId;
8205
+ console.log(`[Gateway] APPROVE_PLAN: agent=${agentId}`);
8206
+ const approvedPlan = orc.getLeaderLastOutput(agentId);
8207
+ if (approvedPlan) {
8208
+ orc.setOriginalTask(agentId, approvedPlan);
8209
+ console.log(`[Gateway] Captured approved plan (${approvedPlan.length} chars) for leader ${agentId}`);
8210
+ }
8211
+ const projectName = extractProjectName(approvedPlan ?? "project");
8212
+ const projectDir = createUniqueProjectDir(config.defaultWorkspace, projectName);
8213
+ orc.setTeamProjectDir(projectDir);
8214
+ let approveTeamId;
8215
+ for (const [teamId, tp] of teamPhases) {
8216
+ if (tp.leadAgentId === agentId) {
8217
+ approveTeamId = teamId;
8218
+ break;
8219
+ }
8220
+ }
8221
+ if (!approveTeamId) {
8222
+ const agentInfo = orc.getAllAgents().find((a) => a.agentId === agentId);
8223
+ if (agentInfo?.teamId) approveTeamId = agentInfo.teamId;
8224
+ }
8225
+ if (approveTeamId) {
8226
+ publishTeamPhase(approveTeamId, "execute", agentId);
8227
+ const taskId = nanoid5();
8228
+ 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" });
8229
+ }
8230
+ break;
8231
+ }
8232
+ case "END_PROJECT": {
8233
+ const agentId = parsed.agentId;
8234
+ console.log(`[Gateway] END_PROJECT: agent=${agentId}`);
8235
+ orc.clearLeaderHistory(agentId);
8236
+ let foundTeamId;
8237
+ for (const [teamId, tp] of teamPhases) {
8238
+ if (tp.leadAgentId === agentId) {
8239
+ foundTeamId = teamId;
8240
+ break;
8241
+ }
8242
+ }
8243
+ if (!foundTeamId) {
8244
+ const agentInfo = orc.getAllAgents().find((a) => a.agentId === agentId);
8245
+ if (agentInfo?.teamId) foundTeamId = agentInfo.teamId;
8246
+ }
8247
+ if (foundTeamId) {
8248
+ publishTeamPhase(foundTeamId, "create", agentId);
8249
+ const greetTaskId = nanoid5();
8250
+ orc.runTask(agentId, greetTaskId, "Greet the user and ask what they would like to build next.", { phaseOverride: "create" });
8251
+ } else {
8252
+ console.log(`[Gateway] END_PROJECT: no team found for agent ${agentId}, ignoring`);
8253
+ }
6846
8254
  break;
6847
8255
  }
6848
8256
  case "PING": {
@@ -6856,7 +8264,7 @@ function handleCommand(parsed) {
6856
8264
  palette: agent.palette,
6857
8265
  personality: void 0,
6858
8266
  backend: agent.backend,
6859
- isTeamLead: agent.isTeamLead,
8267
+ isTeamLead: agent.isTeamLead || void 0,
6860
8268
  teamId: agent.teamId
6861
8269
  });
6862
8270
  publishEvent({
@@ -6864,7 +8272,67 @@ function handleCommand(parsed) {
6864
8272
  agentId: agent.agentId,
6865
8273
  status: agent.status
6866
8274
  });
8275
+ if (agent.isTeamLead && agent.teamId && !teamPhases.has(agent.teamId)) {
8276
+ teamPhases.set(agent.teamId, { phase: "create", leadAgentId: agent.agentId });
8277
+ console.log(`[Gateway] Restored team phase for ${agent.teamId} (leader=${agent.agentId})`);
8278
+ }
8279
+ }
8280
+ for (const [teamId, tp] of teamPhases) {
8281
+ publishEvent({
8282
+ type: "TEAM_PHASE",
8283
+ teamId,
8284
+ phase: tp.phase,
8285
+ leadAgentId: tp.leadAgentId
8286
+ });
8287
+ }
8288
+ for (const [, ext] of externalAgents) {
8289
+ publishEvent({
8290
+ type: "AGENT_CREATED",
8291
+ agentId: ext.agentId,
8292
+ name: ext.name,
8293
+ role: ext.cwd ? ext.cwd.split("/").pop() ?? ext.backendId : ext.backendId,
8294
+ isExternal: true,
8295
+ pid: ext.pid,
8296
+ cwd: ext.cwd ?? void 0,
8297
+ startedAt: ext.startedAt,
8298
+ backend: ext.backendId
8299
+ });
8300
+ publishEvent({
8301
+ type: "AGENT_STATUS",
8302
+ agentId: ext.agentId,
8303
+ status: ext.status
8304
+ });
8305
+ }
8306
+ publishEvent({ type: "AGENT_DEFS", agents: agentDefs });
8307
+ break;
8308
+ }
8309
+ case "SAVE_AGENT_DEF": {
8310
+ const def = parsed.agent;
8311
+ const idx = agentDefs.findIndex((a) => a.id === def.id);
8312
+ if (idx >= 0) {
8313
+ if (agentDefs[idx].isBuiltin) {
8314
+ def.isBuiltin = true;
8315
+ def.teamRole = agentDefs[idx].teamRole;
8316
+ }
8317
+ agentDefs[idx] = def;
8318
+ } else {
8319
+ def.isBuiltin = false;
8320
+ def.teamRole = "dev";
8321
+ agentDefs.push(def);
6867
8322
  }
8323
+ saveAgentDefs(agentDefs);
8324
+ publishEvent({ type: "AGENT_DEFS", agents: agentDefs });
8325
+ break;
8326
+ }
8327
+ case "DELETE_AGENT_DEF": {
8328
+ const target = agentDefs.find((a) => a.id === parsed.agentDefId);
8329
+ if (target?.isBuiltin) {
8330
+ console.log(`[Gateway] Cannot delete built-in agent: ${parsed.agentDefId}`);
8331
+ break;
8332
+ }
8333
+ agentDefs = agentDefs.filter((a) => a.id !== parsed.agentDefId);
8334
+ saveAgentDefs(agentDefs);
8335
+ publishEvent({ type: "AGENT_DEFS", agents: agentDefs });
6868
8336
  break;
6869
8337
  }
6870
8338
  }
@@ -6892,9 +8360,11 @@ async function main() {
6892
8360
  worktree: false,
6893
8361
  // disabled by default for now
6894
8362
  retry: { maxRetries: 2, escalateToLeader: true },
6895
- promptsDir: path5.join(os.homedir(), ".bit-office", "prompts"),
8363
+ promptsDir: path9.join(os2.homedir(), ".bit-office", "prompts"),
6896
8364
  sandboxMode: config.sandboxMode
6897
8365
  });
8366
+ agentDefs = loadAgentDefs();
8367
+ console.log(`[Gateway] Loaded ${agentDefs.length} agent definitions (${agentDefs.filter((a) => !a.isBuiltin).length} custom)`);
6898
8368
  const forwardEvent = (event) => {
6899
8369
  const mapped = mapOrchestratorEvent(event);
6900
8370
  if (mapped) publishEvent(mapped);
@@ -6914,6 +8384,99 @@ async function main() {
6914
8384
  orc.on("agent:created", forwardEvent);
6915
8385
  orc.on("agent:fired", forwardEvent);
6916
8386
  orc.on("task:result-returned", forwardEvent);
8387
+ outputReader = new ExternalOutputReader();
8388
+ outputReader.setOnStatus((agentId, status) => {
8389
+ const ext = externalAgents.get(agentId);
8390
+ if (ext && ext.status !== status) {
8391
+ ext.status = status;
8392
+ publishEvent({
8393
+ type: "AGENT_STATUS",
8394
+ agentId,
8395
+ status
8396
+ });
8397
+ }
8398
+ });
8399
+ outputReader.setOnTokenUpdate((agentId, inputTokens, outputTokens) => {
8400
+ publishEvent({
8401
+ type: "TOKEN_UPDATE",
8402
+ agentId,
8403
+ inputTokens,
8404
+ outputTokens
8405
+ });
8406
+ });
8407
+ scanner = new ProcessScanner(
8408
+ () => orc.getManagedPids(),
8409
+ {
8410
+ onAdded: (agents) => {
8411
+ for (const agent of agents) {
8412
+ const name = agent.command.charAt(0).toUpperCase() + agent.command.slice(1);
8413
+ const displayName = `${name} (${agent.pid})`;
8414
+ externalAgents.set(agent.agentId, {
8415
+ agentId: agent.agentId,
8416
+ name: displayName,
8417
+ backendId: agent.backendId,
8418
+ pid: agent.pid,
8419
+ cwd: agent.cwd,
8420
+ startedAt: agent.startedAt,
8421
+ status: agent.status
8422
+ });
8423
+ console.log(`[ProcessScanner] External agent found: ${displayName} (pid=${agent.pid}, cwd=${agent.cwd})`);
8424
+ publishEvent({
8425
+ type: "AGENT_CREATED",
8426
+ agentId: agent.agentId,
8427
+ name: displayName,
8428
+ role: agent.cwd ? agent.cwd.split("/").pop() ?? agent.backendId : agent.backendId,
8429
+ isExternal: true,
8430
+ pid: agent.pid,
8431
+ cwd: agent.cwd ?? void 0,
8432
+ startedAt: agent.startedAt,
8433
+ backend: agent.backendId
8434
+ });
8435
+ publishEvent({
8436
+ type: "AGENT_STATUS",
8437
+ agentId: agent.agentId,
8438
+ status: agent.status
8439
+ });
8440
+ outputReader?.attach(agent.agentId, agent.pid, agent.cwd, agent.backendId, (chunk) => {
8441
+ publishEvent({
8442
+ type: "LOG_APPEND",
8443
+ agentId: agent.agentId,
8444
+ taskId: "external",
8445
+ stream: "stdout",
8446
+ chunk
8447
+ });
8448
+ });
8449
+ }
8450
+ },
8451
+ onRemoved: (agentIds) => {
8452
+ for (const agentId of agentIds) {
8453
+ const ext = externalAgents.get(agentId);
8454
+ console.log(`[ProcessScanner] External agent gone: ${ext?.name ?? agentId}`);
8455
+ outputReader?.detach(agentId);
8456
+ externalAgents.delete(agentId);
8457
+ publishEvent({
8458
+ type: "AGENT_FIRED",
8459
+ agentId
8460
+ });
8461
+ }
8462
+ },
8463
+ onChanged: (agents) => {
8464
+ for (const agent of agents) {
8465
+ const ext = externalAgents.get(agent.agentId);
8466
+ if (ext?.backendId === "claude") continue;
8467
+ if (ext) {
8468
+ ext.status = agent.status;
8469
+ }
8470
+ publishEvent({
8471
+ type: "AGENT_STATUS",
8472
+ agentId: agent.agentId,
8473
+ status: agent.status
8474
+ });
8475
+ }
8476
+ }
8477
+ }
8478
+ );
8479
+ scanner.start();
6917
8480
  const backendNames = config.detectedBackends.map((id) => getBackend(id)?.name ?? id).join(", ");
6918
8481
  console.log(`[Gateway] AI backends: ${backendNames || "none detected"} (default: ${getBackend(config.defaultBackend)?.name ?? config.defaultBackend})`);
6919
8482
  console.log(`[Gateway] Permissions: ${config.sandboxMode === "full" ? "Full access" : "Sandbox"}`);
@@ -6922,10 +8485,10 @@ async function main() {
6922
8485
  await initTransports(handleCommand);
6923
8486
  console.log("[Gateway] Listening for commands...");
6924
8487
  console.log("[Gateway] Press 'p' + Enter to generate a new pair code");
6925
- if (process.env.NODE_ENV !== "development" && existsSync4(config.webDir)) {
8488
+ if (process.env.NODE_ENV !== "development" && existsSync7(config.webDir)) {
6926
8489
  const url = `http://localhost:${config.wsPort}`;
6927
8490
  console.log(`[Gateway] Opening ${url}`);
6928
- execFile("open", [url]);
8491
+ execFile3("open", [url]);
6929
8492
  }
6930
8493
  if (process.stdin.isTTY) {
6931
8494
  process.stdin.setEncoding("utf-8");
@@ -6939,6 +8502,8 @@ async function main() {
6939
8502
  }
6940
8503
  function cleanup() {
6941
8504
  console.log("[Gateway] Shutting down...");
8505
+ outputReader?.detachAll();
8506
+ scanner?.stop();
6942
8507
  previewServer.stop();
6943
8508
  orc?.destroy();
6944
8509
  destroyTransports();