u-foo 1.0.3 → 1.0.6

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 (91) hide show
  1. package/README.md +67 -8
  2. package/README.zh-CN.md +9 -7
  3. package/SKILLS/ufoo/SKILL.md +117 -0
  4. package/SKILLS/uinit/SKILL.md +73 -0
  5. package/SKILLS/ustatus/SKILL.md +36 -0
  6. package/bin/uclaude.js +13 -0
  7. package/bin/ucodex.js +13 -0
  8. package/bin/ufoo +9 -31
  9. package/bin/ufoo.js +13 -0
  10. package/modules/AGENTS.template.md +15 -7
  11. package/modules/bus/README.md +28 -23
  12. package/modules/bus/SKILLS/ubus/SKILL.md +18 -8
  13. package/modules/context/README.md +18 -40
  14. package/modules/context/SKILLS/uctx/SKILL.md +61 -1
  15. package/package.json +16 -4
  16. package/scripts/.archived/bash-to-js-migration/README.md +46 -0
  17. package/scripts/.archived/bash-to-js-migration/banner.sh +89 -0
  18. package/scripts/{bus-inject.sh → .archived/bash-to-js-migration/bus-inject.sh} +35 -3
  19. package/scripts/{bus.sh → .archived/bash-to-js-migration/bus.sh} +3 -1
  20. package/scripts/banner.sh +2 -89
  21. package/scripts/postinstall.js +59 -0
  22. package/src/agent/cliRunner.js +33 -5
  23. package/src/agent/internalRunner.js +78 -51
  24. package/src/agent/launcher.js +702 -0
  25. package/src/agent/notifier.js +200 -0
  26. package/src/agent/ptyRunner.js +377 -0
  27. package/src/agent/ptyWrapper.js +354 -0
  28. package/src/agent/readyDetector.js +159 -0
  29. package/src/agent/ufooAgent.js +37 -42
  30. package/src/bus/API_DESIGN.md +204 -0
  31. package/src/bus/activate.js +156 -0
  32. package/src/bus/daemon.js +308 -0
  33. package/src/bus/index.js +785 -0
  34. package/src/bus/inject.js +285 -0
  35. package/src/bus/message.js +302 -0
  36. package/src/bus/nickname.js +86 -0
  37. package/src/bus/queue.js +131 -0
  38. package/src/bus/shake.js +26 -0
  39. package/src/bus/subscriber.js +296 -0
  40. package/src/bus/utils.js +357 -0
  41. package/src/chat/index.js +1842 -249
  42. package/src/cli.js +658 -95
  43. package/src/config.js +9 -2
  44. package/src/context/decisions.js +314 -0
  45. package/src/context/doctor.js +183 -0
  46. package/src/context/index.js +38 -0
  47. package/src/daemon/index.js +749 -94
  48. package/src/daemon/ops.js +395 -51
  49. package/src/daemon/providerSessions.js +291 -0
  50. package/src/daemon/run.js +34 -1
  51. package/src/daemon/status.js +24 -7
  52. package/src/doctor/index.js +50 -0
  53. package/src/init/index.js +264 -0
  54. package/src/skills/index.js +159 -0
  55. package/src/status/index.js +252 -0
  56. package/src/terminal/detect.js +64 -0
  57. package/src/terminal/index.js +8 -0
  58. package/src/terminal/iterm2.js +126 -0
  59. package/src/ufoo/agentsStore.js +41 -0
  60. package/src/ufoo/paths.js +46 -0
  61. package/src/utils/banner.js +73 -0
  62. package/bin/uclaude +0 -65
  63. package/bin/ucodex +0 -65
  64. package/modules/bus/scripts/bus-alert.sh +0 -185
  65. package/modules/bus/scripts/bus-listen.sh +0 -117
  66. package/modules/context/ASSUMPTIONS.md +0 -7
  67. package/modules/context/CONSTRAINTS.md +0 -7
  68. package/modules/context/CONTEXT-STRUCTURE.md +0 -49
  69. package/modules/context/DECISION-PROTOCOL.md +0 -62
  70. package/modules/context/HANDOFF.md +0 -33
  71. package/modules/context/RULES.md +0 -15
  72. package/modules/context/SKILLS/README.md +0 -14
  73. package/modules/context/SYSTEM.md +0 -18
  74. package/modules/context/TEMPLATES/assumptions.md +0 -4
  75. package/modules/context/TEMPLATES/constraints.md +0 -4
  76. package/modules/context/TEMPLATES/decision.md +0 -16
  77. package/modules/context/TEMPLATES/project-context-readme.md +0 -6
  78. package/modules/context/TEMPLATES/system.md +0 -3
  79. package/modules/context/TEMPLATES/terminology.md +0 -4
  80. package/modules/context/TERMINOLOGY.md +0 -10
  81. /package/scripts/{bus-alert.sh → .archived/bash-to-js-migration/bus-alert.sh} +0 -0
  82. /package/scripts/{bus-autotrigger.sh → .archived/bash-to-js-migration/bus-autotrigger.sh} +0 -0
  83. /package/scripts/{bus-daemon.sh → .archived/bash-to-js-migration/bus-daemon.sh} +0 -0
  84. /package/scripts/{bus-listen.sh → .archived/bash-to-js-migration/bus-listen.sh} +0 -0
  85. /package/scripts/{context-decisions.sh → .archived/bash-to-js-migration/context-decisions.sh} +0 -0
  86. /package/scripts/{context-doctor.sh → .archived/bash-to-js-migration/context-doctor.sh} +0 -0
  87. /package/scripts/{context-lint.sh → .archived/bash-to-js-migration/context-lint.sh} +0 -0
  88. /package/scripts/{doctor.sh → .archived/bash-to-js-migration/doctor.sh} +0 -0
  89. /package/scripts/{init.sh → .archived/bash-to-js-migration/init.sh} +0 -0
  90. /package/scripts/{skills.sh → .archived/bash-to-js-migration/skills.sh} +0 -0
  91. /package/scripts/{status.sh → .archived/bash-to-js-migration/status.sh} +0 -0
@@ -0,0 +1,354 @@
1
+ const pty = require("node-pty");
2
+ const fs = require("fs");
3
+ const { isITerm2 } = require("../terminal/detect");
4
+
5
+ /**
6
+ * PTY Wrapper - 包装原始agent命令,提供IO控制和监控
7
+ *
8
+ * 特性:
9
+ * - 透明的PTY包装(保持用户体验一致)
10
+ * - JSONL格式日志记录(utf8优先编码)
11
+ * - 可插拔的IO适配器(未来扩展)
12
+ * - 完善的资源清理(防止泄漏)
13
+ *
14
+ * 参考:
15
+ * - ptyRunner.js - PTY实现参考
16
+ * - codex-44 review反馈 (2026-02-05)
17
+ */
18
+ class PtyWrapper {
19
+ constructor(command, args, options = {}) {
20
+ this.command = command;
21
+ this.args = args;
22
+ this.options = options;
23
+
24
+ // PTY实例
25
+ this.pty = null;
26
+
27
+ // IO流引用(用于cleanup)
28
+ this.stdin = null;
29
+ this.stdout = null;
30
+
31
+ // 日志记录
32
+ this.logger = null;
33
+
34
+ // 监控回调
35
+ this.monitor = null;
36
+
37
+ // 退出回调(不直接调用process.exit)
38
+ this.onExit = null;
39
+
40
+ // 可插拔的IO适配器(未来扩展)
41
+ this.ioAdapter = options.ioAdapter || null;
42
+
43
+ // 事件处理器引用(用于cleanup)
44
+ this._stdinHandler = null;
45
+ this._ptyDataHandler = null;
46
+ this._ptyExitHandler = null;
47
+ this._resizeHandler = null;
48
+
49
+ // 清理标志(防止重复清理)
50
+ this._cleaned = false;
51
+ }
52
+
53
+ /**
54
+ * 启动PTY进程
55
+ */
56
+ spawn() {
57
+ if (this.pty) {
58
+ throw new Error("PTY already spawned");
59
+ }
60
+
61
+ // Preserve iTerm2 env vars so child processes can detect the terminal
62
+ const termEnv = {};
63
+ if (isITerm2()) {
64
+ if (process.env.ITERM_SESSION_ID) termEnv.ITERM_SESSION_ID = process.env.ITERM_SESSION_ID;
65
+ if (process.env.TERM_PROGRAM) termEnv.TERM_PROGRAM = process.env.TERM_PROGRAM;
66
+ if (process.env.TERM_PROGRAM_VERSION) termEnv.TERM_PROGRAM_VERSION = process.env.TERM_PROGRAM_VERSION;
67
+ }
68
+
69
+ this.pty = pty.spawn(this.command, this.args, {
70
+ name: "xterm-256color",
71
+ cols: this.stdout?.columns || process.stdout.columns || 80,
72
+ rows: this.stdout?.rows || process.stdout.rows || 24,
73
+ cwd: this.options.cwd || process.cwd(),
74
+ env: { ...process.env, ...termEnv, ...(this.options.env || {}) },
75
+ });
76
+
77
+ return this.pty;
78
+ }
79
+
80
+ /**
81
+ * 连接输入输出流
82
+ *
83
+ * @param {Stream} stdin - 标准输入流
84
+ * @param {Stream} stdout - 标准输出流
85
+ * @param {Stream} stderr - 标准错误流(PTY会合流,此参数保留用于兼容)
86
+ */
87
+ attachStreams(stdin, stdout, stderr) {
88
+ if (!this.pty) {
89
+ throw new Error("PTY not spawned yet. Call spawn() first.");
90
+ }
91
+
92
+ // 保存引用(用于cleanup)
93
+ this.stdin = stdin;
94
+ this.stdout = stdout;
95
+
96
+ if (this.ioAdapter) {
97
+ // 使用IO适配器(未来扩展)
98
+ this.ioAdapter.attach(this.pty, stdin, stdout, stderr);
99
+ } else {
100
+ // 当前:直接连接streams
101
+ this._attachDirectStreams(stdin, stdout);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * 直接连接streams(当前实现)
107
+ * @private
108
+ */
109
+ _attachDirectStreams(stdin, stdout) {
110
+ // PTY输出 -> stdout
111
+ this._ptyDataHandler = (data) => {
112
+ // 1. 输出到terminal
113
+ stdout.write(data);
114
+
115
+ // 2. 可选:日志记录(JSONL格式)
116
+ if (this.logger) {
117
+ const logEntry = {
118
+ ts: Date.now(),
119
+ dir: "out",
120
+ data: this._serializeData(data),
121
+ };
122
+ this.logger.write(JSON.stringify(logEntry) + "\n");
123
+ }
124
+
125
+ // 3. 可选:监控回调
126
+ if (this.monitor) {
127
+ try {
128
+ this.monitor.onOutput(data);
129
+ } catch (err) {
130
+ // 监控失败不应影响IO通路
131
+ if (process.env.UFOO_DEBUG) {
132
+ console.error("[PtyWrapper] Monitor error:", err);
133
+ }
134
+ }
135
+ }
136
+ };
137
+ this.pty.onData(this._ptyDataHandler);
138
+
139
+ // stdin -> PTY(支持raw mode和控制字符)
140
+ if (stdin.isTTY && typeof stdin.setRawMode === "function") {
141
+ stdin.setRawMode(true);
142
+ }
143
+ stdin.resume();
144
+
145
+ this._stdinHandler = (data) => {
146
+ this.pty.write(data);
147
+
148
+ // 可选:日志记录
149
+ if (this.logger) {
150
+ const logEntry = {
151
+ ts: Date.now(),
152
+ dir: "in",
153
+ data: this._serializeData(data),
154
+ source: "terminal",
155
+ };
156
+ this.logger.write(JSON.stringify(logEntry) + "\n");
157
+ }
158
+ };
159
+ stdin.on("data", this._stdinHandler);
160
+
161
+ // 终端大小变化(codex-44:判断isTTY)
162
+ if (stdout.isTTY) {
163
+ this._resizeHandler = () => {
164
+ if (this.pty && !this.pty._closed) {
165
+ // codex-45:默认值兜底(极端环境可能undefined)
166
+ const cols = stdout.columns || 80;
167
+ const rows = stdout.rows || 24;
168
+ this.pty.resize(cols, rows);
169
+ }
170
+ };
171
+ stdout.on("resize", this._resizeHandler);
172
+ }
173
+
174
+ // 进程退出
175
+ this._ptyExitHandler = ({ exitCode, signal }) => {
176
+ this.cleanup();
177
+
178
+ // 回调给launcher处理退出(不直接process.exit)
179
+ if (this.onExit) {
180
+ this.onExit({ exitCode, signal });
181
+ }
182
+ };
183
+ this.pty.onExit(this._ptyExitHandler);
184
+ }
185
+
186
+ /**
187
+ * 写入数据到PTY(用于外部inject)
188
+ *
189
+ * @param {string|Buffer} data - 要写入的数据
190
+ * @returns {boolean} 是否成功
191
+ */
192
+ write(data) {
193
+ if (!this.pty || this.pty._closed) {
194
+ return false;
195
+ }
196
+ try {
197
+ this.pty.write(data);
198
+ return true;
199
+ } catch (err) {
200
+ if (process.env.UFOO_DEBUG) {
201
+ console.error(`[PtyWrapper] write error: ${err.message}`);
202
+ }
203
+ return false;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * 数据序列化(智能编码)
209
+ *
210
+ * codex-44建议:utf8优先,失败才base64
211
+ *
212
+ * @private
213
+ * @param {Buffer|String} data - 原始数据
214
+ * @returns {Object} 序列化后的数据对象
215
+ */
216
+ _serializeData(data) {
217
+ const buf = Buffer.from(data);
218
+
219
+ // 尝试utf8解码
220
+ try {
221
+ const str = buf.toString("utf8");
222
+ // 验证解码结果(检测replacement character \uFFFD)
223
+ if (!str.includes("\uFFFD")) {
224
+ return {
225
+ text: str,
226
+ encoding: "utf8",
227
+ size: buf.length
228
+ };
229
+ }
230
+ } catch (err) {
231
+ // utf8解码失败,使用base64
232
+ }
233
+
234
+ // 二进制数据使用base64
235
+ return {
236
+ text: buf.toString("base64"),
237
+ encoding: "base64",
238
+ size: buf.length
239
+ };
240
+ }
241
+
242
+ /**
243
+ * 启用日志记录
244
+ *
245
+ * @param {String} logFile - 日志文件路径(JSONL格式)
246
+ */
247
+ enableLogging(logFile) {
248
+ if (this.logger) {
249
+ throw new Error("Logging already enabled");
250
+ }
251
+ this.logger = fs.createWriteStream(logFile, { flags: "a" });
252
+ }
253
+
254
+ /**
255
+ * 启用监控
256
+ *
257
+ * @param {Function} monitorCallback - 监控回调函数
258
+ */
259
+ enableMonitoring(monitorCallback) {
260
+ this.monitor = {
261
+ onOutput: monitorCallback,
262
+ };
263
+ }
264
+
265
+ /**
266
+ * 清理资源(codex-44:完善的清理逻辑,codex-45:幂等性)
267
+ *
268
+ * 注意:
269
+ * - 处理异常路径(try-catch)
270
+ * - 移除所有监听器(防止泄漏)
271
+ * - 检查PTY状态(已退出则跳过kill)
272
+ * - 恢复terminal状态(raw mode)
273
+ * - 幂等性(可以安全重复调用)
274
+ */
275
+ cleanup() {
276
+ // codex-45:防止重复清理
277
+ if (this._cleaned) {
278
+ return;
279
+ }
280
+ this._cleaned = true;
281
+
282
+ // 1. 关闭日志流
283
+ if (this.logger) {
284
+ try {
285
+ this.logger.end();
286
+ } catch (err) {
287
+ // 忽略错误
288
+ }
289
+ this.logger = null;
290
+ }
291
+
292
+ // 2. 清理PTY(codex-44:已退出则跳过,codex-45:移除监听器)
293
+ if (this.pty) {
294
+ // 移除PTY监听器
295
+ try {
296
+ if (this._ptyDataHandler) {
297
+ this.pty.removeListener("data", this._ptyDataHandler);
298
+ this._ptyDataHandler = null;
299
+ }
300
+ if (this._ptyExitHandler) {
301
+ this.pty.removeListener("exit", this._ptyExitHandler);
302
+ this._ptyExitHandler = null;
303
+ }
304
+ } catch (err) {
305
+ // 忽略移除监听器的错误
306
+ }
307
+
308
+ // Kill PTY进程(已退出则跳过)
309
+ if (!this.pty._closed) {
310
+ try {
311
+ this.pty.kill();
312
+ } catch (err) {
313
+ // PTY可能已经退出,忽略错误
314
+ }
315
+ }
316
+ this.pty = null;
317
+ }
318
+
319
+ // 3. 清理stdin(codex-45:使用保存的handler引用)
320
+ if (this.stdin) {
321
+ // 移除data监听器
322
+ if (this._stdinHandler) {
323
+ this.stdin.removeListener("data", this._stdinHandler);
324
+ this._stdinHandler = null;
325
+ }
326
+
327
+ // 恢复terminal状态(codex-44:异常路径也要恢复)
328
+ if (this.stdin.isTTY && typeof this.stdin.setRawMode === "function") {
329
+ try {
330
+ this.stdin.setRawMode(false);
331
+ } catch (err) {
332
+ // 恢复失败不阻塞退出
333
+ }
334
+ }
335
+
336
+ this.stdin = null;
337
+ }
338
+
339
+ // 4. 清理stdout(codex-44:移除resize监听器)
340
+ if (this.stdout) {
341
+ if (this.stdout.isTTY && this._resizeHandler) {
342
+ this.stdout.removeListener("resize", this._resizeHandler);
343
+ this._resizeHandler = null;
344
+ }
345
+ this.stdout = null;
346
+ }
347
+
348
+ // 5. 清理监控回调
349
+ this.monitor = null;
350
+ this.onExit = null;
351
+ }
352
+ }
353
+
354
+ module.exports = PtyWrapper;
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Agent Ready检测器
3
+ * 通过分析PTY输出判断agent是否初始化完成并ready接收命令
4
+ */
5
+ class ReadyDetector {
6
+ constructor(agentType) {
7
+ this.agentType = agentType;
8
+ this.ready = false;
9
+ this.buffer = ""; // 缓存最近的输出(用于多行匹配)
10
+ this.maxBufferSize = 2000; // 限制buffer大小
11
+ this.callbacks = [];
12
+ this.createdAt = Date.now(); // 用于性能指标
13
+ this.readyAt = null; // 记录ready的时间
14
+ }
15
+
16
+ /**
17
+ * 注册ready回调
18
+ */
19
+ onReady(callback) {
20
+ if (this.ready) {
21
+ // 已经ready,立即执行
22
+ callback();
23
+ } else {
24
+ this.callbacks.push(callback);
25
+ }
26
+ }
27
+
28
+ /**
29
+ * 触发ready状态
30
+ */
31
+ _triggerReady() {
32
+ if (this.ready) return;
33
+ this.ready = true;
34
+ this.readyAt = Date.now();
35
+
36
+ // 计算检测耗时
37
+ const detectionTime = this.readyAt - this.createdAt;
38
+
39
+ if (process.env.UFOO_DEBUG) {
40
+ console.error(`[ReadyDetector] ${this.agentType} ready detected in ${detectionTime}ms`);
41
+ }
42
+
43
+ this.callbacks.forEach((cb) => {
44
+ try {
45
+ cb();
46
+ } catch (err) {
47
+ // 忽略回调错误,但在debug模式下记录
48
+ if (process.env.UFOO_DEBUG) {
49
+ console.error(`[ReadyDetector] callback error:`, err);
50
+ }
51
+ }
52
+ });
53
+ this.callbacks = [];
54
+ }
55
+
56
+ /**
57
+ * 检测claude-code的ready标记
58
+ * 特征:prompt "❯" 或分隔线 "────────"
59
+ */
60
+ _detectClaudeCodeReady(text) {
61
+ // 1. 检测prompt标记(更可靠)
62
+ if (text.includes("❯")) {
63
+ return true;
64
+ }
65
+
66
+ // 2. 检测分隔线(banner完成后的标记)
67
+ if (text.includes("────────") && text.includes("Try")) {
68
+ return true;
69
+ }
70
+
71
+ return false;
72
+ }
73
+
74
+ /**
75
+ * 检测codex的ready标记
76
+ */
77
+ _detectCodexReady(text) {
78
+ // Codex的prompt检测(更严格,避免误报)
79
+ // 1. 明确的 "codex>" prompt
80
+ if (text.includes("codex>")) {
81
+ return true;
82
+ }
83
+ // 2. 行首或行尾的单独 ">" prompt(避免匹配JSON/HTML中的>)
84
+ if (/(?:^|\n)>\s*$/.test(text)) {
85
+ return true;
86
+ }
87
+ return false;
88
+ }
89
+
90
+ /**
91
+ * 处理PTY输出数据
92
+ * @param {Buffer|string} data - PTY输出数据
93
+ */
94
+ processOutput(data) {
95
+ if (this.ready) return; // 已经ready,跳过后续检测
96
+
97
+ // 跳过null/undefined
98
+ if (data == null) return;
99
+
100
+ // 转换为字符串
101
+ const text = Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
102
+
103
+ if (!text) return; // 跳过空输入
104
+
105
+ // 追加到buffer
106
+ this.buffer += text;
107
+
108
+ // 限制buffer大小(防止内存泄漏)
109
+ if (this.buffer.length > this.maxBufferSize) {
110
+ const keepSize = Math.floor(this.maxBufferSize * 0.5); // 保留50%
111
+ this.buffer = this.buffer.slice(-keepSize);
112
+
113
+ if (process.env.UFOO_DEBUG) {
114
+ console.error(`[ReadyDetector] buffer trimmed, keeping last ${keepSize} bytes`);
115
+ }
116
+ }
117
+
118
+ // 根据agentType检测ready标记
119
+ let isReady = false;
120
+ if (this.agentType === "claude-code") {
121
+ isReady = this._detectClaudeCodeReady(this.buffer);
122
+ } else if (this.agentType === "codex") {
123
+ isReady = this._detectCodexReady(this.buffer);
124
+ }
125
+
126
+ if (isReady) {
127
+ if (process.env.UFOO_DEBUG) {
128
+ console.error(`[ReadyDetector] prompt detected in buffer (${this.buffer.length} bytes)`);
129
+ }
130
+ this._triggerReady();
131
+ }
132
+ }
133
+
134
+ /**
135
+ * 强制标记为ready(用于fallback超时)
136
+ */
137
+ forceReady() {
138
+ if (process.env.UFOO_DEBUG && !this.ready) {
139
+ console.error(`[ReadyDetector] force ready triggered after ${Date.now() - this.createdAt}ms`);
140
+ }
141
+ this._triggerReady();
142
+ }
143
+
144
+ /**
145
+ * 获取性能指标
146
+ */
147
+ getMetrics() {
148
+ return {
149
+ agentType: this.agentType,
150
+ ready: this.ready,
151
+ createdAt: this.createdAt,
152
+ readyAt: this.readyAt,
153
+ detectionTimeMs: this.readyAt ? this.readyAt - this.createdAt : null,
154
+ bufferSize: this.buffer.length,
155
+ };
156
+ }
157
+ }
158
+
159
+ module.exports = ReadyDetector;
@@ -2,9 +2,11 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { runCliAgent } = require("./cliRunner");
4
4
  const { normalizeCliOutput } = require("./normalizeOutput");
5
+ const { buildStatus } = require("../daemon/status");
6
+ const { getUfooPaths } = require("../ufoo/paths");
5
7
 
6
8
  function loadSessionState(projectRoot) {
7
- const dir = path.join(projectRoot, ".ufoo", "agent");
9
+ const dir = getUfooPaths(projectRoot).agentDir;
8
10
  const file = path.join(dir, "ufoo-agent.json");
9
11
  try {
10
12
  const data = JSON.parse(fs.readFileSync(file, "utf8"));
@@ -15,52 +17,38 @@ function loadSessionState(projectRoot) {
15
17
  }
16
18
 
17
19
  function saveSessionState(projectRoot, state) {
18
- const dir = path.join(projectRoot, ".ufoo", "agent");
20
+ const dir = getUfooPaths(projectRoot).agentDir;
19
21
  fs.mkdirSync(dir, { recursive: true });
20
22
  fs.writeFileSync(path.join(dir, "ufoo-agent.json"), JSON.stringify(state, null, 2));
21
23
  }
22
24
 
23
- function isPidAlive(pid) {
24
- if (!pid || typeof pid !== "number") return false;
25
- try {
26
- process.kill(pid, 0);
27
- return true;
28
- } catch (err) {
29
- return Boolean(err && err.code === "EPERM");
30
- }
31
- }
32
-
33
25
  function loadBusSummary(projectRoot, maxLines = 20) {
34
- const busPath = path.join(projectRoot, ".ufoo", "bus", "bus.json");
35
- let subscribers = [];
26
+ // Use daemon's buildStatus as the single source of truth
27
+ let agents = [];
36
28
  let nicknames = {};
37
29
  try {
38
- const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
39
- subscribers = Object.entries(bus.subscribers || {})
40
- .map(([id, meta]) => {
41
- const pid = typeof meta.pid === "number" ? meta.pid : Number(meta.pid || 0);
42
- const status = meta.status || "unknown";
43
- const online = status === "active" && isPidAlive(pid);
44
- const nickname = meta.nickname || "";
45
- if (nickname) {
46
- nicknames[nickname] = id;
47
- }
48
- return {
49
- id,
50
- status,
51
- online,
52
- agent_type: meta.agent_type || "",
53
- nickname,
54
- last_heartbeat: meta.last_heartbeat || "",
55
- };
56
- })
57
- .filter((item) => item.online);
30
+ const status = buildStatus(projectRoot);
31
+ const activeMeta = status.active_meta || [];
32
+ agents = activeMeta.map((item) => {
33
+ const nickname = item.nickname || "";
34
+ if (nickname) {
35
+ nicknames[nickname] = item.id;
36
+ }
37
+ return {
38
+ id: item.id,
39
+ status: "active",
40
+ online: true,
41
+ agent_type: "", // Not included in active_meta, but not needed
42
+ nickname,
43
+ last_heartbeat: "",
44
+ };
45
+ });
58
46
  } catch {
59
- subscribers = [];
47
+ agents = [];
60
48
  nicknames = {};
61
49
  }
62
50
 
63
- const eventsDir = path.join(projectRoot, ".ufoo", "bus", "events");
51
+ const eventsDir = getUfooPaths(projectRoot).busEventsDir;
64
52
  let recent = [];
65
53
  try {
66
54
  const files = fs
@@ -80,10 +68,15 @@ function loadBusSummary(projectRoot, maxLines = 20) {
80
68
  recent = [];
81
69
  }
82
70
 
83
- return { subscribers, nicknames, recent };
71
+ return { agents, nicknames, recent };
84
72
  }
85
73
 
86
74
  function buildSystemPrompt(context) {
75
+ const hasAgents = context.agents && context.agents.length > 0;
76
+ const agentGuidance = hasAgents
77
+ ? ""
78
+ : "\n- IMPORTANT: No agents are currently online. To execute tasks, you MUST launch agents using ops.launch.\n- Example: {\"reply\":\"Creating agent\",\"ops\":[{\"action\":\"launch\",\"agent\":\"claude\",\"count\":1}]}";
79
+
87
80
  return [
88
81
  "You are ufoo-agent, a headless routing controller.",
89
82
  "Return ONLY valid JSON. No extra text.",
@@ -91,14 +84,16 @@ function buildSystemPrompt(context) {
91
84
  "{",
92
85
  ' "reply": "string",',
93
86
  ' "dispatch": [{"target":"broadcast|<agent-id>|<nickname>","message":"string"}],',
94
- ' "ops": [{"action":"spawn|close","agent":"codex|claude","count":1,"agent_id":"id","nickname":"optional"}],',
87
+ ' "ops": [{"action":"launch|close|rename","agent":"codex|claude","count":1,"agent_id":"id","nickname":"optional"}],',
95
88
  ' "disambiguate": {"prompt":"string","candidates":[{"agent_id":"id","reason":"string"}]}',
96
89
  "}",
97
90
  "Rules:",
98
91
  "- target must be 'broadcast', concrete agent-id, or a known nickname",
99
92
  "- If multiple possible agents, use disambiguate with candidates and no dispatch.",
100
- "- If user specifies a nickname for a new agent, include ops.spawn with nickname so daemon can rename.",
93
+ "- If user specifies a nickname for a new agent, include ops.launch with nickname so daemon can rename.",
94
+ "- If user requests rename, use ops.rename with agent_id and nickname (do NOT launch).",
101
95
  "- If no action needed, return reply with empty dispatch/ops.",
96
+ agentGuidance,
102
97
  "",
103
98
  "Context: online agents and recent bus events:",
104
99
  JSON.stringify(context),
@@ -106,7 +101,7 @@ function buildSystemPrompt(context) {
106
101
  }
107
102
 
108
103
  function loadHistory(projectRoot, maxTurns = 6) {
109
- const file = path.join(projectRoot, ".ufoo", "agent", "ufoo-agent.history.jsonl");
104
+ const file = path.join(getUfooPaths(projectRoot).agentDir, "ufoo-agent.history.jsonl");
110
105
  try {
111
106
  const lines = fs.readFileSync(file, "utf8").trim().split(/\r?\n/).filter(Boolean);
112
107
  const items = lines.map((l) => JSON.parse(l));
@@ -117,7 +112,7 @@ function loadHistory(projectRoot, maxTurns = 6) {
117
112
  }
118
113
 
119
114
  function appendHistory(projectRoot, item) {
120
- const dir = path.join(projectRoot, ".ufoo", "agent");
115
+ const dir = getUfooPaths(projectRoot).agentDir;
121
116
  fs.mkdirSync(dir, { recursive: true });
122
117
  const file = path.join(dir, "ufoo-agent.history.jsonl");
123
118
  fs.appendFileSync(file, `${JSON.stringify(item)}\n`);
@@ -200,7 +195,7 @@ async function runUfooAgent({ projectRoot, prompt, provider, model }) {
200
195
  const fallbackNickname = extractNickname(prompt);
201
196
  if (fallbackNickname && payload && Array.isArray(payload.ops)) {
202
197
  for (const op of payload.ops) {
203
- if (op && op.action === "spawn" && !op.nickname) {
198
+ if (op && (op.action === "launch" || op.action === "rename") && !op.nickname) {
204
199
  op.nickname = fallbackNickname;
205
200
  break;
206
201
  }