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.
- package/dist/index.js +1983 -362
- package/dist/index.js.map +1 -1
- package/dist/web/out/404.html +50 -2
- package/dist/web/out/Office Tileset/Office Tileset All 16x16.png +0 -0
- package/dist/web/out/_next/static/chunks/757.6d61de28074ba8d4.js +1 -0
- package/dist/web/out/_next/static/chunks/904.36d527a30eef4423.js +1 -0
- package/dist/web/out/_next/static/chunks/989-4c2c2b3a6c2f8a4a.js +1 -0
- package/dist/web/out/_next/static/chunks/app/office/page-e71fd886e7258e88.js +1 -0
- package/dist/web/out/_next/static/chunks/app/pair/page-667d7c599a584eb6.js +1 -0
- package/dist/web/out/_next/static/chunks/{webpack-131b3d260a3d9ce4.js → webpack-6eaa423ee729538f.js} +1 -1
- package/dist/web/out/index.html +50 -2
- package/dist/web/out/index.txt +67 -17
- package/dist/web/out/office.html +50 -2
- package/dist/web/out/office.txt +68 -17
- package/dist/web/out/pair.html +50 -2
- package/dist/web/out/pair.txt +68 -17
- package/dist/web/out/pixel-agents-layout.json +1 -2802
- package/dist/web/out/sw.js +1 -1
- package/package.json +2 -1
- package/dist/web/out/_next/static/chunks/757.6b9b9f11f348e673.js +0 -1
- package/dist/web/out/_next/static/chunks/904.4649cb92e3c1355c.js +0 -1
- package/dist/web/out/_next/static/chunks/989-f3ebca68e0b5e7ee.js +0 -1
- package/dist/web/out/_next/static/chunks/app/office/page-75459bb9d7bfe004.js +0 -1
- package/dist/web/out/_next/static/chunks/app/pair/page-932b6cbad193cd3d.js +0 -1
- /package/dist/web/out/_next/static/{KVUUasWqYxW6SNCIo9Tbn → e3jju679hIWY5j_P8PYTf}/_buildManifest.js +0 -0
- /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 = [...
|
|
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,
|
|
644
|
+
constructor(parent, value, path6, key) {
|
|
645
645
|
this._cachedPath = [];
|
|
646
646
|
this.parent = parent;
|
|
647
647
|
this.data = value;
|
|
648
|
-
this._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/
|
|
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", "
|
|
4583
|
-
if (opts.
|
|
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/
|
|
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
|
|
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
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
this.
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
this.
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
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
|
-
|
|
4673
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4686
|
-
type: "
|
|
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
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
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
|
-
|
|
4710
|
-
this.
|
|
4711
|
-
|
|
4712
|
-
this.
|
|
4713
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
4722
|
-
|
|
4723
|
-
type: "
|
|
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:
|
|
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
|
|
4735
|
-
|
|
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.
|
|
5288
|
+
this.dequeueNext();
|
|
5289
|
+
return;
|
|
4755
5290
|
} else if (code === 0) {
|
|
4756
5291
|
this.hasHistory = true;
|
|
4757
|
-
|
|
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
|
-
|
|
4760
|
-
type: "
|
|
5297
|
+
this.onEvent({
|
|
5298
|
+
type: "task:done",
|
|
4761
5299
|
agentId: this.agentId,
|
|
4762
5300
|
taskId: completedTaskId,
|
|
4763
|
-
result: { summary, changedFiles
|
|
5301
|
+
result: { summary, fullOutput, changedFiles, diffStat: "", testResult: "unknown", previewUrl, previewPath }
|
|
4764
5302
|
});
|
|
4765
|
-
|
|
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
|
-
|
|
4769
|
-
type: "
|
|
5312
|
+
this.onEvent({
|
|
5313
|
+
type: "task:failed",
|
|
4770
5314
|
agentId: this.agentId,
|
|
4771
5315
|
taskId: completedTaskId,
|
|
4772
|
-
error:
|
|
5316
|
+
error: errorMsg
|
|
4773
5317
|
});
|
|
4774
|
-
|
|
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
|
-
|
|
4786
|
-
type: "
|
|
5335
|
+
this.onEvent({
|
|
5336
|
+
type: "task:failed",
|
|
4787
5337
|
agentId: this.agentId,
|
|
4788
5338
|
taskId,
|
|
4789
5339
|
error: err.message
|
|
4790
5340
|
});
|
|
4791
|
-
setTimeout(() =>
|
|
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
|
-
|
|
4796
|
-
type: "
|
|
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
|
-
|
|
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.
|
|
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.
|
|
4812
|
-
this.
|
|
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 [
|
|
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 =
|
|
5527
|
+
const approvalId = nanoid2();
|
|
4833
5528
|
const taskId = this.currentTaskId ?? "unknown";
|
|
4834
5529
|
this.setStatus("waiting_approval");
|
|
4835
|
-
|
|
4836
|
-
type: "
|
|
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
|
-
|
|
4873
|
-
type: "
|
|
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
|
-
|
|
4884
|
-
|
|
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
|
-
|
|
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/
|
|
4918
|
-
import { nanoid as
|
|
4919
|
-
var
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
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
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
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
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
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
|
-
|
|
4941
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4946
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4951
|
-
|
|
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
|
-
${
|
|
4954
|
-
|
|
6132
|
+
[RETRY \u2014 Attempt ${s.attempt + 1}/${s.maxRetries}]
|
|
6133
|
+
Previous attempt failed with:
|
|
6134
|
+
${lastError.slice(0, 500)}
|
|
4955
6135
|
|
|
4956
|
-
|
|
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
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
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
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
personality: preset.personality
|
|
6407
|
+
taskId,
|
|
6408
|
+
worktreePath: wt,
|
|
6409
|
+
branch
|
|
4981
6410
|
});
|
|
4982
6411
|
}
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
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
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
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
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
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
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
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
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
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
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
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
|
-
|
|
6632
|
+
this.emitEvent(event);
|
|
6633
|
+
}
|
|
6634
|
+
emitEvent(event) {
|
|
6635
|
+
this.emit(event.type, event);
|
|
5056
6636
|
}
|
|
5057
6637
|
};
|
|
5058
6638
|
|
|
5059
|
-
// src/
|
|
5060
|
-
|
|
5061
|
-
|
|
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
|
|
5128
|
-
import {
|
|
5129
|
-
import { existsSync as
|
|
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
|
|
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
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
type: "
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5202
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
6738
|
+
personality: parsed.personality,
|
|
6739
|
+
backend: backendId,
|
|
6740
|
+
resumeHistory: true
|
|
5213
6741
|
});
|
|
6742
|
+
agent = orc.getAgent(parsed.agentId);
|
|
5214
6743
|
}
|
|
5215
|
-
if (
|
|
5216
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5235
|
-
|
|
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
|
|
5240
|
-
|
|
5241
|
-
|
|
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
|
|
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:
|
|
5256
|
-
backend: agent.backend
|
|
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" &&
|
|
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
|
-
|
|
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
|
-
|
|
5309
|
-
|
|
5310
|
-
}
|
|
6930
|
+
previewServer.stop();
|
|
6931
|
+
orc?.destroy();
|
|
5311
6932
|
destroyTransports();
|
|
5312
6933
|
process.exit(0);
|
|
5313
6934
|
}
|