u-foo 2.2.0 → 2.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
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",
package/src/bus/store.js CHANGED
@@ -15,19 +15,6 @@ function readQueueTty(queueDir) {
15
15
  }
16
16
  }
17
17
 
18
- function nicknamePrefixForType(agentType = "") {
19
- return agentType === "claude-code" ? "claude"
20
- : agentType === "ufoo-code" ? "ucode"
21
- : String(agentType || "agent");
22
- }
23
-
24
- function isRecoverableSessionId(sessionId = "") {
25
- const text = String(sessionId || "").trim();
26
- if (!text) return false;
27
- if (text.includes(":") || text.includes("_")) return false;
28
- return true;
29
- }
30
-
31
18
  function buildUsedNicknameSet(agents = {}) {
32
19
  const set = new Set();
33
20
  for (const meta of Object.values(agents || {})) {
@@ -38,15 +25,6 @@ function buildUsedNicknameSet(agents = {}) {
38
25
  return set;
39
26
  }
40
27
 
41
- function allocateRecoveredNickname(agentType, used) {
42
- const prefix = nicknamePrefixForType(agentType);
43
- let idx = 1;
44
- while (used.has(`${prefix}-${idx}`)) idx += 1;
45
- const nick = `${prefix}-${idx}`;
46
- used.add(nick);
47
- return nick;
48
- }
49
-
50
28
  function recoverQueueEntry(data, subscriber, queueDir, usedNicknames, now) {
51
29
  if (!subscriber || data.agents[subscriber]) return false;
52
30
 
@@ -67,31 +45,7 @@ function recoverQueueEntry(data, subscriber, queueDir, usedNicknames, now) {
67
45
  };
68
46
  return true;
69
47
  }
70
-
71
- const parts = subscriber.split(":");
72
- if (parts.length !== 2) return false;
73
- const [agentType, sessionId] = parts;
74
- if (!agentType || !sessionId) return false;
75
- if (!isRecoverableSessionId(sessionId)) return false;
76
-
77
- const tty = readQueueTty(queueDir);
78
- const ttyInfo = tty ? getTtyProcessInfo(tty) : null;
79
- const activeByTty = Boolean(ttyInfo && ttyInfo.alive && ttyInfo.hasAgent);
80
- const nickname = activeByTty ? allocateRecoveredNickname(agentType, usedNicknames) : "";
81
-
82
- data.agents[subscriber] = {
83
- agent_type: agentType,
84
- nickname,
85
- status: activeByTty ? "active" : "inactive",
86
- joined_at: now,
87
- last_seen: now,
88
- pid: 0,
89
- tty,
90
- tty_shell_pid: ttyInfo && ttyInfo.shellPid ? ttyInfo.shellPid : 0,
91
- tmux_pane: "",
92
- launch_mode: "",
93
- };
94
- return true;
48
+ return false;
95
49
  }
96
50
 
97
51
  function reconcileReservedControllerAliases(data, now) {
@@ -98,6 +98,30 @@ class SubscriberManager {
98
98
  this.queueManager = queueManager;
99
99
  }
100
100
 
101
+ cleanupSubscriberArtifacts(subscriber) {
102
+ if (!subscriber || !this.queueManager) return;
103
+ try {
104
+ const queueDir = this.queueManager.getQueueDir
105
+ ? this.queueManager.getQueueDir(subscriber)
106
+ : "";
107
+ if (queueDir) {
108
+ fs.rmSync(queueDir, { recursive: true, force: true });
109
+ }
110
+ } catch {
111
+ // ignore cleanup errors
112
+ }
113
+ try {
114
+ const offsetPath = this.queueManager.getOffsetPath
115
+ ? this.queueManager.getOffsetPath(subscriber)
116
+ : "";
117
+ if (offsetPath) {
118
+ fs.rmSync(offsetPath, { force: true });
119
+ }
120
+ } catch {
121
+ // ignore cleanup errors
122
+ }
123
+ }
124
+
101
125
  async cleanupDuplicateTty(currentSubscriber, ttyPath) {
102
126
  if (!ttyPath) return null;
103
127
  if (!this.busData.agents) return null;
@@ -301,6 +325,7 @@ class SubscriberManager {
301
325
  this.busData.agents[subscriber].status = "inactive";
302
326
  this.busData.agents[subscriber].activity_state = "";
303
327
  this.busData.agents[subscriber].last_seen = getTimestamp();
328
+ this.cleanupSubscriberArtifacts(subscriber);
304
329
 
305
330
  return true;
306
331
  }
@@ -364,6 +389,7 @@ class SubscriberManager {
364
389
  meta.status = "inactive";
365
390
  meta.activity_state = "";
366
391
  meta.last_seen = getTimestamp();
392
+ this.cleanupSubscriberArtifacts(id);
367
393
  }
368
394
  }
369
395
  }
@@ -3,7 +3,13 @@ const EventBus = require("../bus");
3
3
  const { IPC_REQUEST_TYPES } = require("../shared/eventContract");
4
4
  const UfooInit = require("../init");
5
5
  const { runGroupCoreCommand } = require("../cli/groupCoreCommands");
6
- const { loadConfig: loadProjectConfig, saveConfig: saveProjectConfig, loadGlobalUcodeConfig, saveGlobalUcodeConfig } = require("../config");
6
+ const {
7
+ loadConfig: loadProjectConfig,
8
+ saveConfig: saveProjectConfig,
9
+ loadGlobalUcodeConfig,
10
+ saveGlobalUcodeConfig,
11
+ normalizeControllerMode,
12
+ } = require("../config");
7
13
  const { resolveTransport } = require("../code/nativeRunner");
8
14
  const { parseIntervalMs, formatIntervalMs } = require("./cronScheduler");
9
15
  const { isGlobalControllerProjectRoot, resolveGlobalControllerUfooDir } = require("../projects");
@@ -1104,10 +1110,10 @@ function createCommandExecutor(options = {}) {
1104
1110
  kv.group_id ||
1105
1111
  kv.group ||
1106
1112
  kv.alias ||
1107
- (diagramArgs[0] && !String(diagramArgs[0]).includes("=") ? diagramArgs[0] : "")
1113
+ (diagramArgs[0] && !String(diagramArgs[0]).includes("=") ? diagramArgs[0] : "current")
1108
1114
  ).trim();
1109
1115
  if (!target) {
1110
- logMessage("error", "{white-fg}✗{/white-fg} Usage: /group diagram <alias|groupId> [format=ascii|mermaid]");
1116
+ logMessage("error", "{white-fg}✗{/white-fg} Usage: /group diagram [current|alias|groupId] [format=ascii|mermaid]");
1111
1117
  return;
1112
1118
  }
1113
1119
  const format = diagramArgs.includes("--mermaid")
@@ -1133,7 +1139,13 @@ function createCommandExecutor(options = {}) {
1133
1139
  async function handleSettingsCommand(args = []) {
1134
1140
  const section = String(args[0] || "").trim().toLowerCase();
1135
1141
  if (!section) {
1136
- logMessage("error", "{white-fg}✗{/white-fg} Usage: /settings ucode [show|set|clear ...]");
1142
+ logMessage("error", "{white-fg}✗{/white-fg} Usage: /settings <router|ucode> ...");
1143
+ return;
1144
+ }
1145
+
1146
+ if (section === "router") {
1147
+ const subArgs = args.slice(1);
1148
+ await handleRouterSettingsCommand(subArgs);
1137
1149
  return;
1138
1150
  }
1139
1151
 
@@ -1147,7 +1159,88 @@ function createCommandExecutor(options = {}) {
1147
1159
  return;
1148
1160
  }
1149
1161
 
1150
- logMessage("error", "{white-fg}✗{/white-fg} Unknown settings section. Use: ucode");
1162
+ logMessage("error", "{white-fg}✗{/white-fg} Unknown settings section. Use: router, ucode");
1163
+ }
1164
+
1165
+ async function handleRouterSettingsCommand(args = []) {
1166
+ const first = String(args[0] || "").trim().toLowerCase();
1167
+ const hasInlineKv = args.some((item) => String(item || "").includes("="));
1168
+ const action = !first ? "show" : (hasInlineKv ? "set" : first);
1169
+
1170
+ if (action === "show" || action === "status") {
1171
+ const config = loadConfig(projectRoot) || {};
1172
+ const mode = normalizeControllerMode(config.controllerMode);
1173
+ logMessage("system", "{cyan-fg}router config:{/cyan-fg}");
1174
+ logMessage("system", ` • controllerMode: ${mode}`);
1175
+ logMessage("system", ` • provider: ${String(config.routerProvider || "").trim() || "(unset)"}`);
1176
+ logMessage("system", ` • model: ${String(config.routerModel || "").trim() || "(unset)"}`);
1177
+ logMessage("system", " • allowed modes: main | loop | legacy | shadow");
1178
+ return;
1179
+ }
1180
+
1181
+ if (action === "set") {
1182
+ const kvArgs = hasInlineKv ? args : args.slice(1);
1183
+ const kv = parseKeyValueArgs(kvArgs);
1184
+ const updates = {};
1185
+
1186
+ if (Object.prototype.hasOwnProperty.call(kv, "mode")) {
1187
+ const nextMode = normalizeControllerMode(kv.mode);
1188
+ if (!["main", "loop", "legacy", "shadow"].includes(nextMode) || nextMode !== String(kv.mode || "").trim().toLowerCase()) {
1189
+ logMessage("error", "{white-fg}✗{/white-fg} Usage: /settings router set mode=<main|loop|legacy|shadow> provider=<id> model=<id>");
1190
+ return;
1191
+ }
1192
+ updates.controllerMode = nextMode;
1193
+ }
1194
+ if (Object.prototype.hasOwnProperty.call(kv, "provider")) updates.routerProvider = String(kv.provider || "").trim();
1195
+ if (Object.prototype.hasOwnProperty.call(kv, "model")) updates.routerModel = String(kv.model || "").trim();
1196
+
1197
+ if (Object.keys(updates).length === 0) {
1198
+ logMessage("error", "{white-fg}✗{/white-fg} Usage: /settings router set mode=<main|loop|legacy|shadow> provider=<id> model=<id>");
1199
+ return;
1200
+ }
1201
+
1202
+ saveConfig(projectRoot, updates);
1203
+ logMessage("system", "{white-fg}✓{/white-fg} router config updated");
1204
+ if (Object.prototype.hasOwnProperty.call(updates, "controllerMode")) {
1205
+ logMessage("system", ` • controllerMode: ${updates.controllerMode}`);
1206
+ }
1207
+ if (Object.prototype.hasOwnProperty.call(updates, "routerProvider")) {
1208
+ logMessage("system", ` • provider: ${updates.routerProvider || "(unset)"}`);
1209
+ }
1210
+ if (Object.prototype.hasOwnProperty.call(updates, "routerModel")) {
1211
+ logMessage("system", ` • model: ${updates.routerModel || "(unset)"}`);
1212
+ }
1213
+ await restartDaemon(projectRoot);
1214
+ return;
1215
+ }
1216
+
1217
+ if (action === "clear") {
1218
+ const fieldsRaw = args.slice(1).map((item) => String(item || "").trim().toLowerCase()).filter(Boolean);
1219
+ const fields = fieldsRaw.length === 0 ? ["all"] : fieldsRaw;
1220
+ const updates = {};
1221
+ const clearAll = fields.includes("all");
1222
+ if (clearAll || fields.includes("mode")) updates.controllerMode = "main";
1223
+ if (clearAll || fields.includes("provider")) updates.routerProvider = "";
1224
+ if (clearAll || fields.includes("model")) updates.routerModel = "";
1225
+ if (Object.keys(updates).length === 0) {
1226
+ logMessage("error", "{white-fg}✗{/white-fg} Usage: /settings router clear [mode|provider|model|all]");
1227
+ return;
1228
+ }
1229
+ saveConfig(projectRoot, updates);
1230
+ logMessage("system", "{white-fg}✓{/white-fg} router config cleared");
1231
+ await restartDaemon(projectRoot);
1232
+ return;
1233
+ }
1234
+
1235
+ const nextMode = normalizeControllerMode(action);
1236
+ if (nextMode !== action || !["main", "loop", "legacy", "shadow"].includes(nextMode)) {
1237
+ logMessage("error", "{white-fg}✗{/white-fg} Usage: /settings router <main|loop|legacy|shadow>");
1238
+ return;
1239
+ }
1240
+
1241
+ saveConfig(projectRoot, { controllerMode: nextMode });
1242
+ logMessage("system", `{white-fg}✓{/white-fg} router mode set to ${nextMode}`);
1243
+ await restartDaemon(projectRoot);
1151
1244
  }
1152
1245
 
1153
1246
  function parseUcodeConfigKv(args = []) {
@@ -38,10 +38,10 @@ const COMMAND_TREE = {
38
38
  "/group": {
39
39
  desc: "Agent group orchestration",
40
40
  children: {
41
- run: { desc: "Launch a group template" },
41
+ run: { desc: "Launch a group template", order: 1 },
42
+ diagram: { desc: "Render group diagram (ascii|mermaid)", order: 2 },
42
43
  stop: { desc: "Stop a running group" },
43
44
  status: { desc: "Show group runtime status" },
44
- diagram: { desc: "Render group diagram (ascii|mermaid)" },
45
45
  template: { desc: "Template ops (list/show/validate/new)" },
46
46
  templates: { desc: "List available templates" },
47
47
  },
@@ -87,7 +87,26 @@ const COMMAND_TREE = {
87
87
  "/settings": {
88
88
  desc: "Settings operations",
89
89
  children: {
90
- ucode: { desc: "Manage ucode model provider config" },
90
+ router: {
91
+ desc: "Manage controller routing mode/provider/model",
92
+ children: {
93
+ show: { desc: "Show router mode/provider/model", order: 1 },
94
+ set: { desc: "Set router mode/provider/model", order: 2 },
95
+ clear: { desc: "Clear router provider/model or reset mode", order: 3 },
96
+ main: { desc: "Set router mode to main", order: 4 },
97
+ loop: { desc: "Set router mode to loop", order: 5 },
98
+ legacy: { desc: "Set router mode to legacy", order: 6 },
99
+ shadow: { desc: "Set router mode to shadow", order: 7 },
100
+ },
101
+ },
102
+ ucode: {
103
+ desc: "Manage ucode model provider config",
104
+ children: {
105
+ show: { desc: "Show ucode provider/model/url/key", order: 1 },
106
+ set: { desc: "Set ucode provider/model/url/key", order: 2 },
107
+ clear: { desc: "Clear ucode provider/model/url/key", order: 3 },
108
+ },
109
+ },
91
110
  },
92
111
  },
93
112
  "/skills": {
@@ -101,7 +120,7 @@ const COMMAND_TREE = {
101
120
  "/ufoo": { desc: "ufoo protocol (session marker)" },
102
121
  };
103
122
 
104
- const COMMAND_ORDER = ["/launch", "/bus", "/ctx"];
123
+ const COMMAND_ORDER = ["/launch", "/group", "/bus", "/ctx"];
105
124
  const COMMAND_ORDER_MAP = new Map(COMMAND_ORDER.map((cmd, idx) => [cmd, idx]));
106
125
 
107
126
  function sortCommands(a, b) {
@@ -112,30 +131,34 @@ function sortCommands(a, b) {
112
131
  }
113
132
 
114
133
  function buildCommandRegistry(tree) {
134
+ function mapNode(node = {}) {
135
+ const entry = {
136
+ desc: node.desc || "",
137
+ order: Number.isFinite(node.order) ? node.order : undefined,
138
+ };
139
+ if (node.children) {
140
+ entry.subcommands = Object.keys(node.children)
141
+ .sort((a, b) => {
142
+ const aNode = node.children[a] || {};
143
+ const bNode = node.children[b] || {};
144
+ const aOrder = Number.isFinite(aNode.order) ? aNode.order : Number.POSITIVE_INFINITY;
145
+ const bOrder = Number.isFinite(bNode.order) ? bNode.order : Number.POSITIVE_INFINITY;
146
+ if (aOrder !== bOrder) return aOrder - bOrder;
147
+ return a.localeCompare(b, "en", { sensitivity: "base" });
148
+ })
149
+ .map((sub) => ({
150
+ cmd: sub,
151
+ ...mapNode(node.children[sub]),
152
+ }));
153
+ }
154
+ return entry;
155
+ }
156
+
115
157
  return Object.keys(tree)
116
158
  .sort(sortCommands)
117
159
  .map((cmd) => {
118
160
  const node = tree[cmd] || {};
119
- const entry = { cmd, desc: node.desc || "" };
120
- if (node.children) {
121
- entry.subcommands = Object.keys(node.children)
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
- })
130
- .map((sub) => ({
131
- cmd: sub,
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,
136
- }));
137
- }
138
- return entry;
161
+ return { cmd, ...mapNode(node) };
139
162
  });
140
163
  }
141
164
 
@@ -11,6 +11,19 @@ function sortSubcommandEntries(a, b) {
11
11
  return String(a && a.cmd ? a.cmd : "").localeCompare(String(b && b.cmd ? b.cmd : ""), "en", { sensitivity: "base" });
12
12
  }
13
13
 
14
+ function mapSubcommandSuggestions(subs = [], parentCmd, tokenIndex, filterText = "") {
15
+ const normalizedFilter = String(filterText || "").toLowerCase();
16
+ return subs
17
+ .filter((sub) => String(sub && sub.cmd ? sub.cmd : "").toLowerCase().startsWith(normalizedFilter))
18
+ .map((sub) => ({
19
+ ...sub,
20
+ isSubcommand: true,
21
+ parentCmd,
22
+ tokenIndex,
23
+ }))
24
+ .sort(sortSubcommandEntries);
25
+ }
26
+
14
27
  function createCompletionController(options = {}) {
15
28
  const {
16
29
  input,
@@ -217,14 +230,14 @@ function createCompletionController(options = {}) {
217
230
  subs = Array.from(merged.values());
218
231
  }
219
232
  if (isLaunch) {
220
- return subs
221
- .map((sub) => ({ ...sub, isSubcommand: true, parentCmd: mainCmd }))
222
- .sort(sortSubcommandEntries);
233
+ return mapSubcommandSuggestions(subs, mainCmd, 1, "");
234
+ }
235
+ const selectedSub = subs.find((sub) => String(sub && sub.cmd ? sub.cmd : "").toLowerCase() === subFilter.toLowerCase());
236
+ if (selectedSub && selectedSub.subcommands && (parts.length > 2 || endsWithSpace)) {
237
+ const nestedFilter = parts[2] || "";
238
+ return mapSubcommandSuggestions(selectedSub.subcommands, mainCmd, 2, nestedFilter);
223
239
  }
224
- return subs
225
- .filter((sub) => sub.cmd.toLowerCase().startsWith(subFilter.toLowerCase()))
226
- .map((sub) => ({ ...sub, isSubcommand: true, parentCmd: mainCmd }))
227
- .sort(sortSubcommandEntries);
240
+ return mapSubcommandSuggestions(subs, mainCmd, 1, subFilter);
228
241
  }
229
242
  return [];
230
243
  }
@@ -312,8 +325,10 @@ function createCompletionController(options = {}) {
312
325
 
313
326
  if (selected.isSubcommand) {
314
327
  const parts = trimmed.split(/\s+/);
315
- const base = parts[0] || "";
316
- const completedCore = base ? `${base} ${selected.cmd}` : selected.cmd;
328
+ const tokenIndex = Number.isInteger(selected.tokenIndex) ? selected.tokenIndex : 1;
329
+ while (parts.length <= tokenIndex) parts.push("");
330
+ parts[tokenIndex] = selected.cmd;
331
+ const completedCore = parts.slice(0, tokenIndex + 1).join(" ").trim();
317
332
  const isComplete = trimmed === completedCore || trimmed.startsWith(`${completedCore} `);
318
333
  return { text: `${completedCore} `, isComplete };
319
334
  }
@@ -353,8 +368,10 @@ function createCompletionController(options = {}) {
353
368
  input.value = `@${mentionTarget} `;
354
369
  } else if (selected.isSubcommand) {
355
370
  const parts = input.value.split(/\s+/);
356
- parts[parts.length - 1] = selected.cmd;
357
- input.value = `${parts.join(" ")} `;
371
+ const tokenIndex = Number.isInteger(selected.tokenIndex) ? selected.tokenIndex : 1;
372
+ while (parts.length <= tokenIndex) parts.push("");
373
+ parts[tokenIndex] = selected.cmd;
374
+ input.value = `${parts.slice(0, tokenIndex + 1).join(" ")} `;
358
375
  } else if (selected.isArgumentSuggestion) {
359
376
  const prefix = String(selected.argumentPrefix || "").trim();
360
377
  input.value = prefix ? `${prefix} ${selected.cmd} ` : `${selected.cmd} `;
@@ -369,7 +386,7 @@ function createCompletionController(options = {}) {
369
386
  }
370
387
  updateDraftFromInput();
371
388
 
372
- if (!selected.isSubcommand && selected.subcommands && selected.subcommands.length > 0) {
389
+ if (selected.subcommands && selected.subcommands.length > 0) {
373
390
  show(input.value);
374
391
  } else if (
375
392
  selected.isSubcommand
@@ -417,7 +434,7 @@ function createCompletionController(options = {}) {
417
434
  const nextPreview = preview(selected);
418
435
  if (!nextPreview.isComplete) {
419
436
  applyPreview(nextPreview);
420
- if (!selected.isSubcommand && selected.subcommands && selected.subcommands.length > 0) {
437
+ if (selected.subcommands && selected.subcommands.length > 0) {
421
438
  show(input.value);
422
439
  } else if (
423
440
  selected.isSubcommand
package/src/config.js CHANGED
@@ -7,7 +7,7 @@ const UCODE_FIELDS = ["ucodeProvider", "ucodeModel", "ucodeBaseUrl", "ucodeApiKe
7
7
  const DEFAULT_CONFIG = {
8
8
  launchMode: "auto",
9
9
  agentProvider: "codex-cli",
10
- controllerMode: "legacy",
10
+ controllerMode: "main",
11
11
  codexInternalThreadMode: "legacy",
12
12
  codexAuthPath: "",
13
13
  claudeOauthProfile: "",
@@ -42,7 +42,8 @@ function normalizeAgentProvider(value) {
42
42
  function normalizeControllerMode(value) {
43
43
  const raw = String(value || "").trim().toLowerCase();
44
44
  if (raw === "shadow") return "shadow";
45
- if (raw === "router-api") return "router-api";
45
+ if (raw === "router-api") return "main";
46
+ if (raw === "main") return "main";
46
47
  if (raw === "loop") return "loop";
47
48
  return "legacy";
48
49
  }
@@ -95,7 +96,9 @@ function loadConfig(projectRoot) {
95
96
  ...raw,
96
97
  launchMode: normalizeLaunchMode(raw.launchMode),
97
98
  agentProvider: normalizeAgentProvider(raw.agentProvider),
98
- controllerMode: normalizeControllerMode(raw.controllerMode),
99
+ controllerMode: Object.prototype.hasOwnProperty.call(raw, "controllerMode")
100
+ ? normalizeControllerMode(raw.controllerMode)
101
+ : DEFAULT_CONFIG.controllerMode,
99
102
  codexInternalThreadMode: normalizeCodexInternalThreadMode(raw.codexInternalThreadMode),
100
103
  codexAuthPath: normalizeCodexAuthPath(raw.codexAuthPath),
101
104
  claudeOauthProfile: normalizeClaudeOauthProfile(raw.claudeOauthProfile),
@@ -5,7 +5,7 @@ const { loadConfig, normalizeControllerMode } = require("../config");
5
5
  const CONTROLLER_MODES = Object.freeze({
6
6
  LEGACY: "legacy",
7
7
  SHADOW: "shadow",
8
- ROUTER_API: "router-api",
8
+ MAIN: "main",
9
9
  LOOP: "loop",
10
10
  });
11
11
 
@@ -15,7 +15,11 @@ const appliedControllerModes = new Map();
15
15
  const appliedControllerModeHistory = new Map();
16
16
 
17
17
  function readProcessControllerMode(env = process.env) {
18
- return normalizeControllerMode(env.UFOO_CONTROLLER_MODE);
18
+ const normalized = normalizeControllerMode(env.UFOO_CONTROLLER_MODE);
19
+ if (normalized !== CONTROLLER_MODES.LEGACY || String(env.UFOO_CONTROLLER_MODE || "").trim().toLowerCase() === CONTROLLER_MODES.LEGACY) {
20
+ return normalized;
21
+ }
22
+ return CONTROLLER_MODES.MAIN;
19
23
  }
20
24
 
21
25
  function resolveControllerMode({
@@ -37,7 +41,10 @@ function resolveControllerMode({
37
41
  : null;
38
42
  if (loadedConfig) {
39
43
  const projectMode = normalizeControllerMode(loadedConfig.controllerMode);
40
- return projectMode;
44
+ if (projectMode !== CONTROLLER_MODES.LEGACY || String(loadedConfig.controllerMode || "").trim().toLowerCase() === CONTROLLER_MODES.LEGACY) {
45
+ return projectMode;
46
+ }
47
+ return CONTROLLER_MODES.MAIN;
41
48
  }
42
49
 
43
50
  return readProcessControllerMode(env);
@@ -62,7 +69,7 @@ function pushModeHistory(key, previousMode, normalizedMode, messageId) {
62
69
 
63
70
  function applyControllerModeForMessage({
64
71
  projectRoot,
65
- nextMode = CONTROLLER_MODES.LEGACY,
72
+ nextMode = CONTROLLER_MODES.MAIN,
66
73
  messageId = "",
67
74
  } = {}) {
68
75
  const key = getControllerModeStateKey(projectRoot);
@@ -96,7 +103,7 @@ function rollbackControllerModeForMessage({
96
103
  const key = getControllerModeStateKey(projectRoot);
97
104
  const list = appliedControllerModeHistory.get(key) || [];
98
105
  const lastTransition = list.length > 0 ? list[list.length - 1] : null;
99
- const currentMode = appliedControllerModes.get(key) || CONTROLLER_MODES.LEGACY;
106
+ const currentMode = appliedControllerModes.get(key) || CONTROLLER_MODES.MAIN;
100
107
 
101
108
  if (!lastTransition) {
102
109
  return {
@@ -135,7 +142,7 @@ function rollbackControllerModeForMessage({
135
142
 
136
143
  function getAppliedControllerMode(projectRoot) {
137
144
  const key = getControllerModeStateKey(projectRoot);
138
- return appliedControllerModes.get(key) || CONTROLLER_MODES.LEGACY;
145
+ return appliedControllerModes.get(key) || CONTROLLER_MODES.MAIN;
139
146
  }
140
147
 
141
148
  function getControllerModeHistoryForTests(projectRoot) {
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { loadConfig } = require("../config");
4
4
 
5
- const DEFAULT_EXECUTION_PATH = "legacy";
5
+ const DEFAULT_EXECUTION_PATH = "main";
6
6
  const DEFAULT_CONFIDENCE_THRESHOLD = 0.6;
7
7
  const DEFAULT_TIMEOUT_MS = 5000;
8
8
 
@@ -55,7 +55,8 @@ const NON_ROUTING_PATTERNS = [
55
55
 
56
56
  function normalizeExecutionPath(value = "") {
57
57
  const text = String(value || "").trim().toLowerCase();
58
- if (text === "router-api") return "router-api";
58
+ if (text === "router-api") return "main";
59
+ if (text === "main") return "main";
59
60
  if (text === "shadow") return "shadow";
60
61
  if (text === "loop") return "loop";
61
62
  return DEFAULT_EXECUTION_PATH;
@@ -110,7 +111,7 @@ function shouldUseGateRouter({
110
111
  return {
111
112
  executionPath,
112
113
  intent,
113
- enabled: executionPath === "router-api" || executionPath === "loop",
114
+ enabled: executionPath === "main" || executionPath === "loop",
114
115
  };
115
116
  }
116
117
 
@@ -1770,19 +1770,34 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1770
1770
  );
1771
1771
  return;
1772
1772
  }
1773
- const target = req.group_id || req.groupId || req.instance || req.alias || req.target || "";
1774
- if (!target) {
1775
- socket.write(
1776
- `${JSON.stringify({
1777
- type: IPC_RESPONSE_TYPES.ERROR,
1778
- error: "group diagram requires alias|group_id",
1779
- })}
1780
- `,
1781
- );
1782
- return;
1783
- }
1773
+ let target = req.group_id || req.groupId || req.instance || req.alias || req.target || "";
1784
1774
  const format = normalizeFormat(req.format || (req.mermaid ? "mermaid" : "ascii"));
1785
1775
  try {
1776
+ if (!target || target === "current") {
1777
+ const latest = daemonGroupOrchestrator.getStatus({});
1778
+ if (latest && latest.ok && Array.isArray(latest.groups) && latest.groups.length > 0) {
1779
+ target = latest.groups[0].group_id || "";
1780
+ }
1781
+ }
1782
+ if (!target) {
1783
+ socket.write(
1784
+ `${JSON.stringify({
1785
+ type: IPC_RESPONSE_TYPES.RESPONSE,
1786
+ data: {
1787
+ reply: "Group diagram failed: no running groups and no template alias provided",
1788
+ group: {
1789
+ ok: false,
1790
+ mode: "runtime",
1791
+ target: "",
1792
+ format,
1793
+ error: "no running groups",
1794
+ },
1795
+ },
1796
+ })}
1797
+ `,
1798
+ );
1799
+ return;
1800
+ }
1786
1801
  const runtimeState = daemonGroupOrchestrator.getStatus({ group_id: target });
1787
1802
  if (runtimeState && runtimeState.ok === false && runtimeState.error === "invalid group_id") {
1788
1803
  socket.write(