u-foo 1.8.9 → 1.9.1

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/bin/ufoo.js CHANGED
@@ -5,7 +5,7 @@ const { runDaemonCli } = require("../src/daemon/run");
5
5
  const { runChat } = require("../src/chat");
6
6
  const { runInternalRunner } = require("../src/agent/internalRunner");
7
7
  const { runPtyRunner } = require("../src/agent/ptyRunner");
8
- const { resolveGlobalControllerProjectRoot } = require("../src/globalMode");
8
+ const { resolveGlobalControllerProjectRoot } = require("../src/projects");
9
9
 
10
10
  const rawArgv = process.argv.slice(2);
11
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "1.8.9",
3
+ "version": "1.9.1",
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,144 @@
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
+ /**
30
+ * Load the team activity timeline for prompt injection.
31
+ * The daemon syncs manual inputs every ~30s; bus messages are appended in real-time.
32
+ * Agent startup only reads — no build triggered here.
33
+ */
34
+ function loadTeamActivityContext(projectRoot) {
35
+ try {
36
+ const { renderTimelineForPrompt } = require("../history/inputTimeline");
37
+ return renderTimelineForPrompt(projectRoot, 20) || "";
38
+ } catch {
39
+ return "";
40
+ }
41
+ }
42
+
43
+ function buildDefaultStartupBootstrapPrompt({ agentType = "", projectRoot = "" } = {}) {
44
+ const normalizedAgent = asTrimmedString(agentType).toLowerCase();
45
+ const displayAgent = normalizedAgent === "claude-code"
46
+ ? "Claude"
47
+ : (normalizedAgent === "codex" ? "Codex" : "agent");
48
+
49
+ const segments = [
50
+ `Session bootstrap for ${displayAgent}.`,
51
+ "Adopt the following ufoo coordination protocol silently.",
52
+ "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.",
53
+ SHARED_UFOO_PROTOCOL,
54
+ ];
55
+
56
+ const root = asTrimmedString(projectRoot) || process.cwd();
57
+ const teamActivity = loadTeamActivityContext(root);
58
+ if (teamActivity) {
59
+ segments.push(teamActivity);
60
+ }
61
+
62
+ return segments.join("\n\n");
63
+ }
64
+
65
+ function defaultBootstrapFile(projectRoot, agentType = "") {
66
+ const safeAgentType = asTrimmedString(agentType).replace(/[^a-zA-Z0-9._-]/g, "-") || "agent";
67
+ return path.join(getUfooPaths(projectRoot).agentDir, safeAgentType, "default-bootstrap.md");
68
+ }
69
+
70
+ function prepareDefaultBootstrapFile({
71
+ projectRoot,
72
+ agentType = "",
73
+ promptText = "",
74
+ targetFile = "",
75
+ } = {}) {
76
+ const root = asTrimmedString(projectRoot) || process.cwd();
77
+ const file = asTrimmedString(targetFile) || defaultBootstrapFile(root, agentType);
78
+ fs.mkdirSync(path.dirname(file), { recursive: true });
79
+ fs.writeFileSync(file, String(promptText || ""), "utf8");
80
+ return { ok: true, file };
81
+ }
82
+
83
+ function resolveDefaultManualBootstrap({
84
+ projectRoot,
85
+ agentType = "",
86
+ args = [],
87
+ env = process.env,
88
+ } = {}) {
89
+ const normalizedAgent = asTrimmedString(agentType).toLowerCase();
90
+ const currentEnv = env && typeof env === "object" ? env : {};
91
+ const currentArgs = Array.isArray(args) ? args.slice() : [];
92
+ if (
93
+ currentEnv.UFOO_SKIP_DEFAULT_BOOTSTRAP === "1"
94
+ || currentEnv.UFOO_STARTUP_BOOTSTRAP_TEXT
95
+ || hasMetaCommandArgs(currentArgs)
96
+ ) {
97
+ return { args: currentArgs, env: {}, mode: "skip" };
98
+ }
99
+
100
+ if (normalizedAgent === "claude-code") {
101
+ if (hasArg(currentArgs, ["--append-system-prompt", "--system-prompt"])) {
102
+ return { args: currentArgs, env: {}, mode: "skip" };
103
+ }
104
+ const promptText = buildDefaultStartupBootstrapPrompt({ agentType: normalizedAgent, projectRoot });
105
+ const prepared = prepareDefaultBootstrapFile({
106
+ projectRoot,
107
+ agentType: normalizedAgent,
108
+ promptText,
109
+ });
110
+ return {
111
+ args: [...currentArgs, "--append-system-prompt", prepared.file],
112
+ env: {},
113
+ mode: "system-prompt-file",
114
+ file: prepared.file,
115
+ promptText,
116
+ };
117
+ }
118
+
119
+ if (normalizedAgent === "codex") {
120
+ if (currentArgs.length > 0) {
121
+ return { args: currentArgs, env: {}, mode: "skip" };
122
+ }
123
+ const promptText = buildDefaultStartupBootstrapPrompt({ agentType: normalizedAgent, projectRoot });
124
+ return {
125
+ args: currentArgs,
126
+ env: {
127
+ UFOO_STARTUP_BOOTSTRAP_TEXT: promptText,
128
+ },
129
+ mode: "post-launch-inject",
130
+ promptText,
131
+ };
132
+ }
133
+
134
+ return { args: currentArgs, env: {}, mode: "skip" };
135
+ }
136
+
137
+ module.exports = {
138
+ hasArg,
139
+ hasMetaCommandArgs,
140
+ buildDefaultStartupBootstrapPrompt,
141
+ defaultBootstrapFile,
142
+ prepareDefaultBootstrapFile,
143
+ resolveDefaultManualBootstrap,
144
+ };
@@ -224,6 +224,42 @@ function computeInjectedSubmitDelayMs(agentType, text) {
224
224
  return delayMs;
225
225
  }
226
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
+
227
263
  async function resolveHostRegistrationData(launchMode) {
228
264
  if (launchMode !== "host") {
229
265
  return {
@@ -451,6 +487,7 @@ class AgentLauncher {
451
487
  return new Promise((resolve, reject) => {
452
488
  let buffer = "";
453
489
  let settled = false;
490
+ const registerTimeoutMs = parseInt(process.env.UFOO_REGISTER_TIMEOUT_MS, 10) || 8000;
454
491
  const timeout = setTimeout(() => {
455
492
  if (settled) return;
456
493
  settled = true;
@@ -460,7 +497,7 @@ class AgentLauncher {
460
497
  // ignore
461
498
  }
462
499
  reject(new Error("register_agent timeout"));
463
- }, 8000);
500
+ }, registerTimeoutMs);
464
501
 
465
502
  const cleanup = () => {
466
503
  clearTimeout(timeout);
@@ -607,6 +644,7 @@ class AgentLauncher {
607
644
  }
608
645
 
609
646
  // 7. 启动命令(PTY wrapper或直接spawn)
647
+ const startupBootstrapText = String(process.env.UFOO_STARTUP_BOOTSTRAP_TEXT || "").trim();
610
648
 
611
649
  // 7.1 PTY启用条件(显式开关 + 自动检测)
612
650
  let shouldUsePty = false;
@@ -704,6 +742,10 @@ class AgentLauncher {
704
742
  }
705
743
  }
706
744
 
745
+ if (startupBootstrapText && wrapper.pty) {
746
+ await injectPtyCommand(wrapper, this.agentType, startupBootstrapText, "startup-bootstrap");
747
+ }
748
+
707
749
  await notifyDaemonAgentReady(daemonSockPath, subscriberId, wrapper.pty ? wrapper.pty.pid : 0);
708
750
  });
709
751
 
@@ -812,37 +854,8 @@ class AgentLauncher {
812
854
  continue;
813
855
  }
814
856
  // 注入命令到PTY(带延迟确保输入完成)
815
- // Claude Code (Ink TUI) interprets ESC+CR within ~100ms as
816
- // Alt+Enter (newline) instead of two separate keys. Use a
817
- // longer gap so the escape sequence parser times out.
818
- const commandText = String(req.command);
819
- const submitDelayMs = computeInjectedSubmitDelayMs(this.agentType, commandText);
820
- wrapper.write(commandText);
821
- if (normalizedAgentType === "claude-code") {
822
- // Claude Code: send CR directly without ESC.
823
- // ESC before CR is interpreted as Alt+Enter (newline).
824
- setTimeout(() => {
825
- wrapper.write("\r");
826
- }, submitDelayMs);
827
- } else {
828
- // Codex/others: ESC dismisses autocomplete, then CR submits.
829
- setTimeout(() => {
830
- wrapper.write("\x1b");
831
- setTimeout(() => {
832
- wrapper.write("\r");
833
- }, 100);
834
- }, submitDelayMs);
835
- }
857
+ void injectPtyCommand(wrapper, this.agentType, req.command, "inject");
836
858
  client.write(JSON.stringify({ ok: true }) + "\n");
837
- if (wrapper.logger) {
838
- const logEntry = {
839
- ts: Date.now(),
840
- dir: "in",
841
- data: { text: req.command, encoding: "utf8", size: req.command.length },
842
- source: "inject",
843
- };
844
- wrapper.logger.write(JSON.stringify(logEntry) + "\n");
845
- }
846
859
  } else if (req.type === PTY_SOCKET_MESSAGE_TYPES.RAW && req.data) {
847
860
  // Raw PTY write (no Enter appended) - for TTY view passthrough
848
861
  wrapper.write(req.data);
@@ -952,5 +965,6 @@ class AgentLauncher {
952
965
  AgentLauncher._sanitizeNickname = (nick) => nick.replace(/[^a-zA-Z0-9_-]/g, "");
953
966
  AgentLauncher._findPreviousSession = findPreviousSession;
954
967
  AgentLauncher._notifyDaemonAgentReady = notifyDaemonAgentReady;
968
+ AgentLauncher._injectPtyCommand = injectPtyCommand;
955
969
 
956
970
  module.exports = AgentLauncher;
@@ -11,8 +11,7 @@ const {
11
11
  } = require("../code/nativeRunner");
12
12
  const { DEFAULT_ASSISTANT_TIMEOUT_MS } = require("../assistant/constants");
13
13
  const { normalizeAgentTypeAlias } = require("../bus/utils");
14
- const { listProjectRuntimes } = require("../projects/registry");
15
- const { isGlobalControllerProjectRoot } = require("../globalMode");
14
+ const { listProjectRuntimes, isGlobalControllerProjectRoot } = require("../projects");
16
15
 
17
16
  function loadSessionState(projectRoot) {
18
17
  const dir = getUfooPaths(projectRoot).agentDir;
package/src/bus/daemon.js CHANGED
@@ -19,17 +19,21 @@ function isBusyActivityState(value = "") {
19
19
  * Bus Daemon - 监控消息并自动注入命令
20
20
  */
21
21
  class BusDaemon {
22
- constructor(busDir, agentsFile, daemonDir, interval = 2000) {
22
+ constructor(busDir, agentsFile, daemonDir, interval = 2000, projectRoot = "") {
23
23
  this.busDir = busDir;
24
24
  this.agentsFile = agentsFile;
25
25
  this.interval = interval;
26
26
  this.daemonDir = daemonDir;
27
+ this.projectRoot = projectRoot || path.resolve(busDir, "..", "..");
27
28
  this.pidFile = path.join(this.daemonDir, "daemon.pid");
28
29
  this.logFile = path.join(this.daemonDir, "daemon.log");
29
30
  this.countsDir = path.join(this.daemonDir, "counts", `${process.pid}`);
30
31
  this.running = false;
31
32
  this.cleanupCounter = 0;
32
33
  this.cleanupInterval = 5; // 每 5 个周期清理一次
34
+ this.timelineSyncCounter = 0;
35
+ // 每 15 个周期同步一次 manual inputs (~15 × interval, default ~30s)
36
+ this.timelineSyncInterval = 15;
33
37
 
34
38
  this.queueManager = new QueueManager(busDir);
35
39
  this.injector = new Injector(busDir, agentsFile);
@@ -233,6 +237,13 @@ class BusDaemon {
233
237
  this.cleanupCounter = 0;
234
238
  }
235
239
 
240
+ // 定期同步 timeline(manual inputs from session files, ~15 × interval)
241
+ this.timelineSyncCounter++;
242
+ if (this.timelineSyncCounter >= this.timelineSyncInterval) {
243
+ this.syncTimeline();
244
+ this.timelineSyncCounter = 0;
245
+ }
246
+
236
247
  // 检查所有订阅者的队列
237
248
  await this.checkQueues();
238
249
  } catch (err) {
@@ -244,6 +255,20 @@ class BusDaemon {
244
255
  }
245
256
  }
246
257
 
258
+ /**
259
+ * 增量同步 timeline — 捕获 manual inputs(bus 消息已在 send() 时实时追加)
260
+ */
261
+ syncTimeline() {
262
+ try {
263
+ const { buildTimeline } = require("../history/inputTimeline");
264
+ buildTimeline(this.projectRoot);
265
+ } catch (err) {
266
+ if (process.env.UFOO_HISTORY_DEBUG === "1") {
267
+ console.error("[daemon][history] syncTimeline failed:", err.message);
268
+ }
269
+ }
270
+ }
271
+
247
272
  /**
248
273
  * 检查所有队列
249
274
  */
package/src/bus/index.js CHANGED
@@ -328,6 +328,23 @@ class EventBus {
328
328
  `Event sent: event=${eventName} seq=${result.seq} -> ${result.targets.join(", ")}`
329
329
  );
330
330
  }
331
+
332
+ // Real-time timeline append for message events
333
+ if (eventName === "message" && message) {
334
+ try {
335
+ const { appendBusEntry } = require("../history/inputTimeline");
336
+ appendBusEntry(this.projectRoot, {
337
+ seq: result.seq,
338
+ timestamp: new Date().toISOString(),
339
+ publisher,
340
+ target,
341
+ message,
342
+ });
343
+ } catch {
344
+ // non-critical
345
+ }
346
+ }
347
+
331
348
  return result;
332
349
  } catch (err) {
333
350
  logError(err.message);
@@ -364,7 +381,10 @@ class EventBus {
364
381
  console.log();
365
382
 
366
383
  for (const event of pending) {
367
- console.log(` ${colors.yellow}@you${colors.reset} from ${colors.cyan}${event.publisher}${colors.reset}`);
384
+ const publisherMeta = this.busData.agents?.[event.publisher];
385
+ const nick = publisherMeta?.nickname;
386
+ const fromLabel = nick ? `${event.publisher}(${nick})` : event.publisher;
387
+ console.log(` ${colors.yellow}[ufoo]<from:${fromLabel}>${colors.reset}`);
368
388
  console.log(` Type: ${event.type}/${event.event}`);
369
389
  console.log(` Content: ${JSON.stringify(event.data)}`);
370
390
  console.log();
@@ -731,7 +751,7 @@ class EventBus {
731
751
 
732
752
  if (countAfter > countBefore) {
733
753
  await sleep(50);
734
- const daemon = new BusDaemon(this.busDir, this.agentsFile, this.paths.busDaemonDir, 2000);
754
+ const daemon = new BusDaemon(this.busDir, this.agentsFile, this.paths.busDaemonDir, 2000, this.projectRoot);
735
755
  await daemon.injector.inject(target, options.command || "");
736
756
  if (options.shake !== false) {
737
757
  const tty = daemon.injector.readTty(target);
@@ -829,7 +849,7 @@ class EventBus {
829
849
  */
830
850
  async daemon(action, options = {}) {
831
851
  const interval = options.interval || 2000;
832
- const daemon = new BusDaemon(this.busDir, this.agentsFile, this.paths.busDaemonDir, interval);
852
+ const daemon = new BusDaemon(this.busDir, this.agentsFile, this.paths.busDaemonDir, interval, this.projectRoot);
833
853
 
834
854
  switch (action) {
835
855
  case "start":
@@ -6,8 +6,9 @@ const { runGroupCoreCommand } = require("../cli/groupCoreCommands");
6
6
  const { loadConfig: loadProjectConfig, saveConfig: saveProjectConfig, loadGlobalUcodeConfig, saveGlobalUcodeConfig } = require("../config");
7
7
  const { resolveTransport } = require("../code/nativeRunner");
8
8
  const { parseIntervalMs, formatIntervalMs } = require("./cronScheduler");
9
- const { isGlobalControllerProjectRoot, resolveGlobalControllerUfooDir } = require("../globalMode");
9
+ const { isGlobalControllerProjectRoot, resolveGlobalControllerUfooDir } = require("../projects");
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;