u-foo 3.0.13 → 3.0.15

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.
@@ -23,6 +23,12 @@ const MessageManager = require("./message");
23
23
  const NicknameManager = require("./nickname");
24
24
  const Injector = require("./inject");
25
25
  const { BusStore } = require("./store");
26
+ const {
27
+ acquirePollLease,
28
+ assertFollowPollAllowed,
29
+ releasePollLease,
30
+ runPendingPoll,
31
+ } = require("./poll");
26
32
 
27
33
  /**
28
34
  * Event Bus - 项目级 Agent 事件总线
@@ -411,6 +417,90 @@ class EventBus {
411
417
  return pending;
412
418
  }
413
419
 
420
+ /**
421
+ * Observe pending messages continuously without claiming or acknowledging.
422
+ *
423
+ * This is an explicit fallback for agent hosts whose own background-task
424
+ * output is their delivery mechanism. Built-in ufoo agent families keep
425
+ * using their existing injection/internal-consumption paths.
426
+ */
427
+ async poll(subscriber, options = {}) {
428
+ this.ensureBus();
429
+
430
+ const target = String(subscriber || "").trim();
431
+ if (!target) {
432
+ throw new Error("poll --follow requires <subscriber-id>");
433
+ }
434
+
435
+ // Reject known built-in delivery IDs before loading or writing any shared
436
+ // bus state. This keeps accidental invocation side-effect free for them.
437
+ assertFollowPollAllowed(target);
438
+
439
+ this.loadBusData();
440
+ const meta = this.subscriberManager.getSubscriber(target);
441
+ if (!meta) {
442
+ throw new Error(`poll --follow requires a joined subscriber: ${target}`);
443
+ }
444
+ assertFollowPollAllowed(target, meta);
445
+
446
+ const intervalSeconds = Number(options.intervalSeconds);
447
+ const intervalMs = Math.max(
448
+ 250,
449
+ Number.isFinite(intervalSeconds) ? intervalSeconds * 1000 : 2000
450
+ );
451
+ const pollPidFile = path.join(
452
+ this.busDir,
453
+ "pids",
454
+ `poll-${subscriberToSafeName(target)}.pid`
455
+ );
456
+ const lease = acquirePollLease(pollPidFile, { isAlive: isPidAlive });
457
+ const cleanupLease = () => releasePollLease(lease);
458
+ process.once("exit", cleanupLease);
459
+
460
+ try {
461
+ // The resident poll process owns liveness for this explicitly opted-in
462
+ // subscriber. No notifier/injector state is touched.
463
+ meta.status = "active";
464
+ meta.pid = process.pid;
465
+ this.subscriberManager.updateLastSeen(target);
466
+ this.saveBusData();
467
+
468
+ console.log(
469
+ `[ufoo-poll]<subscriber:${target}> following every ${intervalMs / 1000}s`
470
+ );
471
+
472
+ return await runPendingPoll({
473
+ intervalMs,
474
+ signal: options.signal,
475
+ sleep: options.sleep,
476
+ maxIterations: options.maxIterations,
477
+ readPending: () => this.queueManager.peekPending(target),
478
+ onEvents: async (events) => {
479
+ console.log(`[ufoo-poll] ${events.length} new pending event(s)`);
480
+ for (const event of events) {
481
+ const publisherMeta = this.busData.agents?.[event.publisher];
482
+ const nick = publisherMeta?.nickname;
483
+ const fromLabel = nick ? `${event.publisher}(${nick})` : event.publisher;
484
+ console.log(`[ufoo]<from:${fromLabel || "unknown"}>`);
485
+ console.log(`Type: ${event.type}/${event.event}`);
486
+ console.log(`Content: ${JSON.stringify(event.data)}`);
487
+ }
488
+ const sequenced = events
489
+ .map((event) => Number(event && event.seq))
490
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
491
+ const throughSeq = sequenced.length === events.length
492
+ ? Math.max(...sequenced)
493
+ : 0;
494
+ const ackSuffix = throughSeq > 0 ? ` --through ${throughSeq}` : "";
495
+ console.log(`After handling, run: ufoo bus ack ${target}${ackSuffix}`);
496
+ },
497
+ });
498
+ } finally {
499
+ process.removeListener("exit", cleanupLease);
500
+ cleanupLease();
501
+ }
502
+ }
503
+
414
504
  /**
415
505
  * 确认消息
416
506
  */
@@ -429,6 +519,24 @@ class EventBus {
429
519
  return count;
430
520
  }
431
521
 
522
+ /**
523
+ * Confirm only the displayed portion of a sequenced pending queue.
524
+ */
525
+ async ackThrough(subscriber, throughSeq) {
526
+ this.ensureBus();
527
+ this.loadBusData();
528
+
529
+ const count = await this.messageManager.ackThrough(subscriber, throughSeq);
530
+
531
+ if (count > 0) {
532
+ logOk(`Acknowledged ${count} message(s) through seq=${throughSeq}`);
533
+ } else {
534
+ logOk(`No pending messages through seq=${throughSeq}`);
535
+ }
536
+
537
+ return count;
538
+ }
539
+
432
540
  /**
433
541
  * 消费事件
434
542
  */
@@ -529,6 +529,10 @@ class MessageManager {
529
529
  return this.queueManager.ackPending(subscriber);
530
530
  }
531
531
 
532
+ async ackThrough(subscriber, throughSeq) {
533
+ return this.queueManager.ackPendingThrough(subscriber, throughSeq);
534
+ }
535
+
532
536
  /**
533
537
  * 消费事件(从 offset 开始)
534
538
  */
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ const BUILTIN_DELIVERY_AGENT_TYPES = new Set([
7
+ "agy",
8
+ "antigravity",
9
+ "claude",
10
+ "claude-code",
11
+ "codex",
12
+ "kimi",
13
+ "kimi-cli",
14
+ "kimi-code",
15
+ "ucode",
16
+ "ufoo",
17
+ "ufoo-agent",
18
+ "ufoo-code",
19
+ ]);
20
+
21
+ function normalizeAgentType(value = "") {
22
+ return String(value || "").trim().toLowerCase();
23
+ }
24
+
25
+ function resolveSubscriberAgentType(subscriber, meta = null) {
26
+ const explicit = normalizeAgentType(meta && meta.agent_type);
27
+ if (explicit) return explicit;
28
+ const id = String(subscriber || "").trim();
29
+ const separator = id.indexOf(":");
30
+ return normalizeAgentType(separator === -1 ? id : id.slice(0, separator));
31
+ }
32
+
33
+ function assertFollowPollAllowed(subscriber, meta = null) {
34
+ const agentType = resolveSubscriberAgentType(subscriber, meta);
35
+ if (BUILTIN_DELIVERY_AGENT_TYPES.has(agentType)) {
36
+ throw new Error(
37
+ `poll --follow is disabled for built-in delivery agent type "${agentType}"`
38
+ );
39
+ }
40
+ return agentType;
41
+ }
42
+
43
+ function eventIdentity(event = {}) {
44
+ const seq = Number(event && event.seq);
45
+ if (Number.isFinite(seq) && seq > 0) {
46
+ return `seq:${seq}`;
47
+ }
48
+ return `event:${JSON.stringify(event || {})}`;
49
+ }
50
+
51
+ function enumerateEventKeys(events = []) {
52
+ const occurrences = new Map();
53
+ return events.map((event) => {
54
+ const identity = eventIdentity(event);
55
+ const occurrence = occurrences.get(identity) || 0;
56
+ occurrences.set(identity, occurrence + 1);
57
+ return {
58
+ event,
59
+ key: `${identity}#${occurrence}`,
60
+ };
61
+ });
62
+ }
63
+
64
+ function defaultSleep(ms) {
65
+ return new Promise((resolve) => setTimeout(resolve, ms));
66
+ }
67
+
68
+ function defaultIsPidAlive(pid) {
69
+ try {
70
+ process.kill(pid, 0);
71
+ return true;
72
+ } catch (err) {
73
+ return Boolean(err && err.code === "EPERM");
74
+ }
75
+ }
76
+
77
+ function acquirePollLease(pidFile, options = {}) {
78
+ const pid = Number(options.pid) || process.pid;
79
+ const isAlive = typeof options.isAlive === "function"
80
+ ? options.isAlive
81
+ : defaultIsPidAlive;
82
+
83
+ fs.mkdirSync(path.dirname(pidFile), { recursive: true });
84
+
85
+ if (fs.existsSync(pidFile)) {
86
+ let existing = 0;
87
+ try {
88
+ existing = Number.parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
89
+ } catch {
90
+ existing = 0;
91
+ }
92
+ if (Number.isFinite(existing) && existing > 0 && isAlive(existing)) {
93
+ throw new Error(`poll --follow is already running (pid=${existing})`);
94
+ }
95
+ fs.rmSync(pidFile, { force: true });
96
+ }
97
+
98
+ let fd;
99
+ try {
100
+ fd = fs.openSync(pidFile, "wx");
101
+ fs.writeFileSync(fd, `${pid}\n`, "utf8");
102
+ } catch (err) {
103
+ if (err && err.code === "EEXIST") {
104
+ throw new Error("poll --follow is already starting for this subscriber");
105
+ }
106
+ throw err;
107
+ } finally {
108
+ if (typeof fd === "number") fs.closeSync(fd);
109
+ }
110
+
111
+ return { pid, pidFile };
112
+ }
113
+
114
+ function releasePollLease(lease) {
115
+ if (!lease || !lease.pidFile) return false;
116
+ try {
117
+ const existing = Number.parseInt(
118
+ fs.readFileSync(lease.pidFile, "utf8").trim(),
119
+ 10
120
+ );
121
+ if (existing !== lease.pid) return false;
122
+ fs.rmSync(lease.pidFile, { force: true });
123
+ return true;
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ async function runPendingPoll(options = {}) {
130
+ const readPending = options.readPending;
131
+ const onEvents = options.onEvents;
132
+ const sleep = options.sleep || defaultSleep;
133
+ const signal = options.signal || null;
134
+ const intervalMs = Math.max(250, Number(options.intervalMs) || 2000);
135
+ const maxIterations = Number.isFinite(options.maxIterations)
136
+ ? Math.max(0, Math.floor(options.maxIterations))
137
+ : Infinity;
138
+
139
+ if (typeof readPending !== "function") {
140
+ throw new Error("runPendingPoll requires readPending");
141
+ }
142
+ if (typeof onEvents !== "function") {
143
+ throw new Error("runPendingPoll requires onEvents");
144
+ }
145
+ if (maxIterations === 0) {
146
+ return { iterations: 0 };
147
+ }
148
+
149
+ let inFlightKeys = new Set();
150
+ let iterations = 0;
151
+
152
+ while (!signal || !signal.aborted) {
153
+ // eslint-disable-next-line no-await-in-loop
154
+ const pending = await readPending();
155
+ const keyed = enumerateEventKeys(Array.isArray(pending) ? pending : []);
156
+ const currentKeys = new Set(keyed.map((entry) => entry.key));
157
+ const hasUnacknowledgedBatch = Array.from(inFlightKeys)
158
+ .some((key) => currentKeys.has(key));
159
+
160
+ if (!hasUnacknowledgedBatch) {
161
+ inFlightKeys = new Set();
162
+ }
163
+
164
+ if (inFlightKeys.size === 0 && keyed.length > 0) {
165
+ const batch = keyed.map((entry) => entry.event);
166
+ // eslint-disable-next-line no-await-in-loop
167
+ await onEvents(batch);
168
+ inFlightKeys = currentKeys;
169
+ }
170
+
171
+ iterations += 1;
172
+ if (iterations >= maxIterations || (signal && signal.aborted)) break;
173
+
174
+ // eslint-disable-next-line no-await-in-loop
175
+ await sleep(intervalMs);
176
+ }
177
+
178
+ return { iterations };
179
+ }
180
+
181
+ module.exports = {
182
+ BUILTIN_DELIVERY_AGENT_TYPES,
183
+ acquirePollLease,
184
+ assertFollowPollAllowed,
185
+ enumerateEventKeys,
186
+ eventIdentity,
187
+ releasePollLease,
188
+ resolveSubscriberAgentType,
189
+ runPendingPoll,
190
+ };
@@ -5,7 +5,11 @@ const {
5
5
  ensureDir,
6
6
  truncateFile,
7
7
  } = require("./utils");
8
- const { DeliveryQueue, stripQueueEnvelope } = require("./deliveryQueue");
8
+ const {
9
+ DeliveryQueue,
10
+ positiveSeq,
11
+ stripQueueEnvelope,
12
+ } = require("./deliveryQueue");
9
13
 
10
14
  /**
11
15
  * 队列管理器
@@ -87,6 +91,15 @@ class QueueManager {
87
91
  return this.getDeliveryQueue(subscriber).readPending().map(stripQueueEnvelope);
88
92
  }
89
93
 
94
+ /**
95
+ * Non-mutating pending read for opt-in background observers.
96
+ *
97
+ * Unlike readPending(), this deliberately does not recover stale claims.
98
+ */
99
+ async peekPending(subscriber) {
100
+ return this.getDeliveryQueue(subscriber).readPendingRaw().map(stripQueueEnvelope);
101
+ }
102
+
90
103
  /**
91
104
  * 追加待处理消息
92
105
  */
@@ -122,6 +135,36 @@ class QueueManager {
122
135
  return count;
123
136
  }
124
137
 
138
+ /**
139
+ * Acknowledge only sequenced events up to and including throughSeq.
140
+ *
141
+ * Later arrivals remain pending, which prevents a background poll consumer
142
+ * from clearing messages that were not part of the emitted batch.
143
+ */
144
+ async ackPendingThrough(subscriber, throughSeq) {
145
+ const limit = Number(throughSeq);
146
+ if (!Number.isFinite(limit) || limit <= 0) {
147
+ throw new Error("ack --through requires a positive sequence");
148
+ }
149
+
150
+ const deliveryQueue = this.getDeliveryQueue(subscriber);
151
+ let count = 0;
152
+ while (true) {
153
+ const claim = deliveryQueue.claimNext();
154
+ if (!claim) break;
155
+
156
+ const seq = positiveSeq(claim.event);
157
+ if (seq === 0 || seq > limit) {
158
+ deliveryQueue.restoreClaim(claim);
159
+ break;
160
+ }
161
+
162
+ deliveryQueue.completeClaim(claim);
163
+ count += 1;
164
+ }
165
+ return count;
166
+ }
167
+
125
168
  /**
126
169
  * 检查是否有待处理消息
127
170
  */
@@ -95,7 +95,7 @@ class BusStore {
95
95
  ensure() {
96
96
  if (!fs.existsSync(this.busDir) || !fs.existsSync(this.paths.agentDir)) {
97
97
  throw new Error(
98
- "Event bus not initialized. Please run: ufoo bus init or /uinit"
98
+ "Event bus not initialized. Please run: ufoo init --targets bus"
99
99
  );
100
100
  }
101
101
  }
@@ -86,7 +86,7 @@ class ContextDoctor {
86
86
  * Lint bundled context skill.
87
87
  */
88
88
  lintProtocol() {
89
- const repoSkill = path.join(this.projectRoot, "SKILLS", "uctx", "SKILL.md");
89
+ const repoSkill = path.join(this.projectRoot, "SKILLS", "ufoo-context", "SKILL.md");
90
90
 
91
91
  if (!fs.existsSync(repoSkill)) {
92
92
  console.log("No bundled context skill found (skipping protocol lint)");
@@ -94,7 +94,7 @@ class ContextDoctor {
94
94
  }
95
95
 
96
96
  console.log(`Linting bundled context skill: ${repoSkill}`);
97
- this.checkFile(repoSkill, "SKILLS/uctx/SKILL.md");
97
+ this.checkFile(repoSkill, "SKILLS/ufoo-context/SKILL.md");
98
98
 
99
99
  return !this.failed;
100
100
  }
@@ -1,250 +0,0 @@
1
- ---
2
- name: ubus
3
- description: |
4
- Check and handle pending event-bus messages when /ubus is explicitly invoked.
5
- Use when: (1) asked to check messages, (2) view bus status, (3) use watch/listen/auto modes.
6
- If not yet joined bus, will auto-join.
7
- ---
8
-
9
- # /ubus - Check Event Bus Messages
10
-
11
- Check and handle pending messages on the event bus when `/ubus` is explicitly
12
- invoked.
13
-
14
- ## Arguments
15
-
16
- - `/ubus` - Pull pending messages and show status
17
- - `/ubus watch` - Start background auto-notification (title badge + bell + notification center)
18
- - `/ubus stop` - Stop background auto-notification
19
- - `/ubus listen` - Foreground continuous listener, print new messages (suitable for side terminal)
20
- - `/ubus auto` - Unattended auto-execute (auto-inject `/ubus` and press Enter)
21
-
22
- ## Execution Flow
23
-
24
- ### 1. Check if .ufoo/bus exists
25
-
26
- ```bash
27
- if [[ ! -d ".ufoo/bus" ]]; then
28
- echo "Event bus not initialized, please run /uinit and select bus module"
29
- exit
30
- fi
31
- ```
32
-
33
- ### 2. Get or create subscriber ID
34
-
35
- **IMPORTANT**: Always check for existing subscriber ID first to avoid creating duplicates.
36
-
37
- ```bash
38
- # Reuse existing subscriber first (env -> whoami), join only if missing
39
- SUBSCRIBER="${UFOO_SUBSCRIBER_ID:-$(ufoo bus whoami 2>/dev/null || true)}"
40
- if [ -n "$SUBSCRIBER" ]; then
41
- echo "Using existing subscriber ID: $SUBSCRIBER"
42
- else
43
- # Not launched via uclaude/ucodex, need to join manually
44
- SUBSCRIBER=$(ufoo bus join | tail -n 1)
45
- echo "Joined event bus: $SUBSCRIBER"
46
- # Example output: codex:0e293156 (nickname: codex-1)
47
- fi
48
- ```
49
-
50
- **Why this matters**:
51
- - `uclaude`/`ucodex` automatically set `UFOO_SUBSCRIBER_ID` during launch
52
- - `ufoo bus whoami` can recover current ID even when env is missing
53
- - Re-joining may create identity drift and message routing issues
54
- - Always reuse existing ID when available
55
-
56
- To join with a custom nickname:
57
-
58
- ```bash
59
- ufoo bus join [session-id] [agent-type] "your-nickname"
60
- # Example: ufoo bus join abc123 claude-code "architect"
61
- ```
62
-
63
- ### 3. Handle arguments
64
-
65
- If argument is `watch`, use **Bash tool's `run_in_background: true`** to start background notification:
66
-
67
- ```bash
68
- # Title badge + bell + notification center (no accessibility permission needed)
69
- ufoo bus alert "$SUBSCRIBER" 2 --notify --daemon
70
- ```
71
-
72
- If argument is `listen`, foreground blocking listener (no background task tool needed):
73
-
74
- ```bash
75
- ufoo bus listen "$SUBSCRIBER" --from-beginning
76
- ```
77
-
78
- If argument is `auto`, use unattended auto-execute:
79
-
80
- ```bash
81
- # Start daemon (background resident), auto-inject /ubus + Enter on new message
82
- ufoo bus daemon --daemon
83
- ```
84
-
85
- Tips:
86
- - Need to use `uclaude`/`ucodex` wrapper to start Claude Code/Codex (auto-records tty)
87
- - Terminal.app needs Accessibility permission (for keyboard input injection)
88
-
89
- If argument is `stop`, stop background notification:
90
-
91
- ```bash
92
- ufoo bus alert "$SUBSCRIBER" --stop
93
- ```
94
-
95
- ### 4. Check pending events
96
-
97
- ```bash
98
- ufoo bus check "$SUBSCRIBER"
99
- ```
100
-
101
- The system automatically prefixes each message with `[ufoo]<from:id(nickname)>` to identify the sender. You do not need to add this prefix yourself.
102
-
103
- If pending events exist, output looks like:
104
-
105
- ```
106
- [ufoo]<from:claude-code:abc123(architect)>
107
- Type: message/targeted/message
108
- Content: {"message":"review src/main.ts","injection_mode":"immediate"}
109
- ```
110
-
111
- - The sender ID and nickname are in the `[ufoo]<from:...>` line — use the ID to reply
112
- - The actual task is in `Content.message`
113
-
114
- ### 5. IMPORTANT: Acknowledge messages after handling
115
-
116
- After you have read and processed the messages, you MUST acknowledge them to prevent repeated notifications:
117
-
118
- ```bash
119
- ufoo bus ack "$SUBSCRIBER"
120
- ```
121
-
122
- **This is critical** - if you don't ack, the runtime may retry delivery or keep
123
- the event pending.
124
-
125
- **Default behavior is ack-only, no reply.** If there's nothing to do (no actionable task, no question to answer, no follow-up the sender genuinely needs), just ack and stop. Silence is a valid response — see "Handling Received Messages" below for when a reply IS warranted.
126
-
127
- ### 6. Routing Override
128
-
129
- If the message explicitly instructs you to report to a specific PM/DEV/TEST ID, **send the result to that ID instead of the publisher**.
130
-
131
- ### 5. Show bus status
132
-
133
- ```bash
134
- ufoo bus status
135
- ```
136
-
137
- Output (now includes nicknames):
138
-
139
- ```
140
- === Event Bus Status ===
141
- My identity: claude-code:xyz789
142
- Online agents: 2
143
- - claude-code:abc123 (architect)
144
- - claude-code:xyz789 (dev-lead)
145
- Recent events: 5
146
- ```
147
-
148
- ## Managing Nicknames
149
-
150
- ### View and Change Nicknames
151
-
152
- ```bash
153
- # Change an agent's nickname
154
- ufoo bus rename <subscriber-id> "new-nickname"
155
- # Example: ufoo bus rename claude-code:47b1d525 "backend-dev"
156
-
157
- # Nickname alias command
158
- ufoo bus nick <subscriber-id> "new-nickname"
159
- ```
160
-
161
- **Important Notes:**
162
- - Nicknames must be globally unique
163
- - Cannot change nickname during join (use `rename` command instead)
164
- - Re-joining with same subscriber ID will reuse existing nickname
165
- - Auto-generated nicknames: `codex-1`, `codex-2`, `claude-1`, `claude-2`, etc.
166
-
167
- ## Handling Received Messages
168
-
169
- When receiving targeted messages, the default flow is **execute → ack → stop**.
170
- Replies are the exception, not the default.
171
-
172
- 1. **Understand request** — Read message content.
173
- 2. **Execute task** — If the message delegates a task, do it.
174
- 3. **`ufoo bus ack "$SUBSCRIBER"`** — Always ack, even when not replying.
175
- 4. **Reply ONLY when substantive.** Send `ufoo bus send` to the sender only if at least one of the following is true:
176
- - The sender asked a question → reply with the answer.
177
- - The sender delegated a task → reply with the result / artifact / status.
178
- - You discovered something the sender needs to proceed → reply with that fact.
179
-
180
- ```bash
181
- # Use this only when the criteria above are met.
182
- ufoo bus send "<sender-id>" "<substantive-reply>"
183
- ```
184
-
185
- ### Anti-pattern: greet / ack loops
186
-
187
- If the inbound message is itself just a greeting, an acknowledgment, or a
188
- pleasantry, **do not reply**. Acking is enough. A bare-acknowledgment reply
189
- will be auto-injected on the other side, triggering them to reply in kind,
190
- and the two of you will ping-pong forever.
191
-
192
- | Inbound | Reply? |
193
- |---|---|
194
- | `👋` / `hi` / `hello` / `你好` | ❌ ack only |
195
- | `👍` / `ok` / `收到` / `thanks` / `noted` | ❌ ack only |
196
- | `已完成 / done / finished` (without a result the sender asked for) | ❌ ack only |
197
- | `请把 src/foo.ts 改成 ...` (task) | ✅ reply with result |
198
- | `这个 bug 的根因是什么?` (question) | ✅ reply with answer |
199
- | `我帮你找到了 X,需要你做 Y` (request) | ✅ reply with status |
200
-
201
- When in doubt: ack and stop. If the sender genuinely needs something from you,
202
- they will follow up with a concrete question or task.
203
-
204
- ## Sending Messages
205
-
206
- After sending a message, do not run `/ubus`, poll, sleep, or wait for a reply.
207
- Continue the current task. Any follow-up message will be automatically injected
208
- into your prompt/session.
209
-
210
- ### Smart Routing (when you don't know the target ID)
211
-
212
- If the user says "notify codex to do X" without specifying an ID, use smart routing:
213
-
214
- ```bash
215
- # Step 1: Find candidates
216
- ufoo bus resolve "$SUBSCRIBER" codex
217
-
218
- # Output shows:
219
- # - If only 1 codex: directly shows the ID
220
- # - If multiple: shows each with nickname and message history
221
- ```
222
-
223
- Based on the output:
224
- - **Single match**: Use that ID directly
225
- - **Multiple matches**: Analyze the message history to find the right target
226
- - Look for context clues in previous conversations
227
- - If still unclear, ask the user which one, or send to all of that type
228
-
229
- ### Direct Send
230
-
231
- ```bash
232
- # Send to specific Agent by full ID
233
- ufoo bus send "claude-code:abc123" "message content"
234
-
235
- # Send to specific Agent by nickname (NEW!)
236
- ufoo bus send "architect" "message content"
237
- ufoo bus send "backend-dev" "message content"
238
-
239
- # Send to all Agents of same type
240
- ufoo bus send "codex" "message content"
241
-
242
- # Broadcast to everyone
243
- ufoo bus broadcast "message content"
244
- ```
245
-
246
- **Target Resolution Priority:**
247
- 1. Exact subscriber ID (e.g., `claude-code:abc123`)
248
- 2. Nickname match (e.g., `architect` → resolves to subscriber ID)
249
- 3. Agent type (e.g., `codex` → all codex agents)
250
- 4. Wildcard (`*` → all agents)