u-foo 1.7.5 → 1.8.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/README.md +9 -1
- package/README.zh-CN.md +9 -1
- package/bin/ufoo.js +4 -2
- package/package.json +1 -1
- package/src/agent/cliRunner.js +3 -2
- package/src/agent/ucodeBootstrap.js +5 -3
- package/src/agent/ufooAgent.js +184 -5
- package/src/assistant/constants.js +1 -1
- package/src/chat/commandExecutor.js +98 -3
- package/src/chat/commands.js +7 -0
- package/src/chat/completionController.js +40 -0
- package/src/chat/daemonMessageRouter.js +21 -1
- package/src/chat/dashboardKeyController.js +55 -3
- package/src/chat/dashboardView.js +31 -5
- package/src/chat/index.js +152 -36
- package/src/chat/inputListenerController.js +14 -0
- package/src/chat/inputSubmitHandler.js +9 -5
- package/src/chat/transientAgentState.js +64 -0
- package/src/cli/groupCoreCommands.js +21 -12
- package/src/cli.js +23 -1
- package/src/daemon/groupOrchestrator.js +581 -97
- package/src/daemon/index.js +418 -3
- package/src/daemon/ops.js +25 -7
- package/src/daemon/promptLoop.js +16 -0
- package/src/daemon/promptRequest.js +126 -2
- package/src/daemon/reporting.js +18 -0
- package/src/daemon/soloBootstrap.js +435 -0
- package/src/daemon/status.js +5 -1
- package/src/globalMode.js +33 -0
- package/src/group/bootstrap.js +157 -0
- package/src/group/promptProfiles.js +646 -0
- package/src/group/templateValidation.js +99 -0
- package/src/group/validateTemplate.js +36 -5
- package/src/init/index.js +13 -7
- package/src/report/store.js +6 -0
- package/src/shared/eventContract.js +1 -0
- package/templates/groups/{dev-basic.json → build-lane.json} +38 -34
- package/templates/groups/product-discovery.json +79 -0
- package/templates/groups/ui-polish.json +87 -0
- package/templates/groups/verify-ship.json +79 -0
- package/templates/groups/research-quick.json +0 -49
package/src/daemon/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
-
const
|
|
3
|
+
const net = require("net");
|
|
4
|
+
const { spawn, spawnSync } = require("child_process");
|
|
4
5
|
const { runUfooAgent } = require("../agent/ufooAgent");
|
|
5
6
|
const { launchAgent, closeAgent, getRecoverableAgents, resumeAgents } = require("./ops");
|
|
6
7
|
const { buildStatus } = require("./status");
|
|
@@ -21,6 +22,16 @@ const { runAssistantTask } = require("../assistant/bridge");
|
|
|
21
22
|
const { runPromptWithAssistant } = require("./promptLoop");
|
|
22
23
|
const { handlePromptRequest } = require("./promptRequest");
|
|
23
24
|
const { recordAgentReport } = require("./reporting");
|
|
25
|
+
const { isGlobalControllerProjectRoot } = require("../globalMode");
|
|
26
|
+
const {
|
|
27
|
+
assignSoloRoleToExistingAgent,
|
|
28
|
+
resolveSoloPromptProfile,
|
|
29
|
+
buildSoloBootstrap,
|
|
30
|
+
prepareSoloUcodeBootstrap,
|
|
31
|
+
persistSoloRoleMetadata,
|
|
32
|
+
buildSoloBootstrapFingerprint,
|
|
33
|
+
rollbackLaunchAfterRoleAssignmentFailure,
|
|
34
|
+
} = require("./soloBootstrap");
|
|
24
35
|
|
|
25
36
|
let providerSessions = null;
|
|
26
37
|
let probeHandles = new Map();
|
|
@@ -83,6 +94,16 @@ async function renameSpawnedAgent(projectRoot, agentType, nickname, startIso) {
|
|
|
83
94
|
return { ok: false, nickname, error: lastError || "rename timeout" };
|
|
84
95
|
}
|
|
85
96
|
|
|
97
|
+
function pickLaunchSubscriber(projectRoot, launchResult = {}, fallbackTarget = "") {
|
|
98
|
+
if (launchResult && Array.isArray(launchResult.subscriber_ids) && launchResult.subscriber_ids.length > 0) {
|
|
99
|
+
return String(launchResult.subscriber_ids[0] || "").trim();
|
|
100
|
+
}
|
|
101
|
+
if (launchResult && launchResult.agent_id) {
|
|
102
|
+
return String(launchResult.agent_id || "").trim();
|
|
103
|
+
}
|
|
104
|
+
return String(fallbackTarget || "").trim();
|
|
105
|
+
}
|
|
106
|
+
|
|
86
107
|
function ensureDir(dir) {
|
|
87
108
|
fs.mkdirSync(dir, { recursive: true });
|
|
88
109
|
}
|
|
@@ -196,6 +217,129 @@ function removeSocket(projectRoot) {
|
|
|
196
217
|
if (fs.existsSync(sock)) fs.unlinkSync(sock);
|
|
197
218
|
}
|
|
198
219
|
|
|
220
|
+
function connectProjectSocket(sockPath, timeoutMs = 8000) {
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
let timeoutHandle = null;
|
|
223
|
+
const client = net.createConnection(sockPath, () => {
|
|
224
|
+
if (timeoutHandle) {
|
|
225
|
+
clearTimeout(timeoutHandle);
|
|
226
|
+
timeoutHandle = null;
|
|
227
|
+
}
|
|
228
|
+
resolve(client);
|
|
229
|
+
});
|
|
230
|
+
client.on("error", (err) => {
|
|
231
|
+
if (timeoutHandle) {
|
|
232
|
+
clearTimeout(timeoutHandle);
|
|
233
|
+
timeoutHandle = null;
|
|
234
|
+
}
|
|
235
|
+
reject(err);
|
|
236
|
+
});
|
|
237
|
+
timeoutHandle = setTimeout(() => {
|
|
238
|
+
const err = new Error(`connect timeout after ${timeoutMs}ms`);
|
|
239
|
+
err.code = "ETIMEDOUT";
|
|
240
|
+
try {
|
|
241
|
+
client.destroy(err);
|
|
242
|
+
} catch {
|
|
243
|
+
// ignore
|
|
244
|
+
}
|
|
245
|
+
reject(err);
|
|
246
|
+
}, timeoutMs);
|
|
247
|
+
if (typeof timeoutHandle.unref === "function") timeoutHandle.unref();
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function connectProjectSocketWithRetry(sockPath, retries = 25, delayMs = 200, timeoutMs = 8000) {
|
|
252
|
+
for (let i = 0; i < retries; i += 1) {
|
|
253
|
+
try {
|
|
254
|
+
// eslint-disable-next-line no-await-in-loop
|
|
255
|
+
return await connectProjectSocket(sockPath, timeoutMs);
|
|
256
|
+
} catch {
|
|
257
|
+
// eslint-disable-next-line no-await-in-loop
|
|
258
|
+
await sleep(delayMs);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function sendPromptRequestToProject(targetProjectRoot, payload, timeoutMs = 12000) {
|
|
265
|
+
const sock = socketPath(targetProjectRoot);
|
|
266
|
+
const client = await connectProjectSocketWithRetry(sock, 25, 200, 8000);
|
|
267
|
+
if (!client) {
|
|
268
|
+
return { ok: false, error: "Failed to connect target project daemon" };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return new Promise((resolve) => {
|
|
272
|
+
let buffer = "";
|
|
273
|
+
let settled = false;
|
|
274
|
+
const timeout = setTimeout(() => {
|
|
275
|
+
if (settled) return;
|
|
276
|
+
settled = true;
|
|
277
|
+
try {
|
|
278
|
+
client.destroy();
|
|
279
|
+
} catch {
|
|
280
|
+
// ignore
|
|
281
|
+
}
|
|
282
|
+
resolve({ ok: false, error: "Target project daemon request timeout" });
|
|
283
|
+
}, timeoutMs);
|
|
284
|
+
if (typeof timeout.unref === "function") timeout.unref();
|
|
285
|
+
|
|
286
|
+
const cleanup = () => {
|
|
287
|
+
clearTimeout(timeout);
|
|
288
|
+
client.removeAllListeners();
|
|
289
|
+
try {
|
|
290
|
+
client.end();
|
|
291
|
+
} catch {
|
|
292
|
+
// ignore
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
client.on("data", (data) => {
|
|
297
|
+
buffer += data.toString("utf8");
|
|
298
|
+
const lines = buffer.split(/\r?\n/);
|
|
299
|
+
buffer = lines.pop() || "";
|
|
300
|
+
for (const line of lines) {
|
|
301
|
+
if (!line.trim()) continue;
|
|
302
|
+
let msg = null;
|
|
303
|
+
try {
|
|
304
|
+
msg = JSON.parse(line);
|
|
305
|
+
} catch {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (msg.type === IPC_RESPONSE_TYPES.RESPONSE) {
|
|
309
|
+
if (settled) return;
|
|
310
|
+
settled = true;
|
|
311
|
+
cleanup();
|
|
312
|
+
resolve({
|
|
313
|
+
ok: true,
|
|
314
|
+
payload: msg.data || {},
|
|
315
|
+
opsResults: msg.opsResults || [],
|
|
316
|
+
});
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (msg.type === IPC_RESPONSE_TYPES.ERROR) {
|
|
320
|
+
if (settled) return;
|
|
321
|
+
settled = true;
|
|
322
|
+
cleanup();
|
|
323
|
+
resolve({
|
|
324
|
+
ok: false,
|
|
325
|
+
error: msg.error || "Target project daemon error",
|
|
326
|
+
});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
client.on("error", (err) => {
|
|
333
|
+
if (settled) return;
|
|
334
|
+
settled = true;
|
|
335
|
+
cleanup();
|
|
336
|
+
resolve({ ok: false, error: err && err.message ? err.message : "Target project daemon error" });
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
client.write(`${JSON.stringify(payload)}\n`);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
199
343
|
function parseJsonLines(buffer) {
|
|
200
344
|
const lines = buffer.split(/\r?\n/).filter(Boolean);
|
|
201
345
|
const items = [];
|
|
@@ -328,6 +472,10 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
328
472
|
const launchResult = await launchAgent(projectRoot, agent, count, nickname, processManager, {
|
|
329
473
|
launchScope: op.launch_scope || "",
|
|
330
474
|
terminalApp: op.terminal_app || "",
|
|
475
|
+
extraEnv:
|
|
476
|
+
op.extra_env && typeof op.extra_env === "object"
|
|
477
|
+
? op.extra_env
|
|
478
|
+
: ((op.extraEnv && typeof op.extraEnv === "object") ? op.extraEnv : null),
|
|
331
479
|
hostInjectSock: op.host_inject_sock || op.hostInjectSock || "",
|
|
332
480
|
hostDaemonSock: op.host_daemon_sock || op.hostDaemonSock || "",
|
|
333
481
|
hostName: op.host_name || op.hostName || "",
|
|
@@ -447,6 +595,44 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
447
595
|
error: err && err.message ? err.message : String(err || "rename failed"),
|
|
448
596
|
});
|
|
449
597
|
}
|
|
598
|
+
} else if (op.action === "role") {
|
|
599
|
+
const roleTarget = String(op.target || op.agent_id || "").trim();
|
|
600
|
+
const roleProfile = String(op.prompt_profile || op.profile || "").trim();
|
|
601
|
+
if (!roleTarget || !roleProfile) {
|
|
602
|
+
results.push({
|
|
603
|
+
action: "role",
|
|
604
|
+
ok: false,
|
|
605
|
+
error: "role requires target and prompt_profile",
|
|
606
|
+
});
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
const roleResult = await assignSoloRoleToExistingAgent(projectRoot, roleTarget, roleProfile, {
|
|
611
|
+
bootstrapOptions: {
|
|
612
|
+
timeoutMs: 15000,
|
|
613
|
+
retryDelayMs: 250,
|
|
614
|
+
protectionMs: 3000,
|
|
615
|
+
workingGraceMs: 10000,
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
results.push({
|
|
619
|
+
action: "role",
|
|
620
|
+
ok: roleResult.ok !== false,
|
|
621
|
+
target: roleTarget,
|
|
622
|
+
prompt_profile: roleProfile,
|
|
623
|
+
resolved_profile: roleResult.resolved_profile || "",
|
|
624
|
+
skipped: roleResult.skipped || false,
|
|
625
|
+
error: roleResult.error || "",
|
|
626
|
+
});
|
|
627
|
+
} catch (err) {
|
|
628
|
+
results.push({
|
|
629
|
+
action: "role",
|
|
630
|
+
ok: false,
|
|
631
|
+
target: roleTarget,
|
|
632
|
+
prompt_profile: roleProfile,
|
|
633
|
+
error: err && err.message ? err.message : String(err || "role assignment failed"),
|
|
634
|
+
});
|
|
635
|
+
}
|
|
450
636
|
} else if (op.action === "cron") {
|
|
451
637
|
if (!daemonCronController) {
|
|
452
638
|
results.push({
|
|
@@ -697,6 +883,9 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
697
883
|
logFile.write(`[daemon] ${new Date().toISOString()} ${msg}\n`);
|
|
698
884
|
};
|
|
699
885
|
const publishProjectRuntime = (status = "running") => {
|
|
886
|
+
if (isGlobalControllerProjectRoot(projectRoot)) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
700
889
|
try {
|
|
701
890
|
upsertProjectRuntime({
|
|
702
891
|
projectRoot,
|
|
@@ -800,6 +989,54 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
800
989
|
log,
|
|
801
990
|
});
|
|
802
991
|
},
|
|
992
|
+
forwardProjectPrompt: async ({
|
|
993
|
+
targetProjectRoot,
|
|
994
|
+
targetProjectName,
|
|
995
|
+
prompt,
|
|
996
|
+
routeReason,
|
|
997
|
+
requestMeta = {},
|
|
998
|
+
}) => {
|
|
999
|
+
const root = String(targetProjectRoot || "").trim();
|
|
1000
|
+
if (!root) {
|
|
1001
|
+
return { ok: false, error: "target project root is required" };
|
|
1002
|
+
}
|
|
1003
|
+
if (!fs.existsSync(root)) {
|
|
1004
|
+
return { ok: false, error: `target project not found: ${root}` };
|
|
1005
|
+
}
|
|
1006
|
+
const targetPaths = getUfooPaths(root);
|
|
1007
|
+
if (!fs.existsSync(targetPaths.ufooDir)) {
|
|
1008
|
+
const repoRoot = path.join(__dirname, "..", "..");
|
|
1009
|
+
const init = new (require("../init"))(repoRoot);
|
|
1010
|
+
await init.init({ modules: "context,bus", project: root });
|
|
1011
|
+
}
|
|
1012
|
+
if (!isRunning(root)) {
|
|
1013
|
+
const daemonBin = path.join(__dirname, "..", "..", "bin", "ufoo.js");
|
|
1014
|
+
const child = spawn(process.execPath, [daemonBin, "daemon", "--start"], {
|
|
1015
|
+
detached: true,
|
|
1016
|
+
stdio: "ignore",
|
|
1017
|
+
cwd: root,
|
|
1018
|
+
env: process.env,
|
|
1019
|
+
});
|
|
1020
|
+
child.unref();
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const nextMeta = {
|
|
1024
|
+
...(requestMeta && typeof requestMeta === "object" ? requestMeta : {}),
|
|
1025
|
+
via_global_router: true,
|
|
1026
|
+
global_controller_project_root: projectRoot,
|
|
1027
|
+
routed_project_root: root,
|
|
1028
|
+
routed_project_name: targetProjectName || path.basename(root),
|
|
1029
|
+
routed_reason: routeReason || "",
|
|
1030
|
+
};
|
|
1031
|
+
delete nextMeta.force_project_root;
|
|
1032
|
+
delete nextMeta.force_project_name;
|
|
1033
|
+
|
|
1034
|
+
return sendPromptRequestToProject(root, {
|
|
1035
|
+
type: IPC_REQUEST_TYPES.PROMPT,
|
|
1036
|
+
text: String(prompt || ""),
|
|
1037
|
+
request_meta: nextMeta,
|
|
1038
|
+
});
|
|
1039
|
+
},
|
|
803
1040
|
log,
|
|
804
1041
|
});
|
|
805
1042
|
return;
|
|
@@ -831,6 +1068,15 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
831
1068
|
})}
|
|
832
1069
|
`,
|
|
833
1070
|
);
|
|
1071
|
+
ipcServer.sendToSockets({
|
|
1072
|
+
type: IPC_RESPONSE_TYPES.BUS,
|
|
1073
|
+
data: {
|
|
1074
|
+
event: "controller_report",
|
|
1075
|
+
publisher: entry.agent_id,
|
|
1076
|
+
message: entry.summary || entry.message || entry.task_id,
|
|
1077
|
+
report: entry,
|
|
1078
|
+
},
|
|
1079
|
+
});
|
|
834
1080
|
ipcServer.sendToSockets({
|
|
835
1081
|
type: IPC_RESPONSE_TYPES.STATUS,
|
|
836
1082
|
data: buildRuntimeStatus(),
|
|
@@ -1001,6 +1247,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1001
1247
|
agent,
|
|
1002
1248
|
count,
|
|
1003
1249
|
nickname,
|
|
1250
|
+
prompt_profile,
|
|
1004
1251
|
launch_scope,
|
|
1005
1252
|
terminal_app,
|
|
1006
1253
|
host_inject_sock,
|
|
@@ -1022,6 +1269,17 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1022
1269
|
}
|
|
1023
1270
|
const parsedCount = parseInt(count, 10);
|
|
1024
1271
|
const finalCount = Number.isFinite(parsedCount) && parsedCount > 0 ? parsedCount : 1;
|
|
1272
|
+
const requestedProfile = String(prompt_profile || "").trim();
|
|
1273
|
+
if (requestedProfile && finalCount > 1) {
|
|
1274
|
+
socket.write(
|
|
1275
|
+
`${JSON.stringify({
|
|
1276
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1277
|
+
error: "prompt_profile requires count=1",
|
|
1278
|
+
})}
|
|
1279
|
+
`,
|
|
1280
|
+
);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1025
1283
|
const op = {
|
|
1026
1284
|
action: "launch",
|
|
1027
1285
|
agent: normalizedAgent,
|
|
@@ -1038,9 +1296,101 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1038
1296
|
? host_capabilities
|
|
1039
1297
|
: null,
|
|
1040
1298
|
};
|
|
1299
|
+
let soloLaunchBootstrap = null;
|
|
1300
|
+
if (requestedProfile && normalizedAgent === "ufoo") {
|
|
1301
|
+
const profileResult = resolveSoloPromptProfile(projectRoot, requestedProfile);
|
|
1302
|
+
if (!profileResult.ok) {
|
|
1303
|
+
socket.write(
|
|
1304
|
+
`${JSON.stringify({
|
|
1305
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1306
|
+
error: profileResult.error || "prompt profile resolution failed",
|
|
1307
|
+
})}
|
|
1308
|
+
`,
|
|
1309
|
+
);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
const built = buildSoloBootstrap({
|
|
1313
|
+
nickname: nickname || "ucode",
|
|
1314
|
+
agentType: "ufoo-code",
|
|
1315
|
+
requestedProfile: profileResult.requested_profile,
|
|
1316
|
+
profile: profileResult.profile,
|
|
1317
|
+
});
|
|
1318
|
+
if (built.required) {
|
|
1319
|
+
try {
|
|
1320
|
+
const prepared = prepareSoloUcodeBootstrap(projectRoot, nickname || "ucode", built.promptText);
|
|
1321
|
+
op.extra_env = {
|
|
1322
|
+
...(op.extra_env && typeof op.extra_env === "object" ? op.extra_env : {}),
|
|
1323
|
+
UFOO_UCODE_BOOTSTRAP_FILE: prepared.file,
|
|
1324
|
+
};
|
|
1325
|
+
soloLaunchBootstrap = {
|
|
1326
|
+
requested_profile: profileResult.requested_profile,
|
|
1327
|
+
resolved_profile: profileResult.profile.id,
|
|
1328
|
+
promptText: built.promptText,
|
|
1329
|
+
};
|
|
1330
|
+
} catch (err) {
|
|
1331
|
+
socket.write(
|
|
1332
|
+
`${JSON.stringify({
|
|
1333
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1334
|
+
error: err.message || "failed to prepare ucode bootstrap",
|
|
1335
|
+
})}
|
|
1336
|
+
`,
|
|
1337
|
+
);
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1041
1342
|
try {
|
|
1042
1343
|
const opsResults = await handleOps(projectRoot, [op], processManager);
|
|
1043
1344
|
const launchResult = opsResults.find((r) => r.action === "launch");
|
|
1345
|
+
if (soloLaunchBootstrap && launchResult && launchResult.ok !== false) {
|
|
1346
|
+
const subscriberId = pickLaunchSubscriber(projectRoot, launchResult, nickname || "");
|
|
1347
|
+
if (subscriberId) {
|
|
1348
|
+
persistSoloRoleMetadata(projectRoot, subscriberId, {
|
|
1349
|
+
requested_profile: soloLaunchBootstrap.requested_profile,
|
|
1350
|
+
resolved_profile: soloLaunchBootstrap.resolved_profile,
|
|
1351
|
+
bootstrap_fingerprint: buildSoloBootstrapFingerprint({
|
|
1352
|
+
subscriberId,
|
|
1353
|
+
requestedProfile: soloLaunchBootstrap.requested_profile,
|
|
1354
|
+
resolvedProfile: soloLaunchBootstrap.resolved_profile,
|
|
1355
|
+
promptText: soloLaunchBootstrap.promptText,
|
|
1356
|
+
}),
|
|
1357
|
+
bootstrapped_subscriber_id: subscriberId,
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
} else if (requestedProfile && launchResult && launchResult.ok !== false) {
|
|
1361
|
+
const roleTarget = pickLaunchSubscriber(projectRoot, launchResult, nickname || "");
|
|
1362
|
+
const roleResult = await assignSoloRoleToExistingAgent(projectRoot, roleTarget, requestedProfile, {
|
|
1363
|
+
bootstrapOptions: {
|
|
1364
|
+
timeoutMs: 15000,
|
|
1365
|
+
retryDelayMs: 250,
|
|
1366
|
+
protectionMs: 3000,
|
|
1367
|
+
workingGraceMs: 10000,
|
|
1368
|
+
},
|
|
1369
|
+
});
|
|
1370
|
+
if (!roleResult.ok) {
|
|
1371
|
+
const rollback = await rollbackLaunchAfterRoleAssignmentFailure(
|
|
1372
|
+
projectRoot,
|
|
1373
|
+
launchResult,
|
|
1374
|
+
roleTarget,
|
|
1375
|
+
handleOps,
|
|
1376
|
+
processManager
|
|
1377
|
+
);
|
|
1378
|
+
const roleError = roleResult.error || "role assignment failed";
|
|
1379
|
+
const error = rollback.skipped
|
|
1380
|
+
? roleError
|
|
1381
|
+
: (rollback.ok
|
|
1382
|
+
? `${roleError}; launched agent rolled back: ${rollback.target}`
|
|
1383
|
+
: `${roleError}; rollback failed for ${rollback.target || "unknown"}: ${rollback.error || "close failed"}`);
|
|
1384
|
+
socket.write(
|
|
1385
|
+
`${JSON.stringify({
|
|
1386
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1387
|
+
error,
|
|
1388
|
+
})}
|
|
1389
|
+
`,
|
|
1390
|
+
);
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1044
1394
|
const ok = launchResult ? launchResult.ok !== false : true;
|
|
1045
1395
|
const reply = ok
|
|
1046
1396
|
? `Launched ${op.count} ${agent} agent(s)`
|
|
@@ -1068,6 +1418,66 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1068
1418
|
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1069
1419
|
error: err.message || "launch_agent failed",
|
|
1070
1420
|
})}
|
|
1421
|
+
`,
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
if (req.type === IPC_REQUEST_TYPES.ASSIGN_ROLE) {
|
|
1427
|
+
const target = String(req.target || "").trim();
|
|
1428
|
+
const promptProfile = String(req.prompt_profile || req.profile || "").trim();
|
|
1429
|
+
if (!target || !promptProfile) {
|
|
1430
|
+
socket.write(
|
|
1431
|
+
`${JSON.stringify({
|
|
1432
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1433
|
+
error: "assign_role requires target and prompt_profile",
|
|
1434
|
+
})}
|
|
1435
|
+
`,
|
|
1436
|
+
);
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
try {
|
|
1440
|
+
const result = await assignSoloRoleToExistingAgent(projectRoot, target, promptProfile, {
|
|
1441
|
+
bootstrapOptions: {
|
|
1442
|
+
timeoutMs: 15000,
|
|
1443
|
+
retryDelayMs: 250,
|
|
1444
|
+
protectionMs: 3000,
|
|
1445
|
+
workingGraceMs: 10000,
|
|
1446
|
+
},
|
|
1447
|
+
});
|
|
1448
|
+
if (!result.ok) {
|
|
1449
|
+
socket.write(
|
|
1450
|
+
`${JSON.stringify({
|
|
1451
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1452
|
+
error: result.error || "role assignment failed",
|
|
1453
|
+
})}
|
|
1454
|
+
`,
|
|
1455
|
+
);
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
const reply = result.skipped
|
|
1459
|
+
? `Role already applied: ${result.resolved_profile}`
|
|
1460
|
+
: `Assigned role ${result.resolved_profile} to ${result.subscriber_id}`;
|
|
1461
|
+
socket.write(
|
|
1462
|
+
`${JSON.stringify({
|
|
1463
|
+
type: IPC_RESPONSE_TYPES.RESPONSE,
|
|
1464
|
+
data: {
|
|
1465
|
+
reply,
|
|
1466
|
+
role: result,
|
|
1467
|
+
},
|
|
1468
|
+
})}
|
|
1469
|
+
`,
|
|
1470
|
+
);
|
|
1471
|
+
ipcServer.sendToSockets({
|
|
1472
|
+
type: IPC_RESPONSE_TYPES.STATUS,
|
|
1473
|
+
data: buildRuntimeStatus(),
|
|
1474
|
+
});
|
|
1475
|
+
} catch (err) {
|
|
1476
|
+
socket.write(
|
|
1477
|
+
`${JSON.stringify({
|
|
1478
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1479
|
+
error: err.message || "assign_role failed",
|
|
1480
|
+
})}
|
|
1071
1481
|
`,
|
|
1072
1482
|
);
|
|
1073
1483
|
}
|
|
@@ -1259,6 +1669,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1259
1669
|
source: result.entry?.source || "",
|
|
1260
1670
|
filePath: result.entry?.filePath || "",
|
|
1261
1671
|
errors: result.errors || [],
|
|
1672
|
+
prompt_profiles: result.promptProfiles || [],
|
|
1262
1673
|
},
|
|
1263
1674
|
},
|
|
1264
1675
|
})}
|
|
@@ -1731,7 +2142,9 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1731
2142
|
log(`Shutting down daemon (managed agents: ${processManager.count()})`);
|
|
1732
2143
|
clearInterval(runtimeHeartbeat);
|
|
1733
2144
|
try {
|
|
1734
|
-
|
|
2145
|
+
if (!isGlobalControllerProjectRoot(projectRoot)) {
|
|
2146
|
+
markProjectStopped(projectRoot);
|
|
2147
|
+
}
|
|
1735
2148
|
} catch {
|
|
1736
2149
|
// ignore cleanup errors
|
|
1737
2150
|
}
|
|
@@ -1821,7 +2234,9 @@ function stopDaemon(projectRoot) {
|
|
|
1821
2234
|
}
|
|
1822
2235
|
|
|
1823
2236
|
try {
|
|
1824
|
-
|
|
2237
|
+
if (!isGlobalControllerProjectRoot(projectRoot)) {
|
|
2238
|
+
markProjectStopped(projectRoot);
|
|
2239
|
+
}
|
|
1825
2240
|
} catch {
|
|
1826
2241
|
// ignore
|
|
1827
2242
|
}
|
package/src/daemon/ops.js
CHANGED
|
@@ -191,6 +191,14 @@ function escapeAppleScriptString(str) {
|
|
|
191
191
|
return String(str).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
function buildShellEnvPrefix(extraEnv = {}) {
|
|
195
|
+
if (!extraEnv || typeof extraEnv !== "object") return "";
|
|
196
|
+
return Object.entries(extraEnv)
|
|
197
|
+
.filter(([key]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(String(key || "")))
|
|
198
|
+
.map(([key, value]) => `${key}=${shellEscape(String(value ?? ""))}`)
|
|
199
|
+
.join(" ");
|
|
200
|
+
}
|
|
201
|
+
|
|
194
202
|
function runAppleScript(lines) {
|
|
195
203
|
return new Promise((resolve, reject) => {
|
|
196
204
|
const proc = spawn("osascript", lines.flatMap((l) => ["-e", l]));
|
|
@@ -494,7 +502,7 @@ async function spawnManagedHostAgent(
|
|
|
494
502
|
return { child: null, subscriberId: subscriberId || null, sessionId, injectSock };
|
|
495
503
|
}
|
|
496
504
|
|
|
497
|
-
async function spawnInternalAgent(projectRoot, agent, count = 1, nickname = "", processManager = null) {
|
|
505
|
+
async function spawnInternalAgent(projectRoot, agent, count = 1, nickname = "", processManager = null, extraEnv = {}) {
|
|
498
506
|
const runner = path.join(projectRoot, "bin", "ufoo.js");
|
|
499
507
|
const logDir = getUfooPaths(projectRoot).runDir;
|
|
500
508
|
fs.mkdirSync(logDir, { recursive: true });
|
|
@@ -549,6 +557,7 @@ async function spawnInternalAgent(projectRoot, agent, count = 1, nickname = "",
|
|
|
549
557
|
cwd: projectRoot,
|
|
550
558
|
env: {
|
|
551
559
|
...process.env,
|
|
560
|
+
...(extraEnv && typeof extraEnv === "object" ? extraEnv : {}),
|
|
552
561
|
UFOO_INTERNAL_AGENT: "1",
|
|
553
562
|
UFOO_INTERNAL_PTY: usePty ? "1" : "0",
|
|
554
563
|
UFOO_SUBSCRIBER_ID: subscriberId, // 直接传递 subscriber ID
|
|
@@ -711,13 +720,22 @@ async function launchAgent(projectRoot, agent, count = 1, nickname = "", process
|
|
|
711
720
|
const mode = resolveConfiguredLaunchMode(config.launchMode, options);
|
|
712
721
|
const launchScope = normalizeLaunchScope(options.launchScope, "inplace");
|
|
713
722
|
const terminalApp = normalizeTerminalAppPreference(options.terminalApp);
|
|
723
|
+
const extraEnvObject = options.extraEnv && typeof options.extraEnv === "object" ? options.extraEnv : {};
|
|
724
|
+
const extraEnvPrefix = buildShellEnvPrefix(extraEnvObject);
|
|
714
725
|
const normalizedAgent = normalizeLaunchAgent(agent);
|
|
715
726
|
if (!normalizedAgent) {
|
|
716
727
|
throw new Error(`unsupported agent type: ${agent}`);
|
|
717
728
|
}
|
|
718
729
|
|
|
719
730
|
if (mode === "internal") {
|
|
720
|
-
const result = await spawnInternalAgent(
|
|
731
|
+
const result = await spawnInternalAgent(
|
|
732
|
+
projectRoot,
|
|
733
|
+
normalizedAgent,
|
|
734
|
+
count,
|
|
735
|
+
nickname,
|
|
736
|
+
processManager,
|
|
737
|
+
extraEnvObject
|
|
738
|
+
);
|
|
721
739
|
return { mode: "internal", launchScope, subscriberIds: result.subscriberIds };
|
|
722
740
|
}
|
|
723
741
|
if (mode === "tmux") {
|
|
@@ -750,15 +768,15 @@ async function launchAgent(projectRoot, agent, count = 1, nickname = "", process
|
|
|
750
768
|
const nick = count > 1 ? `${nickname || defaultNick}-${i + 1}` : (nickname || "");
|
|
751
769
|
if (useSeparateWindow) {
|
|
752
770
|
// eslint-disable-next-line no-await-in-loop
|
|
753
|
-
await spawnTmuxWindow(projectRoot, normalizedAgent, nick);
|
|
771
|
+
await spawnTmuxWindow(projectRoot, normalizedAgent, nick, [], extraEnvPrefix);
|
|
754
772
|
} else {
|
|
755
773
|
try {
|
|
756
774
|
// eslint-disable-next-line no-await-in-loop
|
|
757
|
-
await spawnTmuxPane(projectRoot, normalizedAgent, nick, [],
|
|
775
|
+
await spawnTmuxPane(projectRoot, normalizedAgent, nick, [], extraEnvPrefix, paneTarget);
|
|
758
776
|
} catch {
|
|
759
777
|
// Fallback to new window when current pane target cannot be resolved.
|
|
760
778
|
// eslint-disable-next-line no-await-in-loop
|
|
761
|
-
await spawnTmuxWindow(projectRoot, normalizedAgent, nick);
|
|
779
|
+
await spawnTmuxWindow(projectRoot, normalizedAgent, nick, [], extraEnvPrefix);
|
|
762
780
|
}
|
|
763
781
|
}
|
|
764
782
|
}
|
|
@@ -777,7 +795,7 @@ async function launchAgent(projectRoot, agent, count = 1, nickname = "", process
|
|
|
777
795
|
nick,
|
|
778
796
|
processManager,
|
|
779
797
|
[],
|
|
780
|
-
|
|
798
|
+
extraEnvPrefix,
|
|
781
799
|
hostContext
|
|
782
800
|
);
|
|
783
801
|
if (result.subscriberId) subscriberIds.push(result.subscriberId);
|
|
@@ -801,7 +819,7 @@ async function launchAgent(projectRoot, agent, count = 1, nickname = "", process
|
|
|
801
819
|
nick,
|
|
802
820
|
processManager,
|
|
803
821
|
[],
|
|
804
|
-
|
|
822
|
+
extraEnvPrefix,
|
|
805
823
|
launchScope,
|
|
806
824
|
terminalApp
|
|
807
825
|
);
|
package/src/daemon/promptLoop.js
CHANGED
|
@@ -120,7 +120,16 @@ async function finalizePromptRun({
|
|
|
120
120
|
dispatchMessages,
|
|
121
121
|
handleOps,
|
|
122
122
|
markPending,
|
|
123
|
+
finalizeLocally = true,
|
|
123
124
|
}) {
|
|
125
|
+
if (finalizeLocally === false) {
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
payload,
|
|
129
|
+
opsResults: [],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
124
133
|
for (const item of payload.dispatch || []) {
|
|
125
134
|
if (item && item.target && item.target !== "broadcast") {
|
|
126
135
|
markPending(item.target);
|
|
@@ -151,12 +160,15 @@ async function runPromptWithAssistant({
|
|
|
151
160
|
reportTaskStatus = () => {},
|
|
152
161
|
maxAssistantLoops = 2,
|
|
153
162
|
log = () => {},
|
|
163
|
+
ufooAgentOptions = {},
|
|
164
|
+
finalizeLocally = true,
|
|
154
165
|
}) {
|
|
155
166
|
const firstResult = await runUfooAgent({
|
|
156
167
|
projectRoot,
|
|
157
168
|
prompt: prompt || "",
|
|
158
169
|
provider,
|
|
159
170
|
model,
|
|
171
|
+
...ufooAgentOptions,
|
|
160
172
|
});
|
|
161
173
|
|
|
162
174
|
if (!firstResult || !firstResult.ok) {
|
|
@@ -183,6 +195,7 @@ async function runPromptWithAssistant({
|
|
|
183
195
|
dispatchMessages,
|
|
184
196
|
handleOps,
|
|
185
197
|
markPending,
|
|
198
|
+
finalizeLocally,
|
|
186
199
|
});
|
|
187
200
|
}
|
|
188
201
|
|
|
@@ -275,6 +288,7 @@ async function runPromptWithAssistant({
|
|
|
275
288
|
prompt: continuationPrompt,
|
|
276
289
|
provider,
|
|
277
290
|
model,
|
|
291
|
+
...ufooAgentOptions,
|
|
278
292
|
});
|
|
279
293
|
|
|
280
294
|
if (!secondResult || !secondResult.ok) {
|
|
@@ -286,6 +300,7 @@ async function runPromptWithAssistant({
|
|
|
286
300
|
dispatchMessages,
|
|
287
301
|
handleOps,
|
|
288
302
|
markPending,
|
|
303
|
+
finalizeLocally,
|
|
289
304
|
});
|
|
290
305
|
}
|
|
291
306
|
|
|
@@ -305,6 +320,7 @@ async function runPromptWithAssistant({
|
|
|
305
320
|
dispatchMessages,
|
|
306
321
|
handleOps,
|
|
307
322
|
markPending,
|
|
323
|
+
finalizeLocally,
|
|
308
324
|
});
|
|
309
325
|
}
|
|
310
326
|
|