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 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 args = process.argv.slice(2);
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
- launcher.launch(args);
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 args = process.argv.slice(2);
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
- launcher.launch(args);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "1.8.8",
3
+ "version": "1.9.0",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
@@ -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
+ };
@@ -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
- }, 8000);
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
- const startTime = Date.now();
669
- try {
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
- // Claude Code (Ink TUI) interprets ESC+CR within ~100ms as
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 = String(args[0] || "").trim();
532
- const profile = String(args[1] || "").trim();
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,
@@ -67,7 +67,15 @@ const COMMAND_TREE = {
67
67
  "/role": {
68
68
  desc: "Assign preset role to an existing agent",
69
69
  children: {
70
- list: { desc: "List available prompt profiles" },
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) => a.localeCompare(b, "en", { sensitivity: "base" }))
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((a, b) => a.cmd.localeCompare(b.cmd));
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((a, b) => a.cmd.localeCompare(b.cmd));
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 (selected.isSubcommand && selected.parentCmd === "/group" && selected.cmd === "run") {
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 (selected.isSubcommand && selected.parentCmd === "/group" && selected.cmd === "run") {
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
- if (restartInProgress) return;
21
- restartInProgress = true;
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
- restartInProgress = false;
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 (!isRunning(target.projectRoot)) {
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
 
@@ -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
- if (looksLikeRunningDaemon(projectRoot, pid)) {
205
- return true;
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 };
@@ -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
+ }