u-foo 2.5.1 → 2.5.3

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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/agents/internal/internalRunner.js +57 -78
  3. package/src/agents/launch/launcher.js +3 -0
  4. package/src/agents/launch/notifier.js +38 -112
  5. package/src/agents/launch/ptyRunner.js +59 -49
  6. package/src/agents/launch/ptyWrapper.js +32 -1
  7. package/src/app/cli/busCoreCommands.js +19 -1
  8. package/src/app/cli/run.js +1 -53
  9. package/src/code/agent.js +41 -140
  10. package/src/coordination/bus/deliveryQueue.js +308 -0
  11. package/src/coordination/bus/index.js +3 -27
  12. package/src/coordination/bus/message.js +35 -15
  13. package/src/coordination/bus/queue.js +23 -7
  14. package/src/runtime/daemon/deliveryScheduler.js +205 -0
  15. package/src/runtime/daemon/index.js +27 -38
  16. package/src/runtime/daemon/ops.js +1 -5
  17. package/src/runtime/daemon/reportControlBus.js +20 -43
  18. package/templates/groups/build-ultra.json +1 -1
  19. package/SKILLS/brandkit/SKILL.md +0 -798
  20. package/SKILLS/brutalist-skill/SKILL.md +0 -92
  21. package/SKILLS/gpt-tasteskill/SKILL.md +0 -74
  22. package/SKILLS/image-to-code-skill/SKILL.md +0 -1228
  23. package/SKILLS/imagegen-frontend-mobile/SKILL.md +0 -1465
  24. package/SKILLS/imagegen-frontend-web/SKILL.md +0 -987
  25. package/SKILLS/llms.txt +0 -13
  26. package/SKILLS/minimalist-skill/SKILL.md +0 -85
  27. package/SKILLS/output-skill/SKILL.md +0 -49
  28. package/SKILLS/redesign-skill/SKILL.md +0 -178
  29. package/SKILLS/soft-skill/SKILL.md +0 -98
  30. package/SKILLS/stitch-skill/DESIGN.md +0 -121
  31. package/SKILLS/stitch-skill/SKILL.md +0 -184
  32. package/SKILLS/taste-skill/SKILL.md +0 -1206
  33. package/SKILLS/taste-skill-v1/SKILL.md +0 -226
  34. package/src/coordination/bus/daemon.js +0 -535
@@ -1,535 +0,0 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const { readJSON, writeJSON, isPidAlive, isAgentPidAlive, isMetaActive, ensureDir, safeNameToSubscriber, subscriberToSafeName } = require("./utils");
4
- const Injector = require("./inject");
5
- const QueueManager = require("./queue");
6
- const MessageManager = require("./message");
7
- const { createTerminalAdapterRouter } = require("../../runtime/terminal/adapterRouter");
8
- const {
9
- INJECTION_MODES,
10
- getInjectionModeFromEvent,
11
- } = require("./messageMeta");
12
- const {
13
- buildPromptInjectionText,
14
- shouldRenderPromptEnvelope,
15
- } = require("./promptEnvelope");
16
-
17
- function isBusyActivityState(value = "") {
18
- const state = String(value || "").trim().toLowerCase();
19
- return state === "working"
20
- || state === "running"
21
- || state === "waiting_input"
22
- || state === "blocked";
23
- }
24
-
25
- /**
26
- * Bus Daemon - 监控消息并自动注入命令
27
- */
28
- class BusDaemon {
29
- constructor(busDir, agentsFile, daemonDir, interval = 2000, projectRoot = "") {
30
- this.busDir = busDir;
31
- this.agentsFile = agentsFile;
32
- this.interval = interval;
33
- this.daemonDir = daemonDir;
34
- this.projectRoot = projectRoot || path.resolve(busDir, "..", "..");
35
- this.pidFile = path.join(this.daemonDir, "daemon.pid");
36
- this.logFile = path.join(this.daemonDir, "daemon.log");
37
- this.countsDir = path.join(this.daemonDir, "counts", `${process.pid}`);
38
- this.running = false;
39
- this.cleanupCounter = 0;
40
- this.cleanupInterval = 5; // 每 5 个周期清理一次
41
- this.timelineSyncCounter = 0;
42
- // 每 15 个周期同步一次 manual inputs (~15 × interval, default ~30s)
43
- this.timelineSyncInterval = 15;
44
-
45
- this.queueManager = new QueueManager(busDir);
46
- this.injector = new Injector(busDir, agentsFile);
47
- this.adapterRouter = createTerminalAdapterRouter();
48
- this.workingHoldMs = Number.parseInt(process.env.UFOO_ACTIVITY_WORKING_HOLD_MS || "", 10) || 5000;
49
- this.lastWorkingAt = new Map();
50
- }
51
-
52
- setLegacyActivityState(subscriber, state) {
53
- const busData = readJSON(this.agentsFile) || { agents: {} };
54
- if (!busData.agents || !busData.agents[subscriber]) return;
55
- busData.agents[subscriber].activity_state = state;
56
- busData.agents[subscriber].activity_since = new Date().toISOString();
57
- writeJSON(this.agentsFile, busData);
58
- }
59
-
60
- refreshLegacyActivityState(subscriber, meta, hasPending) {
61
- const now = Date.now();
62
- const current = String(meta?.activity_state || "").trim().toLowerCase();
63
- const lastWorkingAt = this.lastWorkingAt.get(subscriber) || 0;
64
- const holdActive = lastWorkingAt > 0 && (now - lastWorkingAt) < this.workingHoldMs;
65
- const daemonManagedWorking = lastWorkingAt > 0;
66
-
67
- if (holdActive) {
68
- if (current !== "working") {
69
- this.setLegacyActivityState(subscriber, "working");
70
- }
71
- return "working";
72
- }
73
-
74
- if (lastWorkingAt > 0) {
75
- this.lastWorkingAt.delete(subscriber);
76
- }
77
-
78
- if (current === "starting") {
79
- const next = hasPending ? "ready" : "idle";
80
- this.setLegacyActivityState(subscriber, next);
81
- return next;
82
- }
83
-
84
- if (current === "working" && daemonManagedWorking) {
85
- const next = hasPending ? "ready" : "idle";
86
- this.setLegacyActivityState(subscriber, next);
87
- return next;
88
- }
89
-
90
- return current;
91
- }
92
-
93
- /**
94
- * 检查 daemon 是否正在运行
95
- */
96
- isRunning() {
97
- if (!fs.existsSync(this.pidFile)) {
98
- return false;
99
- }
100
-
101
- const pid = parseInt(fs.readFileSync(this.pidFile, "utf8").trim(), 10);
102
- return isPidAlive(pid);
103
- }
104
-
105
- /**
106
- * 获取运行中的 daemon PID
107
- */
108
- getRunningPid() {
109
- if (!fs.existsSync(this.pidFile)) {
110
- return null;
111
- }
112
-
113
- const pid = parseInt(fs.readFileSync(this.pidFile, "utf8").trim(), 10);
114
- return isPidAlive(pid) ? pid : null;
115
- }
116
-
117
- /**
118
- * 启动 daemon
119
- */
120
- async start(background = false) {
121
- // 检查是否已经在运行
122
- if (this.isRunning()) {
123
- const pid = this.getRunningPid();
124
- console.log(`[daemon] Already running (pid=${pid})`);
125
- return;
126
- }
127
- ensureDir(this.daemonDir);
128
- ensureDir(path.join(this.daemonDir, "counts"));
129
-
130
- if (background) {
131
- // 后台模式:spawn 独立进程
132
- const { spawn } = require("child_process");
133
- const logStream = fs.openSync(this.logFile, "a");
134
-
135
- const child = spawn(
136
- process.execPath,
137
- [
138
- path.join(__dirname, "..", "..", "..", "bin", "ufoo.js"),
139
- "bus",
140
- "daemon",
141
- "--interval",
142
- String(this.interval / 1000),
143
- ],
144
- {
145
- detached: true,
146
- stdio: ["ignore", logStream, logStream],
147
- cwd: process.cwd(),
148
- }
149
- );
150
-
151
- child.unref();
152
-
153
- // 等待 PID 文件创建
154
- await new Promise((resolve) => setTimeout(resolve, 500));
155
-
156
- const pid = this.getRunningPid();
157
- console.log(`[daemon] Started in background (pid=${pid}, log: ${this.logFile})`);
158
- } else {
159
- // 前台模式
160
- this.run();
161
- }
162
- }
163
-
164
- /**
165
- * 停止 daemon
166
- */
167
- stop() {
168
- const pid = this.getRunningPid();
169
- if (!pid) {
170
- console.log("[daemon] Not running");
171
- return;
172
- }
173
-
174
- try {
175
- process.kill(pid, "SIGTERM");
176
- console.log(`[daemon] Stopped (pid=${pid})`);
177
- if (fs.existsSync(this.pidFile)) {
178
- fs.unlinkSync(this.pidFile);
179
- }
180
- } catch (err) {
181
- console.error(`[daemon] Failed to stop: ${err.message}`);
182
- }
183
- }
184
-
185
- /**
186
- * 显示 daemon 状态
187
- */
188
- status() {
189
- const pid = this.getRunningPid();
190
- if (pid) {
191
- console.log(`[daemon] Running (pid=${pid})`);
192
- } else {
193
- console.log("[daemon] Not running");
194
- // 清理过时的 PID 文件
195
- if (fs.existsSync(this.pidFile)) {
196
- fs.unlinkSync(this.pidFile);
197
- }
198
- }
199
- }
200
-
201
- /**
202
- * 运行 daemon(前台)
203
- */
204
- run() {
205
- // 记录 PID
206
- ensureDir(path.dirname(this.pidFile));
207
- fs.writeFileSync(this.pidFile, `${process.pid}\n`, "utf8");
208
-
209
- // 设置清理钩子
210
- const cleanup = () => {
211
- this.running = false;
212
- if (fs.existsSync(this.pidFile)) {
213
- fs.unlinkSync(this.pidFile);
214
- }
215
- if (fs.existsSync(this.countsDir)) {
216
- fs.rmSync(this.countsDir, { recursive: true, force: true });
217
- }
218
- };
219
-
220
- process.on("SIGTERM", cleanup);
221
- process.on("SIGINT", cleanup);
222
- process.on("exit", cleanup);
223
-
224
- // 创建计数目录
225
- ensureDir(this.countsDir);
226
-
227
- console.log(`[daemon] Started (pid=${process.pid}, interval=${this.interval / 1000}s)`);
228
- console.log(`[daemon] Watching: ${this.busDir}/queues/*/pending.jsonl`);
229
-
230
- this.running = true;
231
- this.watchLoop();
232
- }
233
-
234
- /**
235
- * 主监控循环
236
- */
237
- async watchLoop() {
238
- while (this.running) {
239
- try {
240
- // 定期清理死掉的 agent
241
- this.cleanupCounter++;
242
- if (this.cleanupCounter >= this.cleanupInterval) {
243
- await this.cleanupDeadAgents();
244
- this.cleanupCounter = 0;
245
- }
246
-
247
- // 定期同步 timeline(manual inputs from session files, ~15 × interval)
248
- this.timelineSyncCounter++;
249
- if (this.timelineSyncCounter >= this.timelineSyncInterval) {
250
- this.syncTimeline();
251
- this.timelineSyncCounter = 0;
252
- }
253
-
254
- // 检查所有订阅者的队列
255
- await this.checkQueues();
256
- } catch (err) {
257
- console.error(`[daemon] Error: ${err.message}`);
258
- }
259
-
260
- // 等待下一个周期
261
- await new Promise((resolve) => setTimeout(resolve, this.interval));
262
- }
263
- }
264
-
265
- /**
266
- * 增量同步 timeline — 捕获 manual inputs(bus 消息已在 send() 时实时追加)
267
- */
268
- syncTimeline() {
269
- try {
270
- const { buildTimeline } = require("../history/inputTimeline");
271
- buildTimeline(this.projectRoot);
272
- } catch (err) {
273
- if (process.env.UFOO_HISTORY_DEBUG === "1") {
274
- console.error("[daemon][history] syncTimeline failed:", err.message);
275
- }
276
- }
277
- }
278
-
279
- /**
280
- * 检查所有队列
281
- */
282
- async checkQueues() {
283
- const queuesDir = path.join(this.busDir, "queues");
284
- if (!fs.existsSync(queuesDir)) {
285
- return;
286
- }
287
-
288
- const busData = readJSON(this.agentsFile) || { agents: {} };
289
- const messageManager = new MessageManager(this.busDir, busData, this.queueManager);
290
- const subscribers = fs.readdirSync(queuesDir);
291
-
292
- for (const safeName of subscribers) {
293
- const pendingFile = path.join(queuesDir, safeName, "pending.jsonl");
294
- if (!fs.existsSync(pendingFile)) {
295
- continue;
296
- }
297
-
298
- const subscriber = safeNameToSubscriber(safeName);
299
- const meta = busData.agents?.[subscriber];
300
- const launchMode = meta?.launch_mode || "";
301
- // Delivery ownership:
302
- // - notifier/injector: terminal/tmux
303
- // - internal queue loop: internal
304
- // Bus daemon only handles legacy/unknown launch modes.
305
- const adapter = this.adapterRouter.getAdapter({ launchMode, agentId: subscriber, meta });
306
- const { supportsNotifierInjector, supportsInternalQueueLoop } = adapter.capabilities;
307
- if (launchMode && (supportsNotifierInjector || supportsInternalQueueLoop)) {
308
- continue;
309
- }
310
-
311
- // 获取当前消息数
312
- let count = 0;
313
- if (fs.statSync(pendingFile).size > 0) {
314
- const content = fs.readFileSync(pendingFile, "utf8").trim();
315
- count = content ? content.split("\n").length : 0;
316
- }
317
-
318
- // 获取上次的消息数
319
- const lastCount = this.getLastCount(safeName);
320
-
321
- // 如果有新消息,注入命令
322
- const wakePath = path.join(queuesDir, safeName, "wake");
323
- const wakeActive = fs.existsSync(wakePath);
324
-
325
- if (count > 0 || wakeActive) {
326
- const now = new Date().toISOString().split("T")[1].slice(0, 8);
327
- const note = wakeActive && count <= lastCount ? " (wake)" : "";
328
- console.log(`[daemon] ${now} New message for ${subscriber} (${lastCount} -> ${count})${note}`);
329
-
330
- try {
331
- const agentType = String((meta && meta.agent_type) || "").trim().toLowerCase();
332
- const isUfooCode = subscriber.startsWith("ufoo-code:")
333
- || agentType === "ufoo-code"
334
- || agentType === "ucode"
335
- || agentType === "ufoo";
336
- if (isUfooCode) {
337
- // ufoo-code queue is consumed internally by ucode itself.
338
- // Bus daemon should not inject any command/text into terminal.
339
- if (wakeActive) fs.rmSync(wakePath, { force: true });
340
- this.setLastCount(safeName, count);
341
- continue;
342
- }
343
-
344
- let currentActivityState = this.refreshLegacyActivityState(subscriber, meta, count > 0);
345
- const events = this.drainPending(pendingFile);
346
- const failed = [];
347
- let deliveredCount = 0;
348
- for (const evt of events) {
349
- if (!evt || evt.event !== "message" || !evt.data || typeof evt.data.message !== "string") {
350
- continue;
351
- }
352
- const injectionMode = getInjectionModeFromEvent(evt, INJECTION_MODES.IMMEDIATE);
353
- if (injectionMode === INJECTION_MODES.QUEUED && isBusyActivityState(currentActivityState)) {
354
- failed.push(evt);
355
- continue;
356
- }
357
- try {
358
- const injectionText = buildPromptInjectionText(evt, subscriber, busData.agents || {});
359
- // eslint-disable-next-line no-await-in-loop
360
- await this.injector.inject(subscriber, injectionText);
361
- deliveredCount += 1;
362
- currentActivityState = "working";
363
- this.lastWorkingAt.set(subscriber, Date.now());
364
- this.setLegacyActivityState(subscriber, "working");
365
- } catch (err) {
366
- failed.push(evt);
367
- try {
368
- const pub = typeof evt.publisher === "object" && evt.publisher
369
- ? (evt.publisher.subscriber || evt.publisher.nickname || "")
370
- : (evt.publisher || "");
371
- if (pub) {
372
- // eslint-disable-next-line no-await-in-loop
373
- await messageManager.emit(pub, "delivery", {
374
- target: subscriber,
375
- seq: evt.seq,
376
- status: "error",
377
- message: `delivery failed to ${meta?.nickname || subscriber}: ${err.message || "inject failed"}`,
378
- }, subscriber, "status/delivery");
379
- }
380
- } catch {
381
- // ignore delivery emit errors
382
- }
383
- continue;
384
- }
385
- try {
386
- // Emit delivery status back to publisher (best-effort)
387
- const pub = typeof evt.publisher === "object" && evt.publisher
388
- ? (evt.publisher.subscriber || evt.publisher.nickname || "")
389
- : (evt.publisher || "");
390
- if (pub) {
391
- // eslint-disable-next-line no-await-in-loop
392
- await messageManager.emit(pub, "delivery", {
393
- target: subscriber,
394
- seq: evt.seq,
395
- status: "ok",
396
- message: `delivered to ${meta?.nickname || subscriber}`,
397
- }, subscriber, "status/delivery");
398
- }
399
- } catch {
400
- // ignore delivery emit errors
401
- }
402
- }
403
- if (failed.length > 0) {
404
- try {
405
- const content = failed.map((e) => JSON.stringify(e)).join("\n") + "\n";
406
- fs.appendFileSync(pendingFile, content, "utf8");
407
- } catch {
408
- // ignore requeue failures
409
- }
410
- }
411
- console.log(`[daemon] Delivered ${deliveredCount} message(s) to ${subscriber}`);
412
- if (wakeActive) fs.rmSync(wakePath, { force: true });
413
- } catch (err) {
414
- console.error(`[daemon] Failed to inject: ${err.message}`);
415
- }
416
- }
417
-
418
- // 更新计数
419
- this.setLastCount(safeName, count);
420
- }
421
- }
422
-
423
- drainPending(pendingFile) {
424
- if (!fs.existsSync(pendingFile)) return [];
425
- const processingFile = `${pendingFile}.processing.${process.pid}.${Date.now()}`;
426
- let content = "";
427
- let readOk = false;
428
- try {
429
- fs.renameSync(pendingFile, processingFile);
430
- content = fs.readFileSync(processingFile, "utf8");
431
- readOk = true;
432
- } catch {
433
- try {
434
- if (fs.existsSync(processingFile)) {
435
- fs.renameSync(processingFile, pendingFile);
436
- }
437
- } catch {
438
- // ignore rollback errors
439
- }
440
- return [];
441
- } finally {
442
- if (readOk) {
443
- try {
444
- if (fs.existsSync(processingFile)) {
445
- fs.rmSync(processingFile, { force: true });
446
- }
447
- } catch {
448
- // ignore cleanup errors
449
- }
450
- }
451
- }
452
- if (!content.trim()) return [];
453
- return content.split(/\r?\n/).filter(Boolean).map((line) => {
454
- try {
455
- return JSON.parse(line);
456
- } catch {
457
- return null;
458
- }
459
- }).filter(Boolean);
460
- }
461
-
462
- /**
463
- * 获取上次的消息计数
464
- */
465
- getLastCount(safeName) {
466
- const countFile = path.join(this.countsDir, safeName);
467
- if (!fs.existsSync(countFile)) {
468
- return 0;
469
- }
470
- const content = fs.readFileSync(countFile, "utf8").trim();
471
- return parseInt(content, 10) || 0;
472
- }
473
-
474
- /**
475
- * 设置消息计数
476
- */
477
- setLastCount(safeName, count) {
478
- const countFile = path.join(this.countsDir, safeName);
479
- ensureDir(path.dirname(countFile));
480
- fs.writeFileSync(countFile, `${count}\n`, "utf8");
481
- }
482
-
483
- /**
484
- * 清理死掉的 agent
485
- */
486
- async cleanupDeadAgents() {
487
- const agentsFile = this.agentsFile;
488
- if (!fs.existsSync(agentsFile)) {
489
- return;
490
- }
491
-
492
- const busData = readJSON(agentsFile);
493
- if (!busData || !busData.agents) {
494
- return;
495
- }
496
-
497
- let changed = false;
498
-
499
- for (const [subscriber, meta] of Object.entries(busData.agents)) {
500
- if (meta.status !== "active") {
501
- continue;
502
- }
503
-
504
- // 检查 agent 是否仍然存活(PID + TTY 交叉检查)
505
- if (!isMetaActive(meta)) {
506
- const now = new Date().toISOString().split("T")[1].slice(0, 8);
507
- console.log(`[daemon] ${now} Agent ${subscriber} (pid=${meta.pid || 0}) is dead, marking inactive`);
508
-
509
- meta.status = "inactive";
510
- meta.activity_state = "";
511
- changed = true;
512
-
513
- // 清理队列目录和 offset
514
- const safeName = subscriberToSafeName(subscriber);
515
- const queueDir = path.join(this.busDir, "queues", safeName);
516
- const offsetFile = path.join(this.busDir, "offsets", `${safeName}.offset`);
517
-
518
- if (fs.existsSync(queueDir)) {
519
- fs.rmSync(queueDir, { recursive: true, force: true });
520
- }
521
- if (fs.existsSync(offsetFile)) {
522
- fs.unlinkSync(offsetFile);
523
- }
524
- }
525
- }
526
-
527
- if (changed) {
528
- writeJSON(agentsFile, busData);
529
- }
530
- }
531
- }
532
-
533
- module.exports = BusDaemon;
534
- module.exports.buildPromptInjectionText = buildPromptInjectionText;
535
- module.exports.shouldRenderPromptEnvelope = shouldRenderPromptEnvelope;