u-foo 2.4.0 → 2.4.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": "2.4.0",
3
+ "version": "2.4.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",
@@ -2,6 +2,14 @@
2
2
 
3
3
  const crypto = require("crypto");
4
4
 
5
+ const SILENT_BOOTSTRAP_INSTRUCTION = [
6
+ "Bootstrap silence:",
7
+ "- This message is setup only, not a task.",
8
+ "- Apply these instructions silently, then wait for the next user, bus, or controller task.",
9
+ "- Do not reply, summarize, acknowledge, report, hand off, or call tools in response to this bootstrap message.",
10
+ "- Do not send `ufoo report` or `ufoo bus` until real work arrives after this bootstrap.",
11
+ ].join("\n");
12
+
5
13
  const SHARED_UFOO_PROTOCOL = [
6
14
  "Session harness: ufoo",
7
15
  "",
@@ -42,11 +50,15 @@ const SHARED_UFOO_PROTOCOL = [
42
50
  "dedup, so don't worry about report loops.",
43
51
  "",
44
52
  "`ufoo report start|progress|done|error \"<short summary>\"`",
53
+ "Do not emulate report failures with `ufoo bus send ufoo-agent ...`.",
54
+ "If `ufoo report` fails, continue without a fallback bus report.",
45
55
  "",
46
56
  "Then continue the active task.",
47
57
  ].join("\n");
48
58
 
49
59
  const SHARED_GROUP_PREFIX = [
60
+ SILENT_BOOTSTRAP_INSTRUCTION,
61
+ "",
50
62
  "You are part of a ufoo multi-agent group.",
51
63
  "",
52
64
  "Shared rules:",
@@ -193,6 +205,7 @@ function computeBootstrapFingerprint({
193
205
  }
194
206
 
195
207
  module.exports = {
208
+ SILENT_BOOTSTRAP_INSTRUCTION,
196
209
  SHARED_UFOO_PROTOCOL,
197
210
  SHARED_GROUP_PREFIX,
198
211
  SOLO_AGENT_PREFIX,
@@ -112,6 +112,7 @@ async function withCapturedConsole(capture, fn) {
112
112
  }
113
113
 
114
114
  function createCommandExecutor(options = {}) {
115
+ const hasRestartDaemon = typeof options.restartDaemon === "function";
115
116
  const {
116
117
  projectRoot,
117
118
  getActiveProjectRoot = () => projectRoot,
@@ -255,6 +256,18 @@ function createCommandExecutor(options = {}) {
255
256
  }
256
257
 
257
258
  if (subcommand === "restart") {
259
+ if (hasRestartDaemon) {
260
+ statusMsg("{gray-fg}⚙{/gray-fg} Restarting daemon...");
261
+ await restartDaemon(targetRoot);
262
+ await sleep(500);
263
+ if (isDaemonRunning(targetRoot)) {
264
+ statusMsg("{gray-fg}✓{/gray-fg} Daemon restarted");
265
+ } else {
266
+ statusMsg("{gray-fg}✗{/gray-fg} Failed to restart daemon");
267
+ }
268
+ return;
269
+ }
270
+
258
271
  statusMsg("{gray-fg}⚙{/gray-fg} Restarting daemon...");
259
272
  stopDaemon(targetRoot, { source: "chat-command:/daemon restart" });
260
273
  await sleep(500);
@@ -30,6 +30,9 @@ function restartDaemonFlow(options = {}) {
30
30
  startDaemon(projectRoot);
31
31
  const connected = connection ? await connection.connect() : false;
32
32
  if (connected) {
33
+ if (typeof connection.requestStatus === "function") {
34
+ connection.requestStatus();
35
+ }
33
36
  statusMsg("{gray-fg}✓{/gray-fg} Daemon reconnected");
34
37
  } else {
35
38
  statusMsg("{gray-fg}✗{/gray-fg} Failed to reconnect to daemon");
@@ -129,6 +129,41 @@ async function sendDaemonRequest(projectRoot, payload) {
129
129
  });
130
130
  }
131
131
 
132
+ async function sendAgentReportRequest(projectRoot, report) {
133
+ const { normalizeReportInput } = require("../../coordination/report/store");
134
+ const { enqueueAgentReport } = require("../../runtime/daemon/reportControlBus");
135
+ const entry = normalizeReportInput(report);
136
+ const queued = await enqueueAgentReport(projectRoot, entry);
137
+ return {
138
+ type: "response",
139
+ data: {
140
+ reply: `Report queued (${entry.phase})`,
141
+ report: entry,
142
+ queued,
143
+ },
144
+ };
145
+ }
146
+
147
+ function getReportDetail(out = {}) {
148
+ if (out.phase === "error") {
149
+ return out.error || out.summary || out.message || out.task_id;
150
+ }
151
+ return out.summary || out.message || out.task_id;
152
+ }
153
+
154
+ function printReportOutput(out, json = false, queued = null) {
155
+ if (json) {
156
+ console.log(JSON.stringify({
157
+ status: "queued",
158
+ report: out,
159
+ queued,
160
+ }, null, 2));
161
+ return;
162
+ }
163
+ const detail = getReportDetail(out);
164
+ console.log(`[report] queued ${out.phase} ${out.agent_id} ${out.task_id} ${detail}`);
165
+ }
166
+
132
167
  function requireOptional(name) {
133
168
  try {
134
169
  // eslint-disable-next-line global-require, import/no-dynamic-require
@@ -978,20 +1013,9 @@ async function runCli(argv) {
978
1013
  };
979
1014
 
980
1015
  try {
981
- await ensureDaemonRunning(projectRoot);
982
- const resp = await sendDaemonRequest(projectRoot, {
983
- type: "agent_report",
984
- report,
985
- });
1016
+ const resp = await sendAgentReportRequest(projectRoot, report);
986
1017
  const out = resp?.data?.report || report;
987
- if (opts.json) {
988
- console.log(JSON.stringify(out, null, 2));
989
- return;
990
- }
991
- const detail = out.phase === "error"
992
- ? (out.error || out.summary || out.message || out.task_id)
993
- : (out.summary || out.message || out.task_id);
994
- console.log(`[report] ${out.phase} ${out.agent_id} ${out.task_id} ${detail}`);
1018
+ printReportOutput(out, opts.json, resp?.data?.queued || null);
995
1019
  } catch (err) {
996
1020
  console.error(err.message || String(err));
997
1021
  process.exitCode = 1;
@@ -1926,20 +1950,9 @@ async function runCli(argv) {
1926
1950
 
1927
1951
  (async () => {
1928
1952
  try {
1929
- await ensureDaemonRunning(process.cwd());
1930
- const resp = await sendDaemonRequest(process.cwd(), {
1931
- type: "agent_report",
1932
- report,
1933
- });
1953
+ const resp = await sendAgentReportRequest(process.cwd(), report);
1934
1954
  const out = resp?.data?.report || report;
1935
- if (rest.includes("--json")) {
1936
- console.log(JSON.stringify(out, null, 2));
1937
- return;
1938
- }
1939
- const detail = out.phase === "error"
1940
- ? (out.error || out.summary || out.message || out.task_id)
1941
- : (out.summary || out.message || out.task_id);
1942
- console.log(`[report] ${out.phase} ${out.agent_id} ${out.task_id} ${detail}`);
1955
+ printReportOutput(out, rest.includes("--json"), resp?.data?.queued || null);
1943
1956
  } catch (err) {
1944
1957
  console.error(err.message || String(err));
1945
1958
  process.exitCode = 1;
@@ -330,7 +330,7 @@ class EventBus {
330
330
  injectionMode: options.injectionMode,
331
331
  source: options.source,
332
332
  })
333
- : await this.messageManager.emit(target, eventName, data, publisher);
333
+ : await this.messageManager.emit(target, eventName, data, publisher, options.type);
334
334
  const silent = options.silent === true;
335
335
  if (!silent && eventName === "message") {
336
336
  logOk(
@@ -21,6 +21,11 @@ const { normalizeFormat, renderGroupDiagramFromTemplate, renderGroupDiagramFromR
21
21
  const { runPromptWithAssistant } = require("./promptLoop");
22
22
  const { handlePromptRequest } = require("./promptRequest");
23
23
  const { recordAgentReport } = require("./reporting");
24
+ const {
25
+ isAgentReportControlEvent,
26
+ extractAgentReportControl,
27
+ takeReportControlEvents,
28
+ } = require("./reportControlBus");
24
29
  const { isGlobalControllerProjectRoot } = require("../projects");
25
30
  const {
26
31
  assignSoloRoleToExistingAgent,
@@ -855,7 +860,7 @@ async function dispatchMessages(projectRoot, dispatch = []) {
855
860
  }
856
861
  }
857
862
 
858
- function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
863
+ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain, onReport) {
859
864
  const state = {
860
865
  subscriber: null,
861
866
  queueFile: null,
@@ -867,6 +872,7 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
867
872
  };
868
873
  const eventBus = new EventBus(projectRoot);
869
874
  let joinInProgress = false;
875
+ let polling = false;
870
876
 
871
877
  function getAgentNickname(agentId) {
872
878
  if (!agentId) return agentId;
@@ -1057,6 +1063,27 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
1057
1063
  })();
1058
1064
  }
1059
1065
 
1066
+ async function handleReportControlEvent(evt) {
1067
+ if (!isAgentReportControlEvent(evt)) return false;
1068
+ if (typeof onReport !== "function") return false;
1069
+ const control = extractAgentReportControl(evt);
1070
+ if (!control) return false;
1071
+ await onReport(control.report, {
1072
+ event: evt,
1073
+ requestId: control.request_id,
1074
+ queuedAt: control.queued_at,
1075
+ });
1076
+ return true;
1077
+ }
1078
+
1079
+ async function pollReportControlQueue() {
1080
+ const events = takeReportControlEvents(projectRoot);
1081
+ if (!events.length) return;
1082
+ for (const evt of events) {
1083
+ if (await handleReportControlEvent(evt)) continue;
1084
+ }
1085
+ }
1086
+
1060
1087
  function pollQueue() {
1061
1088
  if (!state.queueFile) return;
1062
1089
  if (!fs.existsSync(state.queueFile)) return;
@@ -1109,14 +1136,23 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
1109
1136
  }
1110
1137
  }
1111
1138
 
1112
- function poll() {
1113
- ensureSubscriber();
1114
- if (typeof shouldDrain === "function" && !shouldDrain()) return;
1115
- pollQueue();
1116
- pollWatchedEvents();
1139
+ async function poll() {
1140
+ if (polling) return;
1141
+ polling = true;
1142
+ try {
1143
+ ensureSubscriber();
1144
+ await pollReportControlQueue();
1145
+ if (typeof shouldDrain === "function" && !shouldDrain()) return;
1146
+ pollQueue();
1147
+ pollWatchedEvents();
1148
+ } finally {
1149
+ polling = false;
1150
+ }
1117
1151
  }
1118
1152
 
1119
- const interval = setInterval(poll, 1000);
1153
+ const interval = setInterval(() => {
1154
+ poll().catch(() => {});
1155
+ }, 1000);
1120
1156
  return {
1121
1157
  markPending(target) {
1122
1158
  if (!target) return;
@@ -1300,11 +1336,53 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1300
1336
  log,
1301
1337
  });
1302
1338
 
1339
+ const publishAgentReportResult = (entry) => {
1340
+ ipcServer.sendToSockets({
1341
+ type: IPC_RESPONSE_TYPES.BUS,
1342
+ data: {
1343
+ event: "controller_report",
1344
+ publisher: entry.agent_id,
1345
+ message: entry.summary || entry.message || entry.task_id,
1346
+ report: entry,
1347
+ },
1348
+ });
1349
+ ipcServer.sendToSockets({
1350
+ type: IPC_RESPONSE_TYPES.STATUS,
1351
+ data: buildRuntimeStatus(),
1352
+ });
1353
+ };
1354
+
1355
+ const handleAgentReport = async (report, options = {}) => {
1356
+ const source = options.source || report.source || "cli";
1357
+ const { entry } = await recordAgentReport({
1358
+ projectRoot,
1359
+ report: {
1360
+ ...report,
1361
+ source,
1362
+ },
1363
+ onStatus: (status) => {
1364
+ ipcServer.sendToSockets({
1365
+ type: IPC_RESPONSE_TYPES.STATUS,
1366
+ data: status,
1367
+ });
1368
+ },
1369
+ log,
1370
+ });
1371
+ publishAgentReportResult(entry);
1372
+ return entry;
1373
+ };
1374
+
1303
1375
  const busBridge = startBusBridge(projectRoot, provider, (evt) => {
1304
1376
  ipcServer.sendToSockets({ type: IPC_RESPONSE_TYPES.BUS, data: evt });
1305
1377
  }, (status) => {
1306
1378
  ipcServer.sendToSockets({ type: IPC_RESPONSE_TYPES.STATUS, data: status });
1307
- }, () => ipcServer.hasClients());
1379
+ }, () => ipcServer.hasClients(), async (report, meta = {}) => {
1380
+ try {
1381
+ await handleAgentReport(report || {}, { source: (report && report.source) || "bus" });
1382
+ } catch (err) {
1383
+ log(`report bus event failed request=${meta.requestId || ""} error=${err.message || String(err)}`);
1384
+ }
1385
+ });
1308
1386
 
1309
1387
  handleIpcRequest = async (req, socket) => {
1310
1388
  if (!req || typeof req !== "object") return;
@@ -1399,20 +1477,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1399
1477
  if (req.type === IPC_REQUEST_TYPES.AGENT_REPORT) {
1400
1478
  try {
1401
1479
  const report = req.report && typeof req.report === "object" ? req.report : {};
1402
- const { entry } = await recordAgentReport({
1403
- projectRoot,
1404
- report: {
1405
- ...report,
1406
- source: report.source || "cli",
1407
- },
1408
- onStatus: (status) => {
1409
- ipcServer.sendToSockets({
1410
- type: IPC_RESPONSE_TYPES.STATUS,
1411
- data: status,
1412
- });
1413
- },
1414
- log,
1415
- });
1480
+ const entry = await handleAgentReport(report, { source: report.source || "cli" });
1416
1481
  socket.write(
1417
1482
  `${JSON.stringify({
1418
1483
  type: IPC_RESPONSE_TYPES.RESPONSE,
@@ -1423,19 +1488,6 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1423
1488
  })}
1424
1489
  `,
1425
1490
  );
1426
- ipcServer.sendToSockets({
1427
- type: IPC_RESPONSE_TYPES.BUS,
1428
- data: {
1429
- event: "controller_report",
1430
- publisher: entry.agent_id,
1431
- message: entry.summary || entry.message || entry.task_id,
1432
- report: entry,
1433
- },
1434
- });
1435
- ipcServer.sendToSockets({
1436
- type: IPC_RESPONSE_TYPES.STATUS,
1437
- data: buildRuntimeStatus(),
1438
- });
1439
1491
  } catch (err) {
1440
1492
  socket.write(
1441
1493
  `${JSON.stringify({
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { getUfooPaths } = require("../../coordination/state/paths");
6
+ const {
7
+ appendJSONL,
8
+ ensureDir,
9
+ generateInstanceId,
10
+ } = require("../../coordination/bus/utils");
11
+
12
+ const REPORT_CONTROL_TARGET = "ufoo-agent";
13
+ const REPORT_CONTROL_EVENT = "agent_report";
14
+ const REPORT_CONTROL_TYPE = "control/report";
15
+
16
+ function getReportControlQueueDir(projectRoot) {
17
+ const paths = getUfooPaths(projectRoot);
18
+ return path.join(paths.busDir, "control", "report");
19
+ }
20
+
21
+ function getReportControlQueueFile(projectRoot) {
22
+ return path.join(getReportControlQueueDir(projectRoot), "pending.jsonl");
23
+ }
24
+
25
+ function ensureReportControlQueue(projectRoot) {
26
+ const queueDir = getReportControlQueueDir(projectRoot);
27
+ ensureDir(queueDir);
28
+ return queueDir;
29
+ }
30
+
31
+ function resolveReportPublisher(report = {}) {
32
+ const fromReport = String(report.agent_id || report.agentId || "").trim();
33
+ if (fromReport) return fromReport;
34
+ const fromEnv = String(process.env.UFOO_SUBSCRIBER_ID || "").trim();
35
+ return fromEnv || "unknown-agent";
36
+ }
37
+
38
+ function buildReportControlData(report = {}, options = {}) {
39
+ return {
40
+ request_id: options.requestId || `report-${Date.now().toString(36)}-${generateInstanceId()}`,
41
+ queued_at: options.queuedAt || new Date().toISOString(),
42
+ report,
43
+ };
44
+ }
45
+
46
+ function buildReportControlEvent(report = {}, options = {}) {
47
+ const data = buildReportControlData(report, options);
48
+ return {
49
+ timestamp: data.queued_at,
50
+ type: REPORT_CONTROL_TYPE,
51
+ event: REPORT_CONTROL_EVENT,
52
+ publisher: options.publisher || resolveReportPublisher(report),
53
+ target: REPORT_CONTROL_TARGET,
54
+ data,
55
+ };
56
+ }
57
+
58
+ async function enqueueAgentReport(projectRoot, report, options = {}) {
59
+ ensureReportControlQueue(projectRoot);
60
+
61
+ const event = buildReportControlEvent(report, options);
62
+ appendJSONL(getReportControlQueueFile(projectRoot), event);
63
+
64
+ return {
65
+ queued: true,
66
+ request_id: event.data.request_id,
67
+ target: REPORT_CONTROL_TARGET,
68
+ targets: [REPORT_CONTROL_TARGET],
69
+ report,
70
+ };
71
+ }
72
+
73
+ function takeReportControlEvents(projectRoot) {
74
+ const queueFile = getReportControlQueueFile(projectRoot);
75
+ if (!fs.existsSync(queueFile)) return [];
76
+
77
+ const processingFile = `${queueFile}.processing.${process.pid}.${Date.now()}.${generateInstanceId()}`;
78
+ let content = "";
79
+ let readOk = false;
80
+ try {
81
+ fs.renameSync(queueFile, processingFile);
82
+ content = fs.readFileSync(processingFile, "utf8");
83
+ readOk = true;
84
+ } catch {
85
+ try {
86
+ if (fs.existsSync(processingFile)) {
87
+ fs.renameSync(processingFile, queueFile);
88
+ }
89
+ } catch {
90
+ // ignore rollback errors
91
+ }
92
+ return [];
93
+ } finally {
94
+ if (readOk) {
95
+ try {
96
+ if (fs.existsSync(processingFile)) {
97
+ fs.rmSync(processingFile, { force: true });
98
+ }
99
+ } catch {
100
+ // ignore cleanup errors
101
+ }
102
+ }
103
+ }
104
+
105
+ return content.split(/\r?\n/)
106
+ .filter(Boolean)
107
+ .map((line) => {
108
+ try {
109
+ return JSON.parse(line);
110
+ } catch {
111
+ return null;
112
+ }
113
+ })
114
+ .filter(Boolean);
115
+ }
116
+
117
+ function isAgentReportControlEvent(evt) {
118
+ if (!evt || typeof evt !== "object") return false;
119
+ if (evt.target !== REPORT_CONTROL_TARGET) return false;
120
+ if (evt.type !== REPORT_CONTROL_TYPE) return false;
121
+ if (evt.event !== REPORT_CONTROL_EVENT) return false;
122
+ const data = evt.data && typeof evt.data === "object" ? evt.data : {};
123
+ return Boolean(data.report && typeof data.report === "object");
124
+ }
125
+
126
+ function extractAgentReportControl(evt) {
127
+ if (!isAgentReportControlEvent(evt)) return null;
128
+ const data = evt.data && typeof evt.data === "object" ? evt.data : {};
129
+ return {
130
+ report: data.report,
131
+ request_id: data.request_id || "",
132
+ queued_at: data.queued_at || evt.timestamp || evt.ts || "",
133
+ };
134
+ }
135
+
136
+ module.exports = {
137
+ REPORT_CONTROL_TARGET,
138
+ REPORT_CONTROL_EVENT,
139
+ REPORT_CONTROL_TYPE,
140
+ getReportControlQueueDir,
141
+ getReportControlQueueFile,
142
+ ensureReportControlQueue,
143
+ resolveReportPublisher,
144
+ buildReportControlData,
145
+ buildReportControlEvent,
146
+ enqueueAgentReport,
147
+ takeReportControlEvents,
148
+ isAgentReportControlEvent,
149
+ extractAgentReportControl,
150
+ };