u-foo 1.0.6 → 1.1.9

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 (149) hide show
  1. package/README.md +44 -4
  2. package/SKILLS/ufoo/SKILL.md +17 -2
  3. package/SKILLS/uinit/SKILL.md +8 -3
  4. package/bin/ucode-core.js +15 -0
  5. package/bin/ucode.js +125 -0
  6. package/bin/ufoo-assistant-agent.js +5 -0
  7. package/bin/ufoo-engine.js +25 -0
  8. package/bin/ufoo.js +4 -0
  9. package/modules/AGENTS.template.md +14 -4
  10. package/modules/bus/README.md +8 -5
  11. package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
  12. package/modules/context/SKILLS/uctx/SKILL.md +3 -1
  13. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  14. package/package.json +12 -3
  15. package/scripts/import-pi-mono.js +124 -0
  16. package/scripts/postinstall.js +20 -49
  17. package/scripts/sync-claude-skills.sh +21 -0
  18. package/src/agent/cliRunner.js +524 -31
  19. package/src/agent/internalRunner.js +76 -9
  20. package/src/agent/launcher.js +97 -45
  21. package/src/agent/normalizeOutput.js +1 -1
  22. package/src/agent/notifier.js +144 -4
  23. package/src/agent/ptyRunner.js +480 -10
  24. package/src/agent/ptyWrapper.js +28 -3
  25. package/src/agent/readyDetector.js +16 -0
  26. package/src/agent/ucode.js +443 -0
  27. package/src/agent/ucodeBootstrap.js +113 -0
  28. package/src/agent/ucodeBuild.js +67 -0
  29. package/src/agent/ucodeDoctor.js +184 -0
  30. package/src/agent/ucodeRuntimeConfig.js +129 -0
  31. package/src/agent/ufooAgent.js +11 -2
  32. package/src/assistant/agent.js +260 -0
  33. package/src/assistant/bridge.js +172 -0
  34. package/src/assistant/engine.js +252 -0
  35. package/src/assistant/stdio.js +58 -0
  36. package/src/assistant/ufooEngineCli.js +306 -0
  37. package/src/bus/activate.js +27 -11
  38. package/src/bus/daemon.js +133 -5
  39. package/src/bus/index.js +137 -80
  40. package/src/bus/inject.js +47 -17
  41. package/src/bus/message.js +145 -17
  42. package/src/bus/nickname.js +3 -1
  43. package/src/bus/queue.js +6 -1
  44. package/src/bus/store.js +189 -0
  45. package/src/bus/subscriber.js +20 -4
  46. package/src/bus/utils.js +9 -3
  47. package/src/chat/agentBar.js +117 -0
  48. package/src/chat/agentDirectory.js +88 -0
  49. package/src/chat/agentSockets.js +225 -0
  50. package/src/chat/agentViewController.js +298 -0
  51. package/src/chat/chatLogController.js +115 -0
  52. package/src/chat/commandExecutor.js +700 -0
  53. package/src/chat/commands.js +132 -0
  54. package/src/chat/completionController.js +414 -0
  55. package/src/chat/cronScheduler.js +160 -0
  56. package/src/chat/daemonConnection.js +166 -0
  57. package/src/chat/daemonCoordinator.js +64 -0
  58. package/src/chat/daemonMessageRouter.js +257 -0
  59. package/src/chat/daemonReconnect.js +41 -0
  60. package/src/chat/daemonTransport.js +36 -0
  61. package/src/chat/daemonTransportDefaults.js +10 -0
  62. package/src/chat/dashboardKeyController.js +480 -0
  63. package/src/chat/dashboardView.js +154 -0
  64. package/src/chat/index.js +935 -2909
  65. package/src/chat/inputHistoryController.js +105 -0
  66. package/src/chat/inputListenerController.js +304 -0
  67. package/src/chat/inputMath.js +104 -0
  68. package/src/chat/inputSubmitHandler.js +171 -0
  69. package/src/chat/layout.js +165 -0
  70. package/src/chat/pasteController.js +81 -0
  71. package/src/chat/rawKeyMap.js +42 -0
  72. package/src/chat/settingsController.js +132 -0
  73. package/src/chat/statusLineController.js +177 -0
  74. package/src/chat/streamTracker.js +138 -0
  75. package/src/chat/text.js +70 -0
  76. package/src/chat/transport.js +61 -0
  77. package/src/cli/busCoreCommands.js +59 -0
  78. package/src/cli/ctxCoreCommands.js +199 -0
  79. package/src/cli/onlineCoreCommands.js +379 -0
  80. package/src/cli.js +741 -238
  81. package/src/code/README.md +29 -0
  82. package/src/code/UCODE_PROMPT.md +32 -0
  83. package/src/code/agent.js +1651 -0
  84. package/src/code/cli.js +158 -0
  85. package/src/code/config +0 -0
  86. package/src/code/dispatch.js +42 -0
  87. package/src/code/index.js +70 -0
  88. package/src/code/nativeRunner.js +1213 -0
  89. package/src/code/runtime.js +154 -0
  90. package/src/code/sessionStore.js +162 -0
  91. package/src/code/taskDecomposer.js +269 -0
  92. package/src/code/tools/bash.js +53 -0
  93. package/src/code/tools/common.js +42 -0
  94. package/src/code/tools/edit.js +70 -0
  95. package/src/code/tools/read.js +44 -0
  96. package/src/code/tools/write.js +35 -0
  97. package/src/code/tui.js +1580 -0
  98. package/src/config.js +47 -1
  99. package/src/context/decisions.js +12 -2
  100. package/src/context/index.js +18 -1
  101. package/src/context/sync.js +127 -0
  102. package/src/daemon/agentProcessManager.js +74 -0
  103. package/src/daemon/cronOps.js +241 -0
  104. package/src/daemon/index.js +661 -488
  105. package/src/daemon/ipcServer.js +99 -0
  106. package/src/daemon/ops.js +417 -179
  107. package/src/daemon/promptLoop.js +319 -0
  108. package/src/daemon/promptRequest.js +101 -0
  109. package/src/daemon/providerSessions.js +32 -17
  110. package/src/daemon/reporting.js +90 -0
  111. package/src/daemon/run.js +2 -5
  112. package/src/daemon/status.js +24 -1
  113. package/src/init/index.js +68 -14
  114. package/src/online/bridge.js +663 -0
  115. package/src/online/client.js +245 -0
  116. package/src/online/runner.js +253 -0
  117. package/src/online/server.js +992 -0
  118. package/src/online/tokens.js +103 -0
  119. package/src/report/store.js +331 -0
  120. package/src/shared/eventContract.js +35 -0
  121. package/src/shared/ptySocketContract.js +21 -0
  122. package/src/status/index.js +50 -17
  123. package/src/terminal/adapterContract.js +87 -0
  124. package/src/terminal/adapterRouter.js +84 -0
  125. package/src/terminal/adapters/externalAdapter.js +14 -0
  126. package/src/terminal/adapters/internalAdapter.js +13 -0
  127. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  128. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  129. package/src/terminal/adapters/terminalAdapter.js +31 -0
  130. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  131. package/src/ufoo/agentsStore.js +69 -3
  132. package/src/utils/banner.js +5 -2
  133. package/scripts/.archived/bash-to-js-migration/README.md +0 -46
  134. package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
  135. package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
  136. package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
  137. package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
  138. package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
  139. package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
  140. package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
  141. package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
  142. package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
  143. package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
  144. package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
  145. package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
  146. package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
  147. package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
  148. package/scripts/banner.sh +0 -2
  149. package/src/bus/API_DESIGN.md +0 -204
@@ -0,0 +1,319 @@
1
+ function buildAssistantContinuationPrompt({
2
+ originalPrompt,
3
+ previousReply,
4
+ reports,
5
+ }) {
6
+ const lines = [];
7
+ lines.push(`User: ${originalPrompt}`);
8
+ if (previousReply) {
9
+ lines.push("");
10
+ lines.push(`Your previous reply draft: ${previousReply}`);
11
+ }
12
+ lines.push("");
13
+ lines.push("Assistant execution reports (JSON):");
14
+ lines.push(JSON.stringify(reports, null, 2));
15
+ lines.push("");
16
+ lines.push("Using these reports, return the final JSON response.");
17
+ return lines.join("\n");
18
+ }
19
+
20
+ function normalizePayload(payload) {
21
+ if (!payload || typeof payload !== "object") {
22
+ return { reply: "", dispatch: [], ops: [] };
23
+ }
24
+ return {
25
+ ...payload,
26
+ reply: typeof payload.reply === "string" ? payload.reply : "",
27
+ dispatch: Array.isArray(payload.dispatch) ? payload.dispatch : [],
28
+ ops: Array.isArray(payload.ops) ? payload.ops : [],
29
+ };
30
+ }
31
+
32
+ function annotateAssistantFailureFallback(payload, assistantResult) {
33
+ if (!payload || typeof payload !== "object") return payload;
34
+ const dispatchCount = Array.isArray(payload.dispatch) ? payload.dispatch.length : 0;
35
+ const opsCount = Array.isArray(payload.ops) ? payload.ops.length : 0;
36
+ if (dispatchCount > 0 || opsCount > 0) return payload;
37
+
38
+ const error = assistantResult && typeof assistantResult.error === "string" && assistantResult.error
39
+ ? assistantResult.error
40
+ : "assistant task failed";
41
+ const note = `Assistant execution failed: ${error}. No action was applied.`;
42
+ const reply = typeof payload.reply === "string" && payload.reply
43
+ ? `${payload.reply}\n${note}`
44
+ : note;
45
+ return {
46
+ ...payload,
47
+ reply,
48
+ };
49
+ }
50
+
51
+ function extractAssistantCall(payload) {
52
+ if (!payload || typeof payload !== "object") {
53
+ return { assistantCall: null, ops: [] };
54
+ }
55
+
56
+ const ops = Array.isArray(payload.ops) ? payload.ops : [];
57
+ let assistantCall = payload.assistant_call || null;
58
+ const normalOps = [];
59
+
60
+ for (const op of ops) {
61
+ if (op && op.action === "assistant_call") {
62
+ if (!assistantCall) assistantCall = op;
63
+ continue;
64
+ }
65
+ if (op) normalOps.push(op);
66
+ }
67
+
68
+ return { assistantCall, ops: normalOps };
69
+ }
70
+
71
+ function normalizeAssistantCall(call) {
72
+ if (!call) return null;
73
+ if (typeof call === "string") {
74
+ return { task: call, kind: "mixed", context: "", expect: "" };
75
+ }
76
+ if (typeof call !== "object") return null;
77
+ const task = typeof call.task === "string" ? call.task : "";
78
+ if (!task) return null;
79
+ return {
80
+ task,
81
+ kind: typeof call.kind === "string" ? call.kind : "mixed",
82
+ context: typeof call.context === "string" ? call.context : "",
83
+ expect: typeof call.expect === "string" ? call.expect : "",
84
+ provider: typeof call.provider === "string" ? call.provider : "",
85
+ model: typeof call.model === "string" ? call.model : "",
86
+ timeoutMs: Number.isFinite(call.timeout_ms) ? call.timeout_ms : null,
87
+ };
88
+ }
89
+
90
+ function buildAssistantReport(call, result) {
91
+ return {
92
+ kind: call.kind,
93
+ task: call.task,
94
+ ok: result && result.ok !== false,
95
+ summary: result && typeof result.summary === "string" ? result.summary : "",
96
+ error: result && typeof result.error === "string" ? result.error : "",
97
+ artifacts: result && Array.isArray(result.artifacts) ? result.artifacts : [],
98
+ logs: result && Array.isArray(result.logs) ? result.logs : [],
99
+ metrics: result && typeof result.metrics === "object" ? result.metrics : {},
100
+ };
101
+ }
102
+
103
+ function createAssistantTaskId() {
104
+ return `assistant-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
105
+ }
106
+
107
+ async function emitAssistantReport(reportTaskStatus, payload) {
108
+ if (typeof reportTaskStatus !== "function") return;
109
+ try {
110
+ await reportTaskStatus(payload);
111
+ } catch {
112
+ // best effort: reporting must not break prompt flow
113
+ }
114
+ }
115
+
116
+ async function finalizePromptRun({
117
+ projectRoot,
118
+ payload,
119
+ processManager,
120
+ dispatchMessages,
121
+ handleOps,
122
+ markPending,
123
+ }) {
124
+ for (const item of payload.dispatch || []) {
125
+ if (item && item.target && item.target !== "broadcast") {
126
+ markPending(item.target);
127
+ }
128
+ }
129
+
130
+ await dispatchMessages(projectRoot, payload.dispatch || []);
131
+ const opsResults = await handleOps(projectRoot, payload.ops || [], processManager);
132
+
133
+ return {
134
+ ok: true,
135
+ payload,
136
+ opsResults,
137
+ };
138
+ }
139
+
140
+ async function runPromptWithAssistant({
141
+ projectRoot,
142
+ prompt,
143
+ provider,
144
+ model,
145
+ processManager = null,
146
+ runUfooAgent,
147
+ runAssistantTask,
148
+ dispatchMessages,
149
+ handleOps,
150
+ markPending = () => {},
151
+ reportTaskStatus = () => {},
152
+ maxAssistantLoops = 2,
153
+ log = () => {},
154
+ }) {
155
+ const firstResult = await runUfooAgent({
156
+ projectRoot,
157
+ prompt: prompt || "",
158
+ provider,
159
+ model,
160
+ });
161
+
162
+ if (!firstResult || !firstResult.ok) {
163
+ return {
164
+ ok: false,
165
+ error: (firstResult && firstResult.error) || "agent failed",
166
+ };
167
+ }
168
+
169
+ const firstPayload = normalizePayload(firstResult.payload);
170
+ const extractedFirst = extractAssistantCall(firstPayload);
171
+ const assistantCall = normalizeAssistantCall(extractedFirst.assistantCall);
172
+ const basePayload = {
173
+ ...firstPayload,
174
+ ops: extractedFirst.ops,
175
+ };
176
+ delete basePayload.assistant_call;
177
+
178
+ if (!assistantCall || maxAssistantLoops < 1) {
179
+ return finalizePromptRun({
180
+ projectRoot,
181
+ payload: basePayload,
182
+ processManager,
183
+ dispatchMessages,
184
+ handleOps,
185
+ markPending,
186
+ });
187
+ }
188
+
189
+ const assistantTaskId = createAssistantTaskId();
190
+ await emitAssistantReport(reportTaskStatus, {
191
+ phase: "start",
192
+ source: "assistant",
193
+ agent_id: "ufoo-assistant-agent",
194
+ scope: "private",
195
+ controller_id: "ufoo-agent",
196
+ task_id: assistantTaskId,
197
+ message: assistantCall.task,
198
+ summary: "",
199
+ error: "",
200
+ ok: true,
201
+ meta: {
202
+ kind: assistantCall.kind,
203
+ provider: assistantCall.provider || provider || "",
204
+ model: assistantCall.model || model || "",
205
+ },
206
+ });
207
+
208
+ let assistantResult;
209
+ try {
210
+ assistantResult = await runAssistantTask({
211
+ projectRoot,
212
+ provider: assistantCall.provider || "",
213
+ fallbackProvider: provider,
214
+ model: assistantCall.model || model,
215
+ task: assistantCall.task,
216
+ kind: assistantCall.kind,
217
+ context: assistantCall.context,
218
+ expect: assistantCall.expect,
219
+ timeoutMs: assistantCall.timeoutMs || undefined,
220
+ });
221
+ } catch (err) {
222
+ assistantResult = {
223
+ ok: false,
224
+ summary: "",
225
+ artifacts: [],
226
+ logs: [],
227
+ error: err && err.message ? err.message : "assistant task failed",
228
+ metrics: {},
229
+ };
230
+ }
231
+
232
+ await emitAssistantReport(reportTaskStatus, {
233
+ phase: assistantResult && assistantResult.ok === false ? "error" : "done",
234
+ source: "assistant",
235
+ agent_id: "ufoo-assistant-agent",
236
+ scope: "private",
237
+ controller_id: "ufoo-agent",
238
+ task_id: assistantTaskId,
239
+ message: assistantCall.task,
240
+ summary: assistantResult && typeof assistantResult.summary === "string" ? assistantResult.summary : "",
241
+ error: assistantResult && typeof assistantResult.error === "string" ? assistantResult.error : "",
242
+ ok: assistantResult && assistantResult.ok !== false,
243
+ meta: {
244
+ kind: assistantCall.kind,
245
+ provider: assistantCall.provider || provider || "",
246
+ model: assistantCall.model || model || "",
247
+ metrics: assistantResult && assistantResult.metrics && typeof assistantResult.metrics === "object"
248
+ ? assistantResult.metrics
249
+ : {},
250
+ },
251
+ });
252
+
253
+ if (!assistantResult || assistantResult.ok === false) {
254
+ log("assistant-loop fallback to round1 payload");
255
+ const fallbackPayload = annotateAssistantFailureFallback(basePayload, assistantResult);
256
+ return finalizePromptRun({
257
+ projectRoot,
258
+ payload: fallbackPayload,
259
+ processManager,
260
+ dispatchMessages,
261
+ handleOps,
262
+ markPending,
263
+ });
264
+ }
265
+
266
+ const reports = [buildAssistantReport(assistantCall, assistantResult)];
267
+ const continuationPrompt = buildAssistantContinuationPrompt({
268
+ originalPrompt: prompt || "",
269
+ previousReply: basePayload.reply || "",
270
+ reports,
271
+ });
272
+
273
+ const secondResult = await runUfooAgent({
274
+ projectRoot,
275
+ prompt: continuationPrompt,
276
+ provider,
277
+ model,
278
+ });
279
+
280
+ if (!secondResult || !secondResult.ok) {
281
+ log("assistant-loop fallback to round1 payload (round2 failed)");
282
+ return finalizePromptRun({
283
+ projectRoot,
284
+ payload: basePayload,
285
+ processManager,
286
+ dispatchMessages,
287
+ handleOps,
288
+ markPending,
289
+ });
290
+ }
291
+
292
+ const secondPayload = normalizePayload(secondResult.payload);
293
+ const extractedSecond = extractAssistantCall(secondPayload);
294
+ const finalPayload = {
295
+ ...secondPayload,
296
+ ops: extractedSecond.ops,
297
+ assistant: { runs: reports },
298
+ };
299
+ delete finalPayload.assistant_call;
300
+
301
+ return finalizePromptRun({
302
+ projectRoot,
303
+ payload: finalPayload,
304
+ processManager,
305
+ dispatchMessages,
306
+ handleOps,
307
+ markPending,
308
+ });
309
+ }
310
+
311
+ module.exports = {
312
+ runPromptWithAssistant,
313
+ buildAssistantContinuationPrompt,
314
+ normalizePayload,
315
+ annotateAssistantFailureFallback,
316
+ extractAssistantCall,
317
+ normalizeAssistantCall,
318
+ buildAssistantReport,
319
+ };
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+
3
+ const { IPC_RESPONSE_TYPES } = require("../shared/eventContract");
4
+ const {
5
+ listControllerInboxEntries,
6
+ consumeControllerInboxEntries,
7
+ } = require("../report/store");
8
+
9
+ function buildPromptWithPrivateReports(prompt = "", reports = []) {
10
+ if (!Array.isArray(reports) || reports.length === 0) {
11
+ return prompt;
12
+ }
13
+ const lines = [];
14
+ lines.push(prompt || "");
15
+ lines.push("");
16
+ lines.push("Private runtime reports for ufoo-agent (JSON):");
17
+ lines.push(JSON.stringify(reports, null, 2));
18
+ lines.push("");
19
+ lines.push("Use these runtime reports when deciding reply/dispatch/ops.");
20
+ return lines.join("\n");
21
+ }
22
+
23
+ async function handlePromptRequest(options = {}) {
24
+ const {
25
+ projectRoot,
26
+ req = {},
27
+ socket,
28
+ provider,
29
+ model,
30
+ processManager = null,
31
+ runPromptWithAssistant,
32
+ runUfooAgent,
33
+ runAssistantTask,
34
+ dispatchMessages,
35
+ handleOps,
36
+ markPending = () => {},
37
+ reportTaskStatus = () => {},
38
+ log = () => {},
39
+ } = options;
40
+
41
+ log(`prompt ${String(req.text || "").slice(0, 200)}`);
42
+ const privateReports = listControllerInboxEntries(projectRoot, "ufoo-agent", { num: 100 });
43
+ const promptText = buildPromptWithPrivateReports(req.text || "", privateReports);
44
+
45
+ try {
46
+ const handled = await runPromptWithAssistant({
47
+ projectRoot,
48
+ prompt: promptText,
49
+ provider,
50
+ model,
51
+ processManager,
52
+ runUfooAgent,
53
+ runAssistantTask,
54
+ dispatchMessages,
55
+ handleOps,
56
+ markPending,
57
+ reportTaskStatus,
58
+ maxAssistantLoops: 2,
59
+ log,
60
+ });
61
+
62
+ if (!handled.ok) {
63
+ log(`agent-fail ${handled.error || "agent failed"}`);
64
+ socket.write(
65
+ `${JSON.stringify({
66
+ type: IPC_RESPONSE_TYPES.ERROR,
67
+ error: handled.error || "agent failed",
68
+ })}\n`,
69
+ );
70
+ return false;
71
+ }
72
+
73
+ consumeControllerInboxEntries(projectRoot, "ufoo-agent", privateReports);
74
+
75
+ const payload = handled.payload || {};
76
+ const opsResults = handled.opsResults || [];
77
+ log(`ok reply=${Boolean(payload.reply)} dispatch=${(payload.dispatch || []).length} ops=${(payload.ops || []).length}`);
78
+ socket.write(
79
+ `${JSON.stringify({
80
+ type: IPC_RESPONSE_TYPES.RESPONSE,
81
+ data: payload,
82
+ opsResults,
83
+ })}\n`,
84
+ );
85
+ return true;
86
+ } catch (err) {
87
+ log(`error ${err.message || String(err)}`);
88
+ socket.write(
89
+ `${JSON.stringify({
90
+ type: IPC_RESPONSE_TYPES.ERROR,
91
+ error: err.message || String(err),
92
+ })}\n`,
93
+ );
94
+ return false;
95
+ }
96
+ }
97
+
98
+ module.exports = {
99
+ handlePromptRequest,
100
+ buildPromptWithPrivateReports,
101
+ };
@@ -14,11 +14,16 @@ function buildProbeMarker(nickname) {
14
14
  }
15
15
 
16
16
  /**
17
- * Build probe command: /ufoo <nickname> for claude-code, ufoo <nickname> for codex
17
+ * Build probe command:
18
+ * - claude-code: /ufoo <nickname>
19
+ * - codex: $ufoo <nickname>
18
20
  */
19
21
  function buildProbeCommand(agentType, nickname) {
20
- const base = `ufoo ${nickname}`;
21
- return agentType === "claude-code" ? `/${base}` : base;
22
+ const marker = String(nickname || "").trim();
23
+ if (agentType === "claude-code") {
24
+ return `/ufoo ${marker}`;
25
+ }
26
+ return `$ufoo ${marker}`;
22
27
  }
23
28
 
24
29
  function readLines(filePath) {
@@ -30,22 +35,30 @@ function readLines(filePath) {
30
35
  }
31
36
  }
32
37
 
38
+ function escapeRegExp(value = "") {
39
+ return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
40
+ }
41
+
42
+ function containsProbeCommand(text, marker) {
43
+ if (!text || !marker) return false;
44
+ const escapedMarker = escapeRegExp(marker);
45
+ const pattern = `(?:^|[\\s"'\\\`])(?:\\/ufoo|\\$ufoo|ufoo)\\s+${escapedMarker}(?=$|[\\s"'\\\`.,:;!?\\]\\)\\}])`;
46
+ const re = new RegExp(pattern);
47
+ return re.test(String(text));
48
+ }
49
+
33
50
  /**
34
51
  * Check if a history record contains our probe marker
35
- * Searches for "/ufoo <marker>" or "ufoo <marker>" pattern
52
+ * Searches for probe marker command patterns:
53
+ * - "/ufoo <marker>" (claude)
54
+ * - "$ufoo <marker>" (codex)
55
+ * - "ufoo <marker>" (legacy compatibility)
36
56
  */
37
57
  function recordContainsMarker(record, marker, rawLine) {
38
58
  if (!marker) return false;
39
59
 
40
- // Build both possible patterns
41
- const patterns = [`/ufoo ${marker}`, `ufoo ${marker}`];
42
-
43
60
  // Check raw line first (fastest)
44
- if (rawLine) {
45
- for (const pattern of patterns) {
46
- if (rawLine.includes(pattern)) return true;
47
- }
48
- }
61
+ if (containsProbeCommand(rawLine, marker)) return true;
49
62
 
50
63
  if (!record || typeof record !== "object") return false;
51
64
 
@@ -61,11 +74,7 @@ function recordContainsMarker(record, marker, rawLine) {
61
74
  ];
62
75
 
63
76
  for (const field of fields) {
64
- if (typeof field === "string") {
65
- for (const pattern of patterns) {
66
- if (field.includes(pattern)) return true;
67
- }
68
- }
77
+ if (containsProbeCommand(field, marker)) return true;
69
78
  }
70
79
  return false;
71
80
  }
@@ -288,4 +297,10 @@ function scheduleProviderSessionProbe({
288
297
  module.exports = {
289
298
  scheduleProviderSessionProbe,
290
299
  loadProviderSessionCache,
300
+ __private: {
301
+ buildProbeCommand,
302
+ recordContainsMarker,
303
+ containsProbeCommand,
304
+ escapeRegExp,
305
+ },
291
306
  };
@@ -0,0 +1,90 @@
1
+ const fs = require("fs");
2
+ const { BUS_STATUS_PHASES } = require("../shared/eventContract");
3
+ const {
4
+ REPORT_PHASES,
5
+ normalizeReportInput,
6
+ appendReport,
7
+ updateReportState,
8
+ appendControllerInboxEntry,
9
+ } = require("../report/store");
10
+ const { getUfooPaths } = require("../ufoo/paths");
11
+
12
+ function resolveAgentDisplayName(projectRoot, agentId) {
13
+ if (!agentId) return "unknown-agent";
14
+ try {
15
+ const busPath = getUfooPaths(projectRoot).agentsFile;
16
+ const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
17
+ const meta = bus && bus.agents ? bus.agents[agentId] : null;
18
+ if (meta && typeof meta.nickname === "string" && meta.nickname.trim()) {
19
+ return meta.nickname.trim();
20
+ }
21
+ } catch {
22
+ // ignore
23
+ }
24
+ return agentId;
25
+ }
26
+
27
+ function toStatusPhase(reportPhase) {
28
+ if (reportPhase === REPORT_PHASES.START || reportPhase === REPORT_PHASES.PROGRESS) {
29
+ return BUS_STATUS_PHASES.START;
30
+ }
31
+ if (reportPhase === REPORT_PHASES.ERROR) return BUS_STATUS_PHASES.ERROR;
32
+ return BUS_STATUS_PHASES.DONE;
33
+ }
34
+
35
+ function formatStatusText(displayName, entry) {
36
+ if (entry.phase === REPORT_PHASES.START) {
37
+ const detail = entry.message || entry.summary || entry.task_id;
38
+ return `${displayName} ${detail}`;
39
+ }
40
+ if (entry.phase === REPORT_PHASES.PROGRESS) {
41
+ const detail = entry.message || entry.summary || entry.task_id;
42
+ return `${displayName} progress: ${detail}`;
43
+ }
44
+ if (entry.phase === REPORT_PHASES.ERROR) {
45
+ const detail = entry.error || entry.summary || entry.message || entry.task_id;
46
+ return `${displayName} failed: ${detail}`;
47
+ }
48
+ const detail = entry.summary || entry.message || entry.task_id;
49
+ return `${displayName} done: ${detail}`;
50
+ }
51
+
52
+ function buildReportStatus(entry, displayName) {
53
+ return {
54
+ phase: toStatusPhase(entry.phase),
55
+ key: `report:${entry.agent_id}:${entry.task_id}`,
56
+ text: formatStatusText(displayName, entry),
57
+ };
58
+ }
59
+
60
+ function publishToPrivateController(projectRoot, entry) {
61
+ if (!entry || !entry.controller_id) return;
62
+ appendControllerInboxEntry(projectRoot, entry.controller_id, entry);
63
+ }
64
+
65
+ async function recordAgentReport({
66
+ projectRoot,
67
+ report,
68
+ onStatus = () => {},
69
+ log = () => {},
70
+ }) {
71
+ const entry = normalizeReportInput(report);
72
+ appendReport(projectRoot, entry);
73
+ const state = updateReportState(projectRoot, entry);
74
+ publishToPrivateController(projectRoot, entry);
75
+ const displayName = resolveAgentDisplayName(projectRoot, entry.agent_id);
76
+ if (entry.scope !== "private") {
77
+ onStatus(buildReportStatus(entry, displayName));
78
+ }
79
+ log(`report ${entry.phase} scope=${entry.scope} agent=${entry.agent_id} task=${entry.task_id}`);
80
+ return { entry, state };
81
+ }
82
+
83
+ module.exports = {
84
+ recordAgentReport,
85
+ resolveAgentDisplayName,
86
+ toStatusPhase,
87
+ formatStatusText,
88
+ buildReportStatus,
89
+ publishToPrivateController,
90
+ };
package/src/daemon/run.js CHANGED
@@ -46,9 +46,7 @@ function runDaemonCli(argv) {
46
46
  // Start fresh daemon
47
47
  if (!process.env.UFOO_DAEMON_CHILD) {
48
48
  const { spawn } = require("child_process");
49
- const forceResume = launchMode !== "terminal";
50
49
  const childEnv = { ...process.env, UFOO_DAEMON_CHILD: "1" };
51
- if (forceResume) childEnv.UFOO_FORCE_RESUME = "1";
52
50
  const child = spawn(process.execPath, [path.join(__dirname, "..", "..", "bin", "ufoo.js"), "daemon", "start"], {
53
51
  detached: true,
54
52
  stdio: "ignore",
@@ -58,9 +56,8 @@ function runDaemonCli(argv) {
58
56
  child.unref();
59
57
  return;
60
58
  }
61
- // Skip auto-resume on restart in terminal mode to avoid reopening stale terminals.
62
- const forceResume = launchMode !== "terminal";
63
- startDaemon({ projectRoot, provider, model, resumeMode: forceResume ? "force" : "none" });
59
+ // Manual restart does not auto-resume; crash-recovery is handled on next auto start with stale lock detection.
60
+ startDaemon({ projectRoot, provider, model, resumeMode: "none" });
64
61
  return;
65
62
  }
66
63
  if (cmd === "status" || cmd === "--status") {
@@ -2,6 +2,7 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { getUfooPaths } = require("../ufoo/paths");
4
4
  const { isMetaActive } = require("../bus/utils");
5
+ const { readReportSummary } = require("../report/store");
5
6
 
6
7
  function readBus(projectRoot) {
7
8
  const busPath = getUfooPaths(projectRoot).agentsFile;
@@ -60,11 +61,28 @@ function isHiddenSubscriber(id, meta) {
60
61
  return false;
61
62
  }
62
63
 
63
- function buildStatus(projectRoot) {
64
+ function normalizeCronTasks(raw = []) {
65
+ const items = Array.isArray(raw) ? raw : [];
66
+ return items.map((task) => ({
67
+ id: String(task && task.id ? task.id : ""),
68
+ intervalMs: Number(task && task.intervalMs ? task.intervalMs : 0) || 0,
69
+ interval: String(task && task.interval ? task.interval : ""),
70
+ targets: Array.isArray(task && task.targets) ? task.targets.slice() : [],
71
+ prompt: String(task && task.prompt ? task.prompt : ""),
72
+ summary: String(task && task.summary ? task.summary : ""),
73
+ createdAt: Number(task && task.createdAt ? task.createdAt : 0) || 0,
74
+ lastRunAt: Number(task && task.lastRunAt ? task.lastRunAt : 0) || 0,
75
+ tickCount: Number(task && task.tickCount ? task.tickCount : 0) || 0,
76
+ }));
77
+ }
78
+
79
+ function buildStatus(projectRoot, options = {}) {
64
80
  const bus = readBus(projectRoot);
65
81
  const decisions = readDecisions(projectRoot);
66
82
  const unread = readUnread(projectRoot);
83
+ const reports = readReportSummary(projectRoot);
67
84
  const subscribers = bus ? Object.keys(bus.agents || {}) : [];
85
+ const cronTasks = normalizeCronTasks(options.cronTasks || []);
68
86
 
69
87
  const activeEntries = bus
70
88
  ? Object.entries(bus.agents || {})
@@ -89,6 +107,11 @@ function buildStatus(projectRoot) {
89
107
  active_meta: activeMeta,
90
108
  unread,
91
109
  decisions,
110
+ reports,
111
+ cron: {
112
+ count: cronTasks.length,
113
+ tasks: cronTasks,
114
+ },
92
115
  };
93
116
  }
94
117