u-foo 1.8.1 → 1.8.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": "1.8.1",
3
+ "version": "1.8.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",
@@ -132,6 +132,12 @@ function findPreviousSession(cwd, agentType, tty, tmuxPane) {
132
132
  continue;
133
133
  }
134
134
 
135
+ // 检查旧 shell 进程是否还存活(防止 tty 回收导致误匹配)
136
+ if (meta.tty_shell_pid && !isAgentPidAlive(meta.tty_shell_pid)) {
137
+ // shell 已退出,说明是不同的终端窗口复用了同一个 tty
138
+ continue;
139
+ }
140
+
135
141
  // 找到了可以复用的旧 session
136
142
  const parts = id.split(":");
137
143
  if (parts.length !== 2) continue;
@@ -504,6 +510,8 @@ class AgentLauncher {
504
510
  * 启动 agent
505
511
  */
506
512
  async launch(args) {
513
+ // 保存用户/group 显式传入的 nickname(在 session 复用覆盖前)
514
+ this._originalNickname = process.env.UFOO_NICKNAME || "";
507
515
  try {
508
516
  // 1. 确保初始化
509
517
  await this.ensureInit();
@@ -537,8 +545,9 @@ class AgentLauncher {
537
545
 
538
546
  // 6. 启动消息通知监听器(ufoo-code 改为内部 bus 轮询消费)
539
547
  const shouldStartNotifier = String(this.agentType || "").trim().toLowerCase() !== "ufoo-code";
548
+ let notifier = null;
540
549
  if (shouldStartNotifier) {
541
- const notifier = new AgentNotifier(this.cwd, subscriberId);
550
+ notifier = new AgentNotifier(this.cwd, subscriberId);
542
551
  notifier.start();
543
552
  }
544
553
 
@@ -605,6 +614,10 @@ class AgentLauncher {
605
614
  });
606
615
  readyDetector.onReady(async () => {
607
616
  launcherActivityDetector.markReady();
617
+ // 通知 notifier launcher 已检测到 ready,可以开始设 idle 状态
618
+ if (notifier) {
619
+ notifier.markLauncherReady();
620
+ }
608
621
  // Claude Code's Ink TUI renders ❯ prompt before the input handler
609
622
  // is fully mounted. Wait a short period for the TUI to be ready to
610
623
  // accept injected text, otherwise only the trailing CR is processed
@@ -612,6 +625,29 @@ class AgentLauncher {
612
625
  if (this.agentType === "claude-code") {
613
626
  await new Promise((r) => setTimeout(r, 800));
614
627
  }
628
+
629
+ // Claude Code: inject /rename 设置 session 标签(在 AGENT_READY/bootstrap 之前)
630
+ // /rename 是 slash 命令,瞬间完成,不调 LLM
631
+ // 仅在有显式 nickname 时注入(UFOO_NICKNAME 由 group launch 或用户指定)
632
+ // 使用启动前保存的原始值,避免复用 nickname 污染
633
+ const explicitNickname = this._originalNickname || "";
634
+ if (this.agentType === "claude-code" && explicitNickname && wrapper.pty) {
635
+ try {
636
+ // Strip control chars to prevent PTY command injection
637
+ const safeNick = explicitNickname.replace(/[\x00-\x1f\x7f]/g, "");
638
+ if (safeNick) {
639
+ wrapper.write(`/rename ${safeNick}`);
640
+ await new Promise((r) => setTimeout(r, 200));
641
+ wrapper.write("\r");
642
+ await new Promise((r) => setTimeout(r, 400));
643
+ }
644
+ } catch (err) {
645
+ if (process.env.UFOO_DEBUG) {
646
+ console.error("[rename] inject failed:", err.message);
647
+ }
648
+ }
649
+ }
650
+
615
651
  const startTime = Date.now();
616
652
  try {
617
653
  const daemonSock = await connectWithRetry(daemonSockPath, 3, 100);
@@ -619,6 +655,7 @@ class AgentLauncher {
619
655
  daemonSock.write(`${JSON.stringify({
620
656
  type: IPC_REQUEST_TYPES.AGENT_READY,
621
657
  subscriberId,
658
+ agentPid: wrapper.pty ? wrapper.pty.pid : 0,
622
659
  })}\n`);
623
660
  daemonSock.end();
624
661
 
@@ -637,6 +674,7 @@ class AgentLauncher {
637
674
  console.error(`[ready] daemon notification error: ${err.message}, will use fallback delay`);
638
675
  }
639
676
  }
677
+
640
678
  });
641
679
 
642
680
  // Fallback:如果10秒后还没检测到ready,强制标记为ready
@@ -670,6 +708,7 @@ class AgentLauncher {
670
708
  wrapper.spawn();
671
709
  wrapper.attachStreams(process.stdin, process.stdout, process.stderr);
672
710
 
711
+
673
712
  // 启动inject监听socket(用于外部注入命令到PTY)
674
713
  const injectSockPath = path.join(
675
714
  getUfooPaths(this.cwd).busQueuesDir,
@@ -29,6 +29,7 @@ class AgentNotifier {
29
29
  this.lastNickname = "";
30
30
  this.lastUbusWakeCount = -1;
31
31
 
32
+
32
33
  // 计算队列文件路径
33
34
  const safeSub = subscriber.replace(/:/g, "_");
34
35
  const paths = getUfooPaths(projectRoot);
@@ -69,9 +70,18 @@ class AgentNotifier {
69
70
  }
70
71
  }
71
72
 
73
+ /**
74
+ * 通知 notifier launcher 已检测到 agent ready
75
+ * 在此之前 notifier 不会将 activity_state 设为 idle(避免 bootstrap 提前注入)
76
+ */
77
+ markLauncherReady() {
78
+ this._launcherReady = true;
79
+ }
80
+
72
81
  /**
73
82
  * 设置终端标题为昵称
74
- * iTerm2: 同时设置 badge cwd
83
+ * codex 等: OSC escape sequence + iTerm2 badge
84
+ * claude-code: OSC 会被 Claude 覆盖,仅设 iTerm2 badge
75
85
  */
76
86
  setTitle(nickname) {
77
87
  if (!nickname) return;
@@ -364,7 +374,7 @@ class AgentNotifier {
364
374
  }
365
375
 
366
376
  this.lastCount = this.getMessageCount();
367
- if (!this.lastWorkingAt || nowMs - this.lastWorkingAt >= this.workingHoldMs) {
377
+ if (this._launcherReady && (!this.lastWorkingAt || nowMs - this.lastWorkingAt >= this.workingHoldMs)) {
368
378
  this.updateActivityState("idle");
369
379
  }
370
380
  this.refreshTitle();
@@ -381,7 +391,10 @@ class AgentNotifier {
381
391
  if (this.lastNickname) {
382
392
  this.setTitle(this.lastNickname);
383
393
  }
384
- this.updateActivityState("ready");
394
+ this.updateActivityState("starting");
395
+ // launcher 的 readyDetector 负责在 TUI 真正 ready 后标记 "ready"
396
+ // 在那之前 notifier 不应覆盖 activity_state
397
+ this._launcherReady = false;
385
398
 
386
399
  // 启动轮询
387
400
  this.timer = setInterval(() => {
@@ -13,7 +13,7 @@ const { createDaemonIpcServer } = require("./ipcServer");
13
13
  const { IPC_REQUEST_TYPES, IPC_RESPONSE_TYPES, BUS_STATUS_PHASES } = require("../shared/eventContract");
14
14
  const { getUfooPaths } = require("../ufoo/paths");
15
15
  const { upsertProjectRuntime, markProjectStopped } = require("../projects/registry");
16
- const { scheduleProviderSessionProbe, loadProviderSessionCache } = require("./providerSessions");
16
+ const { scheduleProviderSessionProbe, resolveSessionFromFile, persistProviderSession, loadProviderSessionCache } = require("./providerSessions");
17
17
  const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
18
18
  const { createDaemonCronController } = require("./cronOps");
19
19
  const { createGroupOrchestrator } = require("./groupOrchestrator");
@@ -499,6 +499,7 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
499
499
  subscriberId,
500
500
  agentType: probeAgentType,
501
501
  nickname: resolvedNickname,
502
+ agentCwd: projectRoot,
502
503
  onResolved: (id, resolved) => {
503
504
  if (providerSessions) {
504
505
  providerSessions.set(id, {
@@ -1973,6 +1974,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1973
1974
  subscriberId,
1974
1975
  agentType,
1975
1976
  nickname: resolvedNickname,
1977
+ agentCwd: projectRoot,
1976
1978
  onResolved: (id, resolved) => {
1977
1979
  if (providerSessions) {
1978
1980
  providerSessions.set(id, {
@@ -2009,11 +2011,41 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
2009
2011
  return;
2010
2012
  }
2011
2013
  if (req.type === IPC_REQUEST_TYPES.AGENT_READY) {
2012
- const { subscriberId } = req;
2014
+ const { subscriberId, agentPid } = req;
2013
2015
  if (!subscriberId) {
2014
2016
  return;
2015
2017
  }
2016
- log(`agent_ready id=${subscriberId} - triggering probe immediately`);
2018
+ log(`agent_ready id=${subscriberId} pid=${agentPid || 0} - resolving session`);
2019
+
2020
+ // Try direct file read first if we have agentPid (fast path)
2021
+ const parsedAgentPid = Number.parseInt(agentPid, 10);
2022
+ if (Number.isFinite(parsedAgentPid) && parsedAgentPid > 0) {
2023
+ const agentType = subscriberId.split(":")[0] || "";
2024
+ const resolved = resolveSessionFromFile(agentType, {
2025
+ pid: parsedAgentPid,
2026
+ cwd: projectRoot,
2027
+ });
2028
+ if (resolved && resolved.sessionId) {
2029
+ log(`agent_ready session resolved from file for ${subscriberId}: ${resolved.sessionId}`);
2030
+ persistProviderSession(projectRoot, subscriberId, resolved);
2031
+ if (providerSessions) {
2032
+ providerSessions.set(subscriberId, {
2033
+ sessionId: resolved.sessionId,
2034
+ source: resolved.source || "",
2035
+ updated_at: new Date().toISOString(),
2036
+ });
2037
+ }
2038
+ // Cancel the scheduled probe to prevent /ufoo injection
2039
+ const handle = probeHandles.get(subscriberId);
2040
+ if (handle && typeof handle.cancel === "function") {
2041
+ handle.cancel();
2042
+ }
2043
+ probeHandles.delete(subscriberId);
2044
+ return;
2045
+ }
2046
+ }
2047
+
2048
+ // Fallback: trigger scheduled probe
2017
2049
  const probeHandle = probeHandles.get(subscriberId);
2018
2050
  if (probeHandle && typeof probeHandle.triggerNow === "function") {
2019
2051
  probeHandle.triggerNow().catch((err) => {
@@ -233,74 +233,256 @@ async function executeProbe({
233
233
  }
234
234
 
235
235
  /**
236
- * Schedule a provider session probe
236
+ * Resolve Claude Code session ID directly from session file.
237
+ * Claude writes ~/.claude/sessions/<pid>.json with { sessionId, pid, cwd, ... }
238
+ */
239
+ function resolveClaudeSessionFromFile(pid) {
240
+ if (!pid) return null;
241
+ const filePath = path.join(os.homedir(), ".claude", "sessions", `${pid}.json`);
242
+ try {
243
+ if (!fs.existsSync(filePath)) return null;
244
+ const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
245
+ const sessionId = data.sessionId || data.session_id || "";
246
+ if (!sessionId) return null;
247
+ return { sessionId, source: filePath };
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Resolve Codex session ID from session rollout files.
255
+ * Codex writes ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<id>.jsonl
256
+ * First line contains { type: "session_meta", payload: { id, cwd, ... } }
257
+ */
258
+ function resolveCodexSessionFromFile(cwd) {
259
+ if (!cwd) return null;
260
+ try {
261
+ const now = new Date();
262
+ // Check today and yesterday (session may have started before midnight)
263
+ const dates = [now];
264
+ const yesterday = new Date(now);
265
+ yesterday.setDate(yesterday.getDate() - 1);
266
+ dates.push(yesterday);
267
+
268
+ let bestMatch = null;
269
+ let bestMtime = 0;
270
+
271
+ for (const d of dates) {
272
+ const yyyy = String(d.getFullYear());
273
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
274
+ const dd = String(d.getDate()).padStart(2, "0");
275
+ const dir = path.join(os.homedir(), ".codex", "sessions", yyyy, mm, dd);
276
+ if (!fs.existsSync(dir)) continue;
277
+
278
+ const files = fs.readdirSync(dir)
279
+ .filter((f) => f.startsWith("rollout-") && f.endsWith(".jsonl"));
280
+
281
+ for (const file of files) {
282
+ const filePath = path.join(dir, file);
283
+ try {
284
+ const stat = fs.statSync(filePath);
285
+ if (stat.mtimeMs <= bestMtime) continue;
286
+
287
+ // Read first line for session_meta
288
+ const fd = fs.openSync(filePath, "r");
289
+ const buf = Buffer.alloc(4096);
290
+ const bytesRead = fs.readSync(fd, buf, 0, 4096, 0);
291
+ fs.closeSync(fd);
292
+ const firstLine = buf.toString("utf8", 0, bytesRead).split("\n")[0];
293
+ if (!firstLine) continue;
294
+
295
+ const record = JSON.parse(firstLine);
296
+ const payload = record.payload || record;
297
+ const sessionCwd = payload.cwd || "";
298
+ const sessionId = payload.id || "";
299
+
300
+ if (sessionId && sessionCwd === cwd && stat.mtimeMs > bestMtime) {
301
+ bestMatch = { sessionId, source: filePath };
302
+ bestMtime = stat.mtimeMs;
303
+ }
304
+ } catch {
305
+ continue;
306
+ }
307
+ }
308
+ }
309
+ return bestMatch;
310
+ } catch {
311
+ return null;
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Resolve provider session ID directly from session files (no probe needed).
317
+ * @param {string} agentType - "claude-code" or "codex"
318
+ * @param {object} opts - { pid, cwd }
319
+ */
320
+ function resolveSessionFromFile(agentType, opts = {}) {
321
+ if (agentType === "claude-code") {
322
+ return resolveClaudeSessionFromFile(opts.pid);
323
+ }
324
+ if (agentType === "codex") {
325
+ return resolveCodexSessionFromFile(opts.cwd);
326
+ }
327
+ return null;
328
+ }
329
+
330
+ /**
331
+ * Retry reading session file (agent may not have written it yet)
332
+ */
333
+ async function resolveSessionFromFileWithRetries(agentType, opts = {}, attempts = 10, intervalMs = 1000) {
334
+ for (let i = 0; i < attempts; i += 1) {
335
+ const resolved = resolveSessionFromFile(agentType, opts);
336
+ if (resolved && resolved.sessionId) return resolved;
337
+ // eslint-disable-next-line no-await-in-loop
338
+ await new Promise((r) => setTimeout(r, intervalMs));
339
+ }
340
+ return null;
341
+ }
342
+
343
+ /**
344
+ * Schedule provider session resolution.
345
+ * Tries direct file read first (fast, non-invasive), falls back to probe if needed.
237
346
  *
238
347
  * @param {Object} options
239
348
  * @param {string} options.projectRoot - Project root directory
240
349
  * @param {string} options.subscriberId - Subscriber ID (e.g., "claude-code:abc123")
241
350
  * @param {string} options.agentType - Agent type ("claude-code" or "codex")
242
351
  * @param {string} options.nickname - Agent nickname (e.g., "claude-47")
243
- * @param {number} options.delayMs - Delay before injection
244
- * @param {number} options.attempts - Number of search attempts
245
- * @param {number} options.intervalMs - Interval between attempts
352
+ * @param {number} options.agentPid - Agent child process PID (for claude-code)
353
+ * @param {string} options.agentCwd - Agent working directory (for codex)
354
+ * @param {number} options.delayMs - Delay before starting resolution
355
+ * @param {number} options.fileAttempts - File read retry attempts
356
+ * @param {number} options.fileIntervalMs - File read retry interval
357
+ * @param {number} options.probeAttempts - Probe retry attempts (fallback)
358
+ * @param {number} options.probeIntervalMs - Probe retry interval (fallback)
246
359
  * @param {Function} options.onResolved - Callback when session ID is found
247
360
  */
248
- function scheduleProviderSessionProbe({
361
+ function scheduleProviderSessionResolve({
249
362
  projectRoot,
250
363
  subscriberId,
251
364
  agentType,
252
365
  nickname,
253
- delayMs = 8000,
254
- attempts = 15,
255
- intervalMs = 2000,
366
+ agentPid = 0,
367
+ agentCwd = "",
368
+ delayMs = 3000,
369
+ fileAttempts = 10,
370
+ fileIntervalMs = 1000,
371
+ probeAttempts = 15,
372
+ probeIntervalMs = 2000,
256
373
  onResolved = null,
257
374
  }) {
258
375
  if (!subscriberId || !agentType) return null;
259
376
  if (agentType !== "codex" && agentType !== "claude-code") return null;
260
- if (!nickname) return null;
261
377
 
262
- const marker = buildProbeMarker(nickname);
263
- persistProbeMarker(projectRoot, subscriberId, marker);
378
+ const marker = nickname ? buildProbeMarker(nickname) : "";
379
+ if (marker) {
380
+ persistProbeMarker(projectRoot, subscriberId, marker);
381
+ }
264
382
 
265
383
  let executed = false;
384
+ let cancelled = false;
266
385
  let timer = null;
267
386
 
268
387
  const execute = async () => {
269
- if (executed) return;
388
+ if (executed || cancelled) return;
270
389
  executed = true;
271
390
  if (timer) {
272
391
  clearTimeout(timer);
273
392
  timer = null;
274
393
  }
275
- await executeProbe({
276
- projectRoot,
277
- subscriberId,
278
- agentType,
279
- nickname,
280
- attempts,
281
- intervalMs,
282
- onResolved,
283
- });
394
+
395
+ // 1. Try direct file read (fast, non-invasive)
396
+ const fileOpts = { pid: agentPid, cwd: agentCwd || projectRoot };
397
+ const fileResolved = await resolveSessionFromFileWithRetries(
398
+ agentType, fileOpts, fileAttempts, fileIntervalMs,
399
+ );
400
+ if (cancelled) return;
401
+ if (fileResolved && fileResolved.sessionId) {
402
+ persistProviderSession(projectRoot, subscriberId, fileResolved);
403
+ if (typeof onResolved === "function") {
404
+ onResolved(subscriberId, fileResolved);
405
+ }
406
+ return;
407
+ }
408
+
409
+ // 2. Fallback to probe (inject command + search history)
410
+ // Re-check cancelled: AGENT_READY may have resolved session while we were retrying
411
+ if (cancelled) return;
412
+ if (nickname) {
413
+ await executeProbe({
414
+ projectRoot,
415
+ subscriberId,
416
+ agentType,
417
+ nickname,
418
+ attempts: probeAttempts,
419
+ intervalMs: probeIntervalMs,
420
+ onResolved,
421
+ });
422
+ }
284
423
  };
285
424
 
286
- // Schedule delayed execution (fallback)
425
+ // Schedule delayed execution
287
426
  timer = setTimeout(execute, delayMs);
288
427
 
289
- // Return handle for early trigger
428
+ // Return handle for early trigger or cancellation
290
429
  return {
291
430
  subscriberId,
292
431
  marker,
293
432
  triggerNow: execute,
433
+ cancel: () => {
434
+ cancelled = true;
435
+ executed = true;
436
+ if (timer) {
437
+ clearTimeout(timer);
438
+ timer = null;
439
+ }
440
+ },
294
441
  };
295
442
  }
296
443
 
444
+ /**
445
+ * Schedule a provider session probe (legacy wrapper)
446
+ */
447
+ function scheduleProviderSessionProbe({
448
+ projectRoot,
449
+ subscriberId,
450
+ agentType,
451
+ nickname,
452
+ delayMs = 8000,
453
+ attempts = 15,
454
+ intervalMs = 2000,
455
+ onResolved = null,
456
+ agentPid = 0,
457
+ agentCwd = "",
458
+ }) {
459
+ // Delegate to new resolve function which tries file read first
460
+ return scheduleProviderSessionResolve({
461
+ projectRoot,
462
+ subscriberId,
463
+ agentType,
464
+ nickname,
465
+ agentPid,
466
+ agentCwd,
467
+ delayMs: agentPid || agentCwd ? 3000 : delayMs,
468
+ probeAttempts: attempts,
469
+ probeIntervalMs: intervalMs,
470
+ onResolved,
471
+ });
472
+ }
473
+
297
474
  module.exports = {
298
475
  scheduleProviderSessionProbe,
476
+ scheduleProviderSessionResolve,
477
+ resolveSessionFromFile,
478
+ persistProviderSession,
299
479
  loadProviderSessionCache,
300
480
  __private: {
301
481
  buildProbeCommand,
302
482
  recordContainsMarker,
303
483
  containsProbeCommand,
304
484
  escapeRegExp,
485
+ resolveClaudeSessionFromFile,
486
+ resolveCodexSessionFromFile,
305
487
  },
306
488
  };
@@ -44,6 +44,7 @@ const BUILTIN_PROFILES = [
44
44
  "Handoff:",
45
45
  "- Send the architect a scoped brief.",
46
46
  "- Send the scope challenger any assumptions that feel inflated or weak.",
47
+ "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
47
48
  ].join("\n"),
48
49
  },
49
50
  {
@@ -71,6 +72,7 @@ const BUILTIN_PROFILES = [
71
72
  "",
72
73
  "Handoff:",
73
74
  "- Send approved scope decisions to the architect and builder.",
75
+ "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
74
76
  ].join("\n"),
75
77
  },
76
78
  {
@@ -99,6 +101,7 @@ const BUILTIN_PROFILES = [
99
101
  "Handoff:",
100
102
  "- Send execution-ready slices to the implementation lead.",
101
103
  "- Send risk hotspots to the reviewer and QA roles.",
104
+ "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
102
105
  ].join("\n"),
103
106
  },
104
107
  {
@@ -126,6 +129,7 @@ const BUILTIN_PROFILES = [
126
129
  "",
127
130
  "Handoff:",
128
131
  "- Send changed areas and known risk points to review-critic and qa-driver.",
132
+ "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
129
133
  ].join("\n"),
130
134
  },
131
135
  {
@@ -154,6 +158,7 @@ const BUILTIN_PROFILES = [
154
158
  "",
155
159
  "Handoff:",
156
160
  "- Send changed surfaces and known UI tradeoffs to design-critic and qa-driver.",
161
+ "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
157
162
  ].join("\n"),
158
163
  },
159
164
  {
@@ -183,6 +188,7 @@ const BUILTIN_PROFILES = [
183
188
  "Handoff:",
184
189
  "- Send ranked UI issues and concrete polish guidance to frontend-refiner.",
185
190
  "- Send user-visible risk items and regression watch points to qa-driver.",
191
+ "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
186
192
  ].join("\n"),
187
193
  },
188
194
  {
@@ -209,6 +215,7 @@ const BUILTIN_PROFILES = [
209
215
  "Handoff:",
210
216
  "- Send must-fix items back to implementation lead.",
211
217
  "- Send user-visible risk items to qa-driver.",
218
+ "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
212
219
  ].join("\n"),
213
220
  },
214
221
  {
@@ -235,6 +242,7 @@ const BUILTIN_PROFILES = [
235
242
  "Handoff:",
236
243
  "- Send fixable bugs to implementation lead.",
237
244
  "- Send suspicious root-cause patterns to debug-investigator.",
245
+ "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
238
246
  ].join("\n"),
239
247
  },
240
248
  {
@@ -262,6 +270,7 @@ const BUILTIN_PROFILES = [
262
270
  "",
263
271
  "Handoff:",
264
272
  "- Send confirmed cause and fix guidance to implementation lead.",
273
+ "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
265
274
  ].join("\n"),
266
275
  },
267
276
  {
@@ -288,6 +297,7 @@ const BUILTIN_PROFILES = [
288
297
  "Handoff:",
289
298
  "- Send blockers back to the responsible agent.",
290
299
  "- Send the final readiness note to the human operator.",
300
+ "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
291
301
  ].join("\n"),
292
302
  },
293
303
  {
@@ -313,6 +323,7 @@ const BUILTIN_PROFILES = [
313
323
  "",
314
324
  "Handoff:",
315
325
  "- Send the architect and builder a short ordered plan with explicit blockers.",
326
+ "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
316
327
  ].join("\n"),
317
328
  },
318
329
  {
@@ -338,6 +349,7 @@ const BUILTIN_PROFILES = [
338
349
  "",
339
350
  "Handoff:",
340
351
  "- Send a concise findings brief and source list to the next agent.",
352
+ "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
341
353
  ].join("\n"),
342
354
  },
343
355
  {
@@ -363,6 +375,7 @@ const BUILTIN_PROFILES = [
363
375
  "",
364
376
  "Handoff:",
365
377
  "- Send the prototype status, evidence, and remaining gaps to the next agent.",
378
+ "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
366
379
  ].join("\n"),
367
380
  },
368
381
  ];