u-foo 1.8.8 → 1.9.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/bin/uclaude.js +12 -2
- package/bin/ucodex.js +12 -2
- package/package.json +1 -1
- package/src/agent/defaultBootstrap.js +121 -0
- package/src/agent/launcher.js +84 -56
- package/src/chat/commandExecutor.js +102 -3
- package/src/chat/commands.js +20 -2
- package/src/chat/completionController.js +43 -4
- package/src/chat/daemonReconnect.js +6 -5
- package/src/chat/daemonTransport.js +7 -1
- package/src/chat/index.js +5 -0
- package/src/cli.js +128 -0
- package/src/daemon/groupOrchestrator.js +7 -0
- package/src/daemon/index.js +12 -5
- package/src/group/bootstrap.js +14 -0
- package/src/group/promptProfiles.js +32 -0
- package/src/solo/commands.js +31 -0
- package/templates/groups/build-ultra.json +225 -0
package/bin/uclaude.js
CHANGED
|
@@ -6,8 +6,18 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const AgentLauncher = require("../src/agent/launcher");
|
|
9
|
+
const { resolveDefaultManualBootstrap } = require("../src/agent/defaultBootstrap");
|
|
9
10
|
|
|
10
11
|
const launcher = new AgentLauncher("claude-code", "claude");
|
|
11
|
-
const
|
|
12
|
+
const resolved = resolveDefaultManualBootstrap({
|
|
13
|
+
projectRoot: process.cwd(),
|
|
14
|
+
agentType: "claude-code",
|
|
15
|
+
args: process.argv.slice(2),
|
|
16
|
+
env: process.env,
|
|
17
|
+
});
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
for (const [key, value] of Object.entries(resolved.env || {})) {
|
|
20
|
+
process.env[key] = String(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
launcher.launch(resolved.args);
|
package/bin/ucodex.js
CHANGED
|
@@ -6,8 +6,18 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const AgentLauncher = require("../src/agent/launcher");
|
|
9
|
+
const { resolveDefaultManualBootstrap } = require("../src/agent/defaultBootstrap");
|
|
9
10
|
|
|
10
11
|
const launcher = new AgentLauncher("codex", "codex");
|
|
11
|
-
const
|
|
12
|
+
const resolved = resolveDefaultManualBootstrap({
|
|
13
|
+
projectRoot: process.cwd(),
|
|
14
|
+
agentType: "codex",
|
|
15
|
+
args: process.argv.slice(2),
|
|
16
|
+
env: process.env,
|
|
17
|
+
});
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
for (const [key, value] of Object.entries(resolved.env || {})) {
|
|
20
|
+
process.env[key] = String(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
launcher.launch(resolved.args);
|
package/package.json
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { SHARED_UFOO_PROTOCOL } = require("../group/bootstrap");
|
|
6
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
7
|
+
|
|
8
|
+
function asTrimmedString(value) {
|
|
9
|
+
return typeof value === "string" ? value.trim() : "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function hasArg(args = [], names = []) {
|
|
13
|
+
if (!Array.isArray(args) || args.length === 0) return false;
|
|
14
|
+
const known = new Set((Array.isArray(names) ? names : []).map((item) => asTrimmedString(item)).filter(Boolean));
|
|
15
|
+
return args.some((item) => {
|
|
16
|
+
const text = asTrimmedString(item);
|
|
17
|
+
if (!text) return false;
|
|
18
|
+
if (known.has(text)) return true;
|
|
19
|
+
const eqIndex = text.indexOf("=");
|
|
20
|
+
if (eqIndex <= 0) return false;
|
|
21
|
+
return known.has(text.slice(0, eqIndex).trim());
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hasMetaCommandArgs(args = []) {
|
|
26
|
+
return hasArg(args, ["-h", "--help", "-v", "--version"]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildDefaultStartupBootstrapPrompt({ agentType = "" } = {}) {
|
|
30
|
+
const normalizedAgent = asTrimmedString(agentType).toLowerCase();
|
|
31
|
+
const displayAgent = normalizedAgent === "claude-code"
|
|
32
|
+
? "Claude"
|
|
33
|
+
: (normalizedAgent === "codex" ? "Codex" : "agent");
|
|
34
|
+
return [
|
|
35
|
+
`Session bootstrap for ${displayAgent}.`,
|
|
36
|
+
"Adopt the following ufoo coordination protocol silently.",
|
|
37
|
+
"Do not reply to this bootstrap message unless the user explicitly asks about it. After applying it, continue the active task or wait for user input.",
|
|
38
|
+
SHARED_UFOO_PROTOCOL,
|
|
39
|
+
].join("\n\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function defaultBootstrapFile(projectRoot, agentType = "") {
|
|
43
|
+
const safeAgentType = asTrimmedString(agentType).replace(/[^a-zA-Z0-9._-]/g, "-") || "agent";
|
|
44
|
+
return path.join(getUfooPaths(projectRoot).agentDir, safeAgentType, "default-bootstrap.md");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function prepareDefaultBootstrapFile({
|
|
48
|
+
projectRoot,
|
|
49
|
+
agentType = "",
|
|
50
|
+
promptText = "",
|
|
51
|
+
targetFile = "",
|
|
52
|
+
} = {}) {
|
|
53
|
+
const root = asTrimmedString(projectRoot) || process.cwd();
|
|
54
|
+
const file = asTrimmedString(targetFile) || defaultBootstrapFile(root, agentType);
|
|
55
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
56
|
+
fs.writeFileSync(file, String(promptText || ""), "utf8");
|
|
57
|
+
return { ok: true, file };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveDefaultManualBootstrap({
|
|
61
|
+
projectRoot,
|
|
62
|
+
agentType = "",
|
|
63
|
+
args = [],
|
|
64
|
+
env = process.env,
|
|
65
|
+
} = {}) {
|
|
66
|
+
const normalizedAgent = asTrimmedString(agentType).toLowerCase();
|
|
67
|
+
const currentEnv = env && typeof env === "object" ? env : {};
|
|
68
|
+
const currentArgs = Array.isArray(args) ? args.slice() : [];
|
|
69
|
+
if (
|
|
70
|
+
currentEnv.UFOO_SKIP_DEFAULT_BOOTSTRAP === "1"
|
|
71
|
+
|| currentEnv.UFOO_STARTUP_BOOTSTRAP_TEXT
|
|
72
|
+
|| hasMetaCommandArgs(currentArgs)
|
|
73
|
+
) {
|
|
74
|
+
return { args: currentArgs, env: {}, mode: "skip" };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (normalizedAgent === "claude-code") {
|
|
78
|
+
if (hasArg(currentArgs, ["--append-system-prompt", "--system-prompt"])) {
|
|
79
|
+
return { args: currentArgs, env: {}, mode: "skip" };
|
|
80
|
+
}
|
|
81
|
+
const promptText = buildDefaultStartupBootstrapPrompt({ agentType: normalizedAgent });
|
|
82
|
+
const prepared = prepareDefaultBootstrapFile({
|
|
83
|
+
projectRoot,
|
|
84
|
+
agentType: normalizedAgent,
|
|
85
|
+
promptText,
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
args: [...currentArgs, "--append-system-prompt", prepared.file],
|
|
89
|
+
env: {},
|
|
90
|
+
mode: "system-prompt-file",
|
|
91
|
+
file: prepared.file,
|
|
92
|
+
promptText,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (normalizedAgent === "codex") {
|
|
97
|
+
if (currentArgs.length > 0) {
|
|
98
|
+
return { args: currentArgs, env: {}, mode: "skip" };
|
|
99
|
+
}
|
|
100
|
+
const promptText = buildDefaultStartupBootstrapPrompt({ agentType: normalizedAgent });
|
|
101
|
+
return {
|
|
102
|
+
args: currentArgs,
|
|
103
|
+
env: {
|
|
104
|
+
UFOO_STARTUP_BOOTSTRAP_TEXT: promptText,
|
|
105
|
+
},
|
|
106
|
+
mode: "post-launch-inject",
|
|
107
|
+
promptText,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { args: currentArgs, env: {}, mode: "skip" };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
hasArg,
|
|
116
|
+
hasMetaCommandArgs,
|
|
117
|
+
buildDefaultStartupBootstrapPrompt,
|
|
118
|
+
defaultBootstrapFile,
|
|
119
|
+
prepareDefaultBootstrapFile,
|
|
120
|
+
resolveDefaultManualBootstrap,
|
|
121
|
+
};
|
package/src/agent/launcher.js
CHANGED
|
@@ -37,6 +37,40 @@ async function connectWithRetry(sockPath, retries, delayMs) {
|
|
|
37
37
|
return null;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
async function notifyDaemonAgentReady(daemonSockPath, subscriberId, agentPid) {
|
|
41
|
+
if (!daemonSockPath || !subscriberId) return false;
|
|
42
|
+
const parsedAgentPid = Number.parseInt(agentPid, 10);
|
|
43
|
+
if (!Number.isFinite(parsedAgentPid) || parsedAgentPid <= 0) return false;
|
|
44
|
+
|
|
45
|
+
const startTime = Date.now();
|
|
46
|
+
try {
|
|
47
|
+
const daemonSock = await connectWithRetry(daemonSockPath, 3, 100);
|
|
48
|
+
if (!daemonSock) {
|
|
49
|
+
if (process.env.UFOO_DEBUG) {
|
|
50
|
+
console.error("[ready] failed to connect to daemon after retries, will use fallback delay");
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
daemonSock.write(`${JSON.stringify({
|
|
56
|
+
type: IPC_REQUEST_TYPES.AGENT_READY,
|
|
57
|
+
subscriberId,
|
|
58
|
+
agentPid: parsedAgentPid,
|
|
59
|
+
})}\n`);
|
|
60
|
+
daemonSock.end();
|
|
61
|
+
|
|
62
|
+
if (process.env.UFOO_DEBUG) {
|
|
63
|
+
console.error(`[ready] notified daemon in ${Date.now() - startTime}ms`);
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (process.env.UFOO_DEBUG) {
|
|
68
|
+
console.error(`[ready] daemon notification error: ${err.message}, will use fallback delay`);
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
40
74
|
async function probeDaemonSocket(sockPath) {
|
|
41
75
|
try {
|
|
42
76
|
const client = await connectSocket(sockPath);
|
|
@@ -190,6 +224,42 @@ function computeInjectedSubmitDelayMs(agentType, text) {
|
|
|
190
224
|
return delayMs;
|
|
191
225
|
}
|
|
192
226
|
|
|
227
|
+
function sleep(ms) {
|
|
228
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function logInjectedCommand(wrapper, source, commandText) {
|
|
232
|
+
if (!wrapper || !wrapper.logger) return;
|
|
233
|
+
const text = String(commandText || "");
|
|
234
|
+
if (!text) return;
|
|
235
|
+
const logEntry = {
|
|
236
|
+
ts: Date.now(),
|
|
237
|
+
dir: "in",
|
|
238
|
+
data: { text, encoding: "utf8", size: text.length },
|
|
239
|
+
source,
|
|
240
|
+
};
|
|
241
|
+
wrapper.logger.write(JSON.stringify(logEntry) + "\n");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function injectPtyCommand(wrapper, agentType, commandText, source = "inject") {
|
|
245
|
+
const text = String(commandText || "");
|
|
246
|
+
if (!wrapper || !text) return false;
|
|
247
|
+
const normalizedAgentType = String(agentType || "").trim().toLowerCase();
|
|
248
|
+
const submitDelayMs = computeInjectedSubmitDelayMs(agentType, text);
|
|
249
|
+
wrapper.write(text);
|
|
250
|
+
if (normalizedAgentType === "claude-code") {
|
|
251
|
+
await sleep(submitDelayMs);
|
|
252
|
+
wrapper.write("\r");
|
|
253
|
+
} else {
|
|
254
|
+
await sleep(submitDelayMs);
|
|
255
|
+
wrapper.write("\x1b");
|
|
256
|
+
await sleep(100);
|
|
257
|
+
wrapper.write("\r");
|
|
258
|
+
}
|
|
259
|
+
logInjectedCommand(wrapper, source, text);
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
|
|
193
263
|
async function resolveHostRegistrationData(launchMode) {
|
|
194
264
|
if (launchMode !== "host") {
|
|
195
265
|
return {
|
|
@@ -417,6 +487,7 @@ class AgentLauncher {
|
|
|
417
487
|
return new Promise((resolve, reject) => {
|
|
418
488
|
let buffer = "";
|
|
419
489
|
let settled = false;
|
|
490
|
+
const registerTimeoutMs = parseInt(process.env.UFOO_REGISTER_TIMEOUT_MS, 10) || 8000;
|
|
420
491
|
const timeout = setTimeout(() => {
|
|
421
492
|
if (settled) return;
|
|
422
493
|
settled = true;
|
|
@@ -426,7 +497,7 @@ class AgentLauncher {
|
|
|
426
497
|
// ignore
|
|
427
498
|
}
|
|
428
499
|
reject(new Error("register_agent timeout"));
|
|
429
|
-
},
|
|
500
|
+
}, registerTimeoutMs);
|
|
430
501
|
|
|
431
502
|
const cleanup = () => {
|
|
432
503
|
clearTimeout(timeout);
|
|
@@ -497,6 +568,11 @@ class AgentLauncher {
|
|
|
497
568
|
},
|
|
498
569
|
});
|
|
499
570
|
|
|
571
|
+
if (resolveLaunchMode() === "host" && child.pid) {
|
|
572
|
+
const daemonSockPath = getUfooPaths(this.cwd).ufooSock;
|
|
573
|
+
notifyDaemonAgentReady(daemonSockPath, subscriberId, child.pid).catch(() => {});
|
|
574
|
+
}
|
|
575
|
+
|
|
500
576
|
child.on("error", (err) => {
|
|
501
577
|
console.error(`[${this.command}] Failed to start:`, err.message);
|
|
502
578
|
process.exit(1);
|
|
@@ -568,6 +644,7 @@ class AgentLauncher {
|
|
|
568
644
|
}
|
|
569
645
|
|
|
570
646
|
// 7. 启动命令(PTY wrapper或直接spawn)
|
|
647
|
+
const startupBootstrapText = String(process.env.UFOO_STARTUP_BOOTSTRAP_TEXT || "").trim();
|
|
571
648
|
|
|
572
649
|
// 7.1 PTY启用条件(显式开关 + 自动检测)
|
|
573
650
|
let shouldUsePty = false;
|
|
@@ -665,33 +742,11 @@ class AgentLauncher {
|
|
|
665
742
|
}
|
|
666
743
|
}
|
|
667
744
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
const daemonSock = await connectWithRetry(daemonSockPath, 3, 100);
|
|
671
|
-
if (daemonSock) {
|
|
672
|
-
daemonSock.write(`${JSON.stringify({
|
|
673
|
-
type: IPC_REQUEST_TYPES.AGENT_READY,
|
|
674
|
-
subscriberId,
|
|
675
|
-
agentPid: wrapper.pty ? wrapper.pty.pid : 0,
|
|
676
|
-
})}\n`);
|
|
677
|
-
daemonSock.end();
|
|
678
|
-
|
|
679
|
-
const notifyTime = Date.now() - startTime;
|
|
680
|
-
if (process.env.UFOO_DEBUG) {
|
|
681
|
-
console.error(`[ready] notified daemon in ${notifyTime}ms`);
|
|
682
|
-
}
|
|
683
|
-
} else {
|
|
684
|
-
if (process.env.UFOO_DEBUG) {
|
|
685
|
-
console.error(`[ready] failed to connect to daemon after retries, will use fallback delay`);
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
} catch (err) {
|
|
689
|
-
// 忽略通知失败(probe会通过fallback延迟执行)
|
|
690
|
-
if (process.env.UFOO_DEBUG) {
|
|
691
|
-
console.error(`[ready] daemon notification error: ${err.message}, will use fallback delay`);
|
|
692
|
-
}
|
|
745
|
+
if (startupBootstrapText && wrapper.pty) {
|
|
746
|
+
await injectPtyCommand(wrapper, this.agentType, startupBootstrapText, "startup-bootstrap");
|
|
693
747
|
}
|
|
694
748
|
|
|
749
|
+
await notifyDaemonAgentReady(daemonSockPath, subscriberId, wrapper.pty ? wrapper.pty.pid : 0);
|
|
695
750
|
});
|
|
696
751
|
|
|
697
752
|
// Fallback:如果10秒后还没检测到ready,强制标记为ready
|
|
@@ -799,37 +854,8 @@ class AgentLauncher {
|
|
|
799
854
|
continue;
|
|
800
855
|
}
|
|
801
856
|
// 注入命令到PTY(带延迟确保输入完成)
|
|
802
|
-
|
|
803
|
-
// Alt+Enter (newline) instead of two separate keys. Use a
|
|
804
|
-
// longer gap so the escape sequence parser times out.
|
|
805
|
-
const commandText = String(req.command);
|
|
806
|
-
const submitDelayMs = computeInjectedSubmitDelayMs(this.agentType, commandText);
|
|
807
|
-
wrapper.write(commandText);
|
|
808
|
-
if (normalizedAgentType === "claude-code") {
|
|
809
|
-
// Claude Code: send CR directly without ESC.
|
|
810
|
-
// ESC before CR is interpreted as Alt+Enter (newline).
|
|
811
|
-
setTimeout(() => {
|
|
812
|
-
wrapper.write("\r");
|
|
813
|
-
}, submitDelayMs);
|
|
814
|
-
} else {
|
|
815
|
-
// Codex/others: ESC dismisses autocomplete, then CR submits.
|
|
816
|
-
setTimeout(() => {
|
|
817
|
-
wrapper.write("\x1b");
|
|
818
|
-
setTimeout(() => {
|
|
819
|
-
wrapper.write("\r");
|
|
820
|
-
}, 100);
|
|
821
|
-
}, submitDelayMs);
|
|
822
|
-
}
|
|
857
|
+
void injectPtyCommand(wrapper, this.agentType, req.command, "inject");
|
|
823
858
|
client.write(JSON.stringify({ ok: true }) + "\n");
|
|
824
|
-
if (wrapper.logger) {
|
|
825
|
-
const logEntry = {
|
|
826
|
-
ts: Date.now(),
|
|
827
|
-
dir: "in",
|
|
828
|
-
data: { text: req.command, encoding: "utf8", size: req.command.length },
|
|
829
|
-
source: "inject",
|
|
830
|
-
};
|
|
831
|
-
wrapper.logger.write(JSON.stringify(logEntry) + "\n");
|
|
832
|
-
}
|
|
833
859
|
} else if (req.type === PTY_SOCKET_MESSAGE_TYPES.RAW && req.data) {
|
|
834
860
|
// Raw PTY write (no Enter appended) - for TTY view passthrough
|
|
835
861
|
wrapper.write(req.data);
|
|
@@ -938,5 +964,7 @@ class AgentLauncher {
|
|
|
938
964
|
// Exported for testing
|
|
939
965
|
AgentLauncher._sanitizeNickname = (nick) => nick.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
940
966
|
AgentLauncher._findPreviousSession = findPreviousSession;
|
|
967
|
+
AgentLauncher._notifyDaemonAgentReady = notifyDaemonAgentReady;
|
|
968
|
+
AgentLauncher._injectPtyCommand = injectPtyCommand;
|
|
941
969
|
|
|
942
970
|
module.exports = AgentLauncher;
|
|
@@ -8,6 +8,7 @@ const { resolveTransport } = require("../code/nativeRunner");
|
|
|
8
8
|
const { parseIntervalMs, formatIntervalMs } = require("./cronScheduler");
|
|
9
9
|
const { isGlobalControllerProjectRoot, resolveGlobalControllerUfooDir } = require("../globalMode");
|
|
10
10
|
const { loadPromptProfileRegistry } = require("../group/promptProfiles");
|
|
11
|
+
const { resolveSoloAgentType } = require("../solo/commands");
|
|
11
12
|
|
|
12
13
|
function defaultCreateDoctor(projectRoot) {
|
|
13
14
|
const UfooDoctor = require("../doctor");
|
|
@@ -528,10 +529,15 @@ function createCommandExecutor(options = {}) {
|
|
|
528
529
|
return;
|
|
529
530
|
}
|
|
530
531
|
|
|
531
|
-
const target =
|
|
532
|
-
|
|
532
|
+
const target = action === "assign"
|
|
533
|
+
? String(args[1] || "").trim()
|
|
534
|
+
: String(args[0] || "").trim();
|
|
535
|
+
const profile = action === "assign"
|
|
536
|
+
? String(args[2] || "").trim()
|
|
537
|
+
: String(args[1] || "").trim();
|
|
533
538
|
if (!target || !profile) {
|
|
534
|
-
logMessage("error", "{white-fg}✗{/white-fg} Usage: /role <agent-id|nickname> <prompt-profile>");
|
|
539
|
+
logMessage("error", "{white-fg}✗{/white-fg} Usage: /role assign <agent-id|nickname> <prompt-profile>");
|
|
540
|
+
logMessage("error", " /role <agent-id|nickname> <prompt-profile>");
|
|
535
541
|
logMessage("error", " /role list");
|
|
536
542
|
return;
|
|
537
543
|
}
|
|
@@ -548,6 +554,95 @@ function createCommandExecutor(options = {}) {
|
|
|
548
554
|
}
|
|
549
555
|
}
|
|
550
556
|
|
|
557
|
+
async function handleSoloCommand(args = []) {
|
|
558
|
+
const subcommand = String(args[0] || "").trim().toLowerCase();
|
|
559
|
+
if (!subcommand) {
|
|
560
|
+
logMessage("error", "{white-fg}✗{/white-fg} Usage: /solo <run|list> ...");
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (subcommand === "list" || subcommand === "ls") {
|
|
565
|
+
try {
|
|
566
|
+
const registry = loadPromptProfileRegistry(projectRoot);
|
|
567
|
+
const profiles = registry.profiles || [];
|
|
568
|
+
if (profiles.length === 0) {
|
|
569
|
+
logMessage("system", "{white-fg}⚙{/white-fg} No solo roles found.");
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
logMessage("system", `{white-fg}⚙{/white-fg} Available solo roles (${profiles.length}):`);
|
|
573
|
+
for (const p of profiles) {
|
|
574
|
+
const aliases = p.aliases && p.aliases.length > 0 ? ` {gray-fg}(${p.aliases.join(", ")}){/gray-fg}` : "";
|
|
575
|
+
const source = p.source ? ` {cyan-fg}[${p.source}]{/cyan-fg}` : "";
|
|
576
|
+
const summary = p.summary ? ` ${p.summary}` : "";
|
|
577
|
+
logMessage("system", ` {bold}${escapeBlessed(p.id)}{/bold}${aliases}${source}`);
|
|
578
|
+
if (summary) {
|
|
579
|
+
logMessage("system", ` ${escapeBlessed(summary)}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
} catch (err) {
|
|
583
|
+
logMessage("error", `{white-fg}✗{/white-fg} Failed to list solo roles: ${escapeBlessed(err.message)}`);
|
|
584
|
+
}
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (subcommand !== "run") {
|
|
589
|
+
logMessage("error", `{white-fg}✗{/white-fg} Unknown solo action: ${escapeBlessed(subcommand)}`);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const profile = String(args[1] || "").trim();
|
|
594
|
+
if (!profile) {
|
|
595
|
+
logMessage("error", "{white-fg}✗{/white-fg} Usage: /solo run <prompt-profile> [agent=codex|claude|ucode] [nickname=<name>] [scope=inplace|window]");
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const parsedOptions = {};
|
|
600
|
+
for (let i = 2; i < args.length; i += 1) {
|
|
601
|
+
const arg = args[i];
|
|
602
|
+
if (arg.includes("=")) {
|
|
603
|
+
const [key, value] = arg.split("=", 2);
|
|
604
|
+
parsedOptions[key] = value;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function normalizeLaunchScopeOption(value, fallback = "inplace") {
|
|
609
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
610
|
+
if (!raw) return fallback;
|
|
611
|
+
if (raw === "inplace" || raw === "same" || raw === "current" || raw === "tab" || raw === "pane") {
|
|
612
|
+
return "inplace";
|
|
613
|
+
}
|
|
614
|
+
if (raw === "window" || raw === "separate" || raw === "new" || raw === "new-window" || raw === "external") {
|
|
615
|
+
return "window";
|
|
616
|
+
}
|
|
617
|
+
return "";
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const config = loadConfig(projectRoot);
|
|
621
|
+
const agent = resolveSoloAgentType(config, parsedOptions.agent || parsedOptions.type || "");
|
|
622
|
+
const nickname = String(parsedOptions.nickname || "").trim();
|
|
623
|
+
const scopeRaw = parsedOptions.scope || parsedOptions.launch_scope || "";
|
|
624
|
+
const launchScope = normalizeLaunchScopeOption(scopeRaw, "inplace");
|
|
625
|
+
if (scopeRaw && !launchScope) {
|
|
626
|
+
logMessage("error", "{white-fg}✗{/white-fg} scope must be inplace|window");
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
send({
|
|
632
|
+
type: IPC_REQUEST_TYPES.LAUNCH_AGENT,
|
|
633
|
+
agent: agent === "ucode" ? "ufoo" : agent,
|
|
634
|
+
count: 1,
|
|
635
|
+
nickname,
|
|
636
|
+
prompt_profile: profile,
|
|
637
|
+
launch_scope: launchScope,
|
|
638
|
+
...collectHostLaunchRequestContext(),
|
|
639
|
+
});
|
|
640
|
+
schedule(requestStatus, 1000);
|
|
641
|
+
} catch (err) {
|
|
642
|
+
logMessage("error", `{white-fg}✗{/white-fg} Solo launch failed: ${escapeBlessed(err.message)}`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
551
646
|
async function handleResumeCommand(args = []) {
|
|
552
647
|
const action = String(args[0] || "").toLowerCase();
|
|
553
648
|
if (action === "list" || action === "ls") {
|
|
@@ -1218,6 +1313,9 @@ function createCommandExecutor(options = {}) {
|
|
|
1218
1313
|
case "role":
|
|
1219
1314
|
await handleRoleCommand(args);
|
|
1220
1315
|
return true;
|
|
1316
|
+
case "solo":
|
|
1317
|
+
await handleSoloCommand(args);
|
|
1318
|
+
return true;
|
|
1221
1319
|
case "cron":
|
|
1222
1320
|
await handleCronCommand(args);
|
|
1223
1321
|
return true;
|
|
@@ -1250,6 +1348,7 @@ function createCommandExecutor(options = {}) {
|
|
|
1250
1348
|
handleResumeCommand,
|
|
1251
1349
|
handleProjectCommand,
|
|
1252
1350
|
handleRoleCommand,
|
|
1351
|
+
handleSoloCommand,
|
|
1253
1352
|
handleCronCommand,
|
|
1254
1353
|
handleGroupCommand,
|
|
1255
1354
|
handleSettingsCommand,
|
package/src/chat/commands.js
CHANGED
|
@@ -67,7 +67,15 @@ const COMMAND_TREE = {
|
|
|
67
67
|
"/role": {
|
|
68
68
|
desc: "Assign preset role to an existing agent",
|
|
69
69
|
children: {
|
|
70
|
-
|
|
70
|
+
assign: { desc: "Assign a role to an existing agent", order: 1 },
|
|
71
|
+
list: { desc: "List available prompt profiles", order: 2 },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
"/solo": {
|
|
75
|
+
desc: "Solo role agent operations",
|
|
76
|
+
children: {
|
|
77
|
+
run: { desc: "Launch a solo role agent", order: 1 },
|
|
78
|
+
list: { desc: "List available solo roles", order: 2 },
|
|
71
79
|
},
|
|
72
80
|
},
|
|
73
81
|
"/resume": {
|
|
@@ -111,10 +119,20 @@ function buildCommandRegistry(tree) {
|
|
|
111
119
|
const entry = { cmd, desc: node.desc || "" };
|
|
112
120
|
if (node.children) {
|
|
113
121
|
entry.subcommands = Object.keys(node.children)
|
|
114
|
-
.sort((a, b) =>
|
|
122
|
+
.sort((a, b) => {
|
|
123
|
+
const aNode = node.children[a] || {};
|
|
124
|
+
const bNode = node.children[b] || {};
|
|
125
|
+
const aOrder = Number.isFinite(aNode.order) ? aNode.order : Number.POSITIVE_INFINITY;
|
|
126
|
+
const bOrder = Number.isFinite(bNode.order) ? bNode.order : Number.POSITIVE_INFINITY;
|
|
127
|
+
if (aOrder !== bOrder) return aOrder - bOrder;
|
|
128
|
+
return a.localeCompare(b, "en", { sensitivity: "base" });
|
|
129
|
+
})
|
|
115
130
|
.map((sub) => ({
|
|
116
131
|
cmd: sub,
|
|
117
132
|
desc: (node.children[sub] && node.children[sub].desc) || "",
|
|
133
|
+
order: Number.isFinite(node.children[sub] && node.children[sub].order)
|
|
134
|
+
? node.children[sub].order
|
|
135
|
+
: undefined,
|
|
118
136
|
}));
|
|
119
137
|
}
|
|
120
138
|
return entry;
|
|
@@ -4,6 +4,13 @@ const FALLBACK_LAUNCH_SUBCOMMANDS = [
|
|
|
4
4
|
{ cmd: "ucode", desc: "Launch ucode core agent" },
|
|
5
5
|
];
|
|
6
6
|
|
|
7
|
+
function sortSubcommandEntries(a, b) {
|
|
8
|
+
const aOrder = Number.isFinite(a && a.order) ? a.order : Number.POSITIVE_INFINITY;
|
|
9
|
+
const bOrder = Number.isFinite(b && b.order) ? b.order : Number.POSITIVE_INFINITY;
|
|
10
|
+
if (aOrder !== bOrder) return aOrder - bOrder;
|
|
11
|
+
return String(a && a.cmd ? a.cmd : "").localeCompare(String(b && b.cmd ? b.cmd : ""), "en", { sensitivity: "base" });
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
function createCompletionController(options = {}) {
|
|
8
15
|
const {
|
|
9
16
|
input,
|
|
@@ -12,6 +19,7 @@ function createCompletionController(options = {}) {
|
|
|
12
19
|
promptBox,
|
|
13
20
|
commandRegistry = [],
|
|
14
21
|
getGroupTemplateCandidates = () => [],
|
|
22
|
+
getSoloProfileCandidates = () => [],
|
|
15
23
|
getMentionCandidates = () => [],
|
|
16
24
|
normalizeCommandPrefix = () => {},
|
|
17
25
|
truncateText = (text) => String(text || ""),
|
|
@@ -143,6 +151,7 @@ function createCompletionController(options = {}) {
|
|
|
143
151
|
const mainCmd = parts[0];
|
|
144
152
|
const isLaunch = mainCmd && mainCmd.toLowerCase() === "/launch";
|
|
145
153
|
const isGroup = mainCmd && mainCmd.toLowerCase() === "/group";
|
|
154
|
+
const isSolo = mainCmd && mainCmd.toLowerCase() === "/solo";
|
|
146
155
|
const wantsSubcommands = (parts.length > 1 || (endsWithSpace && parts.length === 1));
|
|
147
156
|
|
|
148
157
|
if (isGroup) {
|
|
@@ -169,6 +178,28 @@ function createCompletionController(options = {}) {
|
|
|
169
178
|
}
|
|
170
179
|
}
|
|
171
180
|
|
|
181
|
+
if (isSolo) {
|
|
182
|
+
const soloSubcommand = String(parts[1] || "").trim().toLowerCase();
|
|
183
|
+
const wantsSoloRunArgs = soloSubcommand === "run" && (parts.length > 2 || endsWithSpace);
|
|
184
|
+
if (wantsSoloRunArgs) {
|
|
185
|
+
const profileFilter = String(parts[2] || "").trim().toLowerCase();
|
|
186
|
+
return (Array.isArray(getSoloProfileCandidates()) ? getSoloProfileCandidates() : [])
|
|
187
|
+
.map((item) => {
|
|
188
|
+
const profileId = String(item && item.cmd ? item.cmd : item && item.id ? item.id : "").trim();
|
|
189
|
+
if (!profileId) return null;
|
|
190
|
+
const desc = String(item && item.desc ? item.desc : item && item.summary ? item.summary : "").trim();
|
|
191
|
+
return {
|
|
192
|
+
cmd: profileId,
|
|
193
|
+
desc,
|
|
194
|
+
isArgumentSuggestion: true,
|
|
195
|
+
argumentPrefix: "/solo run",
|
|
196
|
+
};
|
|
197
|
+
})
|
|
198
|
+
.filter((item) => item && (!profileFilter || item.cmd.toLowerCase().startsWith(profileFilter)))
|
|
199
|
+
.sort((a, b) => a.cmd.localeCompare(b.cmd, "en", { sensitivity: "base" }));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
172
203
|
if ((wantsSubcommands || isLaunch) && mainCmd && mainCmd.startsWith("/")) {
|
|
173
204
|
const subFilter = parts[1] || "";
|
|
174
205
|
const mainCmdObj = commandRegistry.find((item) =>
|
|
@@ -188,12 +219,12 @@ function createCompletionController(options = {}) {
|
|
|
188
219
|
if (isLaunch) {
|
|
189
220
|
return subs
|
|
190
221
|
.map((sub) => ({ ...sub, isSubcommand: true, parentCmd: mainCmd }))
|
|
191
|
-
.sort(
|
|
222
|
+
.sort(sortSubcommandEntries);
|
|
192
223
|
}
|
|
193
224
|
return subs
|
|
194
225
|
.filter((sub) => sub.cmd.toLowerCase().startsWith(subFilter.toLowerCase()))
|
|
195
226
|
.map((sub) => ({ ...sub, isSubcommand: true, parentCmd: mainCmd }))
|
|
196
|
-
.sort(
|
|
227
|
+
.sort(sortSubcommandEntries);
|
|
197
228
|
}
|
|
198
229
|
return [];
|
|
199
230
|
}
|
|
@@ -340,7 +371,11 @@ function createCompletionController(options = {}) {
|
|
|
340
371
|
|
|
341
372
|
if (!selected.isSubcommand && selected.subcommands && selected.subcommands.length > 0) {
|
|
342
373
|
show(input.value);
|
|
343
|
-
} else if (
|
|
374
|
+
} else if (
|
|
375
|
+
selected.isSubcommand
|
|
376
|
+
&& ((selected.parentCmd === "/group" && selected.cmd === "run")
|
|
377
|
+
|| (selected.parentCmd === "/solo" && selected.cmd === "run"))
|
|
378
|
+
) {
|
|
344
379
|
show(input.value);
|
|
345
380
|
} else {
|
|
346
381
|
hide();
|
|
@@ -384,7 +419,11 @@ function createCompletionController(options = {}) {
|
|
|
384
419
|
applyPreview(nextPreview);
|
|
385
420
|
if (!selected.isSubcommand && selected.subcommands && selected.subcommands.length > 0) {
|
|
386
421
|
show(input.value);
|
|
387
|
-
} else if (
|
|
422
|
+
} else if (
|
|
423
|
+
selected.isSubcommand
|
|
424
|
+
&& ((selected.parentCmd === "/group" && selected.cmd === "run")
|
|
425
|
+
|| (selected.parentCmd === "/solo" && selected.cmd === "run"))
|
|
426
|
+
) {
|
|
388
427
|
show(input.value);
|
|
389
428
|
} else {
|
|
390
429
|
hide();
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { restartLocks } = require("./daemonTransport");
|
|
2
|
+
|
|
1
3
|
function resolveDaemonConnection(daemonConnection) {
|
|
2
4
|
return typeof daemonConnection === "function" ? daemonConnection() : daemonConnection;
|
|
3
5
|
}
|
|
@@ -14,11 +16,10 @@ function restartDaemonFlow(options = {}) {
|
|
|
14
16
|
|
|
15
17
|
const statusMsg = resolveStatusLine || ((text) => logMessage("status", text));
|
|
16
18
|
|
|
17
|
-
let restartInProgress = false;
|
|
18
|
-
|
|
19
19
|
return async function restartDaemon() {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// Use global restart lock to prevent concurrent restart flows
|
|
21
|
+
if (restartLocks.get(projectRoot)) return;
|
|
22
|
+
restartLocks.set(projectRoot, true);
|
|
22
23
|
statusMsg("{gray-fg}⚙{/gray-fg} Restarting daemon...");
|
|
23
24
|
try {
|
|
24
25
|
const connection = resolveDaemonConnection(daemonConnection);
|
|
@@ -34,7 +35,7 @@ function restartDaemonFlow(options = {}) {
|
|
|
34
35
|
statusMsg("{gray-fg}✗{/gray-fg} Failed to reconnect to daemon");
|
|
35
36
|
}
|
|
36
37
|
} finally {
|
|
37
|
-
|
|
38
|
+
restartLocks.delete(projectRoot);
|
|
38
39
|
}
|
|
39
40
|
};
|
|
40
41
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const { DAEMON_TRANSPORT_DEFAULTS } = require("./daemonTransportDefaults");
|
|
2
2
|
|
|
3
|
+
// Global restart lock per project to prevent concurrent restart flows
|
|
4
|
+
const restartLocks = new Map();
|
|
5
|
+
|
|
3
6
|
function createDaemonTransport(options = {}) {
|
|
4
7
|
const {
|
|
5
8
|
projectRoot,
|
|
@@ -34,7 +37,9 @@ function createDaemonTransport(options = {}) {
|
|
|
34
37
|
);
|
|
35
38
|
if (!client) {
|
|
36
39
|
// Retry once with a fresh daemon start and longer wait.
|
|
37
|
-
if
|
|
40
|
+
// Check if a restart is already in progress via the explicit restart flow.
|
|
41
|
+
const isExplicitRestartInProgress = restartLocks.get(target.projectRoot);
|
|
42
|
+
if (!isExplicitRestartInProgress && !isRunning(target.projectRoot)) {
|
|
38
43
|
startDaemon(target.projectRoot);
|
|
39
44
|
await new Promise((resolve) => setTimeout(resolve, restartDelayMs));
|
|
40
45
|
}
|
|
@@ -75,4 +80,5 @@ function createDaemonTransport(options = {}) {
|
|
|
75
80
|
|
|
76
81
|
module.exports = {
|
|
77
82
|
createDaemonTransport,
|
|
83
|
+
restartLocks,
|
|
78
84
|
};
|
package/src/chat/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const { getUfooPaths } = require("../ufoo/paths");
|
|
|
18
18
|
const { startDaemon, stopDaemon, connectWithRetry } = require("./transport");
|
|
19
19
|
const { escapeBlessed, stripBlessedTags, truncateText } = require("./text");
|
|
20
20
|
const { COMMAND_REGISTRY, parseCommand, parseAtTarget } = require("./commands");
|
|
21
|
+
const { buildPromptProfileCandidates } = require("../solo/commands");
|
|
21
22
|
const inputMath = require("./inputMath");
|
|
22
23
|
const { createStreamTracker } = require("./streamTracker");
|
|
23
24
|
const agentDirectory = require("./agentDirectory");
|
|
@@ -559,6 +560,10 @@ async function runChat(projectRoot, options = {}) {
|
|
|
559
560
|
source: item.source || "",
|
|
560
561
|
}));
|
|
561
562
|
},
|
|
563
|
+
getSoloProfileCandidates: () => {
|
|
564
|
+
const registry = loadPromptProfileRegistry(activeProjectRoot);
|
|
565
|
+
return buildPromptProfileCandidates(registry);
|
|
566
|
+
},
|
|
562
567
|
getMentionCandidates: () => activeAgents.map((id) => ({
|
|
563
568
|
id,
|
|
564
569
|
label: getAgentLabel(id),
|
package/src/cli.js
CHANGED
|
@@ -7,6 +7,9 @@ const { runBusCoreCommand } = require("./cli/busCoreCommands");
|
|
|
7
7
|
const { runCtxCommand } = require("./cli/ctxCoreCommands");
|
|
8
8
|
const { runOnlineCommand } = require("./cli/onlineCoreCommands");
|
|
9
9
|
const { runGroupCoreCommand } = require("./cli/groupCoreCommands");
|
|
10
|
+
const { loadConfig } = require("./config");
|
|
11
|
+
const { loadPromptProfileRegistry } = require("./group/promptProfiles");
|
|
12
|
+
const { resolveSoloAgentType } = require("./solo/commands");
|
|
10
13
|
const { listProjectRuntimes, getCurrentProjectRuntime } = require("./projects/registry");
|
|
11
14
|
const { canonicalProjectRoot, buildProjectId } = require("./projects/projectId");
|
|
12
15
|
const { getUfooPaths } = require("./ufoo/paths");
|
|
@@ -448,6 +451,68 @@ async function runCli(argv) {
|
|
|
448
451
|
process.exitCode = 1;
|
|
449
452
|
}
|
|
450
453
|
});
|
|
454
|
+
program
|
|
455
|
+
.command("solo")
|
|
456
|
+
.description("Solo role agent operations")
|
|
457
|
+
.argument("<action>", "run|list")
|
|
458
|
+
.argument("[profile]", "Prompt profile id or alias")
|
|
459
|
+
.option("--agent <type>", "Agent type: codex|claude|ucode")
|
|
460
|
+
.option("--nickname <name>", "Optional nickname")
|
|
461
|
+
.option("--scope <scope>", "Launch scope: inplace|window", "inplace")
|
|
462
|
+
.option("--json", "Output role list as JSON")
|
|
463
|
+
.action(async (action, profile, opts) => {
|
|
464
|
+
try {
|
|
465
|
+
const projectRoot = process.cwd();
|
|
466
|
+
const subcommand = String(action || "").trim().toLowerCase();
|
|
467
|
+
if (subcommand === "list" || subcommand === "ls") {
|
|
468
|
+
const registry = loadPromptProfileRegistry(projectRoot);
|
|
469
|
+
if (opts.json) {
|
|
470
|
+
console.log(JSON.stringify({ profiles: registry.profiles || [] }, null, 2));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const profiles = registry.profiles || [];
|
|
474
|
+
if (profiles.length === 0) {
|
|
475
|
+
console.log("No solo roles found.");
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
console.log(`Available solo roles (${profiles.length}):`);
|
|
479
|
+
for (const p of profiles) {
|
|
480
|
+
const aliases = p.aliases && p.aliases.length > 0 ? ` (${p.aliases.join(", ")})` : "";
|
|
481
|
+
const source = p.source ? ` [${p.source}]` : "";
|
|
482
|
+
console.log(` ${p.id}${aliases}${source}`);
|
|
483
|
+
if (p.summary) console.log(` ${p.summary}`);
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (subcommand !== "run") {
|
|
488
|
+
throw new Error(`Unknown solo action: ${subcommand}`);
|
|
489
|
+
}
|
|
490
|
+
const promptProfile = String(profile || "").trim();
|
|
491
|
+
if (!promptProfile) {
|
|
492
|
+
throw new Error("solo run requires <profile>");
|
|
493
|
+
}
|
|
494
|
+
await ensureDaemonRunning(projectRoot);
|
|
495
|
+
const config = loadConfig(projectRoot);
|
|
496
|
+
const agent = resolveSoloAgentType(config, opts.agent || "");
|
|
497
|
+
const scope = String(opts.scope || "inplace").trim().toLowerCase();
|
|
498
|
+
if (scope !== "inplace" && scope !== "window") {
|
|
499
|
+
throw new Error("scope must be inplace|window");
|
|
500
|
+
}
|
|
501
|
+
const resp = await sendDaemonRequest(projectRoot, {
|
|
502
|
+
type: "launch_agent",
|
|
503
|
+
agent: agent === "ucode" ? "ufoo" : agent,
|
|
504
|
+
nickname: String(opts.nickname || "").trim(),
|
|
505
|
+
prompt_profile: promptProfile,
|
|
506
|
+
count: 1,
|
|
507
|
+
launch_scope: scope,
|
|
508
|
+
...collectHostLaunchRequestContext(),
|
|
509
|
+
});
|
|
510
|
+
console.log(resp?.data?.reply || `Launched ${agent} role ${promptProfile}`);
|
|
511
|
+
} catch (err) {
|
|
512
|
+
console.error(err.message || String(err));
|
|
513
|
+
process.exitCode = 1;
|
|
514
|
+
}
|
|
515
|
+
});
|
|
451
516
|
program
|
|
452
517
|
.command("role")
|
|
453
518
|
.description("Assign a preset role to an existing agent")
|
|
@@ -1312,6 +1377,8 @@ async function runCli(argv) {
|
|
|
1312
1377
|
console.log(" ufoo group status [groupId] [--json]");
|
|
1313
1378
|
console.log(" ufoo group diagram <alias|groupId> [--ascii|--mermaid] [--json]");
|
|
1314
1379
|
console.log(" ufoo group stop <groupId> [--json]");
|
|
1380
|
+
console.log(" ufoo solo list [--json]");
|
|
1381
|
+
console.log(" ufoo solo run <profile> [--agent <codex|claude|ucode>] [--nickname <name>] [--scope <inplace|window>]");
|
|
1315
1382
|
console.log(" ufoo online server [--port 8787] [--host 127.0.0.1] [--token-file <path>]");
|
|
1316
1383
|
console.log(" ufoo online token <subscriber> [--nickname <name>] [--server <url>] [--file <path>]");
|
|
1317
1384
|
console.log(" ufoo online room create [--name <room>] --type public|private [--password <pwd>] [--created-by <name>] [--server <url>]");
|
|
@@ -1795,6 +1862,67 @@ async function runCli(argv) {
|
|
|
1795
1862
|
})();
|
|
1796
1863
|
return;
|
|
1797
1864
|
}
|
|
1865
|
+
if (cmd === "solo") {
|
|
1866
|
+
const sub = String(rest[0] || "").trim().toLowerCase();
|
|
1867
|
+
(async () => {
|
|
1868
|
+
try {
|
|
1869
|
+
const projectRoot = process.cwd();
|
|
1870
|
+
if (sub === "list" || sub === "ls") {
|
|
1871
|
+
const outputJson = rest.includes("--json");
|
|
1872
|
+
const registry = loadPromptProfileRegistry(projectRoot);
|
|
1873
|
+
if (outputJson) {
|
|
1874
|
+
console.log(JSON.stringify({ profiles: registry.profiles || [] }, null, 2));
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
const profiles = registry.profiles || [];
|
|
1878
|
+
if (profiles.length === 0) {
|
|
1879
|
+
console.log("No solo roles found.");
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
console.log(`Available solo roles (${profiles.length}):`);
|
|
1883
|
+
for (const p of profiles) {
|
|
1884
|
+
const aliases = p.aliases && p.aliases.length > 0 ? ` (${p.aliases.join(", ")})` : "";
|
|
1885
|
+
const source = p.source ? ` [${p.source}]` : "";
|
|
1886
|
+
console.log(` ${p.id}${aliases}${source}`);
|
|
1887
|
+
if (p.summary) console.log(` ${p.summary}`);
|
|
1888
|
+
}
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
if (sub === "run") {
|
|
1892
|
+
const profile = String(rest[1] || "").trim();
|
|
1893
|
+
if (!profile) throw new Error("solo run requires <profile>");
|
|
1894
|
+
const agentIdx = rest.indexOf("--agent");
|
|
1895
|
+
const agentInput = agentIdx !== -1 ? String(rest[agentIdx + 1] || "").trim() : "";
|
|
1896
|
+
const nickIdx = rest.indexOf("--nickname");
|
|
1897
|
+
const nickname = nickIdx !== -1 ? String(rest[nickIdx + 1] || "").trim() : "";
|
|
1898
|
+
const scopeIdx = rest.indexOf("--scope");
|
|
1899
|
+
const scope = scopeIdx !== -1 ? String(rest[scopeIdx + 1] || "").trim().toLowerCase() : "inplace";
|
|
1900
|
+
if (scope !== "inplace" && scope !== "window") {
|
|
1901
|
+
throw new Error("scope must be inplace|window");
|
|
1902
|
+
}
|
|
1903
|
+
await ensureDaemonRunning(projectRoot);
|
|
1904
|
+
const config = loadConfig(projectRoot);
|
|
1905
|
+
const agent = resolveSoloAgentType(config, agentInput);
|
|
1906
|
+
const resp = await sendDaemonRequest(projectRoot, {
|
|
1907
|
+
type: "launch_agent",
|
|
1908
|
+
agent: agent === "ucode" ? "ufoo" : agent,
|
|
1909
|
+
nickname,
|
|
1910
|
+
prompt_profile: profile,
|
|
1911
|
+
count: 1,
|
|
1912
|
+
launch_scope: scope,
|
|
1913
|
+
...collectHostLaunchRequestContext(),
|
|
1914
|
+
});
|
|
1915
|
+
console.log(resp?.data?.reply || `Launched ${agent} role ${profile}`);
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
throw new Error(`Unknown solo action: ${sub || "(empty)"}`);
|
|
1919
|
+
} catch (err) {
|
|
1920
|
+
console.error(err.message || String(err));
|
|
1921
|
+
process.exitCode = 1;
|
|
1922
|
+
}
|
|
1923
|
+
})();
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1798
1926
|
if (cmd === "online") {
|
|
1799
1927
|
const sub = rest[0] || "";
|
|
1800
1928
|
if (!sub) {
|
|
@@ -753,6 +753,7 @@ function createGroupOrchestrator(options = {}) {
|
|
|
753
753
|
}
|
|
754
754
|
|
|
755
755
|
const plan = buildLaunchPlan(validated.entry.data);
|
|
756
|
+
const templateDefaults = (validated.entry.data && validated.entry.data.defaults) || {};
|
|
756
757
|
const groupId = generateGroupId(validated.entry.alias || alias, instance);
|
|
757
758
|
const projectNicknamePrefix = buildProjectNicknamePrefix(projectRoot);
|
|
758
759
|
|
|
@@ -835,6 +836,12 @@ function createGroupOrchestrator(options = {}) {
|
|
|
835
836
|
const item = compiled.executionPlan[i];
|
|
836
837
|
const member = runtime.members[i];
|
|
837
838
|
const extraEnv = {};
|
|
839
|
+
if (item.bootstrap_required) {
|
|
840
|
+
extraEnv.UFOO_SKIP_DEFAULT_BOOTSTRAP = "1";
|
|
841
|
+
}
|
|
842
|
+
if (Number.isInteger(templateDefaults.start_timeout_ms) && templateDefaults.start_timeout_ms > 0) {
|
|
843
|
+
extraEnv.UFOO_REGISTER_TIMEOUT_MS = String(templateDefaults.start_timeout_ms);
|
|
844
|
+
}
|
|
838
845
|
let extraArgs = [];
|
|
839
846
|
let bootstrapInjected = false;
|
|
840
847
|
|
package/src/daemon/index.js
CHANGED
|
@@ -201,16 +201,16 @@ function looksLikeRunningDaemon(projectRoot, pid) {
|
|
|
201
201
|
function isRunning(projectRoot) {
|
|
202
202
|
const pid = readPid(projectRoot);
|
|
203
203
|
if (!pid) return false;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
return looksLikeRunningDaemon(projectRoot, pid);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function cleanupStaleState(projectRoot) {
|
|
207
208
|
try {
|
|
208
209
|
fs.unlinkSync(pidPath(projectRoot));
|
|
209
210
|
} catch {
|
|
210
211
|
// ignore
|
|
211
212
|
}
|
|
212
213
|
removeSocket(projectRoot);
|
|
213
|
-
return false;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
function removeSocket(projectRoot) {
|
|
@@ -1051,6 +1051,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1051
1051
|
await init.init({ modules: "context,bus", project: root });
|
|
1052
1052
|
}
|
|
1053
1053
|
if (!isRunning(root)) {
|
|
1054
|
+
cleanupStaleState(root);
|
|
1054
1055
|
const daemonBin = path.join(__dirname, "..", "..", "bin", "ufoo.js");
|
|
1055
1056
|
const child = spawn(process.execPath, [daemonBin, "daemon", "--start"], {
|
|
1056
1057
|
detached: true,
|
|
@@ -1384,6 +1385,12 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1384
1385
|
}
|
|
1385
1386
|
}
|
|
1386
1387
|
}
|
|
1388
|
+
if (requestedProfile) {
|
|
1389
|
+
op.extra_env = {
|
|
1390
|
+
...(op.extra_env && typeof op.extra_env === "object" ? op.extra_env : {}),
|
|
1391
|
+
UFOO_SKIP_DEFAULT_BOOTSTRAP: "1",
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1387
1394
|
try {
|
|
1388
1395
|
const opsResults = await handleOps(projectRoot, [op], processManager);
|
|
1389
1396
|
const launchResult = opsResults.find((r) => r.action === "launch");
|
|
@@ -2323,4 +2330,4 @@ function stopDaemon(projectRoot) {
|
|
|
2323
2330
|
return killed;
|
|
2324
2331
|
}
|
|
2325
2332
|
|
|
2326
|
-
module.exports = { startDaemon, stopDaemon, isRunning, socketPath };
|
|
2333
|
+
module.exports = { startDaemon, stopDaemon, isRunning, cleanupStaleState, socketPath };
|
package/src/group/bootstrap.js
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require("crypto");
|
|
4
4
|
|
|
5
|
+
const SHARED_UFOO_PROTOCOL = [
|
|
6
|
+
"ufoo protocol:",
|
|
7
|
+
"- At session start, sync shared context with `ufoo ctx decisions -l` and `ufoo ctx decisions -n 1`.",
|
|
8
|
+
"- If your work produces knowledge with information value, record it before acting via `ufoo ctx decisions new \"Title\"`.",
|
|
9
|
+
"- Use `ufoo bus send <target-nickname> \"<message>\"` for agent-to-agent handoffs.",
|
|
10
|
+
"- If you receive pending bus work, execute it immediately, reply to the sender, then `ufoo bus ack \"$UFOO_SUBSCRIBER_ID\"`.",
|
|
11
|
+
"- Use `ufoo report` for controller/runtime status updates, not as a substitute for direct handoffs.",
|
|
12
|
+
].join("\n");
|
|
13
|
+
|
|
5
14
|
const SHARED_GROUP_PREFIX = [
|
|
6
15
|
"You are part of a ufoo multi-agent group.",
|
|
7
16
|
"",
|
|
@@ -13,6 +22,8 @@ const SHARED_GROUP_PREFIX = [
|
|
|
13
22
|
"- When reporting, separate facts, inferences, and recommendations.",
|
|
14
23
|
"- Preserve continuity with the group's current task rather than restarting analysis from scratch.",
|
|
15
24
|
"",
|
|
25
|
+
SHARED_UFOO_PROTOCOL,
|
|
26
|
+
"",
|
|
16
27
|
"Coordination protocol:",
|
|
17
28
|
"- Use direct handoff for worker-to-worker delivery.",
|
|
18
29
|
"- Use private `ufoo report` updates for ufoo-agent control-plane reporting.",
|
|
@@ -28,6 +39,8 @@ const SOLO_AGENT_PREFIX = [
|
|
|
28
39
|
"- Surface uncertainty explicitly.",
|
|
29
40
|
"- Preserve continuity with the current task instead of restarting from scratch.",
|
|
30
41
|
"- Use ufoo-agent for control-plane coordination, not as a substitute for doing your role.",
|
|
42
|
+
"",
|
|
43
|
+
SHARED_UFOO_PROTOCOL,
|
|
31
44
|
].join("\n");
|
|
32
45
|
|
|
33
46
|
function asTrimmedString(value) {
|
|
@@ -145,6 +158,7 @@ function computeBootstrapFingerprint({
|
|
|
145
158
|
}
|
|
146
159
|
|
|
147
160
|
module.exports = {
|
|
161
|
+
SHARED_UFOO_PROTOCOL,
|
|
148
162
|
SHARED_GROUP_PREFIX,
|
|
149
163
|
SOLO_AGENT_PREFIX,
|
|
150
164
|
buildGroupPromptMetadata,
|
|
@@ -352,6 +352,38 @@ const BUILTIN_PROFILES = [
|
|
|
352
352
|
"- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
|
|
353
353
|
].join("\n"),
|
|
354
354
|
},
|
|
355
|
+
{
|
|
356
|
+
id: "pmo-coordinator",
|
|
357
|
+
display_name: "PMO",
|
|
358
|
+
short_name: "PMO",
|
|
359
|
+
aliases: ["pmo"],
|
|
360
|
+
summary: "Coordinate execution across builders, track progress, unblock dependencies, and enforce delivery cadence.",
|
|
361
|
+
prompt: [
|
|
362
|
+
"You are the PMO coordinator for this ufoo group.",
|
|
363
|
+
"",
|
|
364
|
+
"Mission:",
|
|
365
|
+
"- Coordinate execution across multiple builders to maximize throughput and minimize idle time.",
|
|
366
|
+
"- Track progress, surface blockers early, enforce delivery cadence, and keep the team aligned on priorities.",
|
|
367
|
+
"",
|
|
368
|
+
"Boundaries:",
|
|
369
|
+
"- Do not make architectural or scope decisions — escalate to architect or scope challenger.",
|
|
370
|
+
"- Do not write production code.",
|
|
371
|
+
"- Do not reorder priorities without naming the tradeoff and notifying affected agents.",
|
|
372
|
+
"",
|
|
373
|
+
"Method:",
|
|
374
|
+
"- Assign slices to builders based on dependency order and current load.",
|
|
375
|
+
"- Monitor builder progress and proactively unblock stalled work.",
|
|
376
|
+
"- Maintain a clear view of what is done, in-flight, and blocked at all times.",
|
|
377
|
+
"- Enforce review gates — no slice ships without reviewer sign-off.",
|
|
378
|
+
"- Batch related changes when possible to reduce review churn.",
|
|
379
|
+
"",
|
|
380
|
+
"Handoff:",
|
|
381
|
+
"- Send execution-ready slices to builders with clear acceptance criteria.",
|
|
382
|
+
"- Send completed work to reviewer with context on what changed and why.",
|
|
383
|
+
"- Escalate blockers to architect or the human operator.",
|
|
384
|
+
"- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
|
|
385
|
+
].join("\n"),
|
|
386
|
+
},
|
|
355
387
|
{
|
|
356
388
|
id: "rapid-prototype",
|
|
357
389
|
display_name: "Prototype",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function asTrimmedString(value) {
|
|
4
|
+
return typeof value === "string" ? value.trim() : "";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function resolveSoloAgentType(config = {}, requestedAgent = "") {
|
|
8
|
+
const requested = asTrimmedString(requestedAgent).toLowerCase();
|
|
9
|
+
if (requested === "claude" || requested === "uclaude" || requested === "claude-code") return "claude";
|
|
10
|
+
if (requested === "codex" || requested === "ucodex" || requested === "openai") return "codex";
|
|
11
|
+
if (requested === "ucode" || requested === "ufoo" || requested === "ufoo-code") return "ucode";
|
|
12
|
+
|
|
13
|
+
const provider = asTrimmedString(config && config.agentProvider).toLowerCase();
|
|
14
|
+
if (provider === "claude-cli") return "claude";
|
|
15
|
+
if (provider === "ucode") return "ucode";
|
|
16
|
+
return "codex";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildPromptProfileCandidates(registry = null) {
|
|
20
|
+
const profiles = Array.isArray(registry && registry.profiles) ? registry.profiles : [];
|
|
21
|
+
return profiles.map((item) => ({
|
|
22
|
+
cmd: item.id,
|
|
23
|
+
desc: [item.summary || "", item.source || ""].filter(Boolean).join(" · "),
|
|
24
|
+
source: item.source || "",
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
resolveSoloAgentType,
|
|
30
|
+
buildPromptProfileCandidates,
|
|
31
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"template": {
|
|
4
|
+
"id": "build-ultra",
|
|
5
|
+
"alias": "build-ultra",
|
|
6
|
+
"name": "Build Ultra"
|
|
7
|
+
},
|
|
8
|
+
"defaults": {
|
|
9
|
+
"launch_mode": "auto",
|
|
10
|
+
"start_timeout_ms": 30000
|
|
11
|
+
},
|
|
12
|
+
"agents": [
|
|
13
|
+
{
|
|
14
|
+
"id": "pmo",
|
|
15
|
+
"nickname": "pmo",
|
|
16
|
+
"type": "auto",
|
|
17
|
+
"role": "coordinate builders, track progress, enforce delivery cadence",
|
|
18
|
+
"prompt_profile": "pmo-coordinator",
|
|
19
|
+
"accept_from": [
|
|
20
|
+
"builder-1",
|
|
21
|
+
"builder-2",
|
|
22
|
+
"builder-3",
|
|
23
|
+
"builder-4",
|
|
24
|
+
"reviewer"
|
|
25
|
+
],
|
|
26
|
+
"report_to": [
|
|
27
|
+
"builder-1",
|
|
28
|
+
"builder-2",
|
|
29
|
+
"builder-3",
|
|
30
|
+
"builder-4",
|
|
31
|
+
"reviewer"
|
|
32
|
+
],
|
|
33
|
+
"startup_order": 1,
|
|
34
|
+
"depends_on": []
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "builder-1",
|
|
38
|
+
"nickname": "builder-1",
|
|
39
|
+
"type": "auto",
|
|
40
|
+
"role": "implement approved slices",
|
|
41
|
+
"prompt_profile": "implementation-lead",
|
|
42
|
+
"accept_from": [
|
|
43
|
+
"pmo",
|
|
44
|
+
"reviewer"
|
|
45
|
+
],
|
|
46
|
+
"report_to": [
|
|
47
|
+
"pmo",
|
|
48
|
+
"reviewer"
|
|
49
|
+
],
|
|
50
|
+
"startup_order": 2,
|
|
51
|
+
"depends_on": [
|
|
52
|
+
"pmo"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "builder-2",
|
|
57
|
+
"nickname": "builder-2",
|
|
58
|
+
"type": "auto",
|
|
59
|
+
"role": "implement approved slices",
|
|
60
|
+
"prompt_profile": "implementation-lead",
|
|
61
|
+
"accept_from": [
|
|
62
|
+
"pmo",
|
|
63
|
+
"reviewer"
|
|
64
|
+
],
|
|
65
|
+
"report_to": [
|
|
66
|
+
"pmo",
|
|
67
|
+
"reviewer"
|
|
68
|
+
],
|
|
69
|
+
"startup_order": 2,
|
|
70
|
+
"depends_on": [
|
|
71
|
+
"pmo"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "builder-3",
|
|
76
|
+
"nickname": "builder-3",
|
|
77
|
+
"type": "auto",
|
|
78
|
+
"role": "implement approved slices",
|
|
79
|
+
"prompt_profile": "implementation-lead",
|
|
80
|
+
"accept_from": [
|
|
81
|
+
"pmo",
|
|
82
|
+
"reviewer"
|
|
83
|
+
],
|
|
84
|
+
"report_to": [
|
|
85
|
+
"pmo",
|
|
86
|
+
"reviewer"
|
|
87
|
+
],
|
|
88
|
+
"startup_order": 2,
|
|
89
|
+
"depends_on": [
|
|
90
|
+
"pmo"
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "builder-4",
|
|
95
|
+
"nickname": "builder-4",
|
|
96
|
+
"type": "auto",
|
|
97
|
+
"role": "implement approved slices",
|
|
98
|
+
"prompt_profile": "implementation-lead",
|
|
99
|
+
"accept_from": [
|
|
100
|
+
"pmo",
|
|
101
|
+
"reviewer"
|
|
102
|
+
],
|
|
103
|
+
"report_to": [
|
|
104
|
+
"pmo",
|
|
105
|
+
"reviewer"
|
|
106
|
+
],
|
|
107
|
+
"startup_order": 2,
|
|
108
|
+
"depends_on": [
|
|
109
|
+
"pmo"
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"id": "reviewer",
|
|
114
|
+
"nickname": "reviewer",
|
|
115
|
+
"type": "auto",
|
|
116
|
+
"role": "review correctness and risk",
|
|
117
|
+
"prompt_profile": "review-critic",
|
|
118
|
+
"accept_from": [
|
|
119
|
+
"pmo",
|
|
120
|
+
"builder-1",
|
|
121
|
+
"builder-2",
|
|
122
|
+
"builder-3",
|
|
123
|
+
"builder-4"
|
|
124
|
+
],
|
|
125
|
+
"report_to": [
|
|
126
|
+
"pmo",
|
|
127
|
+
"builder-1",
|
|
128
|
+
"builder-2",
|
|
129
|
+
"builder-3",
|
|
130
|
+
"builder-4"
|
|
131
|
+
],
|
|
132
|
+
"startup_order": 3,
|
|
133
|
+
"depends_on": [
|
|
134
|
+
"pmo"
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
"edges": [
|
|
139
|
+
{
|
|
140
|
+
"from": "pmo",
|
|
141
|
+
"to": "builder-1",
|
|
142
|
+
"kind": "task"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"from": "pmo",
|
|
146
|
+
"to": "builder-2",
|
|
147
|
+
"kind": "task"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"from": "pmo",
|
|
151
|
+
"to": "builder-3",
|
|
152
|
+
"kind": "task"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"from": "pmo",
|
|
156
|
+
"to": "builder-4",
|
|
157
|
+
"kind": "task"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"from": "builder-1",
|
|
161
|
+
"to": "reviewer",
|
|
162
|
+
"kind": "review"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"from": "builder-2",
|
|
166
|
+
"to": "reviewer",
|
|
167
|
+
"kind": "review"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"from": "builder-3",
|
|
171
|
+
"to": "reviewer",
|
|
172
|
+
"kind": "review"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"from": "builder-4",
|
|
176
|
+
"to": "reviewer",
|
|
177
|
+
"kind": "review"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"from": "reviewer",
|
|
181
|
+
"to": "builder-1",
|
|
182
|
+
"kind": "task"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"from": "reviewer",
|
|
186
|
+
"to": "builder-2",
|
|
187
|
+
"kind": "task"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"from": "reviewer",
|
|
191
|
+
"to": "builder-3",
|
|
192
|
+
"kind": "task"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"from": "reviewer",
|
|
196
|
+
"to": "builder-4",
|
|
197
|
+
"kind": "task"
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"from": "reviewer",
|
|
201
|
+
"to": "pmo",
|
|
202
|
+
"kind": "report"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"from": "builder-1",
|
|
206
|
+
"to": "pmo",
|
|
207
|
+
"kind": "report"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"from": "builder-2",
|
|
211
|
+
"to": "pmo",
|
|
212
|
+
"kind": "report"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"from": "builder-3",
|
|
216
|
+
"to": "pmo",
|
|
217
|
+
"kind": "report"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"from": "builder-4",
|
|
221
|
+
"to": "pmo",
|
|
222
|
+
"kind": "report"
|
|
223
|
+
}
|
|
224
|
+
]
|
|
225
|
+
}
|