u-foo 2.2.4 → 2.3.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.
Files changed (59) hide show
  1. package/SKILLS/ufoo/SKILL.md +56 -12
  2. package/SKILLS/uinit/SKILL.md +3 -2
  3. package/modules/AGENTS.template.md +2 -1
  4. package/modules/bus/README.md +1 -1
  5. package/modules/context/SKILLS/uctx/SKILL.md +6 -4
  6. package/package.json +1 -1
  7. package/src/agent/activityStatePublisher.js +6 -2
  8. package/src/agent/codexThreadProvider.js +2 -2
  9. package/src/agent/controllerToolExecutor.js +24 -1
  10. package/src/agent/credentials/claude.js +85 -16
  11. package/src/agent/credentials/codex.js +251 -23
  12. package/src/agent/defaultBootstrap.js +3 -1
  13. package/src/agent/directAuthStatus.js +264 -0
  14. package/src/agent/internalRunner.js +18 -12
  15. package/src/agent/loopObservability.js +10 -0
  16. package/src/agent/loopRuntime.js +19 -0
  17. package/src/agent/notifier.js +12 -3
  18. package/src/agent/ufooAgent.js +43 -13
  19. package/src/agent/upstreamTransport.js +23 -8
  20. package/src/bus/index.js +6 -1
  21. package/src/bus/message.js +156 -8
  22. package/src/chat/commandExecutor.js +187 -7
  23. package/src/chat/commands.js +23 -4
  24. package/src/chat/completionController.js +30 -7
  25. package/src/chat/index.js +3 -5
  26. package/src/cli/groupCoreCommands.js +5 -0
  27. package/src/cli.js +309 -0
  28. package/src/code/UCODE_PROMPT.md +3 -2
  29. package/src/code/prompts/ufoo.js +3 -2
  30. package/src/config.js +16 -3
  31. package/src/context/doctor.js +1 -1
  32. package/src/daemon/groupOrchestrator.js +13 -9
  33. package/src/daemon/promptRequest.js +11 -2
  34. package/src/daemon/soloBootstrap.js +2 -0
  35. package/src/group/bootstrap.js +1 -1
  36. package/src/group/promptProfiles.js +106 -22
  37. package/src/group/templates.js +1 -0
  38. package/src/init/index.js +4 -0
  39. package/src/memory/historySearch.js +308 -0
  40. package/src/memory/index.js +653 -8
  41. package/src/providerapi/redactor.js +4 -1
  42. package/src/status/index.js +24 -1
  43. package/src/tools/handlers/memory.js +168 -0
  44. package/src/tools/index.js +12 -0
  45. package/src/tools/registry.js +12 -0
  46. package/src/tools/schemaFixtures.js +213 -0
  47. package/src/tools/tier1/editMemory.js +14 -0
  48. package/src/tools/tier1/forget.js +14 -0
  49. package/src/tools/tier1/recall.js +14 -0
  50. package/src/tools/tier1/remember.js +14 -0
  51. package/src/tools/tier1/searchHistory.js +14 -0
  52. package/src/tools/tier1/searchMemory.js +14 -0
  53. package/templates/groups/build-lane.json +44 -6
  54. package/templates/groups/build-ultra.json +6 -5
  55. package/templates/groups/design-system.json +84 -0
  56. package/templates/groups/product-discovery.json +9 -4
  57. package/templates/groups/ui-plan-review.json +84 -0
  58. package/templates/groups/ui-polish.json +6 -2
  59. package/templates/groups/verify-ship.json +9 -4
package/src/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const path = require("path");
2
+ const os = require("os");
2
3
  const { spawnSync } = require("child_process");
3
4
  const net = require("net");
4
5
  const fs = require("fs");
@@ -244,6 +245,179 @@ function projectSwitchV1Error() {
244
245
  return err;
245
246
  }
246
247
 
248
+ function parseMemoryTags(value = "") {
249
+ return String(value || "")
250
+ .split(",")
251
+ .map((item) => item.trim())
252
+ .filter(Boolean);
253
+ }
254
+
255
+ function readTextFile(filePath = "") {
256
+ const target = String(filePath || "").trim();
257
+ if (!target) return "";
258
+ return fs.readFileSync(path.resolve(target), "utf8");
259
+ }
260
+
261
+ function formatMemoryLine(entry = {}) {
262
+ const tags = Array.isArray(entry.tags) && entry.tags.length
263
+ ? `[${entry.tags.join(",")}]`
264
+ : "[]";
265
+ const status = entry.status && entry.status !== "active" ? ` (${entry.status})` : "";
266
+ return `${entry.id} ${tags} ${entry.title}${status}`;
267
+ }
268
+
269
+ function printMemoryEntry(entry = {}, write = (line) => console.log(line)) {
270
+ write(`${entry.id} ${entry.status || "active"}`);
271
+ write(`title: ${entry.title || ""}`);
272
+ write(`tags: ${(entry.tags || []).join(",")}`);
273
+ write(`source: ${entry.source || ""}`);
274
+ write(`created_at: ${entry.created_at || ""}`);
275
+ write(`updated_at: ${entry.updated_at || ""}`);
276
+ write("");
277
+ write(entry.body || "");
278
+ }
279
+
280
+ function runMemoryCommand({
281
+ subcommand = "list",
282
+ args = [],
283
+ opts = {},
284
+ cwd = process.cwd(),
285
+ write = (line) => console.log(line),
286
+ writeError = (line) => console.error(line),
287
+ } = {}) {
288
+ const MemoryManager = require("./memory");
289
+ const manager = new MemoryManager(cwd);
290
+ const sub = String(subcommand || "list").trim().toLowerCase();
291
+ const outputJson = opts.json === true;
292
+
293
+ try {
294
+ if (sub === "add") {
295
+ const title = String(args[0] || opts.title || "").trim();
296
+ const body = opts.bodyFile ? readTextFile(opts.bodyFile) : String(opts.body || "").trim();
297
+ if (!body) throw new Error("memory add requires --body or --body-file");
298
+ const entry = manager.add({
299
+ title,
300
+ body,
301
+ tags: parseMemoryTags(opts.tags),
302
+ source: "user",
303
+ }, {
304
+ source: "cli",
305
+ actor: process.env.UFOO_SUBSCRIBER_ID || process.env.USER || "user",
306
+ });
307
+ if (outputJson) write(JSON.stringify(entry, null, 2));
308
+ else write(formatMemoryLine(entry));
309
+ return 0;
310
+ }
311
+
312
+ if (sub === "list" || sub === "ls") {
313
+ const entries = manager.list({
314
+ tag: opts.tag || "",
315
+ all: opts.all === true,
316
+ limit: parseInt(opts.limit, 10) || 0,
317
+ });
318
+ if (outputJson) {
319
+ write(JSON.stringify(entries, null, 2));
320
+ return 0;
321
+ }
322
+ if (entries.length === 0) {
323
+ write("No memory entries found.");
324
+ return 0;
325
+ }
326
+ entries.forEach((entry) => write(formatMemoryLine(entry)));
327
+ return 0;
328
+ }
329
+
330
+ if (sub === "show") {
331
+ const id = String(args[0] || "").trim();
332
+ if (!id) throw new Error("memory show requires <id>");
333
+ const entry = manager.get(id, { includeArchived: opts.all === true });
334
+ if (outputJson) write(JSON.stringify(entry, null, 2));
335
+ else printMemoryEntry(entry, write);
336
+ return 0;
337
+ }
338
+
339
+ if (sub === "edit") {
340
+ const id = String(args[0] || "").trim();
341
+ if (!id) throw new Error("memory edit requires <id>");
342
+ const patch = {};
343
+ if (opts.title) patch.title = opts.title;
344
+ if (opts.tags !== undefined) patch.tags = parseMemoryTags(opts.tags);
345
+ if (opts.bodyFile) patch.body = readTextFile(opts.bodyFile);
346
+ if (opts.body) patch.body = opts.body;
347
+ if (Object.keys(patch).length === 0) {
348
+ const entry = manager.get(id);
349
+ const editor = process.env.EDITOR || "vi";
350
+ const tmp = path.join(os.tmpdir(), `ufoo-memory-${entry.id}-${Date.now()}.md`);
351
+ fs.writeFileSync(tmp, `# ${entry.title}\n\n${entry.body || ""}\n`, "utf8");
352
+ const res = spawnSync(editor, [tmp], { stdio: "inherit" });
353
+ if (res.error) throw res.error;
354
+ if (typeof res.status === "number" && res.status !== 0) {
355
+ throw new Error(`${editor} exited with code ${res.status}`);
356
+ }
357
+ const parsed = String(fs.readFileSync(tmp, "utf8"));
358
+ fs.rmSync(tmp, { force: true });
359
+ const lines = parsed.replace(/\r\n/g, "\n").split("\n");
360
+ const first = lines.findIndex((line) => line.trim());
361
+ if (first !== -1 && /^#\s+/.test(lines[first])) {
362
+ patch.title = lines[first].replace(/^#\s+/, "").trim();
363
+ patch.body = lines.slice(first + 1).join("\n").trim();
364
+ } else {
365
+ patch.body = parsed.trim();
366
+ }
367
+ patch.expected_updated_at = entry.updated_at;
368
+ }
369
+ const updated = manager.update(id, patch, {
370
+ source: "cli",
371
+ actor: process.env.UFOO_SUBSCRIBER_ID || process.env.USER || "user",
372
+ });
373
+ if (outputJson) write(JSON.stringify(updated, null, 2));
374
+ else write(formatMemoryLine(updated));
375
+ return 0;
376
+ }
377
+
378
+ if (sub === "forget" || sub === "archive") {
379
+ const id = String(args[0] || "").trim();
380
+ if (!id) throw new Error("memory forget requires <id>");
381
+ const entry = manager.archive(id, {
382
+ source: "cli",
383
+ actor: process.env.UFOO_SUBSCRIBER_ID || process.env.USER || "user",
384
+ });
385
+ if (outputJson) write(JSON.stringify(entry, null, 2));
386
+ else write(formatMemoryLine(entry));
387
+ return 0;
388
+ }
389
+
390
+ if (sub === "rebuild-index") {
391
+ const entries = manager.rebuildIndex();
392
+ if (outputJson) write(JSON.stringify({ count: entries.length, index_file: manager.indexFile }, null, 2));
393
+ else write(`Rebuilt ${manager.indexFile} (${entries.length} active entries)`);
394
+ return 0;
395
+ }
396
+
397
+ if (sub === "audit") {
398
+ const rows = manager.readAudit(args[0] || "");
399
+ if (outputJson) {
400
+ write(JSON.stringify(rows, null, 2));
401
+ return 0;
402
+ }
403
+ if (rows.length === 0) {
404
+ write("No memory audit entries found.");
405
+ return 0;
406
+ }
407
+ rows.forEach((row) => {
408
+ write(`${row.ts || "-"} ${row.action || "-"} ${row.id || ""} ${row.title || ""}`.trim());
409
+ });
410
+ return 0;
411
+ }
412
+
413
+ writeError("memory requires add|list|show|edit|forget|rebuild-index|audit subcommand");
414
+ return 1;
415
+ } catch (err) {
416
+ writeError(err.message || String(err));
417
+ return err.exitCode || 1;
418
+ }
419
+ }
420
+
247
421
  function runProjectCommand({
248
422
  subcommand = "list",
249
423
  outputJson = false,
@@ -409,6 +583,108 @@ async function runCli(argv) {
409
583
  cwd: process.cwd(),
410
584
  });
411
585
  });
586
+
587
+ const memory = program.command("memory").description("Project shared memory commands");
588
+ memory
589
+ .command("add")
590
+ .description("Add a project memory entry")
591
+ .argument("<title>", "Memory title")
592
+ .option("--body <text>", "Memory body")
593
+ .option("--body-file <path>", "Read memory body from file")
594
+ .option("--tags <tags>", "Comma-separated tags")
595
+ .option("--json", "Output as JSON")
596
+ .action((title, opts) => {
597
+ process.exitCode = runMemoryCommand({
598
+ subcommand: "add",
599
+ args: [title],
600
+ opts,
601
+ cwd: process.cwd(),
602
+ });
603
+ });
604
+ memory
605
+ .command("list")
606
+ .alias("ls")
607
+ .description("List project memory entries")
608
+ .option("--tag <tag>", "Filter by tag")
609
+ .option("--all", "Include archived entries")
610
+ .option("--limit <n>", "Limit result count")
611
+ .option("--json", "Output as JSON")
612
+ .action((opts) => {
613
+ process.exitCode = runMemoryCommand({
614
+ subcommand: "list",
615
+ opts,
616
+ cwd: process.cwd(),
617
+ });
618
+ });
619
+ memory
620
+ .command("show")
621
+ .description("Show one memory entry")
622
+ .argument("<id>", "Memory id")
623
+ .option("--all", "Allow archived entries")
624
+ .option("--json", "Output as JSON")
625
+ .action((id, opts) => {
626
+ process.exitCode = runMemoryCommand({
627
+ subcommand: "show",
628
+ args: [id],
629
+ opts,
630
+ cwd: process.cwd(),
631
+ });
632
+ });
633
+ memory
634
+ .command("edit")
635
+ .description("Edit one memory entry")
636
+ .argument("<id>", "Memory id")
637
+ .option("--title <title>", "Replace title")
638
+ .option("--body <text>", "Replace body")
639
+ .option("--body-file <path>", "Replace body from file")
640
+ .option("--tags <tags>", "Replace tags with comma-separated tags")
641
+ .option("--json", "Output as JSON")
642
+ .action((id, opts) => {
643
+ process.exitCode = runMemoryCommand({
644
+ subcommand: "edit",
645
+ args: [id],
646
+ opts,
647
+ cwd: process.cwd(),
648
+ });
649
+ });
650
+ memory
651
+ .command("forget")
652
+ .description("Archive one memory entry")
653
+ .argument("<id>", "Memory id")
654
+ .option("--json", "Output as JSON")
655
+ .action((id, opts) => {
656
+ process.exitCode = runMemoryCommand({
657
+ subcommand: "forget",
658
+ args: [id],
659
+ opts,
660
+ cwd: process.cwd(),
661
+ });
662
+ });
663
+ memory
664
+ .command("rebuild-index")
665
+ .description("Rebuild memory INDEX.md")
666
+ .option("--json", "Output as JSON")
667
+ .action((opts) => {
668
+ process.exitCode = runMemoryCommand({
669
+ subcommand: "rebuild-index",
670
+ opts,
671
+ cwd: process.cwd(),
672
+ });
673
+ });
674
+ memory
675
+ .command("audit")
676
+ .description("Show memory audit log")
677
+ .argument("[id]", "Optional memory id")
678
+ .option("--json", "Output as JSON")
679
+ .action((id, opts) => {
680
+ process.exitCode = runMemoryCommand({
681
+ subcommand: "audit",
682
+ args: id ? [id] : [],
683
+ opts,
684
+ cwd: process.cwd(),
685
+ });
686
+ });
687
+
412
688
  program
413
689
  .command("launch")
414
690
  .description("Launch an agent (ucode, uclaude, ucodex)")
@@ -1705,6 +1981,39 @@ async function runCli(argv) {
1705
1981
  }
1706
1982
  return;
1707
1983
  }
1984
+ if (cmd === "memory") {
1985
+ const sub = rest[0] || "list";
1986
+ const args = rest.slice(1).filter((token, index, array) => {
1987
+ const prev = array[index - 1] || "";
1988
+ if (prev === "--body" || prev === "--body-file" || prev === "--tags" || prev === "--tag" || prev === "--limit" || prev === "--title") {
1989
+ return false;
1990
+ }
1991
+ return !token.startsWith("--");
1992
+ });
1993
+ const getOpt = (name, fallback = "") => {
1994
+ const idx = rest.indexOf(name);
1995
+ if (idx === -1 || idx + 1 >= rest.length) return fallback;
1996
+ const value = rest[idx + 1];
1997
+ if (!value || value.startsWith("--")) return fallback;
1998
+ return value;
1999
+ };
2000
+ process.exitCode = runMemoryCommand({
2001
+ subcommand: sub,
2002
+ args,
2003
+ opts: {
2004
+ title: getOpt("--title", ""),
2005
+ body: getOpt("--body", ""),
2006
+ bodyFile: getOpt("--body-file", ""),
2007
+ tags: getOpt("--tags", ""),
2008
+ tag: getOpt("--tag", ""),
2009
+ limit: getOpt("--limit", ""),
2010
+ all: rest.includes("--all"),
2011
+ json: rest.includes("--json"),
2012
+ },
2013
+ cwd: process.cwd(),
2014
+ });
2015
+ return;
2016
+ }
1708
2017
  if (cmd === "init") {
1709
2018
  const UfooInit = require("./init");
1710
2019
  const init = new UfooInit(repoRoot);
@@ -13,9 +13,10 @@ Operational constraints:
13
13
 
14
14
  ufoo integration requirements:
15
15
  - Participate in multi-agent coordination through ufoo bus/context.
16
- - Respect shared context decisions and append meaningful decisions when needed.
16
+ - Respect shared context decisions. The default is no new decision; only append one for important, plan-level choices that constrain future work, and keep durable project facts out of decisions.
17
+ - Use shared memory for durable project facts. Read existing memory before writing new memory; do not use it for transient task state.
17
18
  - Support launch/close/resume/inject flows managed by ufoo daemon.
18
- - Prefer canonical ufoo commands (`ufoo ctx`, `ufoo bus`, `ufoo report`) for coordination and status sync.
19
+ - Prefer canonical ufoo commands (`ufoo ctx`, `ufoo bus`, `ufoo memory`, `ufoo report`) for coordination and status sync.
19
20
 
20
21
  Execution protocol:
21
22
  - On session start, check context quickly:
@@ -4,9 +4,10 @@ function getUfooIntegrationSection() {
4
4
  return `# ufoo integration
5
5
 
6
6
  Participate in multi-agent coordination through the ufoo bus/context system:
7
- - Respect shared context decisions and append meaningful decisions when needed.
7
+ - Respect shared context decisions. The default is no new decision; only append one for important, plan-level choices that constrain future work, and keep durable project facts out of decisions.
8
+ - Use shared memory for durable project facts. Read existing memory before writing new memory; do not use it for transient task state.
8
9
  - Support launch/close/resume/inject flows managed by ufoo daemon.
9
- - Prefer canonical ufoo commands (\`ufoo ctx\`, \`ufoo bus\`, \`ufoo report\`) for coordination and status sync.
10
+ - Prefer canonical ufoo commands (\`ufoo ctx\`, \`ufoo bus\`, \`ufoo memory\`, \`ufoo report\`) for coordination and status sync.
10
11
 
11
12
  Execution protocol:
12
13
  - On session start, check context quickly:
package/src/config.js CHANGED
@@ -8,8 +8,9 @@ const DEFAULT_CONFIG = {
8
8
  launchMode: "auto",
9
9
  agentProvider: "codex-cli",
10
10
  controllerMode: "main",
11
- codexInternalThreadMode: "legacy",
11
+ codexInternalThreadMode: "api",
12
12
  codexAuthPath: "",
13
+ codexOauthRefreshWindowSec: 300,
13
14
  claudeOauthProfile: "",
14
15
  claudeOauthTokenPath: "",
15
16
  claudeOauthRefreshWindowSec: 300,
@@ -50,7 +51,8 @@ function normalizeControllerMode(value) {
50
51
 
51
52
  function normalizeCodexInternalThreadMode(value) {
52
53
  const raw = String(value || "").trim().toLowerCase();
53
- if (raw === "sdk") return "sdk";
54
+ if (raw === "api" || raw === "direct" || raw === "direct-api" || raw === "upstream") return "api";
55
+ if (raw === "sdk") return "api";
54
56
  return "legacy";
55
57
  }
56
58
 
@@ -58,6 +60,12 @@ function normalizeCodexAuthPath(value) {
58
60
  return typeof value === "string" ? value.trim() : "";
59
61
  }
60
62
 
63
+ function normalizeCodexOauthRefreshWindowSec(value) {
64
+ const num = Number(value);
65
+ if (!Number.isFinite(num) || num < 0) return 300;
66
+ return Math.floor(num);
67
+ }
68
+
61
69
  function normalizeClaudeOauthProfile(value) {
62
70
  return typeof value === "string" ? value.trim() : "";
63
71
  }
@@ -99,8 +107,11 @@ function loadConfig(projectRoot) {
99
107
  controllerMode: Object.prototype.hasOwnProperty.call(raw, "controllerMode")
100
108
  ? normalizeControllerMode(raw.controllerMode)
101
109
  : DEFAULT_CONFIG.controllerMode,
102
- codexInternalThreadMode: normalizeCodexInternalThreadMode(raw.codexInternalThreadMode),
110
+ codexInternalThreadMode: Object.prototype.hasOwnProperty.call(raw, "codexInternalThreadMode")
111
+ ? normalizeCodexInternalThreadMode(raw.codexInternalThreadMode)
112
+ : DEFAULT_CONFIG.codexInternalThreadMode,
103
113
  codexAuthPath: normalizeCodexAuthPath(raw.codexAuthPath),
114
+ codexOauthRefreshWindowSec: normalizeCodexOauthRefreshWindowSec(raw.codexOauthRefreshWindowSec),
104
115
  claudeOauthProfile: normalizeClaudeOauthProfile(raw.claudeOauthProfile),
105
116
  claudeOauthTokenPath: normalizeClaudeOauthTokenPath(raw.claudeOauthTokenPath),
106
117
  claudeOauthRefreshWindowSec: normalizeClaudeOauthRefreshWindowSec(raw.claudeOauthRefreshWindowSec),
@@ -143,6 +154,7 @@ function saveConfig(projectRoot, config) {
143
154
  merged.controllerMode = normalizeControllerMode(merged.controllerMode);
144
155
  merged.codexInternalThreadMode = normalizeCodexInternalThreadMode(merged.codexInternalThreadMode);
145
156
  merged.codexAuthPath = normalizeCodexAuthPath(merged.codexAuthPath);
157
+ merged.codexOauthRefreshWindowSec = normalizeCodexOauthRefreshWindowSec(merged.codexOauthRefreshWindowSec);
146
158
  merged.claudeOauthProfile = normalizeClaudeOauthProfile(merged.claudeOauthProfile);
147
159
  merged.claudeOauthTokenPath = normalizeClaudeOauthTokenPath(merged.claudeOauthTokenPath);
148
160
  merged.claudeOauthRefreshWindowSec = normalizeClaudeOauthRefreshWindowSec(merged.claudeOauthRefreshWindowSec);
@@ -186,6 +198,7 @@ module.exports = {
186
198
  normalizeControllerMode,
187
199
  normalizeCodexInternalThreadMode,
188
200
  normalizeCodexAuthPath,
201
+ normalizeCodexOauthRefreshWindowSec,
189
202
  normalizeClaudeOauthProfile,
190
203
  normalizeClaudeOauthTokenPath,
191
204
  normalizeClaudeOauthRefreshWindowSec,
@@ -113,7 +113,7 @@ class ContextDoctor {
113
113
 
114
114
  console.log("=== context doctor ===");
115
115
  console.log(
116
- "Reminder: If you provide evaluation/recommendation/plan, write a decision before replying."
116
+ "Reminder: default to no new decision; only record important, plan-level choices that constrain future work."
117
117
  );
118
118
  console.log("");
119
119
 
@@ -73,6 +73,12 @@ function buildRuntimeNickname(projectPrefix, nickname) {
73
73
  return `${prefix}-${logicalName}`;
74
74
  }
75
75
 
76
+ function resolveMemberScopedNickname(member = {}) {
77
+ return asTrimmedString(member.scoped_nickname)
78
+ || asTrimmedString(member.runtime_nickname)
79
+ || asTrimmedString(member.nickname);
80
+ }
81
+
76
82
  function buildLaunchPlan(templateDoc = {}) {
77
83
  const agents = Array.isArray(templateDoc.agents) ? templateDoc.agents : [];
78
84
  const remaining = new Map();
@@ -325,7 +331,6 @@ function buildExecutionPlan({
325
331
  const runtimeNickname = buildRuntimeNickname(projectNicknamePrefix, item.nickname);
326
332
  return {
327
333
  nickname: item.nickname,
328
- runtime_nickname: runtimeNickname,
329
334
  requested_type: item.requested_type || item.type,
330
335
  type: resolvedType,
331
336
  role: item.role,
@@ -401,7 +406,7 @@ function buildExecutionPlan({
401
406
  ...item,
402
407
  requested_type: item.requested_type || item.type,
403
408
  type: resolvedType,
404
- runtime_nickname: runtimeNickname,
409
+ scoped_nickname: runtimeNickname,
405
410
  resolved_profile: profile ? profile.resolved_profile : "",
406
411
  display_name: profile ? profile.display_name : "",
407
412
  short_name: profile ? profile.short_name : "",
@@ -459,7 +464,7 @@ function buildDefaultRuntime({
459
464
  index: idx,
460
465
  template_agent_id: item.id || "",
461
466
  nickname: item.nickname,
462
- runtime_nickname: item.runtime_nickname || "",
467
+ scoped_nickname: item.scoped_nickname || item.runtime_nickname || "",
463
468
  requested_type: item.requested_type || item.type,
464
469
  type: item.type,
465
470
  role: item.role || "",
@@ -795,7 +800,7 @@ function createGroupOrchestrator(options = {}) {
795
800
  roster_version: compiled.rosterVersion,
796
801
  members: compiled.executionPlan.map((item) => ({
797
802
  nickname: item.nickname,
798
- runtime_nickname: item.runtime_nickname,
803
+ scoped_nickname: item.scoped_nickname,
799
804
  type: item.type,
800
805
  role: item.role,
801
806
  startup_order: item.startup_order,
@@ -915,7 +920,7 @@ function createGroupOrchestrator(options = {}) {
915
920
  action: "launch",
916
921
  agent: item.type,
917
922
  count: 1,
918
- nickname: item.runtime_nickname,
923
+ nickname: item.scoped_nickname || item.runtime_nickname,
919
924
  require_activity_monitor: true,
920
925
  tmux_layout_context: tmuxLayoutContext,
921
926
  ...launchHostContext,
@@ -948,7 +953,7 @@ function createGroupOrchestrator(options = {}) {
948
953
  }
949
954
 
950
955
  const reused = Boolean(launchResult.skipped);
951
- const subscriberId = pickLaunchSubscriber(projectRoot, launchResult, item.runtime_nickname);
956
+ const subscriberId = pickLaunchSubscriber(projectRoot, launchResult, item.scoped_nickname || item.runtime_nickname);
952
957
  member.status = reused ? "reused" : "active";
953
958
  member.managed = !reused;
954
959
  member.subscriber_id = subscriberId || "";
@@ -960,7 +965,7 @@ function createGroupOrchestrator(options = {}) {
960
965
  if (!reused) {
961
966
  rollbackTargets.push({
962
967
  memberIndex: i,
963
- target: subscriberId || item.runtime_nickname,
968
+ target: subscriberId || item.scoped_nickname || item.runtime_nickname,
964
969
  });
965
970
  } else if (!canReuseBootstrappedMember(member, item, subscriberId)) {
966
971
  const priorBootstrap = findAppliedBootstrapRecord(
@@ -1124,8 +1129,7 @@ function createGroupOrchestrator(options = {}) {
1124
1129
  if (!member || member.managed === false) continue;
1125
1130
  if (member.status !== "active") continue;
1126
1131
  const target = asTrimmedString(member.subscriber_id)
1127
- || asTrimmedString(member.runtime_nickname)
1128
- || asTrimmedString(member.nickname);
1132
+ || resolveMemberScopedNickname(member);
1129
1133
  if (!target) continue;
1130
1134
  activeMembers.push({ index: i, target });
1131
1135
  }
@@ -212,15 +212,16 @@ async function handlePromptRequest(options = {}) {
212
212
 
213
213
  const privateReports = listControllerInboxEntries(projectRoot, "ufoo-agent", { num: 100 });
214
214
  const useGlobalProjectRouter = isGlobalController;
215
- const promptRunner = runPromptWithAssistant;
216
215
  const ufooAgentOptions = useGlobalProjectRouter ? { routingMode: "global-router" } : { controllerMode };
217
216
  let nextRequestMeta = requestMeta;
218
- if (!Object.prototype.hasOwnProperty.call(nextRequestMeta, "agent_execution_path") && controllerMode !== CONTROLLER_MODES.LEGACY) {
217
+ const hasExplicitRequestMeta = Object.keys(nextRequestMeta).length > 0;
218
+ if (hasExplicitRequestMeta && !Object.prototype.hasOwnProperty.call(nextRequestMeta, "agent_execution_path") && controllerMode !== CONTROLLER_MODES.LEGACY) {
219
219
  nextRequestMeta = {
220
220
  ...nextRequestMeta,
221
221
  agent_execution_path: controllerMode,
222
222
  };
223
223
  }
224
+ let forceMainRouterFallback = false;
224
225
 
225
226
  const logGateRouterEvent = (event, details = {}) => {
226
227
  controllerObserver.emit(event, details);
@@ -275,6 +276,7 @@ async function handlePromptRequest(options = {}) {
275
276
  attachGateRouterMeta("provider_error", {
276
277
  error: routed && routed.error ? routed.error : "route_agent_failed",
277
278
  });
279
+ forceMainRouterFallback = true;
278
280
  logGateRouterEvent("controller.gate_router_upgraded", {
279
281
  reason: "provider_error",
280
282
  fallback_used: "main_router",
@@ -295,6 +297,7 @@ async function handlePromptRequest(options = {}) {
295
297
  confidence: Number(route.confidence || 0),
296
298
  route_reason: route.reason || "",
297
299
  });
300
+ forceMainRouterFallback = true;
298
301
  logGateRouterEvent("controller.gate_router_upgraded", {
299
302
  reason: upgradeReason,
300
303
  decision: route.decision || "",
@@ -339,6 +342,7 @@ async function handlePromptRequest(options = {}) {
339
342
  route_reason: route.reason || "",
340
343
  error: err && err.message ? err.message : String(err),
341
344
  });
345
+ forceMainRouterFallback = true;
342
346
  logGateRouterEvent("controller.gate_router_upgraded", {
343
347
  reason: "dispatch_failed",
344
348
  target: route.target,
@@ -351,6 +355,11 @@ async function handlePromptRequest(options = {}) {
351
355
  }
352
356
 
353
357
  const promptText = buildPromptWithPrivateReports(req.text || "", privateReports, nextRequestMeta);
358
+ const promptRunner = loopRuntime.enabled
359
+ && !forceMainRouterFallback
360
+ && typeof injectedLoopRunner === "function"
361
+ ? injectedLoopRunner
362
+ : runPromptWithAssistant;
354
363
 
355
364
  try {
356
365
  const handled = await promptRunner({
@@ -253,6 +253,8 @@ function findOwningGroup(projectRoot, subscriberId = "") {
253
253
  && (
254
254
  !liveNickname
255
255
  || asTrimmedString(member.nickname) === liveNickname
256
+ || asTrimmedString(member.scoped_nickname) === liveNickname
257
+ || asTrimmedString(member.scoped_nickname) === liveScopedNickname
256
258
  || asTrimmedString(member.runtime_nickname) === liveNickname
257
259
  || asTrimmedString(member.runtime_nickname) === liveScopedNickname
258
260
  )
@@ -5,7 +5,7 @@ const crypto = require("crypto");
5
5
  const SHARED_UFOO_PROTOCOL = [
6
6
  "ufoo protocol:",
7
7
  "- At session start, sync shared context with `ufoo ctx decisions -l` and `ufoo ctx decisions -n 1`.",
8
- "- Record a decision ONLY for important, plan-level knowledge: architectural choices, multi-option trade-off analysis, cross-agent coordination decisions, or plans that affect other agents. Do NOT record routine findings, simple bug fixes, or trivial observations. Use `ufoo ctx decisions new \"Title\"` BEFORE acting.",
8
+ "- Default to no new decision. Record one ONLY for important, plan-level knowledge: architectural choices, multi-option trade-off analysis, cross-agent coordination decisions, or plans that affect other agents. Do NOT record routine findings, simple bug fixes, trivial observations, or generic plan/evaluation/recommendation requests. Durable project facts belong in shared memory, not decisions. Use `ufoo ctx decisions new \"Title\"` BEFORE acting only when that bar is met.",
9
9
  "- Use `ufoo bus send <target-nickname> \"<message>\"` for agent-to-agent handoffs.",
10
10
  "- If you receive pending bus work, execute it immediately, reply to the sender, then `ufoo bus ack \"$UFOO_SUBSCRIBER_ID\"`.",
11
11
  "- Use `ufoo report` for controller/runtime status updates, not as a substitute for direct handoffs.",