bit-office 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/index.js +1983 -362
  2. package/dist/index.js.map +1 -1
  3. package/dist/web/out/404.html +50 -2
  4. package/dist/web/out/Office Tileset/Office Tileset All 16x16.png +0 -0
  5. package/dist/web/out/_next/static/chunks/757.6d61de28074ba8d4.js +1 -0
  6. package/dist/web/out/_next/static/chunks/904.36d527a30eef4423.js +1 -0
  7. package/dist/web/out/_next/static/chunks/989-4c2c2b3a6c2f8a4a.js +1 -0
  8. package/dist/web/out/_next/static/chunks/app/office/page-e71fd886e7258e88.js +1 -0
  9. package/dist/web/out/_next/static/chunks/app/pair/page-667d7c599a584eb6.js +1 -0
  10. package/dist/web/out/_next/static/chunks/{webpack-131b3d260a3d9ce4.js → webpack-6eaa423ee729538f.js} +1 -1
  11. package/dist/web/out/index.html +50 -2
  12. package/dist/web/out/index.txt +67 -17
  13. package/dist/web/out/office.html +50 -2
  14. package/dist/web/out/office.txt +68 -17
  15. package/dist/web/out/pair.html +50 -2
  16. package/dist/web/out/pair.txt +68 -17
  17. package/dist/web/out/pixel-agents-layout.json +1 -2802
  18. package/dist/web/out/sw.js +1 -1
  19. package/package.json +2 -1
  20. package/dist/web/out/_next/static/chunks/757.6b9b9f11f348e673.js +0 -1
  21. package/dist/web/out/_next/static/chunks/904.4649cb92e3c1355c.js +0 -1
  22. package/dist/web/out/_next/static/chunks/989-f3ebca68e0b5e7ee.js +0 -1
  23. package/dist/web/out/_next/static/chunks/app/office/page-75459bb9d7bfe004.js +0 -1
  24. package/dist/web/out/_next/static/chunks/app/pair/page-932b6cbad193cd3d.js +0 -1
  25. /package/dist/web/out/_next/static/{KVUUasWqYxW6SNCIo9Tbn → e3jju679hIWY5j_P8PYTf}/_buildManifest.js +0 -0
  26. /package/dist/web/out/_next/static/{KVUUasWqYxW6SNCIo9Tbn → e3jju679hIWY5j_P8PYTf}/_ssgManifest.js +0 -0
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, errorMaps, issueData } = params;
528
- const fullPath = [...path, ...issueData.path || []];
527
+ const { data, path: path6, errorMaps, issueData } = params;
528
+ const fullPath = [...path6, ...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, path, key) {
644
+ constructor(parent, value, path6, key) {
645
645
  this._cachedPath = [];
646
646
  this.parent = parent;
647
647
  this.data = value;
648
- this._path = path;
648
+ this._path = path6;
649
649
  this._key = key;
650
650
  }
651
651
  get path() {
@@ -4140,6 +4140,22 @@ var OpenFileCommand = external_exports.object({
4140
4140
  type: external_exports.literal("OPEN_FILE"),
4141
4141
  path: external_exports.string()
4142
4142
  });
4143
+ var CreateTeamCommand = external_exports.object({
4144
+ type: external_exports.literal("CREATE_TEAM"),
4145
+ leadPresetIndex: external_exports.number(),
4146
+ memberPresetIndices: external_exports.array(external_exports.number()),
4147
+ backends: external_exports.record(external_exports.string(), external_exports.string()).optional()
4148
+ });
4149
+ var ServePreviewCommand = external_exports.object({
4150
+ type: external_exports.literal("SERVE_PREVIEW"),
4151
+ filePath: external_exports.string()
4152
+ });
4153
+ var StopTeamCommand = external_exports.object({
4154
+ type: external_exports.literal("STOP_TEAM")
4155
+ });
4156
+ var FireTeamCommand = external_exports.object({
4157
+ type: external_exports.literal("FIRE_TEAM")
4158
+ });
4143
4159
  var CommandSchema = external_exports.discriminatedUnion("type", [
4144
4160
  RunTaskCommand,
4145
4161
  ApprovalDecisionCommand,
@@ -4147,7 +4163,11 @@ var CommandSchema = external_exports.discriminatedUnion("type", [
4147
4163
  PingCommand,
4148
4164
  CreateAgentCommand,
4149
4165
  FireAgentCommand,
4150
- OpenFileCommand
4166
+ OpenFileCommand,
4167
+ CreateTeamCommand,
4168
+ ServePreviewCommand,
4169
+ StopTeamCommand,
4170
+ FireTeamCommand
4151
4171
  ]);
4152
4172
 
4153
4173
  // ../../packages/shared/src/events.ts
@@ -4181,16 +4201,20 @@ var ApprovalNeededEvent = external_exports.object({
4181
4201
  });
4182
4202
  var TaskResultPayload = external_exports.object({
4183
4203
  summary: external_exports.string(),
4204
+ fullOutput: external_exports.string().optional(),
4184
4205
  changedFiles: external_exports.array(external_exports.string()),
4185
4206
  diffStat: external_exports.string(),
4186
4207
  testResult: external_exports.enum(["passed", "failed", "unknown"]),
4187
- nextSuggestion: external_exports.string().optional()
4208
+ nextSuggestion: external_exports.string().optional(),
4209
+ previewUrl: external_exports.string().optional(),
4210
+ previewPath: external_exports.string().optional()
4188
4211
  });
4189
4212
  var TaskDoneEvent = external_exports.object({
4190
4213
  type: external_exports.literal("TASK_DONE"),
4191
4214
  agentId: external_exports.string(),
4192
4215
  taskId: external_exports.string(),
4193
- result: TaskResultPayload
4216
+ result: TaskResultPayload,
4217
+ isFinalResult: external_exports.boolean().optional()
4194
4218
  });
4195
4219
  var TaskFailedEvent = external_exports.object({
4196
4220
  type: external_exports.literal("TASK_FAILED"),
@@ -4212,12 +4236,38 @@ var AgentCreatedEvent = external_exports.object({
4212
4236
  role: external_exports.string(),
4213
4237
  palette: external_exports.number().optional(),
4214
4238
  personality: external_exports.string().optional(),
4215
- backend: external_exports.string().optional()
4239
+ backend: external_exports.string().optional(),
4240
+ isTeamLead: external_exports.boolean().optional(),
4241
+ teamId: external_exports.string().optional()
4216
4242
  });
4217
4243
  var AgentFiredEvent = external_exports.object({
4218
4244
  type: external_exports.literal("AGENT_FIRED"),
4219
4245
  agentId: external_exports.string()
4220
4246
  });
4247
+ var TaskResultReturnedEvent = external_exports.object({
4248
+ type: external_exports.literal("TASK_RESULT_RETURNED"),
4249
+ fromAgentId: external_exports.string(),
4250
+ toAgentId: external_exports.string(),
4251
+ taskId: external_exports.string(),
4252
+ summary: external_exports.string(),
4253
+ success: external_exports.boolean()
4254
+ });
4255
+ var TeamChatEvent = external_exports.object({
4256
+ type: external_exports.literal("TEAM_CHAT"),
4257
+ fromAgentId: external_exports.string(),
4258
+ toAgentId: external_exports.string().optional(),
4259
+ message: external_exports.string(),
4260
+ messageType: external_exports.enum(["delegation", "result", "status"]),
4261
+ taskId: external_exports.string().optional(),
4262
+ timestamp: external_exports.number()
4263
+ });
4264
+ var TaskQueuedEvent = external_exports.object({
4265
+ type: external_exports.literal("TASK_QUEUED"),
4266
+ agentId: external_exports.string(),
4267
+ taskId: external_exports.string(),
4268
+ prompt: external_exports.string(),
4269
+ position: external_exports.number()
4270
+ });
4221
4271
  var GatewayEventSchema = external_exports.discriminatedUnion("type", [
4222
4272
  AgentStatusEvent,
4223
4273
  TaskStartedEvent,
@@ -4227,9 +4277,22 @@ var GatewayEventSchema = external_exports.discriminatedUnion("type", [
4227
4277
  TaskFailedEvent,
4228
4278
  TaskDelegatedEvent,
4229
4279
  AgentCreatedEvent,
4230
- AgentFiredEvent
4280
+ AgentFiredEvent,
4281
+ TaskResultReturnedEvent,
4282
+ TeamChatEvent,
4283
+ TaskQueuedEvent
4231
4284
  ]);
4232
4285
 
4286
+ // ../../packages/shared/src/presets.ts
4287
+ var AGENT_PRESETS = [
4288
+ { palette: 0, name: "Alex", role: "Frontend Dev", description: "UI components, React/Next.js/CSS", personality: "You speak in a friendly, casual, encouraging, and natural tone." },
4289
+ { palette: 1, name: "Mia", role: "Backend Dev", description: "APIs, database, server logic", personality: "You speak formally, professionally, in an organized and concise manner." },
4290
+ { 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
+ { 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." }
4294
+ ];
4295
+
4233
4296
  // src/config.ts
4234
4297
  import "dotenv/config";
4235
4298
  import { resolve, dirname } from "path";
@@ -4281,7 +4344,7 @@ function buildConfig() {
4281
4344
  const saved = loadSavedConfig();
4282
4345
  return {
4283
4346
  machineId: getOrCreateMachineId(),
4284
- defaultWorkspace: process.cwd(),
4347
+ defaultWorkspace: process.env.WORKSPACE || process.cwd(),
4285
4348
  wsPort: 9090,
4286
4349
  ablyApiKey: process.env.ABLY_API_KEY || saved.ablyApiKey || void 0,
4287
4350
  webDir: resolveWebDir(),
@@ -4566,24 +4629,163 @@ var ablyChannel = {
4566
4629
 
4567
4630
  // src/telegram-channel.ts
4568
4631
  import TelegramBot from "node-telegram-bot-api";
4569
-
4570
- // src/agent-session.ts
4571
- import { spawn, execSync as execSync2 } from "child_process";
4572
4632
  import { nanoid } from "nanoid";
4633
+ var PRESETS = [
4634
+ { name: "Alex", role: "Frontend Dev", palette: 0, personality: "You speak in a friendly, casual, encouraging, and natural tone." },
4635
+ { name: "Mia", role: "Backend Dev", palette: 1, personality: "You speak formally, professionally, in an organized and concise manner." },
4636
+ { name: "Leo", role: "Fullstack Dev", palette: 2, personality: "You are aggressive, action-first, always pursuing speed and efficiency." },
4637
+ { name: "Sophie", role: "Code Reviewer", palette: 3, personality: "You teach patiently, explain the reasoning, and guide like a mentor." },
4638
+ { name: "Yuki", role: "QA / Tester", palette: 4, personality: "You are extremely concise, no fluff, straight to the result." },
4639
+ { name: "Marcus", role: "Architect", palette: 5, personality: "You speak formally, professionally, in an organized and concise manner." }
4640
+ ];
4641
+ var botAgents = [];
4642
+ function formatEvent(event, agentId) {
4643
+ if (event.type === "TASK_STARTED" && event.agentId === agentId) {
4644
+ return `\u{1F527} Working on it...`;
4645
+ }
4646
+ if (event.type === "TASK_DONE" && event.agentId === agentId) {
4647
+ const r = event.result;
4648
+ let msg = `\u2705 Task completed
4649
+
4650
+ ${r.summary}`;
4651
+ if (r.changedFiles.length > 0) {
4652
+ msg += `
4653
+
4654
+ \u{1F4C1} Changed files:
4655
+ ${r.changedFiles.map((f) => `\u2022 ${f}`).join("\n")}`;
4656
+ }
4657
+ return msg;
4658
+ }
4659
+ if (event.type === "TASK_FAILED" && event.agentId === agentId) {
4660
+ return `\u274C Task failed
4661
+
4662
+ ${event.error}`;
4663
+ }
4664
+ if (event.type === "APPROVAL_NEEDED" && event.agentId === agentId) {
4665
+ return `\u26A0\uFE0F Approval needed
4666
+
4667
+ ${event.title}
4668
+ ${event.summary}
4669
+
4670
+ Reply /yes or /no`;
4671
+ }
4672
+ return null;
4673
+ }
4674
+ var telegramChannel = {
4675
+ name: "Telegram",
4676
+ async init(commandHandler) {
4677
+ const tokens = config.telegramBotTokens;
4678
+ if (!tokens.length || tokens.every((t) => !t)) return false;
4679
+ for (let i = 0; i < tokens.length && i < PRESETS.length; i++) {
4680
+ const token = tokens[i];
4681
+ if (!token) continue;
4682
+ const preset = PRESETS[i];
4683
+ const agentId = `tg-${preset.name.toLowerCase()}`;
4684
+ const bot = new TelegramBot(token, { polling: true });
4685
+ const ba = { bot, preset, agentId, chatIds: /* @__PURE__ */ new Set() };
4686
+ botAgents.push(ba);
4687
+ bot.on("polling_error", (err) => {
4688
+ const code = err?.response?.statusCode ?? err?.code;
4689
+ if (code === 409) {
4690
+ console.warn(
4691
+ `[Telegram] 409 Conflict for @${preset.name}: token is already used by another bot instance. Polling skipped for this token.`
4692
+ );
4693
+ bot.stopPolling();
4694
+ return;
4695
+ }
4696
+ console.error(`[Telegram] Polling error for @${preset.name}:`, err.message ?? err);
4697
+ });
4698
+ commandHandler({
4699
+ type: "CREATE_AGENT",
4700
+ agentId,
4701
+ name: preset.name,
4702
+ role: preset.role,
4703
+ palette: preset.palette,
4704
+ personality: preset.personality
4705
+ });
4706
+ const botInfo = await bot.getMe();
4707
+ console.log(`[Telegram] @${botInfo.username} \u2192 ${preset.name} (${preset.role})`);
4708
+ bot.on("message", (msg) => {
4709
+ if (!msg.text) return;
4710
+ ba.chatIds.add(msg.chat.id);
4711
+ const text = msg.text.trim();
4712
+ if (text === "/yes") {
4713
+ commandHandler({ type: "APPROVAL_DECISION", approvalId: "__all__", decision: "yes" });
4714
+ return;
4715
+ }
4716
+ if (text === "/no") {
4717
+ commandHandler({ type: "APPROVAL_DECISION", approvalId: "__all__", decision: "no" });
4718
+ return;
4719
+ }
4720
+ if (text === "/cancel") {
4721
+ commandHandler({ type: "CANCEL_TASK", agentId, taskId: "" });
4722
+ bot.sendMessage(msg.chat.id, `\u{1F6D1} Cancelled ${preset.name}'s current task`);
4723
+ return;
4724
+ }
4725
+ if (text === "/status") {
4726
+ commandHandler({ type: "PING" });
4727
+ return;
4728
+ }
4729
+ if (text.startsWith("/")) return;
4730
+ const taskId = nanoid();
4731
+ commandHandler({
4732
+ type: "RUN_TASK",
4733
+ agentId,
4734
+ taskId,
4735
+ prompt: text,
4736
+ name: preset.name,
4737
+ role: preset.role,
4738
+ personality: preset.personality
4739
+ });
4740
+ });
4741
+ }
4742
+ console.log(`[Telegram] ${botAgents.length} bot(s) active`);
4743
+ return botAgents.length > 0;
4744
+ },
4745
+ broadcast(event) {
4746
+ for (const ba of botAgents) {
4747
+ const text = formatEvent(event, ba.agentId);
4748
+ if (!text) continue;
4749
+ for (const chatId of ba.chatIds) {
4750
+ ba.bot.sendMessage(chatId, text).catch((err) => {
4751
+ console.error(`[Telegram] Send failed:`, err.message);
4752
+ });
4753
+ }
4754
+ }
4755
+ },
4756
+ destroy() {
4757
+ for (const ba of botAgents) {
4758
+ ba.bot.stopPolling();
4759
+ }
4760
+ botAgents.length = 0;
4761
+ }
4762
+ };
4573
4763
 
4574
- // src/ai-backends.ts
4764
+ // src/setup.ts
4765
+ import { createInterface } from "readline";
4766
+
4767
+ // src/backends.ts
4575
4768
  import { execSync } from "child_process";
4576
4769
  var backends = [
4577
4770
  {
4578
4771
  id: "claude",
4579
4772
  name: "Claude Code",
4580
4773
  command: "claude",
4774
+ supportsStdin: true,
4581
4775
  buildArgs(prompt, opts) {
4582
- const args = ["-p", prompt, "--output-format", "text", "--dangerously-skip-permissions"];
4583
- if (opts.continue) args.push("--continue");
4776
+ const args = ["-p", prompt, "--output-format", "stream-json", "--verbose", "--dangerously-skip-permissions"];
4777
+ if (!opts.skipResume) {
4778
+ if (opts.resumeSessionId) {
4779
+ args.push("--resume", opts.resumeSessionId);
4780
+ } else if (opts.continue) {
4781
+ args.push("--continue");
4782
+ }
4783
+ }
4784
+ if (opts.noTools) args.push("--tools", "");
4785
+ if (opts.model) args.push("--model", opts.model);
4584
4786
  return args;
4585
4787
  },
4586
- deleteEnv: ["CLAUDECODE"]
4788
+ deleteEnv: ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT"]
4587
4789
  },
4588
4790
  {
4589
4791
  id: "codex",
@@ -4625,6 +4827,9 @@ var backendMap = new Map(backends.map((b) => [b.id, b]));
4625
4827
  function getBackend(id) {
4626
4828
  return backendMap.get(id);
4627
4829
  }
4830
+ function getAllBackends() {
4831
+ return backends;
4832
+ }
4628
4833
  function detectBackends() {
4629
4834
  const detected = [];
4630
4835
  for (const backend of backends) {
@@ -4637,7 +4842,148 @@ function detectBackends() {
4637
4842
  return detected;
4638
4843
  }
4639
4844
 
4640
- // src/agent-session.ts
4845
+ // src/setup.ts
4846
+ function ask(rl, question) {
4847
+ return new Promise((resolve2) => {
4848
+ const onClose = () => resolve2("");
4849
+ rl.once("close", onClose);
4850
+ rl.question(question, (answer) => {
4851
+ rl.removeListener("close", onClose);
4852
+ resolve2(answer.trim());
4853
+ });
4854
+ });
4855
+ }
4856
+ async function runSetup() {
4857
+ console.log("[Setup] Detecting AI backends...");
4858
+ const detected = detectBackends();
4859
+ const detectedNames = detected.map((id) => getBackend(id)?.name ?? id).join(", ");
4860
+ console.log(`[Setup] Found: ${detectedNames || "none"}`);
4861
+ if (!process.stdin.isTTY) {
4862
+ saveConfig({ detectedBackends: detected, defaultBackend: detected[0] ?? "claude", sandboxMode: "full" });
4863
+ console.log("\u2713 Default config saved to ~/.bit-office/config.json");
4864
+ console.log(" Run with --setup in a terminal to configure.\n");
4865
+ return;
4866
+ }
4867
+ const rl = createInterface({
4868
+ input: process.stdin,
4869
+ output: process.stdout,
4870
+ terminal: true
4871
+ });
4872
+ console.log("");
4873
+ console.log("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
4874
+ console.log("\u2551 Bit Office \u2014 First Setup \u2551");
4875
+ console.log("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
4876
+ console.log("");
4877
+ console.log("Press Enter to skip any step.\n");
4878
+ console.log("\u2500\u2500 Remote Access (Ably) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
4879
+ console.log("Enables access from outside your LAN.");
4880
+ const ablyApiKey = await ask(rl, "Ably API Key (optional): ");
4881
+ let defaultBackend = detected[0] ?? "claude";
4882
+ if (detected.length > 1) {
4883
+ console.log("\n\u2500\u2500 AI Backends \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
4884
+ console.log(`Detected: ${detectedNames}`);
4885
+ const choices = detected.map((id, i) => `${i + 1}=${getBackend(id)?.name ?? id}`).join(", ");
4886
+ const pick = await ask(rl, `Default backend (${choices}): `);
4887
+ const idx = parseInt(pick, 10) - 1;
4888
+ if (idx >= 0 && idx < detected.length) {
4889
+ defaultBackend = detected[idx];
4890
+ }
4891
+ }
4892
+ console.log("\n\u2500\u2500 Agent Permissions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
4893
+ console.log("1 = Full access (agents can access entire machine)");
4894
+ console.log("2 = Sandbox (agents restricted to working directory)");
4895
+ const sandboxPick = await ask(rl, "Permission mode (1/2, default=1): ");
4896
+ const sandboxMode = sandboxPick === "2" ? "safe" : "full";
4897
+ rl.close();
4898
+ saveConfig({
4899
+ ablyApiKey: ablyApiKey || void 0,
4900
+ detectedBackends: detected,
4901
+ defaultBackend,
4902
+ sandboxMode
4903
+ });
4904
+ console.log("\n\u2713 Config saved to ~/.bit-office/config.json");
4905
+ if (ablyApiKey) console.log(" \u2022 Ably: enabled");
4906
+ console.log(` \u2022 Default AI: ${getBackend(defaultBackend)?.name ?? defaultBackend}`);
4907
+ console.log(` \u2022 Permissions: ${sandboxMode === "full" ? "Full access" : "Sandbox"}`);
4908
+ console.log(" \u2022 Run with --setup to reconfigure\n");
4909
+ }
4910
+
4911
+ // ../../packages/orchestrator/src/orchestrator.ts
4912
+ import { EventEmitter } from "events";
4913
+ import { nanoid as nanoid4 } from "nanoid";
4914
+
4915
+ // ../../packages/orchestrator/src/agent-session.ts
4916
+ import { spawn as spawn2, execSync as execSync2 } from "child_process";
4917
+ import path2 from "path";
4918
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
4919
+ import { homedir as homedir2 } from "os";
4920
+
4921
+ // ../../packages/orchestrator/src/preview-server.ts
4922
+ import { spawn } from "child_process";
4923
+ import path from "path";
4924
+ var PREVIEW_PORT = 9100;
4925
+ var PreviewServer = class {
4926
+ process = null;
4927
+ currentDir = null;
4928
+ /**
4929
+ * Serve a directory on a fixed port. Kills any existing serve process first.
4930
+ * Returns the preview URL for the given file.
4931
+ */
4932
+ serve(filePath) {
4933
+ const dir = path.dirname(filePath);
4934
+ const fileName = path.basename(filePath);
4935
+ this.stop();
4936
+ try {
4937
+ this.process = spawn("npx", ["serve", dir, "-l", String(PREVIEW_PORT), "--no-clipboard"], {
4938
+ stdio: "ignore",
4939
+ detached: true
4940
+ });
4941
+ this.process.unref();
4942
+ this.currentDir = dir;
4943
+ const url = `http://localhost:${PREVIEW_PORT}/${fileName}`;
4944
+ console.log(`[PreviewServer] Serving ${dir} on port ${PREVIEW_PORT}`);
4945
+ return url;
4946
+ } catch (e) {
4947
+ console.log(`[PreviewServer] Failed to start: ${e}`);
4948
+ return void 0;
4949
+ }
4950
+ }
4951
+ /** Kill the current serve process */
4952
+ stop() {
4953
+ if (this.process) {
4954
+ try {
4955
+ this.process.kill("SIGTERM");
4956
+ } catch {
4957
+ }
4958
+ this.process = null;
4959
+ this.currentDir = null;
4960
+ console.log(`[PreviewServer] Stopped`);
4961
+ }
4962
+ }
4963
+ };
4964
+ var previewServer = new PreviewServer();
4965
+
4966
+ // ../../packages/orchestrator/src/agent-session.ts
4967
+ import { nanoid as nanoid2 } from "nanoid";
4968
+ var SESSION_FILE = path2.join(homedir2(), ".bit-office", "agent-sessions.json");
4969
+ function loadSessionMap() {
4970
+ try {
4971
+ if (existsSync2(SESSION_FILE)) return JSON.parse(readFileSync2(SESSION_FILE, "utf-8"));
4972
+ } catch {
4973
+ }
4974
+ return {};
4975
+ }
4976
+ function saveSessionId(agentId, sessionId) {
4977
+ const dir = path2.dirname(SESSION_FILE);
4978
+ if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
4979
+ const map = loadSessionMap();
4980
+ if (sessionId) {
4981
+ map[agentId] = sessionId;
4982
+ } else {
4983
+ delete map[agentId];
4984
+ }
4985
+ writeFileSync2(SESSION_FILE, JSON.stringify(map), "utf-8");
4986
+ }
4641
4987
  var AgentSession = class {
4642
4988
  agentId;
4643
4989
  name;
@@ -4647,6 +4993,8 @@ var AgentSession = class {
4647
4993
  palette;
4648
4994
  process = null;
4649
4995
  currentTaskId = null;
4996
+ taskTimeout = null;
4997
+ idleTimer = null;
4650
4998
  currentCwd = null;
4651
4999
  _status = "idle";
4652
5000
  get status() {
@@ -4654,36 +5002,90 @@ var AgentSession = class {
4654
5002
  }
4655
5003
  pendingApprovals = /* @__PURE__ */ new Map();
4656
5004
  workspace;
5005
+ sandboxMode;
4657
5006
  stdoutBuffer = "";
4658
5007
  stderrBuffer = "";
4659
- hasHistory = false;
5008
+ hasHistory;
5009
+ sessionId;
5010
+ taskQueue = [];
5011
+ onEvent;
5012
+ _renderPrompt;
5013
+ timedOut = false;
5014
+ _isTeamLead;
5015
+ _lastResult = null;
5016
+ /** Original user-facing task prompt (for leader state-summary mode) */
5017
+ originalTask = null;
4660
5018
  onDelegation = null;
4661
- constructor(agentId, name, role, personality, workspace, resumeHistory, backendId) {
4662
- this.agentId = agentId;
4663
- this.name = name;
4664
- this.role = role;
4665
- this.personality = personality ?? "";
4666
- this.workspace = workspace ?? config.defaultWorkspace;
4667
- this.hasHistory = resumeHistory ?? false;
4668
- this.backend = getBackend(backendId ?? config.defaultBackend) ?? getBackend("claude");
4669
- }
4670
- async runTask(taskId, prompt, repoPath) {
5019
+ onTaskComplete = null;
5020
+ /** Whether the last failure was a timeout (not retryable) */
5021
+ get wasTimeout() {
5022
+ return this.timedOut;
5023
+ }
5024
+ get isTeamLead() {
5025
+ return this._isTeamLead;
5026
+ }
5027
+ /** Short summary of last completed/failed task (for roster context) */
5028
+ get lastResult() {
5029
+ return this._lastResult;
5030
+ }
5031
+ _lastResultText = null;
5032
+ set isTeamLead(v) {
5033
+ this._isTeamLead = v;
5034
+ }
5035
+ /** Current working directory of the running task (used by worktree logic) */
5036
+ get currentWorkingDir() {
5037
+ return this.currentCwd;
5038
+ }
5039
+ /** Worktree path if task is running in one (set externally by orchestrator) */
5040
+ worktreePath = null;
5041
+ worktreeBranch = null;
5042
+ teamId;
5043
+ constructor(opts) {
5044
+ this.agentId = opts.agentId;
5045
+ this.name = opts.name;
5046
+ this.role = opts.role;
5047
+ this.personality = opts.personality ?? "";
5048
+ this.workspace = opts.workspace;
5049
+ this.sessionId = loadSessionMap()[opts.agentId] ?? null;
5050
+ this.hasHistory = opts.resumeHistory ?? !!this.sessionId;
5051
+ this.backend = opts.backend;
5052
+ this.sandboxMode = opts.sandboxMode ?? "full";
5053
+ this._isTeamLead = opts.isTeamLead ?? false;
5054
+ this.teamId = opts.teamId;
5055
+ this.onEvent = opts.onEvent;
5056
+ this._renderPrompt = opts.renderPrompt;
5057
+ }
5058
+ async runTask(taskId, prompt, repoPath, teamContext, isUserInitiated = false) {
5059
+ if (this._userCancelled && !isUserInitiated) {
5060
+ console.log(`[Agent ${this.name}] Ignoring internal task restart \u2014 agent was cancelled by user`);
5061
+ return;
5062
+ }
5063
+ if (isUserInitiated) {
5064
+ this._userCancelled = false;
5065
+ }
4671
5066
  if (this.process) {
4672
- publishEvent({
4673
- type: "TASK_FAILED",
5067
+ const position = this.taskQueue.length + 1;
5068
+ this.taskQueue.push({ taskId, prompt, repoPath, teamContext });
5069
+ this.onEvent({
5070
+ type: "task:queued",
4674
5071
  agentId: this.agentId,
4675
5072
  taskId,
4676
- error: "Agent is working hard, please wait..."
5073
+ prompt,
5074
+ position
4677
5075
  });
4678
5076
  return;
4679
5077
  }
5078
+ if (this.idleTimer) {
5079
+ clearTimeout(this.idleTimer);
5080
+ this.idleTimer = null;
5081
+ }
4680
5082
  this.currentTaskId = taskId;
4681
5083
  const cwd = repoPath ?? this.workspace;
4682
5084
  this.currentCwd = cwd;
4683
5085
  this.stdoutBuffer = "";
4684
5086
  this.stderrBuffer = "";
4685
- publishEvent({
4686
- type: "TASK_STARTED",
5087
+ this.onEvent({
5088
+ type: "task:started",
4687
5089
  agentId: this.agentId,
4688
5090
  taskId,
4689
5091
  prompt
@@ -4694,129 +5096,422 @@ var AgentSession = class {
4694
5096
  for (const key of this.backend.deleteEnv ?? []) {
4695
5097
  delete cleanEnv[key];
4696
5098
  }
4697
- const personalityClause = this.personality ? `${this.personality}
4698
-
4699
- ` : "";
4700
- const delegationHint = "To delegate a task to another agent, output on its own line: @AgentName: <task description>\n\n";
4701
- const identityPrompt = `Your name is ${this.name}, your role is ${this.role}. ${personalityClause}${delegationHint}${prompt}`;
4702
- const fullAccess = config.sandboxMode === "full";
4703
- const args = this.backend.buildArgs(identityPrompt, { continue: this.hasHistory, fullAccess });
4704
- this.process = spawn(this.backend.command, args, {
5099
+ const templateVars = {
5100
+ name: this.name,
5101
+ role: this._isTeamLead ? "Team Lead" : this.role,
5102
+ personality: this.personality ? `${this.personality}` : "",
5103
+ teamRoster: teamContext ?? "",
5104
+ originalTask: this._isTeamLead ? this.originalTask ?? prompt : "",
5105
+ prompt
5106
+ };
5107
+ let fullPrompt;
5108
+ if (this._isTeamLead) {
5109
+ fullPrompt = this._renderPrompt(this.hasHistory ? "leader-continue" : "leader-initial", templateVars);
5110
+ } else {
5111
+ fullPrompt = this._renderPrompt(this.hasHistory ? "worker-continue" : "worker-initial", templateVars);
5112
+ }
5113
+ const fullAccess = this.sandboxMode === "full";
5114
+ const verbose = !!process.env.DEBUG;
5115
+ const args = this.backend.buildArgs(fullPrompt, {
5116
+ continue: this.hasHistory,
5117
+ resumeSessionId: this.sessionId ?? void 0,
5118
+ fullAccess,
5119
+ noTools: this._isTeamLead,
5120
+ model: this._isTeamLead ? "sonnet" : void 0,
5121
+ verbose,
5122
+ skipResume: this._isTeamLead && this.hasHistory
5123
+ });
5124
+ try {
5125
+ const whichPath = execSync2(`which ${this.backend.command}`, { env: cleanEnv, encoding: "utf-8", timeout: 3e3 }).trim();
5126
+ console.log(`[Agent ${this.name}] Binary: ${whichPath}, CLAUDECODE=${cleanEnv.CLAUDECODE ?? "unset"}, ENTRYPOINT=${cleanEnv.CLAUDE_CODE_ENTRYPOINT ?? "unset"}`);
5127
+ } catch {
5128
+ }
5129
+ console.log(`[Agent ${this.name}] Spawning: ${this.backend.command} ${args.map((a) => a.length > 80 ? a.slice(0, 80) + "..." : a).join(" ")}`);
5130
+ this.process = spawn2(this.backend.command, args, {
4705
5131
  cwd,
4706
5132
  env: cleanEnv,
4707
- stdio: ["ignore", "pipe", "pipe"]
5133
+ stdio: ["ignore", "pipe", "pipe"],
5134
+ detached: true
4708
5135
  });
4709
- const DELEGATION_RE = /^@(\w+):\s*(.+)$/;
4710
- this.process.stdout?.on("data", (data) => {
4711
- const chunk = data.toString();
4712
- this.stdoutBuffer += chunk;
4713
- const lines = chunk.split("\n").filter((l) => l.trim());
5136
+ this.timedOut = false;
5137
+ const TASK_TIMEOUT_MS = this._isTeamLead ? 3 * 60 * 1e3 : 8 * 60 * 1e3;
5138
+ this.taskTimeout = setTimeout(() => {
5139
+ if (this.process?.pid) {
5140
+ console.log(`[Agent ${this.agentId}] Task timed out after ${TASK_TIMEOUT_MS / 1e3}s, killing`);
5141
+ this.timedOut = true;
5142
+ try {
5143
+ process.kill(-this.process.pid, "SIGKILL");
5144
+ } catch {
5145
+ this.process.kill("SIGKILL");
5146
+ }
5147
+ }
5148
+ }, TASK_TIMEOUT_MS);
5149
+ const DELEGATION_RE = /^\s*(?:[-*>]\s*)?(?:\*\*)?@(\w+)(?:\*\*)?:\s*(.+)$/;
5150
+ const isSystemNoise = (line) => {
5151
+ const t = line.trim().toLowerCase();
5152
+ if (!t) return true;
5153
+ if (t.includes("mcp") && (t.startsWith("[") || t.includes("server") || t.includes("connect") || t.includes("tool"))) return true;
5154
+ if (/^\s*>?\s*(fetching|loaded|reading|writing|searching|running|executing|checking)\s/i.test(line)) return true;
5155
+ if (/^[\s⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✓✗•·…\-]+$/.test(line.trim())) return true;
5156
+ if (/^\s*[\w./\\-]+\.(ts|tsx|js|jsx|json|md|css|py)\s*$/.test(line)) return true;
5157
+ return false;
5158
+ };
5159
+ const handleTextLine = (text) => {
5160
+ const lines = text.split("\n").filter((l) => l.trim());
5161
+ const visibleLines = [];
4714
5162
  for (const line of lines) {
4715
- const match = line.match(DELEGATION_RE);
5163
+ const trimmed = line.trim();
5164
+ console.log(`[Agent ${this.name}] ${trimmed.slice(0, 200)}`);
5165
+ const match = trimmed.match(DELEGATION_RE);
4716
5166
  if (match && this.onDelegation) {
4717
5167
  const [, targetName, delegatedPrompt] = match;
4718
- this.onDelegation(this.agentId, targetName, delegatedPrompt);
5168
+ console.log(`[Delegation detected] ${this.name} -> @${targetName}: ${delegatedPrompt.slice(0, 60)}`);
5169
+ this.onDelegation(this.agentId, targetName, delegatedPrompt.replace(/\*\*$/, "").trim());
5170
+ }
5171
+ if (!isSystemNoise(line)) {
5172
+ visibleLines.push(trimmed);
4719
5173
  }
4720
5174
  }
4721
- if (lines.length > 0) {
4722
- publishEvent({
4723
- type: "LOG_APPEND",
5175
+ if (visibleLines.length > 0) {
5176
+ this.onEvent({
5177
+ type: "log:append",
4724
5178
  agentId: this.agentId,
4725
5179
  taskId,
4726
5180
  stream: "stdout",
4727
- chunk: lines[lines.length - 1]
5181
+ chunk: visibleLines.slice(-3).join("\n")
4728
5182
  });
4729
5183
  }
5184
+ };
5185
+ let jsonLineBuf = "";
5186
+ let stdoutChunkCount = 0;
5187
+ let seenFirstJson = false;
5188
+ this.process.stdout?.on("data", (data) => {
5189
+ const raw = data.toString();
5190
+ stdoutChunkCount++;
5191
+ if (stdoutChunkCount <= 3) {
5192
+ console.log(`[Agent ${this.name} raw-stdout #${stdoutChunkCount}] ${raw.slice(0, 150)}`);
5193
+ }
5194
+ jsonLineBuf += raw;
5195
+ let nlIdx;
5196
+ while ((nlIdx = jsonLineBuf.indexOf("\n")) !== -1) {
5197
+ const line = jsonLineBuf.slice(0, nlIdx).trim();
5198
+ jsonLineBuf = jsonLineBuf.slice(nlIdx + 1);
5199
+ if (!line) continue;
5200
+ if (line.startsWith("{")) {
5201
+ try {
5202
+ const msg = JSON.parse(line);
5203
+ seenFirstJson = true;
5204
+ if (msg.type === "system" && msg.session_id) {
5205
+ this.sessionId = msg.session_id;
5206
+ console.log(`[Agent ${this.name}] Session ID: ${msg.session_id}`);
5207
+ }
5208
+ if (msg.type === "assistant" && msg.message?.content) {
5209
+ for (const block of msg.message.content) {
5210
+ if (block.type === "text" && block.text) {
5211
+ this.stdoutBuffer += block.text + "\n";
5212
+ handleTextLine(block.text);
5213
+ }
5214
+ if (block.type === "thinking" && block.thinking) {
5215
+ console.log(`[Agent ${this.name} thinking] ${block.thinking.slice(0, 120)}...`);
5216
+ }
5217
+ }
5218
+ } else if (msg.type === "result" && msg.result) {
5219
+ if (!this.stdoutBuffer) {
5220
+ this.stdoutBuffer = msg.result;
5221
+ handleTextLine(msg.result);
5222
+ }
5223
+ this._lastResultText = msg.result;
5224
+ }
5225
+ continue;
5226
+ } catch {
5227
+ }
5228
+ }
5229
+ if (!seenFirstJson) {
5230
+ seenFirstJson = true;
5231
+ }
5232
+ this.stdoutBuffer += line + "\n";
5233
+ handleTextLine(line);
5234
+ }
4730
5235
  });
4731
5236
  this.process.stderr?.on("data", (data) => {
4732
5237
  const chunk = data.toString();
4733
5238
  this.stderrBuffer += chunk;
4734
- const lines = chunk.split("\n").filter((l) => l.trim());
4735
- if (lines.length > 0) {
4736
- publishEvent({
4737
- type: "LOG_APPEND",
4738
- agentId: this.agentId,
4739
- taskId,
4740
- stream: "stderr",
4741
- chunk: lines[lines.length - 1]
4742
- });
5239
+ for (const line of chunk.split("\n")) {
5240
+ if (line.trim()) console.log(`[Agent ${this.name} stderr] ${line.slice(0, 200)}`);
4743
5241
  }
4744
5242
  });
4745
5243
  this.process.on("close", (code) => {
4746
5244
  this.process = null;
5245
+ if (this.taskTimeout) {
5246
+ clearTimeout(this.taskTimeout);
5247
+ this.taskTimeout = null;
5248
+ }
5249
+ const remaining = jsonLineBuf.trim();
5250
+ if (remaining) {
5251
+ jsonLineBuf = "";
5252
+ for (const chunk of remaining.split("\n")) {
5253
+ const line = chunk.trim();
5254
+ if (!line) continue;
5255
+ if (line.startsWith("{")) {
5256
+ try {
5257
+ const msg = JSON.parse(line);
5258
+ if (msg.type === "assistant" && msg.message?.content) {
5259
+ for (const block of msg.message.content) {
5260
+ if (block.type === "text" && block.text) {
5261
+ this.stdoutBuffer += block.text + "\n";
5262
+ handleTextLine(block.text);
5263
+ }
5264
+ }
5265
+ } else if (msg.type === "result" && msg.result) {
5266
+ this._lastResultText = msg.result;
5267
+ if (!this.stdoutBuffer) {
5268
+ this.stdoutBuffer = msg.result;
5269
+ handleTextLine(msg.result);
5270
+ }
5271
+ }
5272
+ } catch {
5273
+ }
5274
+ } else {
5275
+ seenFirstJson = true;
5276
+ this.stdoutBuffer += line + "\n";
5277
+ handleTextLine(line);
5278
+ }
5279
+ }
5280
+ }
4747
5281
  const completedTaskId = this.currentTaskId ?? taskId;
4748
5282
  this.currentTaskId = null;
4749
5283
  const wasCancelled = this.cancelled;
4750
5284
  this.cancelled = false;
4751
- console.log(`[Agent ${this.agentId}] ${this.backend.name} exited: code=${code}, cancelled=${wasCancelled}`);
5285
+ console.log(`[Agent ${this.agentId}] ${this.backend.name} exited: code=${code}, cancelled=${wasCancelled}, stdout=${this.stdoutBuffer.length}ch`);
4752
5286
  try {
4753
5287
  if (wasCancelled) {
4754
- this.setStatus("idle");
5288
+ this.dequeueNext();
5289
+ return;
4755
5290
  } else if (code === 0) {
4756
5291
  this.hasHistory = true;
4757
- const summary = this.stdoutBuffer.slice(0, 1e3) || "Task completed";
5292
+ saveSessionId(this.agentId, this.sessionId);
5293
+ const { summary, fullOutput, changedFiles } = this.extractResult();
5294
+ const { previewUrl, previewPath } = this._isTeamLead ? { previewUrl: void 0, previewPath: void 0 } : this.detectPreview();
5295
+ this._lastResult = `done: ${summary.slice(0, 120)}`;
4758
5296
  this.setStatus("done");
4759
- publishEvent({
4760
- type: "TASK_DONE",
5297
+ this.onEvent({
5298
+ type: "task:done",
4761
5299
  agentId: this.agentId,
4762
5300
  taskId: completedTaskId,
4763
- result: { summary, changedFiles: [], diffStat: "", testResult: "unknown" }
5301
+ result: { summary, fullOutput, changedFiles, diffStat: "", testResult: "unknown", previewUrl, previewPath }
4764
5302
  });
4765
- setTimeout(() => this.setStatus("idle"), 5e3);
5303
+ this.onTaskComplete?.(this.agentId, completedTaskId, summary, true);
5304
+ this.idleTimer = setTimeout(() => {
5305
+ this.idleTimer = null;
5306
+ this.setStatus("idle");
5307
+ }, 5e3);
4766
5308
  } else {
5309
+ const errorMsg = this.stdoutBuffer.slice(0, 300) || this.stderrBuffer.slice(-300) || `Process exited with code ${code}`;
5310
+ this._lastResult = `failed: ${errorMsg.slice(0, 120)}`;
4767
5311
  this.setStatus("error");
4768
- publishEvent({
4769
- type: "TASK_FAILED",
5312
+ this.onEvent({
5313
+ type: "task:failed",
4770
5314
  agentId: this.agentId,
4771
5315
  taskId: completedTaskId,
4772
- error: this.stdoutBuffer.slice(0, 300) || this.stderrBuffer.slice(-300) || `Process exited with code ${code}`
5316
+ error: errorMsg
4773
5317
  });
4774
- setTimeout(() => this.setStatus("idle"), 3e3);
5318
+ this.onTaskComplete?.(this.agentId, completedTaskId, errorMsg, false);
5319
+ this.idleTimer = setTimeout(() => {
5320
+ this.idleTimer = null;
5321
+ this.setStatus("idle");
5322
+ }, 3e3);
4775
5323
  }
5324
+ this.dequeueNext();
4776
5325
  } catch (err) {
4777
5326
  console.error(`[Agent ${this.agentId}] Error in close handler:`, err);
4778
5327
  this.setStatus("idle");
5328
+ this.dequeueNext();
4779
5329
  }
4780
5330
  });
4781
5331
  this.process.on("error", (err) => {
4782
5332
  this.process = null;
4783
5333
  this.currentTaskId = null;
4784
5334
  this.setStatus("error");
4785
- publishEvent({
4786
- type: "TASK_FAILED",
5335
+ this.onEvent({
5336
+ type: "task:failed",
4787
5337
  agentId: this.agentId,
4788
5338
  taskId,
4789
5339
  error: err.message
4790
5340
  });
4791
- setTimeout(() => this.setStatus("idle"), 3e3);
5341
+ this.idleTimer = setTimeout(() => {
5342
+ this.idleTimer = null;
5343
+ this.setStatus("idle");
5344
+ }, 3e3);
4792
5345
  });
4793
5346
  } catch (err) {
4794
5347
  this.setStatus("error");
4795
- publishEvent({
4796
- type: "TASK_FAILED",
5348
+ this.onEvent({
5349
+ type: "task:failed",
4797
5350
  agentId: this.agentId,
4798
5351
  taskId,
4799
5352
  error: err.message
4800
5353
  });
4801
5354
  }
4802
5355
  }
5356
+ /**
5357
+ * Send a message to the agent's stdin.
5358
+ * NOTE: Currently a no-op because stdin is set to "ignore" (pipe causes Claude Code to hang).
5359
+ * Future: use --input-format stream-json for bidirectional communication.
5360
+ */
5361
+ sendMessage(_message) {
5362
+ return false;
5363
+ }
5364
+ /**
5365
+ * Detect preview URL/path from agent output.
5366
+ * Called directly for workers; called by orchestrator for leader's final result.
5367
+ */
5368
+ detectPreview() {
5369
+ const previewMatch = this.stdoutBuffer.match(/PREVIEW:\s*(https?:\/\/[^\s*)\]>]+)/i);
5370
+ let previewUrl = previewMatch?.[1]?.replace(/[*)\]>]+$/, "");
5371
+ let previewPath;
5372
+ if (!previewUrl) {
5373
+ const localhostMatch = this.stdoutBuffer.match(/https?:\/\/localhost[:\d]*/);
5374
+ previewUrl = localhostMatch?.[0];
5375
+ }
5376
+ if (!previewUrl) {
5377
+ const fileMatch = this.stdoutBuffer.match(/(?:open\s+)?((?:\/[\w./_-]+|[\w./_-]+)\.html?)\b/i);
5378
+ if (fileMatch) {
5379
+ previewPath = path2.isAbsolute(fileMatch[1]) ? fileMatch[1] : path2.join(this.currentCwd ?? this.workspace, fileMatch[1]);
5380
+ previewUrl = previewServer.serve(previewPath);
5381
+ }
5382
+ }
5383
+ if (!previewUrl) {
5384
+ const { changedFiles } = this.extractResult();
5385
+ const htmlFile = changedFiles.find((f) => /\.html?$/i.test(f));
5386
+ if (htmlFile) {
5387
+ previewPath = path2.isAbsolute(htmlFile) ? htmlFile : path2.join(this.currentCwd ?? this.workspace, htmlFile);
5388
+ previewUrl = previewServer.serve(previewPath);
5389
+ }
5390
+ }
5391
+ return { previewUrl, previewPath };
5392
+ }
5393
+ /**
5394
+ * Parse stdoutBuffer for structured result (SUMMARY/STATUS/FILES_CHANGED).
5395
+ * Falls back to a cleaned-up excerpt of the raw output.
5396
+ */
5397
+ extractResult() {
5398
+ const raw = this.stdoutBuffer || this._lastResultText || "";
5399
+ const fullOutput = raw.slice(0, 3e3);
5400
+ const summaryMatch = raw.match(/SUMMARY:\s*(.+)/i);
5401
+ const filesMatch = raw.match(/FILES_CHANGED:\s*(.+)/i);
5402
+ const changedFiles = [];
5403
+ if (filesMatch) {
5404
+ const fileList = filesMatch[1].trim();
5405
+ for (const f of fileList.split(/[,\n]+/)) {
5406
+ const cleaned = f.trim().replace(/^[-*]\s*/, "");
5407
+ if (cleaned) changedFiles.push(cleaned);
5408
+ }
5409
+ }
5410
+ if (summaryMatch) {
5411
+ return { summary: summaryMatch[1].trim(), fullOutput, changedFiles };
5412
+ }
5413
+ const lines = raw.split("\n").filter((l) => l.trim());
5414
+ const delegationRe = /^@(\w+):/;
5415
+ const noisePatterns = [
5416
+ /^STATUS:\s/i,
5417
+ /^FILES_CHANGED:\s/i,
5418
+ /^SUMMARY:\s/i,
5419
+ /^\[Assigned by /,
5420
+ /^mcp\s/i,
5421
+ /^╔|^║|^╚/,
5422
+ /^\s*[-*]{3,}\s*$/
5423
+ ];
5424
+ const delegationTargets = [];
5425
+ const meaningful = [];
5426
+ for (const l of lines) {
5427
+ const trimmed = l.trim();
5428
+ const dm = trimmed.match(delegationRe);
5429
+ if (dm) {
5430
+ delegationTargets.push(dm[1]);
5431
+ } else if (!noisePatterns.some((p) => p.test(trimmed))) {
5432
+ meaningful.push(l);
5433
+ }
5434
+ }
5435
+ if (meaningful.length === 0 && delegationTargets.length > 0) {
5436
+ return { summary: `Delegated tasks to ${delegationTargets.join(", ")}`, fullOutput, changedFiles };
5437
+ }
5438
+ const lastChunk = meaningful.slice(-5).join("\n").trim();
5439
+ const summary = lastChunk.slice(0, 500) || "Task completed";
5440
+ return { summary, fullOutput, changedFiles };
5441
+ }
5442
+ dequeueNext() {
5443
+ if (this.taskQueue.length === 0) return;
5444
+ const next = this.taskQueue.shift();
5445
+ setTimeout(() => {
5446
+ this.runTask(next.taskId, next.prompt, next.repoPath, next.teamContext);
5447
+ }, 100);
5448
+ }
4803
5449
  cancelled = false;
5450
+ /** Set by cancelTask(); prevents flushResults / delegation from auto-restarting this agent. */
5451
+ _userCancelled = false;
4804
5452
  cancelTask() {
4805
- if (this.process) {
5453
+ this.taskQueue = [];
5454
+ this._userCancelled = true;
5455
+ if (this.taskTimeout) {
5456
+ clearTimeout(this.taskTimeout);
5457
+ this.taskTimeout = null;
5458
+ }
5459
+ if (this.idleTimer) {
5460
+ clearTimeout(this.idleTimer);
5461
+ this.idleTimer = null;
5462
+ }
5463
+ const cancelledTaskId = this.currentTaskId ?? "";
5464
+ if (this.process && this.process.pid) {
4806
5465
  this.cancelled = true;
4807
- this.process.kill("SIGTERM");
5466
+ this.hasHistory = true;
5467
+ saveSessionId(this.agentId, this.sessionId);
5468
+ this.onTaskComplete?.(this.agentId, cancelledTaskId, "Task cancelled by user", false);
5469
+ const pgid = this.process.pid;
5470
+ try {
5471
+ process.kill(-pgid, "SIGKILL");
5472
+ } catch {
5473
+ try {
5474
+ this.process.kill("SIGKILL");
5475
+ } catch {
5476
+ }
5477
+ }
4808
5478
  }
5479
+ this._lastResult = "cancelled: Task cancelled by user";
5480
+ this.setStatus("error");
5481
+ this.onEvent({
5482
+ type: "task:failed",
5483
+ agentId: this.agentId,
5484
+ taskId: cancelledTaskId,
5485
+ error: "Task cancelled by user"
5486
+ });
5487
+ this.idleTimer = setTimeout(() => {
5488
+ this.idleTimer = null;
5489
+ this.setStatus("idle");
5490
+ }, 3e3);
4809
5491
  }
4810
5492
  destroy() {
4811
- if (this.process) {
4812
- this.process.kill("SIGTERM");
5493
+ if (this.taskTimeout) {
5494
+ clearTimeout(this.taskTimeout);
5495
+ this.taskTimeout = null;
5496
+ }
5497
+ if (this.idleTimer) {
5498
+ clearTimeout(this.idleTimer);
5499
+ this.idleTimer = null;
5500
+ }
5501
+ if (this.process?.pid) {
5502
+ try {
5503
+ process.kill(-this.process.pid, "SIGTERM");
5504
+ } catch {
5505
+ this.process.kill("SIGTERM");
5506
+ }
4813
5507
  this.process = null;
4814
5508
  }
4815
5509
  this.pendingApprovals.clear();
5510
+ saveSessionId(this.agentId, null);
4816
5511
  }
4817
5512
  resolveApproval(approvalId, decision) {
4818
5513
  if (approvalId === "__all__") {
4819
- for (const [id, pending2] of this.pendingApprovals) {
5514
+ for (const [, pending2] of this.pendingApprovals) {
4820
5515
  pending2.resolve(decision);
4821
5516
  }
4822
5517
  this.pendingApprovals.clear();
@@ -4829,11 +5524,11 @@ var AgentSession = class {
4829
5524
  }
4830
5525
  }
4831
5526
  async requestApproval(title, summary, riskLevel) {
4832
- const approvalId = nanoid();
5527
+ const approvalId = nanoid2();
4833
5528
  const taskId = this.currentTaskId ?? "unknown";
4834
5529
  this.setStatus("waiting_approval");
4835
- publishEvent({
4836
- type: "APPROVAL_NEEDED",
5530
+ this.onEvent({
5531
+ type: "approval:needed",
4837
5532
  approvalId,
4838
5533
  agentId: this.agentId,
4839
5534
  taskId,
@@ -4845,50 +5540,54 @@ var AgentSession = class {
4845
5540
  this.pendingApprovals.set(approvalId, { approvalId, resolve: resolve2 });
4846
5541
  });
4847
5542
  }
4848
- generateResultSummary(cwd) {
4849
- const summary = this.stdoutBuffer.slice(0, 1e3) || "Task completed";
4850
- let changedFiles = [];
4851
- let diffStat = "";
4852
- try {
4853
- const nameOnly = execSync2("git diff --name-only HEAD~1 2>/dev/null || git diff --name-only", { cwd, encoding: "utf-8", timeout: 3e3 }).trim();
4854
- changedFiles = nameOnly ? nameOnly.split("\n") : [];
4855
- } catch {
4856
- }
4857
- if (changedFiles.length > 0) {
4858
- try {
4859
- diffStat = execSync2("git diff --stat HEAD~1 2>/dev/null || git diff --stat", { cwd, encoding: "utf-8", timeout: 3e3 }).trim();
4860
- } catch {
4861
- }
4862
- }
4863
- return {
4864
- summary,
4865
- changedFiles,
4866
- diffStat,
4867
- testResult: "unknown"
4868
- };
4869
- }
4870
5543
  setStatus(status) {
5544
+ if (status === "idle" && this.process) return;
4871
5545
  this._status = status;
4872
- publishEvent({
4873
- type: "AGENT_STATUS",
5546
+ this.onEvent({
5547
+ type: "agent:status",
4874
5548
  agentId: this.agentId,
4875
5549
  status
4876
5550
  });
4877
5551
  }
4878
5552
  };
4879
5553
 
4880
- // src/agent-manager.ts
5554
+ // ../../packages/orchestrator/src/agent-manager.ts
4881
5555
  var AgentManager = class {
4882
5556
  agents = /* @__PURE__ */ new Map();
4883
- create(agentId, name, role, personality, workspace, resumeHistory, backendId, palette) {
4884
- const existing = this.agents.get(agentId);
5557
+ _teamLeadId = null;
5558
+ setTeamLead(id) {
5559
+ this._teamLeadId = id;
5560
+ }
5561
+ getTeamLead() {
5562
+ return this._teamLeadId;
5563
+ }
5564
+ isTeamLead(id) {
5565
+ return this._teamLeadId === id;
5566
+ }
5567
+ getTeamRoster() {
5568
+ const lines = [];
5569
+ for (const session of this.agents.values()) {
5570
+ const lead = this.isTeamLead(session.agentId) ? " (Team Lead)" : "";
5571
+ const result = session.lastResult ? ` \u2014 ${session.lastResult}` : "";
5572
+ lines.push(`- ${session.name} (${session.role}) [${session.status}]${lead}${result}`);
5573
+ }
5574
+ return lines.join("\n");
5575
+ }
5576
+ getTeamMembers() {
5577
+ return Array.from(this.agents.values()).map((s) => ({
5578
+ name: s.name,
5579
+ role: s.role,
5580
+ status: s.status,
5581
+ isLead: this.isTeamLead(s.agentId),
5582
+ lastResult: s.lastResult
5583
+ }));
5584
+ }
5585
+ add(session) {
5586
+ const existing = this.agents.get(session.agentId);
4885
5587
  if (existing) {
4886
5588
  existing.destroy();
4887
5589
  }
4888
- const session = new AgentSession(agentId, name, role, personality, workspace, resumeHistory, backendId);
4889
- session.palette = palette;
4890
- this.agents.set(agentId, session);
4891
- return session;
5590
+ this.agents.set(session.agentId, session);
4892
5591
  }
4893
5592
  delete(agentId) {
4894
5593
  const session = this.agents.get(agentId);
@@ -4912,226 +5611,1048 @@ var AgentManager = class {
4912
5611
  return void 0;
4913
5612
  }
4914
5613
  };
4915
- var agentManager = new AgentManager();
4916
5614
 
4917
- // src/telegram-channel.ts
4918
- import { nanoid as nanoid2 } from "nanoid";
4919
- var PRESETS = [
4920
- { name: "Alex", role: "Frontend Dev", palette: 0, personality: "You speak in a friendly, casual, encouraging, and natural tone." },
4921
- { name: "Mia", role: "Backend Dev", palette: 1, personality: "You speak formally, professionally, in an organized and concise manner." },
4922
- { name: "Leo", role: "Fullstack Dev", palette: 2, personality: "You are aggressive, action-first, always pursuing speed and efficiency." },
4923
- { name: "Sophie", role: "Code Reviewer", palette: 3, personality: "You teach patiently, explain the reasoning, and guide like a mentor." },
4924
- { name: "Yuki", role: "QA / Tester", palette: 4, personality: "You are extremely concise, no fluff, straight to the result." },
4925
- { name: "Marcus", role: "Architect", palette: 5, personality: "You speak formally, professionally, in an organized and concise manner." }
4926
- ];
4927
- var botAgents = [];
4928
- function formatEvent(event, agentId) {
4929
- if (event.type === "TASK_STARTED" && event.agentId === agentId) {
4930
- return `\u{1F527} Working on it...`;
5615
+ // ../../packages/orchestrator/src/delegation.ts
5616
+ import { nanoid as nanoid3 } from "nanoid";
5617
+ var MAX_DELEGATION_DEPTH = 5;
5618
+ var MAX_TOTAL_DELEGATIONS = 20;
5619
+ var DELEGATION_BUDGET_ROUNDS = 5;
5620
+ var HARD_CEILING_ROUNDS = 10;
5621
+ var RESULT_BATCH_WINDOW_MS = 2e4;
5622
+ var DelegationRouter = class {
5623
+ /** taskId fromAgentId */
5624
+ delegationOrigin = /* @__PURE__ */ new Map();
5625
+ /** taskId delegation depth (how many hops from original user task) */
5626
+ delegationDepth = /* @__PURE__ */ new Map();
5627
+ /** agentId taskId of the delegated task currently assigned TO this agent */
5628
+ assignedTask = /* @__PURE__ */ new Map();
5629
+ /** Total delegations in current team session (reset on clearAll) */
5630
+ totalDelegations = 0;
5631
+ /** How many times the leader has been invoked to process results */
5632
+ leaderRounds = 0;
5633
+ /** When true, all new delegations and result forwarding are blocked */
5634
+ stopped = false;
5635
+ /** TaskIds created by flushResults — only these can produce a final result */
5636
+ resultTaskIds = /* @__PURE__ */ new Set();
5637
+ /** Tracks the totalDelegations count when a resultTask started, so we can detect if new delegations were created */
5638
+ delegationsAtResultStart = /* @__PURE__ */ new Map();
5639
+ /** Batch result forwarding: originAgentId → pending results + timer */
5640
+ pendingResults = /* @__PURE__ */ new Map();
5641
+ agentManager;
5642
+ promptEngine;
5643
+ emitEvent;
5644
+ constructor(agentManager, promptEngine, emitEvent) {
5645
+ this.agentManager = agentManager;
5646
+ this.promptEngine = promptEngine;
5647
+ this.emitEvent = emitEvent;
4931
5648
  }
4932
- if (event.type === "TASK_DONE" && event.agentId === agentId) {
4933
- const r = event.result;
4934
- let msg = `\u2705 Task completed
5649
+ /**
5650
+ * Wire delegation and result forwarding callbacks onto a session.
5651
+ */
5652
+ wireAgent(session) {
5653
+ this.wireDelegation(session);
5654
+ this.wireResultForwarding(session);
5655
+ }
5656
+ /**
5657
+ * Check if a taskId was delegated (has an origin).
5658
+ */
5659
+ isDelegated(taskId) {
5660
+ return this.delegationOrigin.has(taskId);
5661
+ }
5662
+ /**
5663
+ * True if this taskId was created by flushResults (leader processing worker results).
5664
+ * Only result-processing tasks are eligible to be marked as isFinalResult.
5665
+ */
5666
+ isResultTask(taskId) {
5667
+ return this.resultTaskIds.has(taskId);
5668
+ }
5669
+ /**
5670
+ * True when the delegation budget is exhausted — leader should finalize even
5671
+ * if the current task is not a "resultTask" (safety net for convergence).
5672
+ */
5673
+ isBudgetExhausted() {
5674
+ return this.leaderRounds >= DELEGATION_BUDGET_ROUNDS;
5675
+ }
5676
+ /**
5677
+ * True if the given resultTask completed WITHOUT creating any new delegations.
5678
+ * This means the leader decided to summarize/finish rather than delegate more work.
5679
+ */
5680
+ resultTaskDidNotDelegate(taskId) {
5681
+ const startCount = this.delegationsAtResultStart.get(taskId);
5682
+ if (startCount === void 0) return false;
5683
+ return this.totalDelegations === startCount;
5684
+ }
5685
+ /**
5686
+ * Check if there are any pending delegated tasks originating from a given agent.
5687
+ */
5688
+ hasPendingFrom(agentId) {
5689
+ for (const origin of this.delegationOrigin.values()) {
5690
+ if (origin === agentId) return true;
5691
+ }
5692
+ return false;
5693
+ }
5694
+ /**
5695
+ * Remove all delegation tracking for a specific agent (on fire/cancel).
5696
+ */
5697
+ clearAgent(agentId) {
5698
+ for (const [taskId, origin] of this.delegationOrigin) {
5699
+ if (origin === agentId) {
5700
+ this.delegationOrigin.delete(taskId);
5701
+ this.delegationDepth.delete(taskId);
5702
+ }
5703
+ }
5704
+ }
5705
+ /**
5706
+ * Block all future delegations and result forwarding. Call before cancelling tasks.
5707
+ */
5708
+ stop() {
5709
+ this.stopped = true;
5710
+ for (const pending of this.pendingResults.values()) {
5711
+ clearTimeout(pending.timer);
5712
+ }
5713
+ this.pendingResults.clear();
5714
+ }
5715
+ /**
5716
+ * Reset all delegation state (on new team task).
5717
+ */
5718
+ clearAll() {
5719
+ this.delegationOrigin.clear();
5720
+ this.delegationDepth.clear();
5721
+ this.assignedTask.clear();
5722
+ this.resultTaskIds.clear();
5723
+ this.delegationsAtResultStart.clear();
5724
+ this.totalDelegations = 0;
5725
+ this.leaderRounds = 0;
5726
+ this.stopped = false;
5727
+ for (const pending of this.pendingResults.values()) {
5728
+ clearTimeout(pending.timer);
5729
+ }
5730
+ this.pendingResults.clear();
5731
+ }
5732
+ wireDelegation(session) {
5733
+ session.onDelegation = (fromAgentId, targetName, prompt) => {
5734
+ if (this.stopped) return;
5735
+ if (this.leaderRounds >= DELEGATION_BUDGET_ROUNDS) {
5736
+ console.log(`[Delegation] BLOCKED: delegation budget exhausted (round ${this.leaderRounds}/${DELEGATION_BUDGET_ROUNDS})`);
5737
+ return;
5738
+ }
5739
+ const target = this.agentManager.findByName(targetName);
5740
+ if (!target) {
5741
+ console.log(`[Delegation] Target agent "${targetName}" not found, ignoring`);
5742
+ return;
5743
+ }
5744
+ if (this.totalDelegations >= MAX_TOTAL_DELEGATIONS) {
5745
+ console.log(`[Delegation] BLOCKED: total delegation limit (${MAX_TOTAL_DELEGATIONS}) reached`);
5746
+ this.emitEvent({
5747
+ type: "team:chat",
5748
+ fromAgentId,
5749
+ message: `Delegation blocked: total limit of ${MAX_TOTAL_DELEGATIONS} delegations reached. Summarize current results for the user.`,
5750
+ messageType: "status",
5751
+ timestamp: Date.now()
5752
+ });
5753
+ return;
5754
+ }
5755
+ const myTaskId = this.assignedTask.get(fromAgentId);
5756
+ const parentDepth = myTaskId ? this.delegationDepth.get(myTaskId) ?? 0 : 0;
5757
+ const newDepth = parentDepth + 1;
5758
+ if (newDepth > MAX_DELEGATION_DEPTH) {
5759
+ console.log(`[Delegation] BLOCKED: depth ${newDepth} exceeds max ${MAX_DELEGATION_DEPTH}`);
5760
+ this.emitEvent({
5761
+ type: "team:chat",
5762
+ fromAgentId,
5763
+ message: `Delegation blocked: chain depth (${newDepth}) exceeds limit. Complete current work directly.`,
5764
+ messageType: "status",
5765
+ timestamp: Date.now()
5766
+ });
5767
+ return;
5768
+ }
5769
+ const taskId = nanoid3();
5770
+ this.delegationOrigin.set(taskId, fromAgentId);
5771
+ this.delegationDepth.set(taskId, newDepth);
5772
+ this.totalDelegations++;
5773
+ const fromSession = this.agentManager.get(fromAgentId);
5774
+ const fromName = fromSession?.name ?? fromAgentId;
5775
+ const fromRole = fromSession?.role ?? "Team Lead";
5776
+ const fullPrompt = this.promptEngine.render("delegation-prefix", { fromName, fromRole, prompt });
5777
+ console.log(`[Delegation] ${fromAgentId} -> ${target.agentId} (${targetName}) depth=${newDepth} total=${this.totalDelegations}: ${prompt.slice(0, 80)}`);
5778
+ this.emitEvent({
5779
+ type: "task:delegated",
5780
+ fromAgentId,
5781
+ toAgentId: target.agentId,
5782
+ taskId,
5783
+ prompt
5784
+ });
5785
+ this.emitEvent({
5786
+ type: "team:chat",
5787
+ fromAgentId,
5788
+ toAgentId: target.agentId,
5789
+ message: prompt,
5790
+ messageType: "delegation",
5791
+ taskId,
5792
+ timestamp: Date.now()
5793
+ });
5794
+ this.assignedTask.set(target.agentId, taskId);
5795
+ target.runTask(taskId, fullPrompt);
5796
+ };
5797
+ }
5798
+ wireResultForwarding(session) {
5799
+ session.onTaskComplete = (agentId, taskId, summary, success) => {
5800
+ if (this.stopped) return;
5801
+ const originAgentId = this.delegationOrigin.get(taskId);
5802
+ if (!originAgentId) return;
5803
+ this.delegationOrigin.delete(taskId);
5804
+ this.delegationDepth.delete(taskId);
5805
+ if (this.assignedTask.get(agentId) === taskId) {
5806
+ this.assignedTask.delete(agentId);
5807
+ }
5808
+ const originSession = this.agentManager.get(originAgentId);
5809
+ if (!originSession) return;
5810
+ const fromSession = this.agentManager.get(agentId);
5811
+ const fromName = fromSession?.name ?? agentId;
5812
+ const statusWord = success ? "completed successfully" : "failed";
5813
+ console.log(`[ResultForward] ${agentId} -> ${originAgentId}: ${summary.slice(0, 80)} (success=${success})`);
5814
+ this.emitEvent({
5815
+ type: "task:result-returned",
5816
+ fromAgentId: agentId,
5817
+ toAgentId: originAgentId,
5818
+ taskId,
5819
+ summary,
5820
+ success
5821
+ });
5822
+ this.emitEvent({
5823
+ type: "team:chat",
5824
+ fromAgentId: agentId,
5825
+ toAgentId: originAgentId,
5826
+ message: summary.slice(0, 400),
5827
+ messageType: "result",
5828
+ taskId,
5829
+ timestamp: Date.now()
5830
+ });
5831
+ this.enqueueResult(originAgentId, { fromName, statusWord, summary: summary.slice(0, 400) });
5832
+ };
5833
+ }
5834
+ /**
5835
+ * Queue a result for batched forwarding to the origin agent.
5836
+ * Flush only when ALL delegated tasks from this origin have returned.
5837
+ * The timer is a safety net — if a worker somehow disappears without returning,
5838
+ * we don't want the leader to wait forever.
5839
+ */
5840
+ enqueueResult(originAgentId, result) {
5841
+ let pending = this.pendingResults.get(originAgentId);
5842
+ if (pending) {
5843
+ clearTimeout(pending.timer);
5844
+ pending.results.push(result);
5845
+ } else {
5846
+ pending = { results: [result], timer: null };
5847
+ this.pendingResults.set(originAgentId, pending);
5848
+ }
5849
+ if (!this.hasPendingFrom(originAgentId)) {
5850
+ console.log(`[ResultBatch] All delegated tasks returned for ${originAgentId}, flushing ${pending.results.length} result(s)`);
5851
+ this.flushResults(originAgentId);
5852
+ return;
5853
+ }
5854
+ console.log(`[ResultBatch] ${originAgentId} still has pending delegations, waiting (safety timeout: ${RESULT_BATCH_WINDOW_MS / 1e3}s)`);
5855
+ pending.timer = setTimeout(() => {
5856
+ console.log(`[ResultBatch] Safety timeout reached for ${originAgentId}, flushing ${pending.results.length} partial result(s)`);
5857
+ this.flushResults(originAgentId);
5858
+ }, RESULT_BATCH_WINDOW_MS);
5859
+ }
5860
+ /** Flush all pending results for an origin agent as a single leader prompt. */
5861
+ flushResults(originAgentId) {
5862
+ if (this.stopped) return;
5863
+ const pending = this.pendingResults.get(originAgentId);
5864
+ if (!pending || pending.results.length === 0) return;
5865
+ this.pendingResults.delete(originAgentId);
5866
+ clearTimeout(pending.timer);
5867
+ const originSession = this.agentManager.get(originAgentId);
5868
+ if (!originSession) return;
5869
+ this.leaderRounds++;
5870
+ if (this.leaderRounds > HARD_CEILING_ROUNDS) {
5871
+ console.log(`[ResultBatch] Hard ceiling reached (${HARD_CEILING_ROUNDS} rounds). Force-completing.`);
5872
+ const resultLines2 = pending.results.map(
5873
+ (r) => `- ${r.fromName} (${r.statusWord}): ${r.summary}`
5874
+ ).join("\n");
5875
+ this.emitEvent({
5876
+ type: "team:chat",
5877
+ fromAgentId: originAgentId,
5878
+ message: `Team work auto-completed after ${HARD_CEILING_ROUNDS} rounds.`,
5879
+ messageType: "status",
5880
+ timestamp: Date.now()
5881
+ });
5882
+ this.emitEvent({
5883
+ type: "task:done",
5884
+ agentId: originAgentId,
5885
+ taskId: `auto-complete-${Date.now()}`,
5886
+ result: {
5887
+ summary: `Auto-completed after ${HARD_CEILING_ROUNDS} rounds.
5888
+ ${resultLines2}`,
5889
+ changedFiles: [],
5890
+ diffStat: "",
5891
+ testResult: "unknown"
5892
+ },
5893
+ isFinalResult: true
5894
+ });
5895
+ return;
5896
+ }
5897
+ let roundInfo;
5898
+ const budgetExhausted = this.leaderRounds >= DELEGATION_BUDGET_ROUNDS;
5899
+ if (budgetExhausted) {
5900
+ 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.`;
5901
+ } else if (this.leaderRounds >= DELEGATION_BUDGET_ROUNDS - 1) {
5902
+ roundInfo = `Round ${this.leaderRounds}/${DELEGATION_BUDGET_ROUNDS} \u2014 LAST delegation round. Only delegate if something is critically broken. Prefer to accept and summarize.`;
5903
+ } else {
5904
+ roundInfo = `Round ${this.leaderRounds}/${DELEGATION_BUDGET_ROUNDS}`;
5905
+ }
5906
+ const resultLines = pending.results.map(
5907
+ (r) => `- ${r.fromName} (${r.statusWord}): ${r.summary}`
5908
+ ).join("\n\n");
5909
+ const followUpTaskId = nanoid3();
5910
+ this.resultTaskIds.add(followUpTaskId);
5911
+ this.delegationsAtResultStart.set(followUpTaskId, this.totalDelegations);
5912
+ const teamContext = this.agentManager.isTeamLead(originAgentId) ? this.agentManager.getTeamRoster() : void 0;
5913
+ const batchPrompt = this.promptEngine.render("leader-result", {
5914
+ fromName: pending.results.length === 1 ? pending.results[0].fromName : `${pending.results.length} team members`,
5915
+ resultStatus: pending.results.every((r) => r.statusWord.includes("success")) ? "completed successfully" : "mixed results",
5916
+ resultSummary: resultLines,
5917
+ originalTask: originSession.originalTask ?? "",
5918
+ roundInfo
5919
+ });
5920
+ console.log(`[ResultBatch] Flushing ${pending.results.length} result(s) to ${originAgentId} (round ${this.leaderRounds}, budget=${DELEGATION_BUDGET_ROUNDS}, ceiling=${HARD_CEILING_ROUNDS})`);
5921
+ originSession.runTask(followUpTaskId, batchPrompt, void 0, teamContext);
5922
+ }
5923
+ };
4935
5924
 
4936
- ${r.summary}`;
4937
- if (r.changedFiles.length > 0) {
4938
- msg += `
5925
+ // ../../packages/orchestrator/src/prompt-templates.ts
5926
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3 } from "fs";
5927
+ import path3 from "path";
5928
+ var PROMPT_DEFAULTS = {
5929
+ "leader-initial": `You are {{name}}, the Team Lead. {{personality}}
5930
+ You CANNOT write code, run commands, or use any tools. You can ONLY delegate.
4939
5931
 
4940
- \u{1F4C1} Changed files:
4941
- ${r.changedFiles.map((f) => `\u2022 ${f}`).join("\n")}`;
5932
+ Team:
5933
+ {{teamRoster}}
5934
+
5935
+ Delegate using this exact format (one per line):
5936
+ @AgentName: [project-dir] task description
5937
+
5938
+ 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.
5939
+
5940
+ Execution phases:
5941
+ 1. BUILD: Assign coding tasks to developers now. Include file paths and specific instructions.
5942
+ 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.
5943
+ 3. REPORT: After both review and QA report back, summarize and finish. Done.
5944
+
5945
+ Rules:
5946
+ - Never write code yourself. Only delegate.
5947
+ - Each delegation must be specific and bounded: include exact file paths, function names, and expected output. Vague delegations ("improve the UI") cause scope creep.
5948
+ - Phase 1 (this round): Assign developers ONLY. Do NOT assign QA or code review yet \u2014 there is no code to test.
5949
+ - Phase 2 (one round only): Assign Code Reviewer AND QA Tester simultaneously in one response. This saves a full round.
5950
+ - Skip QA entirely for trivial changes (config tweaks, typo fixes, renaming, comment-only changes).
5951
+ - Keep the total number of rounds to 2-3. Ship working code now \u2014 the user can request improvements later.
5952
+
5953
+ Task: {{prompt}}`,
5954
+ "leader-continue": `You are {{name}}, the Team Lead. {{personality}}
5955
+ You CANNOT write code, run commands, or use any tools. You can ONLY delegate.
5956
+
5957
+ Team status:
5958
+ {{teamRoster}}
5959
+
5960
+ {{originalTask}}
5961
+
5962
+ Delegate using: @AgentName: task description
5963
+
5964
+ {{prompt}}`,
5965
+ "leader-result": `You are the Team Lead. You CANNOT write or fix code. You can ONLY delegate using @Name: [project-dir] <task>.
5966
+
5967
+ Original user task: {{originalTask}}
5968
+
5969
+ {{roundInfo}}
5970
+
5971
+ Team status:
5972
+ {{teamRoster}}
5973
+
5974
+ New result from {{fromName}} ({{resultStatus}}):
5975
+ {{resultSummary}}
5976
+
5977
+ Decision priority (choose the FIRST that applies):
5978
+ 1. ALL SUCCEEDED \u2192 Summarize the outcome for the user. You are DONE.
5979
+ 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.
5980
+ 3. Dev succeeded + trivial change (config, rename, typo, style-only) \u2192 Summarize and you are DONE. No QA needed.
5981
+ 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.
5982
+ 5. FAILED + critically broken (won't run, crash on start) \u2192 delegate ONE targeted fix to the original developer.
5983
+ 6. FAILED + permanent blocker (auth error, service down, missing dependency) \u2192 report the blocker to the user.
5984
+ 7. Same error repeated \u2192 STOP and report to the user.
5985
+
5986
+ CRITICAL RULES:
5987
+ - QA/testing findings are ALWAYS informational. NEVER delegate fixes based on QA results. Note them and finish.
5988
+ - Code review comments are ALWAYS informational. Only delegate a fix if the code literally doesn't compile or run.
5989
+ - When assigning parallel QA + Review (rule 2): write BOTH @Name: lines in one response. Do not wait for one to finish.
5990
+ - Maximum ONE fix round after a failure. After that, accept and summarize.
5991
+ - Prefer DONE over more delegation. The user can request improvements later.`,
5992
+ "worker-initial": `Your name is {{name}}, your role is {{role}}. {{personality}}
5993
+
5994
+ CONVERGENCE RULES (follow strictly):
5995
+ - Do the MINIMUM needed to satisfy the task. Simple and working beats perfect and slow.
5996
+ - Only touch files directly required by this task. Do NOT refactor, clean up, or "improve" unrelated code.
5997
+ - If you are uncertain between two approaches, choose the simpler one and note it in SUMMARY.
5998
+ - Do NOT add features, error handling, or improvements that were not explicitly asked for.
5999
+ - 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.
6000
+
6001
+ HARD LIMITS:
6002
+ - 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.
6003
+ - Do NOT install new dependencies unless the task explicitly requires it. Note any missing deps in SUMMARY instead.
6004
+
6005
+ 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.
6006
+
6007
+ When you finish, report your result in this exact format:
6008
+ STATUS: done | failed
6009
+ FILES_CHANGED: (list of files you created or modified, one per line)
6010
+ SUMMARY: (one sentence \u2014 what you did and why it satisfies the task)
6011
+
6012
+ {{prompt}}`,
6013
+ "worker-continue": `{{prompt}}`,
6014
+ "delegation-prefix": `[Assigned by {{fromName}} ({{fromRole}})]
6015
+ {{prompt}}`,
6016
+ "delegation-hint": `To delegate a task to another agent, output on its own line: @AgentName: <task description>`
6017
+ };
6018
+ var PromptEngine = class {
6019
+ templates = { ...PROMPT_DEFAULTS };
6020
+ promptsDir;
6021
+ constructor(promptsDir) {
6022
+ this.promptsDir = promptsDir;
6023
+ }
6024
+ /**
6025
+ * Initialize prompt templates on startup.
6026
+ * Creates promptsDir with defaults if it doesn't exist,
6027
+ * then loads all .md files (falling back to defaults for missing ones).
6028
+ */
6029
+ init() {
6030
+ if (!this.promptsDir) {
6031
+ console.log(`[Prompts] No promptsDir configured, using ${Object.keys(PROMPT_DEFAULTS).length} default templates`);
6032
+ return;
4942
6033
  }
4943
- return msg;
6034
+ if (!existsSync3(this.promptsDir)) {
6035
+ mkdirSync3(this.promptsDir, { recursive: true });
6036
+ for (const [name, content] of Object.entries(PROMPT_DEFAULTS)) {
6037
+ writeFileSync3(path3.join(this.promptsDir, `${name}.md`), content, "utf-8");
6038
+ }
6039
+ console.log(`[Prompts] Created ${this.promptsDir} with ${Object.keys(PROMPT_DEFAULTS).length} default templates`);
6040
+ }
6041
+ this.reload();
4944
6042
  }
4945
- if (event.type === "TASK_FAILED" && event.agentId === agentId) {
4946
- return `\u274C Task failed
6043
+ /**
6044
+ * Re-read all templates from disk. Missing files fall back to built-in defaults.
6045
+ */
6046
+ reload() {
6047
+ const merged = { ...PROMPT_DEFAULTS };
6048
+ let loaded = 0;
6049
+ let defaulted = 0;
6050
+ if (this.promptsDir) {
6051
+ for (const name of Object.keys(PROMPT_DEFAULTS)) {
6052
+ const filePath = path3.join(this.promptsDir, `${name}.md`);
6053
+ if (existsSync3(filePath)) {
6054
+ try {
6055
+ merged[name] = readFileSync3(filePath, "utf-8");
6056
+ loaded++;
6057
+ } catch {
6058
+ defaulted++;
6059
+ }
6060
+ } else {
6061
+ defaulted++;
6062
+ }
6063
+ }
6064
+ } else {
6065
+ defaulted = Object.keys(PROMPT_DEFAULTS).length;
6066
+ }
6067
+ this.templates = merged;
6068
+ console.log(`[Prompts] Loaded ${loaded} templates (${defaulted} using defaults)`);
6069
+ }
6070
+ /**
6071
+ * Render a named template with variable substitution.
6072
+ * {{variable}} placeholders are replaced with the provided values.
6073
+ */
6074
+ render(templateName, vars) {
6075
+ const template = this.templates[templateName] ?? PROMPT_DEFAULTS[templateName];
6076
+ if (!template) {
6077
+ console.warn(`[Prompts] Unknown template: ${templateName}`);
6078
+ return vars["prompt"] ?? "";
6079
+ }
6080
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
6081
+ }
6082
+ };
4947
6083
 
4948
- ${event.error}`;
6084
+ // ../../packages/orchestrator/src/retry.ts
6085
+ var RetryTracker = class {
6086
+ state = /* @__PURE__ */ new Map();
6087
+ maxRetries;
6088
+ escalateToLeader;
6089
+ constructor(maxRetries = 2, escalateToLeader = true) {
6090
+ this.maxRetries = maxRetries;
6091
+ this.escalateToLeader = escalateToLeader;
4949
6092
  }
4950
- if (event.type === "APPROVAL_NEEDED" && event.agentId === agentId) {
4951
- return `\u26A0\uFE0F Approval needed
6093
+ /**
6094
+ * Initialize tracking for a task. Call before first attempt.
6095
+ */
6096
+ track(taskId, originalPrompt) {
6097
+ this.state.set(taskId, {
6098
+ taskId,
6099
+ originalPrompt,
6100
+ attempt: 0,
6101
+ maxRetries: this.maxRetries,
6102
+ errors: []
6103
+ });
6104
+ }
6105
+ /**
6106
+ * Check if the task has retries remaining.
6107
+ */
6108
+ shouldRetry(taskId) {
6109
+ const s = this.state.get(taskId);
6110
+ if (!s) return false;
6111
+ return s.attempt < s.maxRetries;
6112
+ }
6113
+ /**
6114
+ * Record a failed attempt. Returns the updated state.
6115
+ */
6116
+ recordAttempt(taskId, error) {
6117
+ const s = this.state.get(taskId);
6118
+ if (!s) return void 0;
6119
+ s.attempt++;
6120
+ s.errors.push(error);
6121
+ return { ...s };
6122
+ }
6123
+ /**
6124
+ * Get the original prompt for retrying (with error context appended).
6125
+ */
6126
+ getRetryPrompt(taskId) {
6127
+ const s = this.state.get(taskId);
6128
+ if (!s) return null;
6129
+ const lastError = s.errors[s.errors.length - 1] ?? "unknown error";
6130
+ return `${s.originalPrompt}
4952
6131
 
4953
- ${event.title}
4954
- ${event.summary}
6132
+ [RETRY \u2014 Attempt ${s.attempt + 1}/${s.maxRetries}]
6133
+ Previous attempt failed with:
6134
+ ${lastError.slice(0, 500)}
4955
6135
 
4956
- Reply /yes or /no`;
6136
+ Before retrying, follow this protocol:
6137
+ 1. DIAGNOSE: Read the error carefully. Identify the root cause, not just the symptom.
6138
+ 2. FIX: Address the root cause first (missing dependency, wrong path, syntax error, etc.)
6139
+ 3. VERIFY: After fixing, confirm the fix works before moving on.
6140
+ Do NOT repeat the same approach that failed.`;
6141
+ }
6142
+ /**
6143
+ * Get escalation prompt for the team lead (when all retries exhausted).
6144
+ * Returns null if escalation is disabled or task not tracked.
6145
+ */
6146
+ getEscalation(taskId) {
6147
+ if (!this.escalateToLeader) return null;
6148
+ const s = this.state.get(taskId);
6149
+ if (!s) return null;
6150
+ if (s.attempt < s.maxRetries) return null;
6151
+ const errorList = s.errors.map((e, i) => ` Attempt ${i + 1}: ${e.slice(0, 200)}`).join("\n");
6152
+ const sameError = s.errors.length >= 2 && s.errors.every((e) => {
6153
+ const key = e.slice(0, 80).toLowerCase();
6154
+ return key === s.errors[0].slice(0, 80).toLowerCase();
6155
+ });
6156
+ return {
6157
+ prompt: `[ESCALATION] A task has failed after ${s.attempt} attempts and needs your decision.
6158
+
6159
+ Original task: "${s.originalPrompt.slice(0, 300)}"
6160
+
6161
+ Failure history:
6162
+ ${errorList}
6163
+ ${sameError ? "\n\u26A0\uFE0F All attempts failed with the SAME error. This is likely a PERMANENT blocker (missing credentials, API limits, service unavailable). Do NOT reassign \u2014 report to user.\n" : ""}
6164
+ Options (choose ONE):
6165
+ 1. If the error is FIXABLE (code bug, wrong path): Reassign to a DIFFERENT team member with revised instructions
6166
+ 2. If the task is too large: Break into smaller pieces and delegate each part
6167
+ 3. If the error is PERMANENT (auth failure, service down, insufficient balance, missing API key): Report the blocker to the user. Do NOT reassign.
6168
+
6169
+ IMPORTANT: If the same error keeps repeating, choose option 3. Do not waste resources retrying.`
6170
+ };
6171
+ }
6172
+ /**
6173
+ * Remove tracking for a completed/cancelled task.
6174
+ */
6175
+ clear(taskId) {
6176
+ this.state.delete(taskId);
6177
+ }
6178
+ };
6179
+
6180
+ // ../../packages/orchestrator/src/worktree.ts
6181
+ import { execSync as execSync3 } from "child_process";
6182
+ import path4 from "path";
6183
+ var TIMEOUT = 5e3;
6184
+ function isGitRepo(cwd) {
6185
+ try {
6186
+ execSync3("git rev-parse --is-inside-work-tree", { cwd, stdio: "ignore", timeout: TIMEOUT });
6187
+ return true;
6188
+ } catch {
6189
+ return false;
4957
6190
  }
4958
- return null;
4959
6191
  }
4960
- var telegramChannel = {
4961
- name: "Telegram",
4962
- async init(commandHandler) {
4963
- const tokens = config.telegramBotTokens;
4964
- if (!tokens.length || tokens.every((t) => !t)) return false;
4965
- for (let i = 0; i < tokens.length && i < PRESETS.length; i++) {
4966
- const token = tokens[i];
4967
- if (!token) continue;
4968
- const preset = PRESETS[i];
4969
- const agentId = `tg-${preset.name.toLowerCase()}`;
4970
- const bot = new TelegramBot(token, { polling: true });
4971
- const ba = { bot, preset, agentId, chatIds: /* @__PURE__ */ new Set() };
4972
- botAgents.push(ba);
4973
- if (!agentManager.get(agentId)) {
4974
- commandHandler({
4975
- type: "CREATE_AGENT",
6192
+ function createWorktree(workspace, agentId, taskId, agentName) {
6193
+ if (!isGitRepo(workspace)) return null;
6194
+ const worktreeDir = path4.join(workspace, ".worktrees");
6195
+ const worktreeName = `${agentId}-${taskId}`;
6196
+ const worktreePath = path4.join(worktreeDir, worktreeName);
6197
+ const branch = `agent/${agentName.toLowerCase().replace(/\s+/g, "-")}/${taskId}`;
6198
+ try {
6199
+ execSync3(`git worktree add "${worktreePath}" -b "${branch}"`, {
6200
+ cwd: workspace,
6201
+ stdio: "pipe",
6202
+ timeout: TIMEOUT
6203
+ });
6204
+ return worktreePath;
6205
+ } catch (err) {
6206
+ console.error(`[Worktree] Failed to create worktree: ${err.message}`);
6207
+ return null;
6208
+ }
6209
+ }
6210
+ function mergeWorktree(workspace, worktreePath, branch) {
6211
+ try {
6212
+ execSync3(`git merge --no-ff "${branch}"`, {
6213
+ cwd: workspace,
6214
+ stdio: "pipe",
6215
+ timeout: TIMEOUT
6216
+ });
6217
+ try {
6218
+ execSync3(`git worktree remove "${worktreePath}"`, { cwd: workspace, stdio: "pipe", timeout: TIMEOUT });
6219
+ } catch {
6220
+ }
6221
+ try {
6222
+ execSync3(`git branch -d "${branch}"`, { cwd: workspace, stdio: "pipe", timeout: TIMEOUT });
6223
+ } catch {
6224
+ }
6225
+ return { success: true };
6226
+ } catch (err) {
6227
+ let conflictFiles = [];
6228
+ try {
6229
+ const output = execSync3("git diff --name-only --diff-filter=U", {
6230
+ cwd: workspace,
6231
+ encoding: "utf-8",
6232
+ timeout: TIMEOUT
6233
+ }).trim();
6234
+ conflictFiles = output ? output.split("\n") : [];
6235
+ execSync3("git merge --abort", { cwd: workspace, stdio: "pipe", timeout: TIMEOUT });
6236
+ } catch {
6237
+ }
6238
+ return { success: false, conflictFiles };
6239
+ }
6240
+ }
6241
+ function removeWorktree(worktreePath, branch, workspace) {
6242
+ const cwd = workspace ?? path4.dirname(path4.dirname(worktreePath));
6243
+ try {
6244
+ execSync3(`git worktree remove --force "${worktreePath}"`, { cwd, stdio: "pipe", timeout: TIMEOUT });
6245
+ } catch {
6246
+ }
6247
+ try {
6248
+ execSync3(`git branch -D "${branch}"`, { cwd, stdio: "pipe", timeout: TIMEOUT });
6249
+ } catch {
6250
+ }
6251
+ }
6252
+
6253
+ // ../../packages/orchestrator/src/orchestrator.ts
6254
+ var Orchestrator = class extends EventEmitter {
6255
+ agentManager = new AgentManager();
6256
+ delegationRouter;
6257
+ promptEngine;
6258
+ retryTracker;
6259
+ backends = /* @__PURE__ */ new Map();
6260
+ defaultBackendId;
6261
+ workspace;
6262
+ sandboxMode;
6263
+ worktreeEnabled;
6264
+ worktreeMerge;
6265
+ /** Preview captured from the first dev worker that produces one — not from QA/reviewer */
6266
+ teamPreview = null;
6267
+ constructor(opts) {
6268
+ super();
6269
+ this.workspace = opts.workspace;
6270
+ this.sandboxMode = opts.sandboxMode ?? "full";
6271
+ for (const b of opts.backends) {
6272
+ this.backends.set(b.id, b);
6273
+ }
6274
+ this.defaultBackendId = opts.defaultBackend ?? opts.backends[0]?.id ?? "claude";
6275
+ this.promptEngine = new PromptEngine(opts.promptsDir);
6276
+ this.promptEngine.init();
6277
+ this.delegationRouter = new DelegationRouter(
6278
+ this.agentManager,
6279
+ this.promptEngine,
6280
+ (e) => this.emitEvent(e)
6281
+ );
6282
+ if (opts.retry === false) {
6283
+ this.retryTracker = null;
6284
+ } else {
6285
+ const r = opts.retry ?? {};
6286
+ this.retryTracker = new RetryTracker(r.maxRetries, r.escalateToLeader);
6287
+ }
6288
+ if (opts.worktree === false) {
6289
+ this.worktreeEnabled = false;
6290
+ this.worktreeMerge = false;
6291
+ } else {
6292
+ this.worktreeEnabled = true;
6293
+ this.worktreeMerge = opts.worktree?.mergeOnComplete ?? true;
6294
+ }
6295
+ }
6296
+ // ---------------------------------------------------------------------------
6297
+ // Agent lifecycle
6298
+ // ---------------------------------------------------------------------------
6299
+ createAgent(opts) {
6300
+ const backend = this.backends.get(opts.backend ?? this.defaultBackendId) ?? this.backends.get(this.defaultBackendId);
6301
+ const session = new AgentSession({
6302
+ agentId: opts.agentId,
6303
+ name: opts.name,
6304
+ role: opts.role,
6305
+ personality: opts.personality,
6306
+ workspace: this.workspace,
6307
+ resumeHistory: opts.resumeHistory,
6308
+ backend,
6309
+ sandboxMode: this.sandboxMode,
6310
+ isTeamLead: this.agentManager.isTeamLead(opts.agentId),
6311
+ teamId: opts.teamId,
6312
+ onEvent: (e) => this.handleSessionEvent(e, opts.agentId),
6313
+ renderPrompt: (name, vars) => this.promptEngine.render(name, vars)
6314
+ });
6315
+ session.palette = opts.palette;
6316
+ this.agentManager.add(session);
6317
+ this.delegationRouter.wireAgent(session);
6318
+ this.emitEvent({
6319
+ type: "agent:created",
6320
+ agentId: opts.agentId,
6321
+ name: opts.name,
6322
+ role: opts.role,
6323
+ palette: opts.palette,
6324
+ personality: opts.personality,
6325
+ backend: backend.id,
6326
+ isTeamLead: this.agentManager.isTeamLead(opts.agentId),
6327
+ teamId: opts.teamId
6328
+ });
6329
+ this.emitEvent({
6330
+ type: "agent:status",
6331
+ agentId: opts.agentId,
6332
+ status: "idle"
6333
+ });
6334
+ }
6335
+ removeAgent(agentId) {
6336
+ this.cancelTask(agentId);
6337
+ this.delegationRouter.clearAgent(agentId);
6338
+ this.agentManager.delete(agentId);
6339
+ this.emitEvent({ type: "agent:fired", agentId });
6340
+ }
6341
+ setTeamLead(agentId) {
6342
+ this.agentManager.setTeamLead(agentId);
6343
+ const session = this.agentManager.get(agentId);
6344
+ if (session) session.isTeamLead = true;
6345
+ }
6346
+ createTeam(opts) {
6347
+ const presets = [
6348
+ { ...opts.memberPresets[opts.leadPresetIndex] ?? opts.memberPresets[0], isLead: true },
6349
+ ...opts.memberPresets.filter((_, i) => i !== opts.leadPresetIndex).map((p) => ({ ...p, isLead: false }))
6350
+ ];
6351
+ let leadAgentId = null;
6352
+ for (const preset of presets) {
6353
+ const agentId = `agent-${nanoid4(6)}`;
6354
+ const backendId = opts.backends?.[String(opts.memberPresets.indexOf(preset))] ?? this.defaultBackendId;
6355
+ this.createAgent({
6356
+ agentId,
6357
+ name: preset.name,
6358
+ role: preset.role,
6359
+ personality: preset.personality,
6360
+ palette: preset.palette,
6361
+ backend: backendId
6362
+ });
6363
+ if (preset.isLead) {
6364
+ leadAgentId = agentId;
6365
+ this.agentManager.setTeamLead(agentId);
6366
+ }
6367
+ }
6368
+ if (leadAgentId) {
6369
+ this.emitEvent({
6370
+ type: "team:chat",
6371
+ fromAgentId: leadAgentId,
6372
+ message: `Team created! ${presets.length} members ready.`,
6373
+ messageType: "status",
6374
+ timestamp: Date.now()
6375
+ });
6376
+ }
6377
+ }
6378
+ // ---------------------------------------------------------------------------
6379
+ // Task execution
6380
+ // ---------------------------------------------------------------------------
6381
+ runTask(agentId, taskId, prompt, opts) {
6382
+ const session = this.agentManager.get(agentId);
6383
+ if (!session) {
6384
+ this.emitEvent({
6385
+ type: "task:failed",
6386
+ agentId,
6387
+ taskId,
6388
+ error: "Agent not found. Create it first."
6389
+ });
6390
+ return;
6391
+ }
6392
+ if (this.agentManager.isTeamLead(agentId) && !this.delegationRouter.isDelegated(taskId)) {
6393
+ session.originalTask = prompt;
6394
+ this.delegationRouter.clearAll();
6395
+ this.teamPreview = null;
6396
+ }
6397
+ this.retryTracker?.track(taskId, prompt);
6398
+ if (this.worktreeEnabled && !session.worktreePath) {
6399
+ const wt = createWorktree(this.workspace, agentId, taskId, session.name);
6400
+ if (wt) {
6401
+ const branch = `agent/${session.name.toLowerCase().replace(/\s+/g, "-")}/${taskId}`;
6402
+ session.worktreePath = wt;
6403
+ session.worktreeBranch = branch;
6404
+ this.emitEvent({
6405
+ type: "worktree:created",
4976
6406
  agentId,
4977
- name: preset.name,
4978
- role: preset.role,
4979
- palette: preset.palette,
4980
- personality: preset.personality
6407
+ taskId,
6408
+ worktreePath: wt,
6409
+ branch
4981
6410
  });
4982
6411
  }
4983
- const botInfo = await bot.getMe();
4984
- console.log(`[Telegram] @${botInfo.username} \u2192 ${preset.name} (${preset.role})`);
4985
- bot.on("message", (msg) => {
4986
- if (!msg.text) return;
4987
- ba.chatIds.add(msg.chat.id);
4988
- const text = msg.text.trim();
4989
- if (text === "/yes") {
4990
- for (const agent of agentManager.getAll()) {
4991
- if (agent.agentId === agentId) {
4992
- agent.resolveApproval("__all__", "yes");
6412
+ }
6413
+ const repoPath = session.worktreePath ?? opts?.repoPath;
6414
+ const teamContext = this.agentManager.isTeamLead(agentId) ? this.agentManager.getTeamRoster() : void 0;
6415
+ session.runTask(
6416
+ taskId,
6417
+ prompt,
6418
+ repoPath,
6419
+ teamContext,
6420
+ true
6421
+ /* isUserInitiated */
6422
+ );
6423
+ }
6424
+ cancelTask(agentId) {
6425
+ const session = this.agentManager.get(agentId);
6426
+ if (!session) return;
6427
+ if (session.worktreePath && session.worktreeBranch) {
6428
+ removeWorktree(session.worktreePath, session.worktreeBranch, this.workspace);
6429
+ session.worktreePath = null;
6430
+ session.worktreeBranch = null;
6431
+ }
6432
+ session.cancelTask();
6433
+ }
6434
+ /**
6435
+ * Stop all team agents — cancel their tasks but keep them alive.
6436
+ * Safe to call before fireTeam, or to just pause work.
6437
+ */
6438
+ stopTeam() {
6439
+ this.delegationRouter.stop();
6440
+ const teamAgents = this.agentManager.getAll().filter((a) => !!a.teamId);
6441
+ for (const agent of teamAgents) {
6442
+ this.cancelTask(agent.agentId);
6443
+ }
6444
+ this.emitEvent({
6445
+ type: "team:chat",
6446
+ fromAgentId: teamAgents.find((a) => this.agentManager.isTeamLead(a.agentId))?.agentId ?? "system",
6447
+ message: "Team work stopped. All tasks cancelled.",
6448
+ messageType: "status",
6449
+ timestamp: Date.now()
6450
+ });
6451
+ }
6452
+ /**
6453
+ * Fire the entire team — stop all work silently, then remove all agents.
6454
+ */
6455
+ fireTeam() {
6456
+ this.delegationRouter.stop();
6457
+ const teamAgents = this.agentManager.getAll().filter((a) => !!a.teamId);
6458
+ for (const agent of teamAgents) {
6459
+ this.cancelTask(agent.agentId);
6460
+ }
6461
+ for (const agent of teamAgents) {
6462
+ this.agentManager.delete(agent.agentId);
6463
+ this.emitEvent({ type: "agent:fired", agentId: agent.agentId });
6464
+ }
6465
+ }
6466
+ sendMessage(agentId, message) {
6467
+ const session = this.agentManager.get(agentId);
6468
+ if (!session) return false;
6469
+ return session.sendMessage(message);
6470
+ }
6471
+ resolveApproval(approvalId, decision) {
6472
+ for (const agent of this.agentManager.getAll()) {
6473
+ agent.resolveApproval(approvalId, decision);
6474
+ }
6475
+ }
6476
+ // ---------------------------------------------------------------------------
6477
+ // Query
6478
+ // ---------------------------------------------------------------------------
6479
+ getAgent(agentId) {
6480
+ const s = this.agentManager.get(agentId);
6481
+ if (!s) return void 0;
6482
+ return { agentId: s.agentId, name: s.name, role: s.role, status: s.status, palette: s.palette, backend: s.backend.id };
6483
+ }
6484
+ getAllAgents() {
6485
+ return this.agentManager.getAll().map((s) => ({
6486
+ agentId: s.agentId,
6487
+ name: s.name,
6488
+ role: s.role,
6489
+ status: s.status,
6490
+ palette: s.palette,
6491
+ backend: s.backend.id,
6492
+ isTeamLead: this.agentManager.isTeamLead(s.agentId),
6493
+ teamId: s.teamId
6494
+ }));
6495
+ }
6496
+ getTeamRoster() {
6497
+ return this.agentManager.getTeamRoster();
6498
+ }
6499
+ isTeamLead(agentId) {
6500
+ return this.agentManager.isTeamLead(agentId);
6501
+ }
6502
+ // ---------------------------------------------------------------------------
6503
+ // Cleanup
6504
+ // ---------------------------------------------------------------------------
6505
+ destroy() {
6506
+ for (const agent of this.agentManager.getAll()) {
6507
+ if (agent.worktreePath && agent.worktreeBranch) {
6508
+ removeWorktree(agent.worktreePath, agent.worktreeBranch, this.workspace);
6509
+ }
6510
+ agent.destroy();
6511
+ }
6512
+ }
6513
+ // ---------------------------------------------------------------------------
6514
+ // Internal
6515
+ // ---------------------------------------------------------------------------
6516
+ handleSessionEvent(event, agentId) {
6517
+ if (event.type === "task:failed" && this.retryTracker) {
6518
+ const taskId = event.taskId;
6519
+ const session = this.agentManager.get(agentId);
6520
+ const wasCancelled = event.error === "Task cancelled by user";
6521
+ const wasTimeout = session?.wasTimeout ?? false;
6522
+ if (!wasCancelled && !wasTimeout && this.retryTracker.shouldRetry(taskId) && !this.delegationRouter.isDelegated(taskId)) {
6523
+ const state = this.retryTracker.recordAttempt(taskId, event.error);
6524
+ if (state) {
6525
+ this.emitEvent({
6526
+ type: "task:retrying",
6527
+ agentId,
6528
+ taskId,
6529
+ attempt: state.attempt,
6530
+ maxRetries: state.maxRetries,
6531
+ error: event.error
6532
+ });
6533
+ const retryPrompt = this.retryTracker.getRetryPrompt(taskId);
6534
+ if (retryPrompt) {
6535
+ const session2 = this.agentManager.get(agentId);
6536
+ if (session2) {
6537
+ setTimeout(() => session2.runTask(taskId, retryPrompt), 500);
6538
+ return;
4993
6539
  }
4994
6540
  }
4995
- return;
4996
6541
  }
4997
- if (text === "/no") {
4998
- for (const agent of agentManager.getAll()) {
4999
- if (agent.agentId === agentId) {
5000
- agent.resolveApproval("__all__", "no");
5001
- }
6542
+ }
6543
+ const escalation = wasCancelled ? null : this.retryTracker.getEscalation(taskId);
6544
+ if (escalation) {
6545
+ const leadId = this.agentManager.getTeamLead();
6546
+ if (leadId && leadId !== agentId) {
6547
+ const leadSession = this.agentManager.get(leadId);
6548
+ if (leadSession) {
6549
+ const escalationTaskId = nanoid4();
6550
+ const teamContext = this.agentManager.getTeamRoster();
6551
+ leadSession.runTask(escalationTaskId, escalation.prompt, void 0, teamContext);
5002
6552
  }
5003
- return;
5004
6553
  }
5005
- if (text === "/cancel") {
5006
- const session2 = agentManager.get(agentId);
5007
- if (session2) {
5008
- session2.cancelTask();
5009
- bot.sendMessage(msg.chat.id, `\u{1F6D1} Cancelled ${preset.name}'s current task`);
5010
- }
5011
- return;
6554
+ }
6555
+ this.retryTracker.clear(taskId);
6556
+ }
6557
+ if (event.type === "task:done") {
6558
+ const session = this.agentManager.get(agentId);
6559
+ if (session?.worktreePath && session.worktreeBranch) {
6560
+ if (this.worktreeMerge) {
6561
+ const result = mergeWorktree(this.workspace, session.worktreePath, session.worktreeBranch);
6562
+ this.emitEvent({
6563
+ type: "worktree:merged",
6564
+ agentId,
6565
+ taskId: event.taskId,
6566
+ branch: session.worktreeBranch,
6567
+ success: result.success,
6568
+ conflictFiles: result.conflictFiles
6569
+ });
6570
+ } else {
6571
+ removeWorktree(session.worktreePath, session.worktreeBranch, this.workspace);
5012
6572
  }
5013
- if (text === "/status") {
5014
- const session2 = agentManager.get(agentId);
5015
- const status = session2?.status ?? "unknown";
5016
- bot.sendMessage(msg.chat.id, `${preset.name} (${preset.role}): ${status}`);
5017
- return;
6573
+ session.worktreePath = null;
6574
+ session.worktreeBranch = null;
6575
+ }
6576
+ this.retryTracker?.clear(event.taskId);
6577
+ if (!this.agentManager.isTeamLead(agentId) && !this.teamPreview) {
6578
+ const role = session?.role?.toLowerCase() ?? "";
6579
+ const isDevWorker = !role.includes("qa") && !role.includes("tester") && !role.includes("review");
6580
+ if (isDevWorker && event.result?.previewUrl) {
6581
+ this.teamPreview = {
6582
+ previewUrl: event.result.previewUrl,
6583
+ previewPath: event.result.previewPath
6584
+ };
6585
+ console.log(`[Orchestrator] Preview captured from ${session?.name}: ${this.teamPreview.previewUrl}`);
5018
6586
  }
5019
- if (text.startsWith("/")) return;
5020
- const session = agentManager.get(agentId);
5021
- if (session?.status === "working") {
5022
- bot.sendMessage(msg.chat.id, `\u23F3 ${preset.name} is busy. Wait or send /cancel`);
5023
- return;
6587
+ }
6588
+ if (this.agentManager.isTeamLead(agentId)) {
6589
+ const isResultTask = this.delegationRouter.isResultTask(event.taskId);
6590
+ const leaderDidNotDelegateNewWork = isResultTask && this.delegationRouter.resultTaskDidNotDelegate(event.taskId);
6591
+ const budgetForced = this.delegationRouter.isBudgetExhausted() && !this.delegationRouter.hasPendingFrom(agentId);
6592
+ const shouldFinalize = leaderDidNotDelegateNewWork || budgetForced;
6593
+ if (shouldFinalize) {
6594
+ event.isFinalResult = true;
6595
+ this.delegationRouter.clearAgent(agentId);
6596
+ if (!event.result?.previewUrl && this.teamPreview && event.result) {
6597
+ event.result.previewUrl = this.teamPreview.previewUrl;
6598
+ event.result.previewPath = this.teamPreview.previewPath;
6599
+ }
6600
+ if (!event.result?.previewUrl && event.result) {
6601
+ for (const worker of this.agentManager.getAll()) {
6602
+ if (worker.agentId === agentId) continue;
6603
+ const { previewUrl, previewPath } = worker.detectPreview();
6604
+ if (previewUrl) {
6605
+ event.result.previewUrl = previewUrl;
6606
+ event.result.previewPath = previewPath;
6607
+ break;
6608
+ }
6609
+ }
6610
+ }
6611
+ const summary = event.result?.summary?.slice(0, 200) ?? "All tasks completed.";
6612
+ this.emitEvent({
6613
+ type: "team:chat",
6614
+ fromAgentId: agentId,
6615
+ message: `Project complete: ${summary}`,
6616
+ messageType: "status",
6617
+ timestamp: Date.now()
6618
+ });
6619
+ } else if (!isResultTask && !this.delegationRouter.hasPendingFrom(agentId)) {
6620
+ console.warn(`[Orchestrator] Leader ${agentId} completed initial task with no delegations. Output may have failed to parse.`);
5024
6621
  }
5025
- const taskId = nanoid2();
5026
- commandHandler({
5027
- type: "RUN_TASK",
5028
- agentId,
5029
- taskId,
5030
- prompt: text,
5031
- name: preset.name,
5032
- role: preset.role,
5033
- personality: preset.personality
5034
- });
5035
- });
5036
- }
5037
- console.log(`[Telegram] ${botAgents.length} bot(s) active`);
5038
- return botAgents.length > 0;
5039
- },
5040
- broadcast(event) {
5041
- for (const ba of botAgents) {
5042
- const text = formatEvent(event, ba.agentId);
5043
- if (!text) continue;
5044
- for (const chatId of ba.chatIds) {
5045
- ba.bot.sendMessage(chatId, text).catch((err) => {
5046
- console.error(`[Telegram] Send failed:`, err.message);
5047
- });
5048
6622
  }
5049
6623
  }
5050
- },
5051
- destroy() {
5052
- for (const ba of botAgents) {
5053
- ba.bot.stopPolling();
6624
+ if (event.type === "task:failed") {
6625
+ const session = this.agentManager.get(agentId);
6626
+ if (session?.worktreePath && session.worktreeBranch) {
6627
+ removeWorktree(session.worktreePath, session.worktreeBranch, this.workspace);
6628
+ session.worktreePath = null;
6629
+ session.worktreeBranch = null;
6630
+ }
5054
6631
  }
5055
- botAgents.length = 0;
6632
+ this.emitEvent(event);
6633
+ }
6634
+ emitEvent(event) {
6635
+ this.emit(event.type, event);
5056
6636
  }
5057
6637
  };
5058
6638
 
5059
- // src/setup.ts
5060
- import { createInterface } from "readline";
5061
- function ask(rl, question) {
5062
- return new Promise((resolve2) => {
5063
- const onClose = () => resolve2("");
5064
- rl.once("close", onClose);
5065
- rl.question(question, (answer) => {
5066
- rl.removeListener("close", onClose);
5067
- resolve2(answer.trim());
5068
- });
5069
- });
5070
- }
5071
- async function runSetup() {
5072
- console.log("[Setup] Detecting AI backends...");
5073
- const detected = detectBackends();
5074
- const detectedNames = detected.map((id) => getBackend(id)?.name ?? id).join(", ");
5075
- console.log(`[Setup] Found: ${detectedNames || "none"}`);
5076
- if (!process.stdin.isTTY) {
5077
- saveConfig({ detectedBackends: detected, defaultBackend: detected[0] ?? "claude", sandboxMode: "full" });
5078
- console.log("\u2713 Default config saved to ~/.bit-office/config.json");
5079
- console.log(" Run with --setup in a terminal to configure.\n");
5080
- return;
5081
- }
5082
- const rl = createInterface({
5083
- input: process.stdin,
5084
- output: process.stdout,
5085
- terminal: true
5086
- });
5087
- console.log("");
5088
- console.log("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
5089
- console.log("\u2551 Bit Office \u2014 First Setup \u2551");
5090
- console.log("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
5091
- console.log("");
5092
- console.log("Press Enter to skip any step.\n");
5093
- console.log("\u2500\u2500 Remote Access (Ably) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
5094
- console.log("Enables access from outside your LAN.");
5095
- const ablyApiKey = await ask(rl, "Ably API Key (optional): ");
5096
- let defaultBackend = detected[0] ?? "claude";
5097
- if (detected.length > 1) {
5098
- console.log("\n\u2500\u2500 AI Backends \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
5099
- console.log(`Detected: ${detectedNames}`);
5100
- const choices = detected.map((id, i) => `${i + 1}=${getBackend(id)?.name ?? id}`).join(", ");
5101
- const pick = await ask(rl, `Default backend (${choices}): `);
5102
- const idx = parseInt(pick, 10) - 1;
5103
- if (idx >= 0 && idx < detected.length) {
5104
- defaultBackend = detected[idx];
5105
- }
5106
- }
5107
- console.log("\n\u2500\u2500 Agent Permissions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
5108
- console.log("1 = Full access (agents can access entire machine)");
5109
- console.log("2 = Sandbox (agents restricted to working directory)");
5110
- const sandboxPick = await ask(rl, "Permission mode (1/2, default=1): ");
5111
- const sandboxMode = sandboxPick === "2" ? "safe" : "full";
5112
- rl.close();
5113
- saveConfig({
5114
- ablyApiKey: ablyApiKey || void 0,
5115
- detectedBackends: detected,
5116
- defaultBackend,
5117
- sandboxMode
5118
- });
5119
- console.log("\n\u2713 Config saved to ~/.bit-office/config.json");
5120
- if (ablyApiKey) console.log(" \u2022 Ably: enabled");
5121
- console.log(` \u2022 Default AI: ${getBackend(defaultBackend)?.name ?? defaultBackend}`);
5122
- console.log(` \u2022 Permissions: ${sandboxMode === "full" ? "Full access" : "Sandbox"}`);
5123
- console.log(" \u2022 Run with --setup to reconfigure\n");
6639
+ // ../../packages/orchestrator/src/index.ts
6640
+ function createOrchestrator(options) {
6641
+ return new Orchestrator(options);
5124
6642
  }
5125
6643
 
5126
6644
  // src/index.ts
5127
- import { nanoid as nanoid3 } from "nanoid";
5128
- import { exec } from "child_process";
5129
- import { existsSync as existsSync2 } from "fs";
6645
+ import { nanoid as nanoid5 } from "nanoid";
6646
+ import { execFile } from "child_process";
6647
+ import { existsSync as existsSync4 } from "fs";
6648
+ import path5 from "path";
6649
+ import os from "os";
5130
6650
  registerChannel(wsChannel);
5131
6651
  registerChannel(ablyChannel);
5132
6652
  registerChannel(telegramChannel);
6653
+ var orc;
5133
6654
  function generatePairCode() {
5134
- return nanoid3(6).toUpperCase();
6655
+ return nanoid5(6).toUpperCase();
5135
6656
  }
5136
6657
  function showPairCode() {
5137
6658
  const code = generatePairCode();
@@ -5144,25 +6665,45 @@ function showPairCode() {
5144
6665
  console.log(`Open your phone \u2192 enter gateway address + code`);
5145
6666
  console.log("");
5146
6667
  }
5147
- function wireDelegation(session) {
5148
- if (!session) return;
5149
- session.onDelegation = (fromAgentId, targetName, prompt) => {
5150
- const target = agentManager.findByName(targetName);
5151
- if (!target) {
5152
- console.log(`[Delegation] Target agent "${targetName}" not found, ignoring`);
5153
- return;
5154
- }
5155
- const taskId = nanoid3();
5156
- console.log(`[Delegation] ${fromAgentId} -> ${target.agentId} (${targetName}): ${prompt.slice(0, 80)}`);
5157
- publishEvent({
5158
- type: "TASK_DELEGATED",
5159
- fromAgentId,
5160
- toAgentId: target.agentId,
5161
- taskId,
5162
- prompt
5163
- });
5164
- target.runTask(taskId, prompt);
5165
- };
6668
+ function mapOrchestratorEvent(e) {
6669
+ switch (e.type) {
6670
+ case "task:started":
6671
+ return { type: "TASK_STARTED", agentId: e.agentId, taskId: e.taskId, prompt: e.prompt };
6672
+ case "task:done":
6673
+ return { type: "TASK_DONE", agentId: e.agentId, taskId: e.taskId, result: e.result, isFinalResult: e.isFinalResult };
6674
+ case "task:failed":
6675
+ return { type: "TASK_FAILED", agentId: e.agentId, taskId: e.taskId, error: e.error };
6676
+ case "task:delegated":
6677
+ return { type: "TASK_DELEGATED", fromAgentId: e.fromAgentId, toAgentId: e.toAgentId, taskId: e.taskId, prompt: e.prompt };
6678
+ case "agent:status":
6679
+ return { type: "AGENT_STATUS", agentId: e.agentId, status: e.status };
6680
+ case "approval:needed":
6681
+ return { type: "APPROVAL_NEEDED", approvalId: e.approvalId, agentId: e.agentId, taskId: e.taskId, title: e.title, summary: e.summary, riskLevel: e.riskLevel };
6682
+ case "log:append":
6683
+ return { type: "LOG_APPEND", agentId: e.agentId, taskId: e.taskId, stream: e.stream, chunk: e.chunk };
6684
+ case "team:chat":
6685
+ return { type: "TEAM_CHAT", fromAgentId: e.fromAgentId, toAgentId: e.toAgentId, message: e.message, messageType: e.messageType, taskId: e.taskId, timestamp: e.timestamp };
6686
+ case "task:queued":
6687
+ return { type: "TASK_QUEUED", agentId: e.agentId, taskId: e.taskId, prompt: e.prompt, position: e.position };
6688
+ case "agent:created":
6689
+ 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 };
6690
+ case "agent:fired":
6691
+ return { type: "AGENT_FIRED", agentId: e.agentId };
6692
+ case "task:result-returned":
6693
+ return { type: "TASK_RESULT_RETURNED", fromAgentId: e.fromAgentId, toAgentId: e.toAgentId, taskId: e.taskId, summary: e.summary, success: e.success };
6694
+ // New events (worktree, retry) — log only, no wire protocol equivalent yet
6695
+ case "task:retrying":
6696
+ console.log(`[Retry] Agent ${e.agentId} retrying task ${e.taskId} (attempt ${e.attempt}/${e.maxRetries})`);
6697
+ return null;
6698
+ case "worktree:created":
6699
+ console.log(`[Worktree] Created ${e.worktreePath} for agent ${e.agentId}`);
6700
+ return null;
6701
+ case "worktree:merged":
6702
+ console.log(`[Worktree] Merged branch ${e.branch} for agent ${e.agentId} (success=${e.success})`);
6703
+ return null;
6704
+ default:
6705
+ return null;
6706
+ }
5166
6707
  }
5167
6708
  function handleCommand(parsed) {
5168
6709
  console.log("[Gateway] Received command:", parsed.type, JSON.stringify(parsed));
@@ -5170,50 +6711,39 @@ function handleCommand(parsed) {
5170
6711
  case "CREATE_AGENT": {
5171
6712
  const backendId = parsed.backend ?? config.defaultBackend;
5172
6713
  console.log(`[Gateway] Creating agent: ${parsed.agentId} (${parsed.name} - ${parsed.role}) backend=${backendId}`);
5173
- const session = agentManager.create(parsed.agentId, parsed.name, parsed.role, parsed.personality, void 0, false, backendId, parsed.palette);
5174
- wireDelegation(session);
5175
- publishEvent({
5176
- type: "AGENT_CREATED",
6714
+ orc.createAgent({
5177
6715
  agentId: parsed.agentId,
5178
6716
  name: parsed.name,
5179
6717
  role: parsed.role,
5180
- palette: parsed.palette,
5181
6718
  personality: parsed.personality,
5182
- backend: backendId
5183
- });
5184
- publishEvent({
5185
- type: "AGENT_STATUS",
5186
- agentId: parsed.agentId,
5187
- status: "idle"
6719
+ backend: backendId,
6720
+ palette: parsed.palette
5188
6721
  });
5189
6722
  break;
5190
6723
  }
5191
6724
  case "FIRE_AGENT": {
5192
6725
  console.log(`[Gateway] Firing agent: ${parsed.agentId}`);
5193
- agentManager.delete(parsed.agentId);
5194
- publishEvent({
5195
- type: "AGENT_FIRED",
5196
- agentId: parsed.agentId
5197
- });
6726
+ orc.removeAgent(parsed.agentId);
5198
6727
  break;
5199
6728
  }
5200
6729
  case "RUN_TASK": {
5201
- let session = agentManager.get(parsed.agentId);
5202
- if (!session && parsed.name) {
6730
+ let agent = orc.getAgent(parsed.agentId);
6731
+ if (!agent && parsed.name) {
5203
6732
  const backendId = parsed.backend ?? config.defaultBackend;
5204
6733
  console.log(`[Gateway] Auto-creating agent for RUN_TASK: ${parsed.agentId} backend=${backendId}`);
5205
- session = agentManager.create(parsed.agentId, parsed.name, parsed.role ?? "", parsed.personality, void 0, true, backendId);
5206
- wireDelegation(session);
5207
- publishEvent({
5208
- type: "AGENT_CREATED",
6734
+ orc.createAgent({
5209
6735
  agentId: parsed.agentId,
5210
6736
  name: parsed.name,
5211
6737
  role: parsed.role ?? "",
5212
- backend: backendId
6738
+ personality: parsed.personality,
6739
+ backend: backendId,
6740
+ resumeHistory: true
5213
6741
  });
6742
+ agent = orc.getAgent(parsed.agentId);
5214
6743
  }
5215
- if (session) {
5216
- session.runTask(parsed.taskId, parsed.prompt, parsed.repoPath);
6744
+ if (agent) {
6745
+ console.log(`[Gateway] RUN_TASK: agent=${parsed.agentId}, isLead=${orc.isTeamLead(parsed.agentId)}, hasTeam=${orc.getAllAgents().length > 1}`);
6746
+ orc.runTask(parsed.agentId, parsed.taskId, parsed.prompt, { repoPath: parsed.repoPath });
5217
6747
  } else {
5218
6748
  publishEvent({
5219
6749
  type: "TASK_FAILED",
@@ -5225,35 +6755,97 @@ function handleCommand(parsed) {
5225
6755
  break;
5226
6756
  }
5227
6757
  case "APPROVAL_DECISION": {
5228
- for (const agent of agentManager.getAll()) {
5229
- agent.resolveApproval(parsed.approvalId, parsed.decision);
5230
- }
6758
+ orc.resolveApproval(parsed.approvalId, parsed.decision);
5231
6759
  break;
5232
6760
  }
5233
6761
  case "CANCEL_TASK": {
5234
- const session = agentManager.get(parsed.agentId);
5235
- session?.cancelTask();
6762
+ orc.cancelTask(parsed.agentId);
6763
+ break;
6764
+ }
6765
+ case "SERVE_PREVIEW": {
6766
+ const filePath = parsed.filePath;
6767
+ console.log(`[Gateway] SERVE_PREVIEW: ${filePath}`);
6768
+ previewServer.serve(filePath);
5236
6769
  break;
5237
6770
  }
5238
6771
  case "OPEN_FILE": {
5239
- const filePath = parsed.path;
5240
- console.log(`[Gateway] Opening file: ${filePath}`);
5241
- exec(`open "${filePath}"`, (err) => {
6772
+ const raw = parsed.path;
6773
+ const resolved = path5.resolve(config.defaultWorkspace, raw);
6774
+ const normalized = path5.normalize(resolved);
6775
+ if (!normalized.startsWith(config.defaultWorkspace + path5.sep) && normalized !== config.defaultWorkspace) {
6776
+ console.error(`[Gateway] Blocked OPEN_FILE: path "${raw}" resolves outside workspace`);
6777
+ break;
6778
+ }
6779
+ if (!existsSync4(normalized)) {
6780
+ console.error(`[Gateway] OPEN_FILE: path does not exist: ${normalized}`);
6781
+ break;
6782
+ }
6783
+ console.log(`[Gateway] Opening file: ${normalized}`);
6784
+ execFile("open", [normalized], (err) => {
5242
6785
  if (err) console.error(`[Gateway] Failed to open file: ${err.message}`);
5243
6786
  });
5244
6787
  break;
5245
6788
  }
6789
+ case "CREATE_TEAM": {
6790
+ const { leadPresetIndex, memberPresetIndices, backends: backends2 } = parsed;
6791
+ const allIndices = [leadPresetIndex, ...memberPresetIndices.filter((i) => i !== leadPresetIndex)];
6792
+ console.log(`[Gateway] Creating team: lead=${leadPresetIndex}, members=${memberPresetIndices.join(",")}`);
6793
+ let leadAgentId = null;
6794
+ const teamId = `team-${nanoid5(6)}`;
6795
+ for (const idx of allIndices) {
6796
+ const preset = AGENT_PRESETS[idx];
6797
+ if (!preset) continue;
6798
+ const agentId = `agent-${nanoid5(6)}`;
6799
+ const backendId = backends2?.[String(idx)] ?? config.defaultBackend;
6800
+ if (idx === leadPresetIndex) {
6801
+ leadAgentId = agentId;
6802
+ orc.setTeamLead(agentId);
6803
+ }
6804
+ orc.createAgent({
6805
+ agentId,
6806
+ name: preset.name,
6807
+ role: preset.role,
6808
+ personality: preset.personality,
6809
+ backend: backendId,
6810
+ palette: preset.palette,
6811
+ teamId
6812
+ });
6813
+ }
6814
+ if (leadAgentId) {
6815
+ const leadPreset = AGENT_PRESETS[leadPresetIndex];
6816
+ publishEvent({
6817
+ type: "TEAM_CHAT",
6818
+ fromAgentId: leadAgentId,
6819
+ message: `Team created! ${leadPreset?.name ?? "Lead"} is the Team Lead with ${memberPresetIndices.length} team members.`,
6820
+ messageType: "status",
6821
+ timestamp: Date.now()
6822
+ });
6823
+ }
6824
+ break;
6825
+ }
6826
+ case "STOP_TEAM": {
6827
+ console.log("[Gateway] Stopping team work");
6828
+ orc.stopTeam();
6829
+ break;
6830
+ }
6831
+ case "FIRE_TEAM": {
6832
+ console.log("[Gateway] Firing entire team");
6833
+ orc.fireTeam();
6834
+ break;
6835
+ }
5246
6836
  case "PING": {
5247
6837
  console.log("[Gateway] Received PING, broadcasting agent statuses");
5248
- for (const agent of agentManager.getAll()) {
6838
+ for (const agent of orc.getAllAgents()) {
5249
6839
  publishEvent({
5250
6840
  type: "AGENT_CREATED",
5251
6841
  agentId: agent.agentId,
5252
6842
  name: agent.name,
5253
6843
  role: agent.role,
5254
6844
  palette: agent.palette,
5255
- personality: agent.personality,
5256
- backend: agent.backend.id
6845
+ personality: void 0,
6846
+ backend: agent.backend,
6847
+ isTeamLead: agent.isTeamLead,
6848
+ teamId: agent.teamId
5257
6849
  });
5258
6850
  publishEvent({
5259
6851
  type: "AGENT_STATUS",
@@ -5280,6 +6872,36 @@ async function main() {
5280
6872
  saveConfig({ detectedBackends: detected, defaultBackend: config.defaultBackend });
5281
6873
  }
5282
6874
  }
6875
+ const backendsToUse = getAllBackends();
6876
+ orc = createOrchestrator({
6877
+ workspace: config.defaultWorkspace,
6878
+ backends: backendsToUse,
6879
+ defaultBackend: config.defaultBackend,
6880
+ worktree: false,
6881
+ // disabled by default for now
6882
+ retry: { maxRetries: 2, escalateToLeader: true },
6883
+ promptsDir: path5.join(os.homedir(), ".bit-office", "prompts"),
6884
+ sandboxMode: config.sandboxMode
6885
+ });
6886
+ const forwardEvent = (event) => {
6887
+ const mapped = mapOrchestratorEvent(event);
6888
+ if (mapped) publishEvent(mapped);
6889
+ };
6890
+ orc.on("task:started", forwardEvent);
6891
+ orc.on("task:done", forwardEvent);
6892
+ orc.on("task:failed", forwardEvent);
6893
+ orc.on("task:delegated", forwardEvent);
6894
+ orc.on("task:retrying", forwardEvent);
6895
+ orc.on("agent:status", forwardEvent);
6896
+ orc.on("approval:needed", forwardEvent);
6897
+ orc.on("log:append", forwardEvent);
6898
+ orc.on("team:chat", forwardEvent);
6899
+ orc.on("task:queued", forwardEvent);
6900
+ orc.on("worktree:created", forwardEvent);
6901
+ orc.on("worktree:merged", forwardEvent);
6902
+ orc.on("agent:created", forwardEvent);
6903
+ orc.on("agent:fired", forwardEvent);
6904
+ orc.on("task:result-returned", forwardEvent);
5283
6905
  const backendNames = config.detectedBackends.map((id) => getBackend(id)?.name ?? id).join(", ");
5284
6906
  console.log(`[Gateway] AI backends: ${backendNames || "none detected"} (default: ${getBackend(config.defaultBackend)?.name ?? config.defaultBackend})`);
5285
6907
  console.log(`[Gateway] Permissions: ${config.sandboxMode === "full" ? "Full access" : "Sandbox"}`);
@@ -5288,10 +6910,10 @@ async function main() {
5288
6910
  await initTransports(handleCommand);
5289
6911
  console.log("[Gateway] Listening for commands...");
5290
6912
  console.log("[Gateway] Press 'p' + Enter to generate a new pair code");
5291
- if (process.env.NODE_ENV !== "development" && existsSync2(config.webDir)) {
6913
+ if (process.env.NODE_ENV !== "development" && existsSync4(config.webDir)) {
5292
6914
  const url = `http://localhost:${config.wsPort}`;
5293
6915
  console.log(`[Gateway] Opening ${url}`);
5294
- exec(`open "${url}"`);
6916
+ execFile("open", [url]);
5295
6917
  }
5296
6918
  if (process.stdin.isTTY) {
5297
6919
  process.stdin.setEncoding("utf-8");
@@ -5305,9 +6927,8 @@ async function main() {
5305
6927
  }
5306
6928
  function cleanup() {
5307
6929
  console.log("[Gateway] Shutting down...");
5308
- for (const agent of agentManager.getAll()) {
5309
- agent.destroy();
5310
- }
6930
+ previewServer.stop();
6931
+ orc?.destroy();
5311
6932
  destroyTransports();
5312
6933
  process.exit(0);
5313
6934
  }