u-foo 1.0.3 → 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 (179) hide show
  1. package/README.md +110 -11
  2. package/README.zh-CN.md +9 -7
  3. package/SKILLS/ufoo/SKILL.md +132 -0
  4. package/SKILLS/uinit/SKILL.md +78 -0
  5. package/SKILLS/ustatus/SKILL.md +36 -0
  6. package/bin/uclaude.js +13 -0
  7. package/bin/ucode-core.js +15 -0
  8. package/bin/ucode.js +125 -0
  9. package/bin/ucodex.js +13 -0
  10. package/bin/ufoo +9 -31
  11. package/bin/ufoo-assistant-agent.js +5 -0
  12. package/bin/ufoo-engine.js +25 -0
  13. package/bin/ufoo.js +17 -0
  14. package/modules/AGENTS.template.md +29 -11
  15. package/modules/bus/README.md +33 -25
  16. package/modules/bus/SKILLS/ubus/SKILL.md +19 -8
  17. package/modules/context/README.md +18 -40
  18. package/modules/context/SKILLS/uctx/SKILL.md +63 -1
  19. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  20. package/package.json +25 -4
  21. package/scripts/import-pi-mono.js +124 -0
  22. package/scripts/postinstall.js +30 -0
  23. package/scripts/sync-claude-skills.sh +21 -0
  24. package/src/agent/cliRunner.js +554 -33
  25. package/src/agent/internalRunner.js +150 -56
  26. package/src/agent/launcher.js +754 -0
  27. package/src/agent/normalizeOutput.js +1 -1
  28. package/src/agent/notifier.js +340 -0
  29. package/src/agent/ptyRunner.js +847 -0
  30. package/src/agent/ptyWrapper.js +379 -0
  31. package/src/agent/readyDetector.js +175 -0
  32. package/src/agent/ucode.js +443 -0
  33. package/src/agent/ucodeBootstrap.js +113 -0
  34. package/src/agent/ucodeBuild.js +67 -0
  35. package/src/agent/ucodeDoctor.js +184 -0
  36. package/src/agent/ucodeRuntimeConfig.js +129 -0
  37. package/src/agent/ufooAgent.js +46 -42
  38. package/src/assistant/agent.js +260 -0
  39. package/src/assistant/bridge.js +172 -0
  40. package/src/assistant/engine.js +252 -0
  41. package/src/assistant/stdio.js +58 -0
  42. package/src/assistant/ufooEngineCli.js +306 -0
  43. package/src/bus/activate.js +172 -0
  44. package/src/bus/daemon.js +436 -0
  45. package/src/bus/index.js +842 -0
  46. package/src/bus/inject.js +315 -0
  47. package/src/bus/message.js +430 -0
  48. package/src/bus/nickname.js +88 -0
  49. package/src/bus/queue.js +136 -0
  50. package/src/bus/shake.js +26 -0
  51. package/src/bus/store.js +189 -0
  52. package/src/bus/subscriber.js +312 -0
  53. package/src/bus/utils.js +363 -0
  54. package/src/chat/agentBar.js +117 -0
  55. package/src/chat/agentDirectory.js +88 -0
  56. package/src/chat/agentSockets.js +225 -0
  57. package/src/chat/agentViewController.js +298 -0
  58. package/src/chat/chatLogController.js +115 -0
  59. package/src/chat/commandExecutor.js +700 -0
  60. package/src/chat/commands.js +132 -0
  61. package/src/chat/completionController.js +414 -0
  62. package/src/chat/cronScheduler.js +160 -0
  63. package/src/chat/daemonConnection.js +166 -0
  64. package/src/chat/daemonCoordinator.js +64 -0
  65. package/src/chat/daemonMessageRouter.js +257 -0
  66. package/src/chat/daemonReconnect.js +41 -0
  67. package/src/chat/daemonTransport.js +36 -0
  68. package/src/chat/daemonTransportDefaults.js +10 -0
  69. package/src/chat/dashboardKeyController.js +480 -0
  70. package/src/chat/dashboardView.js +154 -0
  71. package/src/chat/index.js +1011 -1392
  72. package/src/chat/inputHistoryController.js +105 -0
  73. package/src/chat/inputListenerController.js +304 -0
  74. package/src/chat/inputMath.js +104 -0
  75. package/src/chat/inputSubmitHandler.js +171 -0
  76. package/src/chat/layout.js +165 -0
  77. package/src/chat/pasteController.js +81 -0
  78. package/src/chat/rawKeyMap.js +42 -0
  79. package/src/chat/settingsController.js +132 -0
  80. package/src/chat/statusLineController.js +177 -0
  81. package/src/chat/streamTracker.js +138 -0
  82. package/src/chat/text.js +70 -0
  83. package/src/chat/transport.js +61 -0
  84. package/src/cli/busCoreCommands.js +59 -0
  85. package/src/cli/ctxCoreCommands.js +199 -0
  86. package/src/cli/onlineCoreCommands.js +379 -0
  87. package/src/cli.js +1162 -96
  88. package/src/code/README.md +29 -0
  89. package/src/code/UCODE_PROMPT.md +32 -0
  90. package/src/code/agent.js +1651 -0
  91. package/src/code/cli.js +158 -0
  92. package/src/code/config +0 -0
  93. package/src/code/dispatch.js +42 -0
  94. package/src/code/index.js +70 -0
  95. package/src/code/nativeRunner.js +1213 -0
  96. package/src/code/runtime.js +154 -0
  97. package/src/code/sessionStore.js +162 -0
  98. package/src/code/taskDecomposer.js +269 -0
  99. package/src/code/tools/bash.js +53 -0
  100. package/src/code/tools/common.js +42 -0
  101. package/src/code/tools/edit.js +70 -0
  102. package/src/code/tools/read.js +44 -0
  103. package/src/code/tools/write.js +35 -0
  104. package/src/code/tui.js +1580 -0
  105. package/src/config.js +56 -3
  106. package/src/context/decisions.js +324 -0
  107. package/src/context/doctor.js +183 -0
  108. package/src/context/index.js +55 -0
  109. package/src/context/sync.js +127 -0
  110. package/src/daemon/agentProcessManager.js +74 -0
  111. package/src/daemon/cronOps.js +241 -0
  112. package/src/daemon/index.js +998 -170
  113. package/src/daemon/ipcServer.js +99 -0
  114. package/src/daemon/ops.js +630 -48
  115. package/src/daemon/promptLoop.js +319 -0
  116. package/src/daemon/promptRequest.js +101 -0
  117. package/src/daemon/providerSessions.js +306 -0
  118. package/src/daemon/reporting.js +90 -0
  119. package/src/daemon/run.js +31 -1
  120. package/src/daemon/status.js +48 -8
  121. package/src/doctor/index.js +50 -0
  122. package/src/init/index.js +318 -0
  123. package/src/online/bridge.js +663 -0
  124. package/src/online/client.js +245 -0
  125. package/src/online/runner.js +253 -0
  126. package/src/online/server.js +992 -0
  127. package/src/online/tokens.js +103 -0
  128. package/src/report/store.js +331 -0
  129. package/src/shared/eventContract.js +35 -0
  130. package/src/shared/ptySocketContract.js +21 -0
  131. package/src/skills/index.js +159 -0
  132. package/src/status/index.js +285 -0
  133. package/src/terminal/adapterContract.js +87 -0
  134. package/src/terminal/adapterRouter.js +84 -0
  135. package/src/terminal/adapters/externalAdapter.js +14 -0
  136. package/src/terminal/adapters/internalAdapter.js +13 -0
  137. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  138. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  139. package/src/terminal/adapters/terminalAdapter.js +31 -0
  140. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  141. package/src/terminal/detect.js +64 -0
  142. package/src/terminal/index.js +8 -0
  143. package/src/terminal/iterm2.js +126 -0
  144. package/src/ufoo/agentsStore.js +107 -0
  145. package/src/ufoo/paths.js +46 -0
  146. package/src/utils/banner.js +76 -0
  147. package/bin/uclaude +0 -65
  148. package/bin/ucodex +0 -65
  149. package/modules/bus/scripts/bus-alert.sh +0 -185
  150. package/modules/bus/scripts/bus-listen.sh +0 -117
  151. package/modules/context/ASSUMPTIONS.md +0 -7
  152. package/modules/context/CONSTRAINTS.md +0 -7
  153. package/modules/context/CONTEXT-STRUCTURE.md +0 -49
  154. package/modules/context/DECISION-PROTOCOL.md +0 -62
  155. package/modules/context/HANDOFF.md +0 -33
  156. package/modules/context/RULES.md +0 -15
  157. package/modules/context/SKILLS/README.md +0 -14
  158. package/modules/context/SYSTEM.md +0 -18
  159. package/modules/context/TEMPLATES/assumptions.md +0 -4
  160. package/modules/context/TEMPLATES/constraints.md +0 -4
  161. package/modules/context/TEMPLATES/decision.md +0 -16
  162. package/modules/context/TEMPLATES/project-context-readme.md +0 -6
  163. package/modules/context/TEMPLATES/system.md +0 -3
  164. package/modules/context/TEMPLATES/terminology.md +0 -4
  165. package/modules/context/TERMINOLOGY.md +0 -10
  166. package/scripts/banner.sh +0 -89
  167. package/scripts/bus-alert.sh +0 -6
  168. package/scripts/bus-autotrigger.sh +0 -6
  169. package/scripts/bus-daemon.sh +0 -231
  170. package/scripts/bus-inject.sh +0 -144
  171. package/scripts/bus-listen.sh +0 -6
  172. package/scripts/bus.sh +0 -984
  173. package/scripts/context-decisions.sh +0 -167
  174. package/scripts/context-doctor.sh +0 -72
  175. package/scripts/context-lint.sh +0 -110
  176. package/scripts/doctor.sh +0 -22
  177. package/scripts/init.sh +0 -247
  178. package/scripts/skills.sh +0 -113
  179. package/scripts/status.sh +0 -125
@@ -0,0 +1,1651 @@
1
+ const readline = require("readline");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const { execSync } = require("child_process");
5
+ const { runToolCall } = require("./dispatch");
6
+ const { runNativeAgentTask } = require("./nativeRunner");
7
+ const {
8
+ runDecomposedTask,
9
+ createBusProgressReporter,
10
+ } = require("./taskDecomposer");
11
+ const {
12
+ runUcodeTui,
13
+ shouldUseUcodeTui,
14
+ buildUcodeBannerLines,
15
+ StreamBuffer,
16
+ createEscapeTagStripper,
17
+ stripLeakedEscapeTags,
18
+ } = require("./tui");
19
+ const { stripBlessedTags } = require("../chat/text");
20
+ const { loadConfig } = require("../config");
21
+ const {
22
+ resolveSessionId,
23
+ normalizeSessionId,
24
+ saveSessionSnapshot,
25
+ loadSessionSnapshot,
26
+ } = require("./sessionStore");
27
+
28
+ function printPrompt() {
29
+ process.stdout.write("> ");
30
+ }
31
+
32
+ function printUcodeBanner(stdout = process.stdout, { model = "", workspaceRoot = process.cwd(), sessionId = "" } = {}) {
33
+ stdout.write(`${buildUcodeBannerLines({
34
+ model,
35
+ engine: "ufoo-core",
36
+ workspaceRoot,
37
+ sessionId,
38
+ width: (stdout && stdout.columns) || 0,
39
+ }).join("\n")}\n`);
40
+ }
41
+
42
+ function normalizeLine(input = "") {
43
+ return String(input || "").trim();
44
+ }
45
+
46
+ function parseJson(text = "") {
47
+ const raw = String(text || "").trim();
48
+ if (!raw) return {};
49
+ const parsed = JSON.parse(raw);
50
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
51
+ return parsed;
52
+ }
53
+
54
+ function readTextOrFile(value = "") {
55
+ const raw = String(value || "").trim();
56
+ if (!raw) return "";
57
+ try {
58
+ if (fs.existsSync(raw)) return String(fs.readFileSync(raw, "utf8") || "");
59
+ } catch {
60
+ // ignore
61
+ }
62
+ return raw;
63
+ }
64
+
65
+ function extractAgentNickname(agentId = "") {
66
+ // Extract nickname from agent ID like "ufoo-agent:abc123" -> "ufoo"
67
+ const id = String(agentId || "").trim();
68
+ if (!id) return "";
69
+
70
+ // Remove the instance ID part (after colon)
71
+ const base = id.split(":")[0];
72
+
73
+ // Common agent nickname mappings
74
+ if (base === "ufoo-agent") return "ufoo";
75
+ if (base === "claude-code") return "claude";
76
+ if (base === "ufoo-code") return "ucode";
77
+
78
+ // Return base name as-is for others
79
+ return base;
80
+ }
81
+
82
+ function resolveUcodeProviderModel({
83
+ workspaceRoot = process.cwd(),
84
+ provider = "",
85
+ model = "",
86
+ } = {}) {
87
+ const root = path.resolve(workspaceRoot || process.cwd());
88
+ const config = loadConfig(root);
89
+ const fallbackProviderFromAgent = resolvePlannerProvider(String(config.agentProvider || "").trim());
90
+ const explicitProvider = String(
91
+ provider
92
+ || process.env.UFOO_UCODE_PROVIDER
93
+ || config.ucodeProvider
94
+ || ""
95
+ ).trim();
96
+ const resolvedProvider = resolvePlannerProvider(explicitProvider || fallbackProviderFromAgent);
97
+ const resolvedModel = String(
98
+ model
99
+ || process.env.UFOO_UCODE_MODEL
100
+ || config.ucodeModel
101
+ || config.agentModel
102
+ || ""
103
+ ).trim();
104
+ return {
105
+ provider: resolvedProvider,
106
+ model: resolvedModel || "default",
107
+ };
108
+ }
109
+
110
+ function clampContext(text = "", maxChars = 32000) {
111
+ const value = String(text || "");
112
+ if (value.length <= maxChars) return value;
113
+ return `${value.slice(0, maxChars)}\n...[truncated]`;
114
+ }
115
+
116
+ function resolvePlannerProvider(value = "") {
117
+ const text = String(value || "").trim().toLowerCase();
118
+ if (!text) return "";
119
+ if (text === "claude" || text === "claude-cli" || text === "claude-code" || text === "anthropic") return "anthropic";
120
+ if (text === "codex" || text === "codex-cli" || text === "codex-code" || text === "openai") return "openai";
121
+ return text;
122
+ }
123
+
124
+ function extractJsonSummary(text = "") {
125
+ const raw = String(text || "").trim();
126
+ if (!raw) return "";
127
+ const direct = (() => {
128
+ try {
129
+ return JSON.parse(raw);
130
+ } catch {
131
+ return null;
132
+ }
133
+ })();
134
+ if (direct && typeof direct === "object") {
135
+ if (typeof direct.summary === "string" && direct.summary.trim()) return direct.summary.trim();
136
+ if (typeof direct.reply === "string" && direct.reply.trim()) return direct.reply.trim();
137
+ }
138
+ const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
139
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
140
+ try {
141
+ const parsed = JSON.parse(lines[i]);
142
+ if (parsed && typeof parsed === "object") {
143
+ if (typeof parsed.summary === "string" && parsed.summary.trim()) return parsed.summary.trim();
144
+ if (typeof parsed.reply === "string" && parsed.reply.trim()) return parsed.reply.trim();
145
+ }
146
+ } catch {
147
+ // keep scanning
148
+ }
149
+ }
150
+ return raw;
151
+ }
152
+
153
+ function isCliTimeoutError(message = "") {
154
+ const text = String(message || "").toLowerCase();
155
+ return text.includes("cli timeout");
156
+ }
157
+
158
+ function isCliCancelledError(message = "") {
159
+ const text = String(message || "").toLowerCase();
160
+ return text.includes("cli cancelled") || text.includes("canceled");
161
+ }
162
+
163
+ function computeExtendedTimeout(baseTimeoutMs) {
164
+ const base = Number.isFinite(baseTimeoutMs) ? Math.max(1000, Math.floor(baseTimeoutMs)) : 300000;
165
+ return Math.min(1800000, Math.max(base * 2, base + 120000));
166
+ }
167
+
168
+ function enrichNativeError(errorMessage = "") {
169
+ const text = String(errorMessage || "").trim();
170
+ if (!text) return "nl task failed";
171
+
172
+ const lower = text.toLowerCase();
173
+ if (
174
+ lower.includes("fetch failed")
175
+ || lower.includes("enotfound")
176
+ || lower.includes("econnrefused")
177
+ || lower.includes("network error")
178
+ || lower.includes("other side closed")
179
+ ) {
180
+ return `${text}. Network connection to provider failed. Check VPN/proxy/network and verify endpoint/key via /settings ucode show.`;
181
+ }
182
+ if (lower.includes("model is not configured")) {
183
+ return `${text}. Configure ucode with /settings ucode set provider=<openai|anthropic> model=<id> key=<apiKey> [url=<baseUrl>]`;
184
+ }
185
+ if (lower.includes("baseurl is not configured")) {
186
+ return `${text}. Configure endpoint with /settings ucode set url=<baseUrl> (and key/model if missing).`;
187
+ }
188
+ if (
189
+ /provider request failed \((401|403)\)/i.test(text)
190
+ || lower.includes("unauthorized")
191
+ || lower.includes("invalid api key")
192
+ ) {
193
+ return `${text}. Check provider/url/key via /settings ucode show.`;
194
+ }
195
+ return text;
196
+ }
197
+
198
+ function normalizeToolLogEvent(event = {}) {
199
+ if (!event || typeof event !== "object") return null;
200
+ const tool = String(event.tool || event.name || "").trim().toLowerCase();
201
+ if (!tool) return null;
202
+ if (tool !== "read" && tool !== "write" && tool !== "edit" && tool !== "bash") return null;
203
+ const phase = String(event.phase || "update").trim().toLowerCase();
204
+ const normalizedPhase = phase === "error" ? "error" : (phase === "start" ? "start" : "");
205
+ if (!normalizedPhase) return null;
206
+ const rawArgs = event.args && typeof event.args === "object" ? event.args : {};
207
+ const args = { ...rawArgs };
208
+ const error = String(event.error || "").trim();
209
+ return {
210
+ type: "tool",
211
+ tool,
212
+ phase: normalizedPhase,
213
+ args,
214
+ error,
215
+ };
216
+ }
217
+
218
+ function createToolLogCollector(logs = [], onToolLog = null) {
219
+ const list = Array.isArray(logs) ? logs : [];
220
+ const callback = typeof onToolLog === "function" ? onToolLog : null;
221
+
222
+ return (event = {}) => {
223
+ const log = normalizeToolLogEvent(event);
224
+ if (!log) return null;
225
+ list.push(log);
226
+ if (callback) {
227
+ try {
228
+ callback(log);
229
+ } catch {
230
+ // ignore callback failures
231
+ }
232
+ }
233
+ return log;
234
+ };
235
+ }
236
+
237
+ function isProjectAnalysisTask(task = "") {
238
+ const text = String(task || "").trim().toLowerCase();
239
+ if (!text) return false;
240
+ return /(?:analy[sz]e|analysis|review|audit|status|architecture|codebase|repo|project|现状|架构|审查|分析|项目|代码库)/i.test(text);
241
+ }
242
+
243
+ function createProjectPreflightContext({
244
+ workspaceRoot = process.cwd(),
245
+ pushToolLog = () => null,
246
+ } = {}) {
247
+ const root = String(workspaceRoot || process.cwd());
248
+ const readCandidates = [
249
+ "AGENTS.md",
250
+ "README.md",
251
+ "README.zh-CN.md",
252
+ "package.json",
253
+ ];
254
+ const blocks = [];
255
+
256
+ for (const relPath of readCandidates) {
257
+ pushToolLog({
258
+ tool: "read",
259
+ phase: "start",
260
+ args: { path: relPath },
261
+ error: "",
262
+ });
263
+ const readRes = runToolCall(
264
+ {
265
+ tool: "read",
266
+ args: { path: relPath, maxBytes: 12000 },
267
+ },
268
+ {
269
+ workspaceRoot: root,
270
+ cwd: root,
271
+ }
272
+ );
273
+ pushToolLog({
274
+ tool: "read",
275
+ phase: readRes && readRes.ok === false ? "error" : "",
276
+ args: { path: relPath },
277
+ error: readRes && readRes.ok === false ? String(readRes.error || "") : "",
278
+ });
279
+ if (!readRes || readRes.ok === false) continue;
280
+ const content = String(readRes.content || "").trim();
281
+ if (!content) continue;
282
+ const clipped = content.length > 2400
283
+ ? `${content.slice(0, 2400)}\n...[truncated]`
284
+ : content;
285
+ blocks.push(`File: ${relPath}\n${clipped}`);
286
+ if (blocks.length >= 2) break;
287
+ }
288
+
289
+ if (blocks.length === 0) {
290
+ const command = "ls -la";
291
+ pushToolLog({
292
+ tool: "bash",
293
+ phase: "start",
294
+ args: { command },
295
+ error: "",
296
+ });
297
+ const bashRes = runToolCall(
298
+ {
299
+ tool: "bash",
300
+ args: { command, timeoutMs: 4000 },
301
+ },
302
+ {
303
+ workspaceRoot: root,
304
+ cwd: root,
305
+ }
306
+ );
307
+ pushToolLog({
308
+ tool: "bash",
309
+ phase: bashRes && bashRes.ok === false ? "error" : "",
310
+ args: { command },
311
+ error: bashRes && bashRes.ok === false ? String(bashRes.error || "") : "",
312
+ });
313
+ if (bashRes && bashRes.ok !== false) {
314
+ const stdout = String(bashRes.stdout || "").trim();
315
+ const clipped = stdout.length > 1200
316
+ ? `${stdout.slice(0, 1200)}\n...[truncated]`
317
+ : stdout;
318
+ if (clipped) {
319
+ blocks.push(`Command: ${command}\n${clipped}`);
320
+ }
321
+ }
322
+ }
323
+
324
+ if (blocks.length === 0) return "";
325
+ return [
326
+ "Preflight snapshot (captured by ucode):",
327
+ ...blocks.map((block) => `---\n${block}`),
328
+ ].join("\n");
329
+ }
330
+
331
+ function buildNlFallbackSummary(logs = []) {
332
+ const list = Array.isArray(logs) ? logs : [];
333
+ const started = list.filter((entry) => entry && entry.phase === "start").length;
334
+ const failed = list.filter((entry) => entry && entry.phase === "error").length;
335
+
336
+ if (started > 0 || failed > 0) {
337
+ const parts = [`${started} tool step${started === 1 ? "" : "s"} started`];
338
+ if (failed > 0) parts.push(`${failed} failed`);
339
+ return `Done (${parts.join(", ")}).`;
340
+ }
341
+ if (list.length > 0) {
342
+ return `Done (${list.length} tool events).`;
343
+ }
344
+ return "Done (no model text response).";
345
+ }
346
+
347
+ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
348
+ const taskText = String(task || "").trim();
349
+ if (!taskText) {
350
+ return {
351
+ ok: false,
352
+ summary: "",
353
+ artifacts: [],
354
+ logs: [],
355
+ error: "empty task",
356
+ metrics: {},
357
+ streamed: false,
358
+ streamLastChar: "",
359
+ };
360
+ }
361
+
362
+ const provider = resolvePlannerProvider(
363
+ state.provider || process.env.UFOO_UCODE_PROVIDER || ""
364
+ );
365
+ const model = String(state.model || process.env.UFOO_UCODE_MODEL || "").trim();
366
+ const timeoutMs = Number.isFinite(state.timeoutMs) ? state.timeoutMs : 300000;
367
+ let streamed = false;
368
+ let streamLastChar = "";
369
+ const onDelta = typeof options.onDelta === "function"
370
+ ? options.onDelta
371
+ : null;
372
+ const logs = [];
373
+ const onToolLog = typeof options.onToolLog === "function"
374
+ ? options.onToolLog
375
+ : null;
376
+ const pushToolLog = createToolLogCollector(logs, onToolLog);
377
+
378
+ // Detect bug fix tasks and use decomposed runner
379
+ const isBugFixTask = /fix|bug|issue|problem|error|broken|not work/i.test(taskText);
380
+ const useDecomposition = isBugFixTask && !options.disableDecomposition;
381
+ const analysisTask = isProjectAnalysisTask(taskText);
382
+ const workspaceRoot = String(state.workspaceRoot || process.cwd());
383
+ const preflightContext = analysisTask
384
+ ? createProjectPreflightContext({
385
+ workspaceRoot,
386
+ pushToolLog,
387
+ })
388
+ : "";
389
+ const taskPrompt = analysisTask
390
+ ? `${taskText}\n\nAnalysis requirements:\n- Inspect repository evidence before concluding.\n- Cite concrete file observations.\n- Keep findings concise and actionable.`
391
+ : taskText;
392
+ const systemContext = [String(state.context || "").trim(), preflightContext]
393
+ .filter(Boolean)
394
+ .join("\n\n");
395
+
396
+ const onStream = onDelta
397
+ ? (delta) => {
398
+ const text = String(delta || "");
399
+ if (!text) return;
400
+ streamed = true;
401
+ streamLastChar = text.slice(-1);
402
+ try {
403
+ onDelta(text);
404
+ } catch {
405
+ // ignore stream callback failures
406
+ }
407
+ }
408
+ : null;
409
+ const runNativeAgentImpl = typeof options.runNativeAgentImpl === "function"
410
+ ? options.runNativeAgentImpl
411
+ : runNativeAgentTask;
412
+ const invokeNative = (sessionIdValue = "", timeoutOverrideMs = timeoutMs) => runNativeAgentImpl({
413
+ workspaceRoot,
414
+ provider,
415
+ model,
416
+ prompt: taskPrompt,
417
+ systemPrompt: systemContext,
418
+ messages: Array.isArray(state.nlMessages) ? state.nlMessages : [],
419
+ sessionId: String(sessionIdValue || ""),
420
+ timeoutMs: timeoutOverrideMs,
421
+ onStreamDelta: onStream,
422
+ onToolEvent: (event) => {
423
+ pushToolLog(event);
424
+ },
425
+ signal: options.signal,
426
+ });
427
+
428
+ try {
429
+ let cliRes;
430
+
431
+ // Use decomposed runner for bug fix tasks
432
+ if (useDecomposition) {
433
+ const decomposedResult = await runDecomposedTask({
434
+ task: taskText,
435
+ state,
436
+ onProgress: options.onProgress,
437
+ onToolEvent: pushToolLog,
438
+ signal: options.signal,
439
+ workspaceRoot,
440
+ provider,
441
+ model,
442
+ systemPrompt: systemContext,
443
+ messages: Array.isArray(state.nlMessages) ? state.nlMessages : [],
444
+ sessionId: String(state.sessionId || ""),
445
+ });
446
+
447
+ if (decomposedResult.ok) {
448
+ cliRes = {
449
+ ok: true,
450
+ output: decomposedResult.summary,
451
+ sessionId: state.sessionId,
452
+ messages: state.nlMessages,
453
+ };
454
+ } else {
455
+ cliRes = {
456
+ ok: false,
457
+ error: decomposedResult.error,
458
+ };
459
+ }
460
+ } else {
461
+ // Original single-step execution
462
+ cliRes = await invokeNative(String(state.sessionId || ""));
463
+
464
+ if (!cliRes || cliRes.ok === false) {
465
+ const errMsg = String((cliRes && cliRes.error) || "");
466
+ if (isCliTimeoutError(errMsg)) {
467
+ const extendedTimeoutMs = computeExtendedTimeout(timeoutMs);
468
+ cliRes = await invokeNative(String(state.sessionId || ""), extendedTimeoutMs);
469
+ }
470
+ }
471
+ }
472
+
473
+ if (!cliRes || cliRes.ok === false) {
474
+ const errMsg = String((cliRes && cliRes.error) || "");
475
+ return {
476
+ ok: false,
477
+ summary: "",
478
+ artifacts: [],
479
+ logs: logs.slice(),
480
+ error: enrichNativeError(errMsg),
481
+ cancelled: isCliCancelledError(errMsg),
482
+ metrics: {},
483
+ streamed: Boolean(streamed || (cliRes && cliRes.streamed)),
484
+ streamLastChar,
485
+ };
486
+ }
487
+ if (cliRes && typeof cliRes.sessionId === "string" && cliRes.sessionId.trim()) {
488
+ state.sessionId = cliRes.sessionId.trim();
489
+ }
490
+ if (cliRes && Array.isArray(cliRes.messages)) {
491
+ state.nlMessages = cliRes.messages;
492
+ }
493
+ const normalized = String(cliRes.output || "").trim();
494
+ const summary = extractJsonSummary(normalized);
495
+ const resolvedSummary = String(summary || "").trim() || buildNlFallbackSummary(logs);
496
+ return {
497
+ ok: true,
498
+ summary: resolvedSummary,
499
+ artifacts: [],
500
+ logs: logs.slice(),
501
+ error: "",
502
+ metrics: {},
503
+ streamed: Boolean(streamed || cliRes.streamed),
504
+ streamLastChar,
505
+ };
506
+ } catch (err) {
507
+ return {
508
+ ok: false,
509
+ summary: "",
510
+ artifacts: [],
511
+ logs: logs.slice(),
512
+ error: enrichNativeError(err && err.message ? err.message : "nl task failed"),
513
+ cancelled: isCliCancelledError(err && err.message ? err.message : ""),
514
+ metrics: {},
515
+ streamed,
516
+ streamLastChar,
517
+ };
518
+ }
519
+ }
520
+
521
+ function formatNlResult(result, asJson = false) {
522
+ if (asJson) {
523
+ return JSON.stringify(result && typeof result === "object" ? result : {
524
+ ok: false,
525
+ summary: "",
526
+ artifacts: [],
527
+ logs: [],
528
+ error: "invalid nl result",
529
+ metrics: {},
530
+ });
531
+ }
532
+ if (result && result.cancelled) {
533
+ return "Cancelled.";
534
+ }
535
+ if (!result || result.ok === false) {
536
+ return `Error: ${(result && result.error) || "task failed"}`;
537
+ }
538
+ const summary = String(result.summary || "").trim();
539
+ if (summary) return summary;
540
+ const artifacts = Array.isArray(result.artifacts) ? result.artifacts.filter(Boolean) : [];
541
+ if (artifacts.length > 0) return artifacts.join("\n");
542
+ return buildNlFallbackSummary(result && Array.isArray(result.logs) ? result.logs : []);
543
+ }
544
+
545
+ function buildNlContext({
546
+ appendSystemPrompt = "",
547
+ systemPrompt = "",
548
+ } = {}) {
549
+ const inline = readTextOrFile(appendSystemPrompt) || readTextOrFile(systemPrompt);
550
+ if (inline) return clampContext(inline);
551
+
552
+ const envFallback = readTextOrFile(process.env.UFOO_UCODE_APPEND_SYSTEM_PROMPT)
553
+ || readTextOrFile(process.env.UFOO_UCODE_BOOTSTRAP_FILE)
554
+ || readTextOrFile(process.env.UFOO_UCODE_PROMPT_FILE);
555
+ return clampContext(envFallback);
556
+ }
557
+
558
+ function buildSessionSnapshotFromState(state = {}) {
559
+ const source = state && typeof state === "object" ? state : {};
560
+ return {
561
+ sessionId: resolveSessionId(source.sessionId),
562
+ workspaceRoot: String(source.workspaceRoot || process.cwd()).trim() || process.cwd(),
563
+ provider: String(source.provider || "").trim(),
564
+ model: String(source.model || "").trim(),
565
+ context: String(source.context || ""),
566
+ nlMessages: Array.isArray(source.nlMessages) ? source.nlMessages : [],
567
+ createdAt: String(source.sessionCreatedAt || "").trim(),
568
+ };
569
+ }
570
+
571
+ function persistSessionState(state = {}) {
572
+ const snapshot = buildSessionSnapshotFromState(state);
573
+ const saved = saveSessionSnapshot(snapshot.workspaceRoot, snapshot);
574
+ if (saved && saved.ok) {
575
+ state.sessionId = saved.sessionId;
576
+ const savedSnapshot = saved.snapshot && typeof saved.snapshot === "object"
577
+ ? saved.snapshot
578
+ : {};
579
+ const createdAt = String(savedSnapshot.createdAt || "").trim();
580
+ if (createdAt) {
581
+ state.sessionCreatedAt = createdAt;
582
+ }
583
+ }
584
+ return saved;
585
+ }
586
+
587
+ function resumeSessionState(state = {}, sessionId = "", workspaceRoot = process.cwd()) {
588
+ const targetId = normalizeSessionId(sessionId);
589
+ if (!targetId) {
590
+ return {
591
+ ok: false,
592
+ error: "invalid session id",
593
+ sessionId: "",
594
+ restoredMessages: 0,
595
+ };
596
+ }
597
+
598
+ const loaded = loadSessionSnapshot(workspaceRoot, targetId);
599
+ if (!loaded || loaded.ok === false || !loaded.snapshot) {
600
+ return {
601
+ ok: false,
602
+ error: String((loaded && loaded.error) || `session not found: ${targetId}`),
603
+ sessionId: targetId,
604
+ restoredMessages: 0,
605
+ };
606
+ }
607
+
608
+ const snapshot = loaded.snapshot;
609
+ state.sessionId = String(snapshot.sessionId || targetId);
610
+ state.workspaceRoot = String(snapshot.workspaceRoot || workspaceRoot || process.cwd());
611
+ state.provider = String(snapshot.provider || "");
612
+ state.model = String(snapshot.model || "");
613
+ state.context = String(snapshot.context || "");
614
+ state.nlMessages = Array.isArray(snapshot.nlMessages) ? snapshot.nlMessages : [];
615
+ state.sessionCreatedAt = String(snapshot.createdAt || "").trim();
616
+
617
+ return {
618
+ ok: true,
619
+ error: "",
620
+ sessionId: state.sessionId,
621
+ restoredMessages: state.nlMessages.length,
622
+ };
623
+ }
624
+
625
+ function shellQuote(value = "") {
626
+ const text = String(value == null ? "" : value);
627
+ return `'${text.replace(/'/g, `'\"'\"'`)}'`;
628
+ }
629
+
630
+ function toText(value = "") {
631
+ if (typeof value === "string") return value;
632
+ if (Buffer.isBuffer(value)) return value.toString("utf8");
633
+ return String(value == null ? "" : value);
634
+ }
635
+
636
+ function stripAnsi(text = "") {
637
+ const raw = String(text || "");
638
+ if (!raw) return "";
639
+ // CSI + OSC sequences (best-effort).
640
+ return raw
641
+ .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "")
642
+ .replace(/\x1b\][^\x07]*\x07/g, "")
643
+ .replace(/\x1b\][^\x1b]*(?:\x1b\\)/g, "");
644
+ }
645
+
646
+ function runShellCapture(command = "", workspaceRoot = process.cwd()) {
647
+ try {
648
+ const output = execSync(String(command || ""), {
649
+ cwd: workspaceRoot,
650
+ encoding: "utf8",
651
+ stdio: ["pipe", "pipe", "pipe"],
652
+ });
653
+ return {
654
+ ok: true,
655
+ output: toText(output),
656
+ error: "",
657
+ };
658
+ } catch (err) {
659
+ const stdout = toText(err && err.stdout);
660
+ const stderr = toText(err && err.stderr);
661
+ const detail = [stdout, stderr].filter(Boolean).join("\n").trim();
662
+ return {
663
+ ok: false,
664
+ output: detail,
665
+ error: detail || (err && err.message ? err.message : "shell command failed"),
666
+ };
667
+ }
668
+ }
669
+
670
+ function safeSubscriberName(subscriberId = "") {
671
+ return String(subscriberId || "").replace(/:/g, "_");
672
+ }
673
+
674
+ function resolvePendingQueueFile(workspaceRoot = process.cwd(), subscriberId = "") {
675
+ const root = String(workspaceRoot || process.cwd()).trim() || process.cwd();
676
+ const sub = String(subscriberId || "").trim();
677
+ if (!sub) return "";
678
+ return path.join(root, ".ufoo", "bus", "queues", safeSubscriberName(sub), "pending.jsonl");
679
+ }
680
+
681
+ function resolveUfooProjectRoot(preferredRoot = "", env = process.env) {
682
+ const candidates = [
683
+ String(preferredRoot || "").trim(),
684
+ String((env && env.UFOO_UCODE_PROJECT_ROOT) || "").trim(),
685
+ String((env && env.UFOO_PROJECT_ROOT) || "").trim(),
686
+ process.cwd(),
687
+ ].filter(Boolean);
688
+
689
+ for (const root of candidates) {
690
+ try {
691
+ const busDir = path.join(root, ".ufoo", "bus");
692
+ if (fs.existsSync(busDir)) return root;
693
+ } catch {
694
+ // ignore
695
+ }
696
+ }
697
+
698
+ return candidates[0] || process.cwd();
699
+ }
700
+
701
+ function countPendingQueueLines(filePath = "") {
702
+ const target = String(filePath || "").trim();
703
+ if (!target) return 0;
704
+ try {
705
+ if (!fs.existsSync(target)) return 0;
706
+ const content = String(fs.readFileSync(target, "utf8") || "");
707
+ if (!content.trim()) return 0;
708
+ return content.split(/\r?\n/).filter((line) => line.trim()).length;
709
+ } catch {
710
+ return 0;
711
+ }
712
+ }
713
+
714
+ function isPidAlive(pid) {
715
+ const p = parseInt(String(pid || "").trim(), 10);
716
+ if (!Number.isFinite(p) || p <= 0) return false;
717
+ try {
718
+ process.kill(p, 0);
719
+ return true;
720
+ } catch {
721
+ return false;
722
+ }
723
+ }
724
+
725
+ function listProcessingFiles(pendingFilePath = "") {
726
+ const pendingFile = String(pendingFilePath || "").trim();
727
+ if (!pendingFile) return [];
728
+ const dir = path.dirname(pendingFile);
729
+ const base = path.basename(pendingFile);
730
+ const prefix = `${base}.processing.`;
731
+ try {
732
+ if (!fs.existsSync(dir)) return [];
733
+ return fs.readdirSync(dir)
734
+ .filter((name) => name && name.startsWith(prefix))
735
+ .map((name) => path.join(dir, name));
736
+ } catch {
737
+ return [];
738
+ }
739
+ }
740
+
741
+ function countRecoverableProcessingFiles(pendingFilePath = "", options = {}) {
742
+ const pendingFile = String(pendingFilePath || "").trim();
743
+ if (!pendingFile) return 0;
744
+ const maxAgeMs = Number.isFinite(options.maxAgeMs) ? options.maxAgeMs : 60000;
745
+ const now = Date.now();
746
+ const files = listProcessingFiles(pendingFile);
747
+ let count = 0;
748
+
749
+ for (const file of files) {
750
+ const name = path.basename(file);
751
+ const m = name.match(/\.processing\.(\d+)\./);
752
+ const pid = m ? parseInt(m[1], 10) : NaN;
753
+
754
+ if (Number.isFinite(pid) && pid > 0 && !isPidAlive(pid)) {
755
+ count += 1;
756
+ continue;
757
+ }
758
+
759
+ if (!Number.isFinite(maxAgeMs) || maxAgeMs <= 0) continue;
760
+ try {
761
+ const stat = fs.statSync(file);
762
+ if (stat && stat.isFile() && (now - stat.mtimeMs > maxAgeMs)) {
763
+ count += 1;
764
+ }
765
+ } catch {
766
+ // ignore
767
+ }
768
+ }
769
+
770
+ return count;
771
+ }
772
+
773
+ function getPendingBusCount(workspaceRoot = process.cwd(), subscriberId = "") {
774
+ const pendingFile = resolvePendingQueueFile(workspaceRoot, subscriberId);
775
+ const pendingLines = countPendingQueueLines(pendingFile);
776
+ if (!pendingFile) return pendingLines;
777
+ // If a prior crash left `.processing.*` behind, count it so autoBus can self-heal.
778
+ const recoverable = countRecoverableProcessingFiles(pendingFile, { maxAgeMs: 60000 });
779
+ return pendingLines + recoverable;
780
+ }
781
+
782
+ function drainJsonlFile(filePath = "") {
783
+ const target = String(filePath || "").trim();
784
+ if (!target) return { drained: [], rawLines: [], error: "" };
785
+ if (!fs.existsSync(target)) return { drained: [], rawLines: [], error: "" };
786
+
787
+ const processingFile = `${target}.processing.${process.pid}.${Date.now()}`;
788
+ let content = "";
789
+ let renamed = false;
790
+ try {
791
+ fs.renameSync(target, processingFile);
792
+ renamed = true;
793
+ content = String(fs.readFileSync(processingFile, "utf8") || "");
794
+ } catch (err) {
795
+ // Restore on failure.
796
+ try {
797
+ if (renamed && fs.existsSync(processingFile)) {
798
+ fs.renameSync(processingFile, target);
799
+ }
800
+ } catch {
801
+ // ignore
802
+ }
803
+ return { drained: [], rawLines: [], error: err && err.message ? err.message : "drain failed" };
804
+ }
805
+
806
+ const rawLines = content.split(/\r?\n/).filter((line) => line.trim());
807
+ const drained = rawLines.map((line) => {
808
+ try {
809
+ return JSON.parse(line);
810
+ } catch {
811
+ return null;
812
+ }
813
+ }).filter(Boolean);
814
+
815
+ // Keep processing file around for potential requeue decisions by caller.
816
+ return { drained, rawLines, error: "", processingFile };
817
+ }
818
+
819
+ function requeueJsonlLines(filePath = "", lines = []) {
820
+ const target = String(filePath || "").trim();
821
+ const list = Array.isArray(lines) ? lines.filter((l) => String(l || "").trim()) : [];
822
+ if (!target || list.length === 0) return;
823
+ try {
824
+ fs.mkdirSync(path.dirname(target), { recursive: true });
825
+ fs.appendFileSync(target, `${list.join("\n")}\n`, "utf8");
826
+ } catch {
827
+ // ignore requeue errors
828
+ }
829
+ }
830
+
831
+ function cleanupProcessingFile(filePath = "") {
832
+ const target = String(filePath || "").trim();
833
+ if (!target) return;
834
+ try {
835
+ if (fs.existsSync(target)) fs.rmSync(target, { force: true });
836
+ } catch {
837
+ // ignore
838
+ }
839
+ }
840
+
841
+ function recoverStaleProcessingFiles(pendingFilePath = "", options = {}) {
842
+ const pendingFile = String(pendingFilePath || "").trim();
843
+ if (!pendingFile) return 0;
844
+ const maxAgeMs = Number.isFinite(options.maxAgeMs) ? options.maxAgeMs : 30000;
845
+ const dir = path.dirname(pendingFile);
846
+ const base = path.basename(pendingFile);
847
+ const prefix = `${base}.processing.`;
848
+ const now = Date.now();
849
+ let recovered = 0;
850
+
851
+ try {
852
+ if (!fs.existsSync(dir)) return 0;
853
+ const names = fs.readdirSync(dir);
854
+ for (const name of names) {
855
+ if (!name || !name.startsWith(prefix)) continue;
856
+ const fullPath = path.join(dir, name);
857
+ let stat = null;
858
+ try {
859
+ stat = fs.statSync(fullPath);
860
+ } catch {
861
+ continue;
862
+ }
863
+ if (!stat || !stat.isFile()) continue;
864
+ const pidMatch = name.match(/\.processing\.(\d+)\./);
865
+ const pid = pidMatch ? parseInt(pidMatch[1], 10) : NaN;
866
+ const pidDead = Number.isFinite(pid) && pid > 0 && !isPidAlive(pid);
867
+ const tooOld = Number.isFinite(maxAgeMs) && maxAgeMs > 0 && (now - stat.mtimeMs >= maxAgeMs);
868
+ if (!pidDead && !tooOld) continue;
869
+
870
+ let content = "";
871
+ try {
872
+ content = String(fs.readFileSync(fullPath, "utf8") || "");
873
+ } catch {
874
+ content = "";
875
+ }
876
+
877
+ const lines = content.split(/\r?\n/).filter((line) => String(line || "").trim());
878
+ if (lines.length > 0) {
879
+ requeueJsonlLines(pendingFile, lines);
880
+ }
881
+ cleanupProcessingFile(fullPath);
882
+ recovered += 1;
883
+ }
884
+ } catch {
885
+ return recovered;
886
+ }
887
+
888
+ return recovered;
889
+ }
890
+
891
+ function extractTaskFromBusEvent(evt) {
892
+ if (!evt || typeof evt !== "object") return null;
893
+ if (String(evt.event || "").trim().toLowerCase() !== "message") return null;
894
+ let publisher = "";
895
+ if (typeof evt.publisher === "string") {
896
+ publisher = String(evt.publisher || "").trim();
897
+ } else if (evt.publisher && typeof evt.publisher === "object") {
898
+ publisher = String(evt.publisher.subscriber || evt.publisher.nickname || "").trim();
899
+ } else {
900
+ publisher = String(evt.publisher || "").trim();
901
+ }
902
+ if (publisher === "[object Object]") publisher = "";
903
+ if (!publisher) return null;
904
+ const data = evt.data && typeof evt.data === "object" ? evt.data : {};
905
+ const message = typeof data.message === "string"
906
+ ? data.message
907
+ : (typeof data.text === "string" ? data.text : "");
908
+ const task = String(message || "").trim();
909
+ if (!task) return null;
910
+ return { publisher, task };
911
+ }
912
+
913
+ function shouldAutoConsumeBus(subscriberId = "") {
914
+ const id = String(subscriberId || "").trim().toLowerCase();
915
+ if (!id) return false;
916
+ return id.startsWith("ufoo-code:")
917
+ || id.startsWith("ucode:")
918
+ || id.startsWith("ufoo:");
919
+ }
920
+
921
+ function extractBusMessageTask(contentRaw = "") {
922
+ const raw = String(contentRaw || "").trim();
923
+ if (!raw) return "";
924
+ try {
925
+ const parsed = JSON.parse(raw);
926
+ if (parsed && typeof parsed === "object") {
927
+ if (typeof parsed.message === "string" && parsed.message.trim()) return parsed.message.trim();
928
+ if (typeof parsed.text === "string" && parsed.text.trim()) return parsed.text.trim();
929
+ if (typeof parsed.prompt === "string" && parsed.prompt.trim()) return parsed.prompt.trim();
930
+ }
931
+ } catch {
932
+ // treat as plain text below
933
+ }
934
+ return raw;
935
+ }
936
+
937
+ function busCheckOutputIndicatesPending(raw = "") {
938
+ const text = stripAnsi(String(raw || ""));
939
+ if (!text.trim()) return false;
940
+ if (/no pending messages/i.test(text)) return false;
941
+ if (/you have\s+\d+\s+pending/i.test(text)) return true;
942
+ if (/after handling,\s*run:\s*ufoo bus ack/i.test(text)) return true;
943
+ if (/pending event/i.test(text)) return true;
944
+ return false;
945
+ }
946
+
947
+ function parseBusCheckOutput(raw = "") {
948
+ const text = stripAnsi(String(raw || ""));
949
+ if (!text.trim()) return [];
950
+ if (/no pending messages/i.test(text)) return [];
951
+
952
+ const lines = text.split(/\r?\n/);
953
+ const rows = [];
954
+ let current = null;
955
+
956
+ for (const line of lines) {
957
+ const trimmed = String(line || "").trim();
958
+ if (!trimmed) continue;
959
+
960
+ const header = trimmed.match(/^@.+\s+from\s+([^\s]+)\s*$/i);
961
+ if (header) {
962
+ if (current && current.publisher) rows.push(current);
963
+ current = {
964
+ publisher: String(header[1] || "").trim(),
965
+ content: "",
966
+ };
967
+ continue;
968
+ }
969
+
970
+ if (!current) continue;
971
+
972
+ const contentMatch = trimmed.match(/^content:\s*(.*)$/i);
973
+ if (contentMatch) {
974
+ current.content = String(contentMatch[1] || "").trim();
975
+ continue;
976
+ }
977
+
978
+ if (
979
+ current.content
980
+ && !/^(type|event|seq|target|timestamp):\s*/i.test(trimmed)
981
+ && !trimmed.startsWith("@")
982
+ ) {
983
+ current.content = `${current.content}\n${trimmed}`;
984
+ }
985
+ }
986
+
987
+ if (current && current.publisher) rows.push(current);
988
+
989
+ return rows
990
+ .map((entry) => {
991
+ const publisher = String(entry.publisher || "").trim();
992
+ const content = String(entry.content || "").trim();
993
+ const task = extractBusMessageTask(content);
994
+ if (!publisher || !task) return null;
995
+ return {
996
+ publisher,
997
+ content,
998
+ task,
999
+ };
1000
+ })
1001
+ .filter(Boolean);
1002
+ }
1003
+
1004
+ async function runUbusCommand(state = {}, options = {}) {
1005
+ const runtimeWorkspace = resolveUfooProjectRoot(String(
1006
+ options.workspaceRoot
1007
+ || (state && state.workspaceRoot)
1008
+ || ""
1009
+ ));
1010
+ const shell = typeof options.execShell === "function"
1011
+ ? options.execShell
1012
+ : (command) => runShellCapture(command, runtimeWorkspace);
1013
+ const runNl = typeof options.runNaturalLanguageTaskImpl === "function"
1014
+ ? options.runNaturalLanguageTaskImpl
1015
+ : runNaturalLanguageTask;
1016
+ const formatNl = typeof options.formatNlResultImpl === "function"
1017
+ ? options.formatNlResultImpl
1018
+ : formatNlResult;
1019
+ const onMessageReceived = typeof options.onMessageReceived === "function"
1020
+ ? options.onMessageReceived
1021
+ : null;
1022
+
1023
+ const explicitSubscriber = String(options.subscriberId || "").trim();
1024
+ const envSubscriber = String(process.env.UFOO_SUBSCRIBER_ID || "").trim();
1025
+ let subscriberId = explicitSubscriber || envSubscriber;
1026
+ if (!subscriberId) {
1027
+ const whoami = shell("ufoo bus whoami 2>/dev/null || true");
1028
+ subscriberId = String((whoami && whoami.output) || "").trim();
1029
+ }
1030
+ if (!subscriberId) {
1031
+ const joined = shell("ufoo bus join | tail -1");
1032
+ subscriberId = String((joined && joined.output) || "").trim();
1033
+ }
1034
+ if (!subscriberId) {
1035
+ return {
1036
+ ok: false,
1037
+ summary: "",
1038
+ error: "failed to resolve bus subscriber id",
1039
+ handled: 0,
1040
+ subscriberId: "",
1041
+ };
1042
+ }
1043
+
1044
+ // Prefer consuming pending.jsonl directly (stable, ANSI/wrapping-proof).
1045
+ const pendingFile = resolvePendingQueueFile(runtimeWorkspace, subscriberId);
1046
+ // Recover any stale processing files from prior crashes so they don't "black hole" messages.
1047
+ recoverStaleProcessingFiles(pendingFile, { maxAgeMs: 30000 });
1048
+ const hasPendingFile = Boolean(pendingFile && fs.existsSync(pendingFile));
1049
+ const drainedRes = hasPendingFile ? drainJsonlFile(pendingFile) : { drained: [], rawLines: [], error: "", processingFile: "" };
1050
+ if (drainedRes && drainedRes.error) {
1051
+ return {
1052
+ ok: false,
1053
+ summary: "",
1054
+ error: drainedRes.error,
1055
+ handled: 0,
1056
+ subscriberId,
1057
+ };
1058
+ }
1059
+ const rawLines = Array.isArray(drainedRes.rawLines) ? drainedRes.rawLines : [];
1060
+ const messages = rawLines
1061
+ .map((rawLine) => {
1062
+ try {
1063
+ const evt = JSON.parse(rawLine);
1064
+ const msg = extractTaskFromBusEvent(evt);
1065
+ if (!msg) return null;
1066
+ return { ...msg, rawLine };
1067
+ } catch {
1068
+ return null;
1069
+ }
1070
+ })
1071
+ .filter(Boolean);
1072
+ let handled = 0;
1073
+ const sendErrors = [];
1074
+ const failedRawLines = [];
1075
+ const messageExchanges = [];
1076
+
1077
+ try {
1078
+ for (const message of messages) {
1079
+ let nlResult;
1080
+
1081
+ // Notify that we received the message (for immediate display)
1082
+ if (onMessageReceived) {
1083
+ onMessageReceived({
1084
+ from: message.publisher,
1085
+ task: message.task,
1086
+ });
1087
+ }
1088
+
1089
+ // Create progress reporter for this message
1090
+ const progressReporter = createBusProgressReporter(shell, message.publisher);
1091
+
1092
+ try {
1093
+ // Send initial acknowledgment
1094
+ shell(`ufoo bus send ${shellQuote(message.publisher)} ${shellQuote("🚀 Starting task...")}`);
1095
+
1096
+ // eslint-disable-next-line no-await-in-loop
1097
+ nlResult = await runNl(message.task, state, {
1098
+ onProgress: progressReporter,
1099
+ });
1100
+ } catch (err) {
1101
+ sendErrors.push(`task from ${message.publisher} failed: ${err && err.message ? err.message : "task failed"}`);
1102
+ failedRawLines.push(message.rawLine);
1103
+ // Send error notification
1104
+ shell(`ufoo bus send ${shellQuote(message.publisher)} ${shellQuote(`❌ Error: ${err.message}`)}`);
1105
+ continue;
1106
+ }
1107
+ const reply = String(formatNl(nlResult, false) || "").replace(/\s+/g, " ").trim() || "Done.";
1108
+ const sendRes = shell(`ufoo bus send ${shellQuote(message.publisher)} ${shellQuote(reply.slice(0, 2000))}`);
1109
+ if (!sendRes.ok) {
1110
+ sendErrors.push(`reply to ${message.publisher} failed: ${sendRes.error || "send failed"}`);
1111
+ failedRawLines.push(message.rawLine);
1112
+ continue;
1113
+ }
1114
+ handled += 1;
1115
+ messageExchanges.push({
1116
+ from: message.publisher,
1117
+ task: message.task,
1118
+ reply,
1119
+ });
1120
+ }
1121
+ } finally {
1122
+ // If we drained the pending file but had failures, requeue only failed lines.
1123
+ if (failedRawLines.length > 0) {
1124
+ requeueJsonlLines(pendingFile, failedRawLines);
1125
+ }
1126
+ cleanupProcessingFile(drainedRes.processingFile);
1127
+ }
1128
+
1129
+ // Fallback: if there is no pending file, fall back to CLI `bus check` parsing.
1130
+ if (!hasPendingFile) {
1131
+ const checked = shell(`ufoo bus check ${shellQuote(subscriberId)}`);
1132
+ if (!checked.ok) {
1133
+ return {
1134
+ ok: false,
1135
+ summary: "",
1136
+ error: checked.error || "ufoo bus check failed",
1137
+ handled: 0,
1138
+ subscriberId,
1139
+ };
1140
+ }
1141
+ const parsed = parseBusCheckOutput(checked.output);
1142
+ if (parsed.length === 0 && busCheckOutputIndicatesPending(checked.output)) {
1143
+ return {
1144
+ ok: false,
1145
+ summary: "",
1146
+ error: "failed to parse ufoo bus check output (pending events detected).",
1147
+ handled: 0,
1148
+ subscriberId,
1149
+ };
1150
+ }
1151
+ for (const item of parsed) {
1152
+ // Notify that we received the message (for immediate display)
1153
+ if (onMessageReceived) {
1154
+ onMessageReceived({
1155
+ from: item.publisher,
1156
+ task: item.task,
1157
+ });
1158
+ }
1159
+
1160
+ const nlResult = await runNl(item.task, state);
1161
+ const reply = String(formatNl(nlResult, false) || "").replace(/\s+/g, " ").trim() || "Done.";
1162
+ const sendRes = shell(`ufoo bus send ${shellQuote(item.publisher)} ${shellQuote(reply.slice(0, 2000))}`);
1163
+ if (!sendRes.ok) {
1164
+ sendErrors.push(`reply to ${item.publisher} failed: ${sendRes.error || "send failed"}`);
1165
+ continue;
1166
+ }
1167
+ handled += 1;
1168
+ messageExchanges.push({
1169
+ from: item.publisher,
1170
+ task: item.task,
1171
+ reply,
1172
+ });
1173
+ }
1174
+ }
1175
+
1176
+ if (sendErrors.length > 0) {
1177
+ return {
1178
+ ok: false,
1179
+ summary: "",
1180
+ error: sendErrors.join("; "),
1181
+ handled,
1182
+ subscriberId,
1183
+ messageExchanges,
1184
+ };
1185
+ }
1186
+
1187
+ const summary = handled > 0
1188
+ ? `ubus: handled ${handled} message${handled === 1 ? "" : "s"} for ${subscriberId}.`
1189
+ : `ubus: no pending messages for ${subscriberId}.`;
1190
+ return {
1191
+ ok: true,
1192
+ summary,
1193
+ error: "",
1194
+ handled,
1195
+ subscriberId,
1196
+ messageExchanges,
1197
+ };
1198
+ }
1199
+
1200
+ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
1201
+ const text = normalizeLine(line);
1202
+ if (!text) return { kind: "empty" };
1203
+ if (text === "exit" || text === "quit") return { kind: "exit" };
1204
+ if (text === "help") {
1205
+ return {
1206
+ kind: "help",
1207
+ output: [
1208
+ "Commands:",
1209
+ " help",
1210
+ " exit|quit",
1211
+ " ubus|/ubus",
1212
+ " resume <session-id>",
1213
+ " tool <read|write|edit|bash> <args-json>",
1214
+ " run <read|write|edit|bash> <args-json>",
1215
+ ].join("\n"),
1216
+ };
1217
+ }
1218
+ if (text.startsWith("$ufoo ") || text.startsWith("/ufoo ") || text.startsWith("ufoo ")) {
1219
+ return {
1220
+ kind: "probe",
1221
+ output: text.split(/\s+/).slice(1).join(" ").trim(),
1222
+ };
1223
+ }
1224
+ if (text === "ubus" || text === "/ubus") {
1225
+ return {
1226
+ kind: "ubus",
1227
+ };
1228
+ }
1229
+ const resumeMatch = text.match(/^resume(?:\s+(.+))?$/i);
1230
+ if (resumeMatch) {
1231
+ const session = String(resumeMatch[1] || "").trim();
1232
+ if (!session) {
1233
+ return {
1234
+ kind: "error",
1235
+ output: "usage: resume <session-id>",
1236
+ };
1237
+ }
1238
+ return {
1239
+ kind: "resume",
1240
+ sessionId: session,
1241
+ };
1242
+ }
1243
+
1244
+ const match = text.match(/^(tool|run)\s+([a-zA-Z_-]+)\s*(.*)$/);
1245
+ if (!match) {
1246
+ return {
1247
+ kind: "nl",
1248
+ task: text,
1249
+ };
1250
+ }
1251
+ const tool = String(match[2] || "").trim().toLowerCase();
1252
+ const payload = String(match[3] || "").trim();
1253
+ let args = {};
1254
+ try {
1255
+ args = parseJson(payload);
1256
+ } catch (err) {
1257
+ return {
1258
+ kind: "error",
1259
+ output: JSON.stringify({ ok: false, error: err && err.message ? err.message : "invalid json" }),
1260
+ };
1261
+ }
1262
+ const result = runToolCall(
1263
+ { tool, args },
1264
+ { workspaceRoot, cwd: workspaceRoot }
1265
+ );
1266
+ return {
1267
+ kind: "tool",
1268
+ tool,
1269
+ args,
1270
+ result,
1271
+ output: JSON.stringify(result),
1272
+ };
1273
+ }
1274
+
1275
+ async function runUcodeCoreAgent({
1276
+ stdin = process.stdin,
1277
+ stdout = process.stdout,
1278
+ workspaceRoot = process.cwd(),
1279
+ provider = "",
1280
+ model = "",
1281
+ appendSystemPrompt = "",
1282
+ systemPrompt = "",
1283
+ sessionId = "",
1284
+ timeoutMs = 300000,
1285
+ jsonOutput = false,
1286
+ forceTui = false,
1287
+ disableTui = false,
1288
+ } = {}) {
1289
+ const resolvedWorkspaceRoot = resolveUfooProjectRoot(workspaceRoot);
1290
+ const resolvedUcode = resolveUcodeProviderModel({
1291
+ workspaceRoot: resolvedWorkspaceRoot,
1292
+ provider,
1293
+ model,
1294
+ });
1295
+ const state = {
1296
+ workspaceRoot: resolvedWorkspaceRoot,
1297
+ provider: resolvedUcode.provider,
1298
+ model: resolvedUcode.model,
1299
+ engine: "ufoo-core",
1300
+ context: buildNlContext({ appendSystemPrompt, systemPrompt }),
1301
+ nlMessages: [],
1302
+ sessionId: resolveSessionId(String(sessionId || "").trim()),
1303
+ timeoutMs,
1304
+ jsonOutput,
1305
+ };
1306
+ persistSessionState(state);
1307
+
1308
+ if (shouldUseUcodeTui({
1309
+ stdin,
1310
+ stdout,
1311
+ jsonOutput,
1312
+ forceTui,
1313
+ disableTui: disableTui || process.env.UFOO_UCODE_NO_TUI === "1",
1314
+ })) {
1315
+ return runUcodeTui({
1316
+ stdin,
1317
+ stdout,
1318
+ runSingleCommand,
1319
+ runNaturalLanguageTask,
1320
+ runUbusCommand,
1321
+ formatNlResult,
1322
+ workspaceRoot,
1323
+ state,
1324
+ resumeSessionState,
1325
+ persistSessionState,
1326
+ autoBus: {
1327
+ enabled: shouldAutoConsumeBus(process.env.UFOO_SUBSCRIBER_ID || ""),
1328
+ getPendingCount: () => getPendingBusCount(state.workspaceRoot || workspaceRoot, process.env.UFOO_SUBSCRIBER_ID || ""),
1329
+ subscriberId: String(process.env.UFOO_SUBSCRIBER_ID || "").trim(),
1330
+ },
1331
+ });
1332
+ }
1333
+
1334
+ printUcodeBanner(stdout, {
1335
+ model: state.model || "default",
1336
+ workspaceRoot: workspaceRoot,
1337
+ sessionId: state.sessionId,
1338
+ });
1339
+ printPrompt();
1340
+ const rl = readline.createInterface({
1341
+ input: stdin,
1342
+ output: stdout,
1343
+ terminal: true,
1344
+ historySize: 200,
1345
+ });
1346
+ return new Promise((resolve) => {
1347
+ let chain = Promise.resolve();
1348
+ const subscriberId = String(process.env.UFOO_SUBSCRIBER_ID || "").trim();
1349
+ const autoBusEnabled = shouldAutoConsumeBus(subscriberId);
1350
+ let autoBusTimer = null;
1351
+ let autoBusQueued = false;
1352
+ let autoBusError = "";
1353
+ let closing = false;
1354
+
1355
+ const runAutoBusOnce = async () => {
1356
+ if (!autoBusEnabled || closing) return;
1357
+ if (getPendingBusCount(state.workspaceRoot || workspaceRoot, subscriberId) <= 0) {
1358
+ autoBusError = "";
1359
+ return;
1360
+ }
1361
+ const ubusResult = await runUbusCommand(state, {
1362
+ workspaceRoot: state.workspaceRoot || workspaceRoot,
1363
+ subscriberId,
1364
+ });
1365
+ if (!ubusResult.ok) {
1366
+ const nextError = String(ubusResult.error || "ubus failed");
1367
+ if (nextError !== autoBusError) {
1368
+ autoBusError = nextError;
1369
+ stdout.write(`Error: ${nextError}\n`);
1370
+ printPrompt();
1371
+ }
1372
+ return;
1373
+ }
1374
+ autoBusError = "";
1375
+ if (ubusResult.handled > 0) {
1376
+ const persisted = persistSessionState(state);
1377
+ if (!persisted || persisted.ok === false) {
1378
+ stdout.write(`Warning: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}\n`);
1379
+ printPrompt();
1380
+ }
1381
+ }
1382
+ };
1383
+
1384
+ const scheduleAutoBus = () => {
1385
+ if (!autoBusEnabled || closing || autoBusQueued) return;
1386
+ if (getPendingBusCount(state.workspaceRoot || workspaceRoot, subscriberId) <= 0) return;
1387
+ autoBusQueued = true;
1388
+ chain = chain
1389
+ .then(() => runAutoBusOnce())
1390
+ .catch(() => {})
1391
+ .finally(() => {
1392
+ autoBusQueued = false;
1393
+ });
1394
+ };
1395
+
1396
+ if (autoBusEnabled) {
1397
+ autoBusTimer = setInterval(() => {
1398
+ scheduleAutoBus();
1399
+ }, 800);
1400
+ scheduleAutoBus();
1401
+ }
1402
+
1403
+ const handleLine = async (line) => {
1404
+ const runtimeWorkspace = String(state.workspaceRoot || workspaceRoot || process.cwd());
1405
+ const result = runSingleCommand(line, runtimeWorkspace);
1406
+ if (result.kind === "exit") {
1407
+ rl.close();
1408
+ return;
1409
+ }
1410
+ if (result.kind === "help" || result.kind === "probe" || result.kind === "tool" || result.kind === "error") {
1411
+ stdout.write(`${result.output}\n`);
1412
+ }
1413
+ if (result.kind === "ubus") {
1414
+ const ubusResult = await runUbusCommand(state, {
1415
+ workspaceRoot: runtimeWorkspace,
1416
+ onMessageReceived: (msg) => {
1417
+ // Display the incoming message immediately
1418
+ const nickname = extractAgentNickname(msg.from) || msg.from;
1419
+ stdout.write(`${nickname}: ${msg.task}\n`);
1420
+ },
1421
+ });
1422
+ if (!ubusResult.ok) {
1423
+ stdout.write(`Error: ${ubusResult.error}\n`);
1424
+ } else {
1425
+ // Display replies for each message
1426
+ if (ubusResult.messageExchanges && ubusResult.messageExchanges.length > 0) {
1427
+ for (const exchange of ubusResult.messageExchanges) {
1428
+ const nickname = extractAgentNickname(exchange.from) || exchange.from;
1429
+ stdout.write(`@${nickname} ${exchange.reply}\n`);
1430
+ }
1431
+ } else {
1432
+ stdout.write(`${ubusResult.summary}\n`);
1433
+ }
1434
+ persistSessionState(state);
1435
+ }
1436
+ }
1437
+ if (result.kind === "resume") {
1438
+ const resumed = resumeSessionState(state, result.sessionId, workspaceRoot);
1439
+ if (!resumed.ok) {
1440
+ stdout.write(`Error: ${resumed.error}\n`);
1441
+ } else {
1442
+ stdout.write(`Resumed session ${resumed.sessionId} (${resumed.restoredMessages} messages).\n`);
1443
+ }
1444
+ }
1445
+ if (result.kind === "nl") {
1446
+ let streamBuffer = null;
1447
+ let streamedVisible = false;
1448
+ const escapeStripper = createEscapeTagStripper();
1449
+ if (!state.jsonOutput) {
1450
+ streamBuffer = new StreamBuffer(stdout.write.bind(stdout), {
1451
+ delay: 10,
1452
+ chunkSize: 4,
1453
+ });
1454
+ }
1455
+
1456
+ const nlResult = await runNaturalLanguageTask(result.task, state, {
1457
+ onDelta: state.jsonOutput
1458
+ ? null
1459
+ : async (delta) => {
1460
+ const text = escapeStripper.write(String(delta || ""));
1461
+ const safeText = stripBlessedTags(stripLeakedEscapeTags(text));
1462
+ if (!safeText) return;
1463
+ if (/[^\s]/.test(safeText)) {
1464
+ streamedVisible = true;
1465
+ }
1466
+ if (streamBuffer) {
1467
+ await streamBuffer.write(safeText);
1468
+ } else {
1469
+ stdout.write(safeText);
1470
+ }
1471
+ },
1472
+ });
1473
+
1474
+ if (!state.jsonOutput) {
1475
+ const tail = escapeStripper.flush();
1476
+ const safeTail = stripBlessedTags(stripLeakedEscapeTags(tail));
1477
+ if (safeTail) {
1478
+ if (/[^\s]/.test(safeTail)) {
1479
+ streamedVisible = true;
1480
+ }
1481
+ if (streamBuffer) {
1482
+ await streamBuffer.write(safeTail);
1483
+ } else {
1484
+ stdout.write(safeTail);
1485
+ }
1486
+ }
1487
+ }
1488
+
1489
+ // Ensure buffer is flushed
1490
+ if (streamBuffer) {
1491
+ await streamBuffer.finish();
1492
+ }
1493
+
1494
+ const streamed = !state.jsonOutput && Boolean(nlResult && nlResult.streamed);
1495
+ if (streamed && streamedVisible && nlResult && nlResult.streamLastChar !== "\n") {
1496
+ stdout.write("\n");
1497
+ }
1498
+ const shouldSkipSummary = Boolean(streamed && nlResult && nlResult.ok && streamedVisible);
1499
+ if (!shouldSkipSummary) {
1500
+ const formatted = formatNlResult(nlResult, state.jsonOutput);
1501
+ const safeOutput = state.jsonOutput
1502
+ ? formatted
1503
+ : stripBlessedTags(stripLeakedEscapeTags(formatted));
1504
+ stdout.write(`${safeOutput}\n`);
1505
+ }
1506
+ const persisted = persistSessionState(state);
1507
+ if (!state.jsonOutput && (!persisted || persisted.ok === false)) {
1508
+ stdout.write(`Warning: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}\n`);
1509
+ }
1510
+ }
1511
+ printPrompt();
1512
+ };
1513
+
1514
+ rl.on("line", (line) => {
1515
+ chain = chain.then(() => handleLine(line)).catch((err) => {
1516
+ stdout.write(`${JSON.stringify({ ok: false, error: err && err.message ? err.message : "agent loop failed" })}\n`);
1517
+ printPrompt();
1518
+ });
1519
+ });
1520
+
1521
+ rl.on("close", () => {
1522
+ closing = true;
1523
+ if (autoBusTimer) {
1524
+ clearInterval(autoBusTimer);
1525
+ autoBusTimer = null;
1526
+ }
1527
+ chain.finally(() => resolve({ code: 0 }));
1528
+ });
1529
+ });
1530
+ }
1531
+
1532
+ function parseAgentArgs(argv = []) {
1533
+ const args = Array.isArray(argv) ? argv.slice() : [];
1534
+ const out = {
1535
+ workspaceRoot: "",
1536
+ provider: "",
1537
+ model: "",
1538
+ appendSystemPrompt: "",
1539
+ systemPrompt: "",
1540
+ sessionId: "",
1541
+ timeoutMs: 300000,
1542
+ jsonOutput: false,
1543
+ forceTui: false,
1544
+ disableTui: false,
1545
+ };
1546
+ for (let i = 0; i < args.length; i += 1) {
1547
+ const item = String(args[i] || "").trim();
1548
+ if (!item) continue;
1549
+ if (item === "--workspace" || item === "--cwd") {
1550
+ out.workspaceRoot = String(args[i + 1] || "").trim();
1551
+ i += 1;
1552
+ continue;
1553
+ }
1554
+ if (item === "--provider") {
1555
+ out.provider = String(args[i + 1] || "").trim();
1556
+ i += 1;
1557
+ continue;
1558
+ }
1559
+ if (item === "--model") {
1560
+ out.model = String(args[i + 1] || "").trim();
1561
+ i += 1;
1562
+ continue;
1563
+ }
1564
+ if (item === "--append-system-prompt") {
1565
+ out.appendSystemPrompt = String(args[i + 1] || "").trim();
1566
+ i += 1;
1567
+ continue;
1568
+ }
1569
+ if (item === "--system-prompt") {
1570
+ out.systemPrompt = String(args[i + 1] || "").trim();
1571
+ i += 1;
1572
+ continue;
1573
+ }
1574
+ if (item === "--session-id") {
1575
+ out.sessionId = String(args[i + 1] || "").trim();
1576
+ i += 1;
1577
+ continue;
1578
+ }
1579
+ if (item === "--timeout-ms") {
1580
+ const parsed = Number(args[i + 1]);
1581
+ if (Number.isFinite(parsed)) out.timeoutMs = Math.max(1000, Math.floor(parsed));
1582
+ i += 1;
1583
+ continue;
1584
+ }
1585
+ if (item === "--json") {
1586
+ out.jsonOutput = true;
1587
+ continue;
1588
+ }
1589
+ if (item === "--tui") {
1590
+ out.forceTui = true;
1591
+ continue;
1592
+ }
1593
+ if (item === "--no-tui") {
1594
+ out.disableTui = true;
1595
+ continue;
1596
+ }
1597
+ }
1598
+ return out;
1599
+ }
1600
+
1601
+ module.exports = {
1602
+ runUcodeCoreAgent,
1603
+ runSingleCommand,
1604
+ runNaturalLanguageTask,
1605
+ formatNlResult,
1606
+ normalizeToolLogEvent,
1607
+ isProjectAnalysisTask,
1608
+ buildNlFallbackSummary,
1609
+ resolvePlannerProvider,
1610
+ extractJsonSummary,
1611
+ enrichNativeError,
1612
+ resolveUcodeProviderModel,
1613
+ buildSessionSnapshotFromState,
1614
+ persistSessionState,
1615
+ resumeSessionState,
1616
+ parseBusCheckOutput,
1617
+ extractBusMessageTask,
1618
+ runUbusCommand,
1619
+ stripAnsi,
1620
+ busCheckOutputIndicatesPending,
1621
+ resolvePendingQueueFile,
1622
+ extractAgentNickname,
1623
+ resolveUfooProjectRoot,
1624
+ countPendingQueueLines,
1625
+ getPendingBusCount,
1626
+ drainJsonlFile,
1627
+ extractTaskFromBusEvent,
1628
+ shouldAutoConsumeBus,
1629
+ parseAgentArgs,
1630
+ };
1631
+
1632
+ if (require.main === module) {
1633
+ const parsed = parseAgentArgs(process.argv.slice(2));
1634
+ runUcodeCoreAgent({
1635
+ workspaceRoot: parsed.workspaceRoot || process.cwd(),
1636
+ provider: parsed.provider,
1637
+ model: parsed.model,
1638
+ appendSystemPrompt: parsed.appendSystemPrompt,
1639
+ systemPrompt: parsed.systemPrompt,
1640
+ sessionId: parsed.sessionId,
1641
+ timeoutMs: parsed.timeoutMs,
1642
+ jsonOutput: parsed.jsonOutput,
1643
+ forceTui: parsed.forceTui,
1644
+ disableTui: parsed.disableTui,
1645
+ }).then((res) => {
1646
+ process.exit(typeof res.code === "number" ? res.code : 0);
1647
+ }).catch((err) => {
1648
+ process.stderr.write(`${err && err.message ? err.message : "ucode agent failed"}\n`);
1649
+ process.exit(1);
1650
+ });
1651
+ }