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,154 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { runToolCall } = require("./dispatch");
4
+
5
+ function getRuntimePaths(projectRoot = process.cwd()) {
6
+ const root = path.resolve(projectRoot || process.cwd());
7
+ const runtimeDir = path.join(root, ".ufoo", "agent", "ucode-core");
8
+ return {
9
+ projectRoot: root,
10
+ runtimeDir,
11
+ tasksFile: path.join(runtimeDir, "tasks.jsonl"),
12
+ resultsFile: path.join(runtimeDir, "results.jsonl"),
13
+ stateFile: path.join(runtimeDir, "state.json"),
14
+ };
15
+ }
16
+
17
+ function ensureRuntimeDir(projectRoot = process.cwd()) {
18
+ const { runtimeDir } = getRuntimePaths(projectRoot);
19
+ fs.mkdirSync(runtimeDir, { recursive: true });
20
+ }
21
+
22
+ function parseJsonLines(filePath = "") {
23
+ try {
24
+ if (!fs.existsSync(filePath)) return [];
25
+ const raw = fs.readFileSync(filePath, "utf8");
26
+ if (!raw.trim()) return [];
27
+ const rows = [];
28
+ for (const line of raw.split(/\r?\n/).map((item) => item.trim()).filter(Boolean)) {
29
+ try {
30
+ rows.push(JSON.parse(line));
31
+ } catch {
32
+ // ignore malformed line
33
+ }
34
+ }
35
+ return rows;
36
+ } catch {
37
+ return [];
38
+ }
39
+ }
40
+
41
+ function appendJsonLine(filePath = "", payload = {}) {
42
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
43
+ fs.appendFileSync(filePath, `${JSON.stringify(payload)}\n`, "utf8");
44
+ }
45
+
46
+ function loadState(projectRoot = process.cwd()) {
47
+ const { stateFile } = getRuntimePaths(projectRoot);
48
+ try {
49
+ const parsed = JSON.parse(fs.readFileSync(stateFile, "utf8"));
50
+ const offset = Number.isFinite(parsed.offset) ? Math.max(0, Math.floor(parsed.offset)) : 0;
51
+ return { offset };
52
+ } catch {
53
+ return { offset: 0 };
54
+ }
55
+ }
56
+
57
+ function saveState(projectRoot = process.cwd(), state = {}) {
58
+ const { stateFile } = getRuntimePaths(projectRoot);
59
+ fs.mkdirSync(path.dirname(stateFile), { recursive: true });
60
+ fs.writeFileSync(stateFile, `${JSON.stringify(state, null, 2)}\n`, "utf8");
61
+ }
62
+
63
+ function createTaskId() {
64
+ return `task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
65
+ }
66
+
67
+ function normalizeTask(input = {}) {
68
+ const tool = String(input.tool || input.name || "").trim().toLowerCase();
69
+ const args = input.args && typeof input.args === "object" ? input.args : {};
70
+ return {
71
+ task_id: String(input.task_id || input.taskId || createTaskId()).trim(),
72
+ created_at: new Date().toISOString(),
73
+ tool,
74
+ args,
75
+ workspace_root: String(input.workspace_root || input.workspaceRoot || "").trim(),
76
+ meta: input.meta && typeof input.meta === "object" ? input.meta : {},
77
+ };
78
+ }
79
+
80
+ function submitTask(projectRoot = process.cwd(), input = {}) {
81
+ ensureRuntimeDir(projectRoot);
82
+ const task = normalizeTask(input);
83
+ const { tasksFile } = getRuntimePaths(projectRoot);
84
+ appendJsonLine(tasksFile, task);
85
+ return task;
86
+ }
87
+
88
+ function normalizeMax(value, fallback = 1) {
89
+ if (!Number.isFinite(value)) return fallback;
90
+ const n = Math.max(1, Math.floor(value));
91
+ return Math.min(n, 500);
92
+ }
93
+
94
+ function runOnce(projectRoot = process.cwd(), options = {}) {
95
+ ensureRuntimeDir(projectRoot);
96
+ const paths = getRuntimePaths(projectRoot);
97
+ const state = loadState(projectRoot);
98
+ const tasks = parseJsonLines(paths.tasksFile);
99
+ const startOffset = Number.isFinite(state.offset) ? state.offset : 0;
100
+ const maxTasks = normalizeMax(options.maxTasks, 1);
101
+ const selected = tasks.slice(startOffset, startOffset + maxTasks);
102
+ const results = [];
103
+
104
+ for (const task of selected) {
105
+ const startedAt = new Date().toISOString();
106
+ const run = runToolCall(
107
+ { tool: task.tool, args: task.args },
108
+ {
109
+ workspaceRoot: task.workspace_root || options.workspaceRoot || projectRoot,
110
+ cwd: projectRoot,
111
+ }
112
+ );
113
+ const finishedAt = new Date().toISOString();
114
+ const resultEntry = {
115
+ task_id: String(task.task_id || ""),
116
+ tool: String(task.tool || ""),
117
+ ok: run.ok !== false,
118
+ error: run && typeof run.error === "string" ? run.error : "",
119
+ output: run,
120
+ started_at: startedAt,
121
+ finished_at: finishedAt,
122
+ created_at: String(task.created_at || ""),
123
+ };
124
+ appendJsonLine(paths.resultsFile, resultEntry);
125
+ results.push(resultEntry);
126
+ }
127
+
128
+ const nextOffset = startOffset + selected.length;
129
+ saveState(projectRoot, { offset: nextOffset });
130
+ return {
131
+ processed: selected.length,
132
+ offset: nextOffset,
133
+ results,
134
+ };
135
+ }
136
+
137
+ function listResults(projectRoot = process.cwd(), options = {}) {
138
+ const paths = getRuntimePaths(projectRoot);
139
+ const rows = parseJsonLines(paths.resultsFile);
140
+ const num = normalizeMax(options.num, 20);
141
+ if (rows.length <= num) return rows;
142
+ return rows.slice(rows.length - num);
143
+ }
144
+
145
+ module.exports = {
146
+ getRuntimePaths,
147
+ ensureRuntimeDir,
148
+ parseJsonLines,
149
+ loadState,
150
+ saveState,
151
+ submitTask,
152
+ runOnce,
153
+ listResults,
154
+ };
@@ -0,0 +1,162 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { randomUUID } = require("crypto");
4
+
5
+ function getSessionsDir(workspaceRoot = process.cwd()) {
6
+ const root = path.resolve(workspaceRoot || process.cwd());
7
+ return path.join(root, ".ufoo", "agent", "ucode-core", "sessions");
8
+ }
9
+
10
+ function normalizeSessionId(value = "") {
11
+ const raw = String(value || "").trim();
12
+ if (!raw) return "";
13
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._:-]{2,127}$/.test(raw)) return "";
14
+ return raw;
15
+ }
16
+
17
+ function createSessionId(prefix = "ucode") {
18
+ const safePrefix = String(prefix || "ucode").trim().replace(/[^a-zA-Z0-9_-]+/g, "") || "ucode";
19
+ return `${safePrefix}-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
20
+ }
21
+
22
+ function resolveSessionId(value = "") {
23
+ const normalized = normalizeSessionId(value);
24
+ if (normalized) return normalized;
25
+ return createSessionId("ucode");
26
+ }
27
+
28
+ function toIsoNow() {
29
+ return new Date().toISOString();
30
+ }
31
+
32
+ function cloneMessages(value = []) {
33
+ if (!Array.isArray(value)) return [];
34
+ try {
35
+ const parsed = JSON.parse(JSON.stringify(value));
36
+ if (!Array.isArray(parsed)) return [];
37
+ return parsed.filter((entry) => entry && typeof entry === "object" && !Array.isArray(entry));
38
+ } catch {
39
+ return [];
40
+ }
41
+ }
42
+
43
+ function buildSessionSnapshot(input = {}) {
44
+ const source = input && typeof input === "object" ? input : {};
45
+ const sessionId = resolveSessionId(source.sessionId);
46
+ const createdAt = String(source.createdAt || "").trim() || toIsoNow();
47
+ return {
48
+ version: 1,
49
+ sessionId,
50
+ workspaceRoot: String(source.workspaceRoot || process.cwd()).trim() || process.cwd(),
51
+ provider: String(source.provider || "").trim(),
52
+ model: String(source.model || "").trim(),
53
+ context: String(source.context || ""),
54
+ nlMessages: cloneMessages(source.nlMessages),
55
+ createdAt,
56
+ updatedAt: toIsoNow(),
57
+ };
58
+ }
59
+
60
+ function getSessionFilePath(workspaceRoot = process.cwd(), sessionId = "") {
61
+ const normalizedId = normalizeSessionId(sessionId);
62
+ if (!normalizedId) return "";
63
+ return path.join(getSessionsDir(workspaceRoot), `${normalizedId}.json`);
64
+ }
65
+
66
+ function saveSessionSnapshot(workspaceRoot = process.cwd(), snapshot = {}) {
67
+ const normalizedRoot = path.resolve(workspaceRoot || process.cwd());
68
+ const payload = buildSessionSnapshot({
69
+ ...snapshot,
70
+ workspaceRoot: normalizedRoot,
71
+ });
72
+ const filePath = getSessionFilePath(normalizedRoot, payload.sessionId);
73
+ if (!filePath) {
74
+ return {
75
+ ok: false,
76
+ error: "invalid session id",
77
+ sessionId: "",
78
+ filePath: "",
79
+ };
80
+ }
81
+
82
+ try {
83
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
84
+ fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
85
+ return {
86
+ ok: true,
87
+ error: "",
88
+ sessionId: payload.sessionId,
89
+ filePath,
90
+ snapshot: payload,
91
+ };
92
+ } catch (err) {
93
+ return {
94
+ ok: false,
95
+ error: err && err.message ? err.message : "failed to save session",
96
+ sessionId: payload.sessionId,
97
+ filePath,
98
+ };
99
+ }
100
+ }
101
+
102
+ function loadSessionSnapshot(workspaceRoot = process.cwd(), sessionId = "") {
103
+ const normalizedRoot = path.resolve(workspaceRoot || process.cwd());
104
+ const normalizedId = normalizeSessionId(sessionId);
105
+ if (!normalizedId) {
106
+ return {
107
+ ok: false,
108
+ error: "invalid session id",
109
+ sessionId: "",
110
+ snapshot: null,
111
+ filePath: "",
112
+ };
113
+ }
114
+
115
+ const filePath = getSessionFilePath(normalizedRoot, normalizedId);
116
+ if (!filePath || !fs.existsSync(filePath)) {
117
+ return {
118
+ ok: false,
119
+ error: `session not found: ${normalizedId}`,
120
+ sessionId: normalizedId,
121
+ snapshot: null,
122
+ filePath: filePath || "",
123
+ };
124
+ }
125
+
126
+ try {
127
+ const raw = fs.readFileSync(filePath, "utf8");
128
+ const parsed = JSON.parse(raw);
129
+ const snapshot = buildSessionSnapshot({
130
+ ...parsed,
131
+ sessionId: normalizedId,
132
+ workspaceRoot: normalizedRoot,
133
+ createdAt: parsed && parsed.createdAt ? parsed.createdAt : "",
134
+ });
135
+ return {
136
+ ok: true,
137
+ error: "",
138
+ sessionId: normalizedId,
139
+ snapshot,
140
+ filePath,
141
+ };
142
+ } catch (err) {
143
+ return {
144
+ ok: false,
145
+ error: err && err.message ? err.message : "failed to load session",
146
+ sessionId: normalizedId,
147
+ snapshot: null,
148
+ filePath,
149
+ };
150
+ }
151
+ }
152
+
153
+ module.exports = {
154
+ getSessionsDir,
155
+ normalizeSessionId,
156
+ createSessionId,
157
+ resolveSessionId,
158
+ buildSessionSnapshot,
159
+ getSessionFilePath,
160
+ saveSessionSnapshot,
161
+ loadSessionSnapshot,
162
+ };
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Task decomposition and progress reporting for ucode
3
+ * Based on Claude Code's design principles
4
+ */
5
+
6
+ const { runNativeAgentTask } = require("./nativeRunner");
7
+
8
+ /**
9
+ * Decompose a bug fix task into manageable steps
10
+ */
11
+ function decomposeBugFixTask(task) {
12
+ const steps = [];
13
+
14
+ // Analyze task to determine if it's a bug fix
15
+ const isBugFix = /fix|bug|issue|problem|error|broken|doesn't work|not work/i.test(task);
16
+
17
+ if (isBugFix) {
18
+ steps.push({
19
+ id: "identify",
20
+ name: "Identifying the issue",
21
+ prompt: `Identify the specific problem: ${task}\n\nBe concise. Focus only on:\n1. What is broken\n2. What file/function is likely involved\n3. What the expected behavior should be\n\nDo NOT analyze entire codebases. Find the specific issue quickly.`,
22
+ timeoutMs: 30000, // 30 seconds
23
+ earlyExit: true,
24
+ });
25
+
26
+ steps.push({
27
+ id: "locate",
28
+ name: "Locating relevant code",
29
+ prompt: `Based on the identified issue, find the exact location of the bug.\n\nSearch for and read ONLY the relevant function/file. Stop as soon as you find the problematic code.`,
30
+ timeoutMs: 30000,
31
+ earlyExit: true,
32
+ });
33
+
34
+ steps.push({
35
+ id: "fix",
36
+ name: "Applying the fix",
37
+ prompt: `Apply the minimal fix needed. Do NOT refactor or improve unrelated code. Just fix the specific issue.`,
38
+ timeoutMs: 60000,
39
+ earlyExit: false,
40
+ });
41
+
42
+ steps.push({
43
+ id: "verify",
44
+ name: "Verifying the fix",
45
+ prompt: `Verify the fix resolves the issue. Check that:\n1. The specific problem is fixed\n2. No new issues were introduced\n\nBe brief.`,
46
+ timeoutMs: 20000,
47
+ earlyExit: false,
48
+ });
49
+ } else {
50
+ // For non-bug tasks, use a single step
51
+ steps.push({
52
+ id: "execute",
53
+ name: "Executing task",
54
+ prompt: task,
55
+ timeoutMs: 120000,
56
+ earlyExit: false,
57
+ });
58
+ }
59
+
60
+ return steps;
61
+ }
62
+
63
+ /**
64
+ * Run a task with decomposition and progress reporting
65
+ */
66
+ async function runDecomposedTask({
67
+ task,
68
+ state,
69
+ onProgress,
70
+ onToolEvent,
71
+ signal,
72
+ workspaceRoot,
73
+ provider,
74
+ model,
75
+ systemPrompt,
76
+ messages = [],
77
+ sessionId = "",
78
+ }) {
79
+ const steps = decomposeBugFixTask(task);
80
+ const results = [];
81
+ let aborted = false;
82
+
83
+ // Check if already aborted
84
+ if (signal && signal.aborted) {
85
+ return {
86
+ ok: false,
87
+ error: "Task aborted",
88
+ results,
89
+ };
90
+ }
91
+
92
+ for (const step of steps) {
93
+ // Check abort signal
94
+ if (signal && signal.aborted) {
95
+ aborted = true;
96
+ break;
97
+ }
98
+
99
+ // Report progress
100
+ if (onProgress) {
101
+ onProgress({
102
+ type: "step_start",
103
+ step: step.id,
104
+ name: step.name,
105
+ current: steps.indexOf(step) + 1,
106
+ total: steps.length,
107
+ });
108
+ }
109
+
110
+ try {
111
+ // Run the step with its own timeout
112
+ const stepResult = await runNativeAgentTask({
113
+ workspaceRoot,
114
+ provider,
115
+ model,
116
+ prompt: step.prompt,
117
+ systemPrompt,
118
+ messages,
119
+ sessionId,
120
+ timeoutMs: step.timeoutMs,
121
+ onToolEvent,
122
+ signal,
123
+ });
124
+
125
+ results.push({
126
+ step: step.id,
127
+ name: step.name,
128
+ result: stepResult,
129
+ });
130
+
131
+ // Report step completion
132
+ if (onProgress) {
133
+ onProgress({
134
+ type: "step_complete",
135
+ step: step.id,
136
+ name: step.name,
137
+ success: stepResult.ok,
138
+ });
139
+ }
140
+
141
+ // Early exit if solution found
142
+ if (step.earlyExit && stepResult.ok) {
143
+ const output = String(stepResult.output || "").toLowerCase();
144
+ if (output.includes("fixed") || output.includes("resolved") || output.includes("solution")) {
145
+ // Found the fix early, skip remaining analysis
146
+ break;
147
+ }
148
+ }
149
+
150
+ // Stop on error for critical steps
151
+ if (!stepResult.ok && (step.id === "identify" || step.id === "locate")) {
152
+ return {
153
+ ok: false,
154
+ error: `Failed at ${step.name}: ${stepResult.error}`,
155
+ results,
156
+ };
157
+ }
158
+
159
+ } catch (err) {
160
+ // Report step error
161
+ if (onProgress) {
162
+ onProgress({
163
+ type: "step_error",
164
+ step: step.id,
165
+ name: step.name,
166
+ error: err.message,
167
+ });
168
+ }
169
+
170
+ return {
171
+ ok: false,
172
+ error: `Error at ${step.name}: ${err.message}`,
173
+ results,
174
+ };
175
+ }
176
+ }
177
+
178
+ if (aborted) {
179
+ return {
180
+ ok: false,
181
+ error: "Task aborted by user",
182
+ results,
183
+ };
184
+ }
185
+
186
+ // Compile final summary
187
+ const summary = compileSummary(results);
188
+
189
+ return {
190
+ ok: true,
191
+ summary,
192
+ results,
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Compile results into a concise summary
198
+ */
199
+ function compileSummary(results) {
200
+ if (!results || results.length === 0) {
201
+ return "No results";
202
+ }
203
+
204
+ // Extract key information from each step
205
+ const summaryParts = [];
206
+
207
+ for (const stepResult of results) {
208
+ if (stepResult.result && stepResult.result.ok) {
209
+ const output = String(stepResult.result.output || "").trim();
210
+
211
+ // Extract only the important parts (skip verbose thinking)
212
+ const lines = output.split("\n");
213
+ const keyLines = lines.filter(line => {
214
+ const lower = line.toLowerCase();
215
+ // Keep lines with actual findings/actions
216
+ return (
217
+ lower.includes("fixed") ||
218
+ lower.includes("found") ||
219
+ lower.includes("issue") ||
220
+ lower.includes("problem") ||
221
+ lower.includes("solution") ||
222
+ lower.includes("edit") ||
223
+ lower.includes("changed") ||
224
+ line.includes("src/") ||
225
+ line.includes("✓") ||
226
+ line.includes("✅")
227
+ );
228
+ });
229
+
230
+ if (keyLines.length > 0) {
231
+ summaryParts.push(keyLines.slice(0, 3).join("\n"));
232
+ }
233
+ }
234
+ }
235
+
236
+ return summaryParts.join("\n\n");
237
+ }
238
+
239
+ /**
240
+ * Create a progress reporter that sends updates via bus
241
+ */
242
+ function createBusProgressReporter(shell, publisher) {
243
+ let lastReportTime = Date.now();
244
+ const MIN_REPORT_INTERVAL = 5000; // Report at most every 5 seconds
245
+
246
+ return (progress) => {
247
+ const now = Date.now();
248
+ if (now - lastReportTime < MIN_REPORT_INTERVAL) {
249
+ return; // Throttle progress reports
250
+ }
251
+
252
+ lastReportTime = now;
253
+
254
+ if (progress.type === "step_start") {
255
+ const message = `⏳ ${progress.name} (${progress.current}/${progress.total})`;
256
+ shell(`ufoo bus send ${publisher} ${JSON.stringify(message)}`);
257
+ } else if (progress.type === "step_complete" && progress.success) {
258
+ const message = `✅ ${progress.name} completed`;
259
+ shell(`ufoo bus send ${publisher} ${JSON.stringify(message)}`);
260
+ }
261
+ };
262
+ }
263
+
264
+ module.exports = {
265
+ decomposeBugFixTask,
266
+ runDecomposedTask,
267
+ compileSummary,
268
+ createBusProgressReporter,
269
+ };
@@ -0,0 +1,53 @@
1
+ const { spawnSync } = require("child_process");
2
+ const { normalizeWorkspaceRoot } = require("./common");
3
+
4
+ function runBashTool(input = {}, options = {}) {
5
+ try {
6
+ const command = String(input.command || "").trim();
7
+ if (!command) {
8
+ return {
9
+ ok: false,
10
+ error: "command is required",
11
+ };
12
+ }
13
+ const workspaceRoot = normalizeWorkspaceRoot(options.workspaceRoot, options.cwd);
14
+ const timeoutMs = Number.isFinite(input.timeoutMs) ? Math.max(100, Math.floor(input.timeoutMs)) : 60000;
15
+ const result = spawnSync(command, {
16
+ cwd: workspaceRoot,
17
+ shell: true,
18
+ timeout: timeoutMs,
19
+ encoding: "utf8",
20
+ maxBuffer: 2 * 1024 * 1024,
21
+ });
22
+
23
+ if (result.error) {
24
+ return {
25
+ ok: false,
26
+ workspaceRoot,
27
+ code: typeof result.status === "number" ? result.status : -1,
28
+ stdout: String(result.stdout || ""),
29
+ stderr: String(result.stderr || ""),
30
+ error: result.error.message || "bash failed",
31
+ };
32
+ }
33
+
34
+ const code = typeof result.status === "number" ? result.status : 0;
35
+ return {
36
+ ok: code === 0,
37
+ workspaceRoot,
38
+ code,
39
+ stdout: String(result.stdout || ""),
40
+ stderr: String(result.stderr || ""),
41
+ error: code === 0 ? "" : `command exited with ${code}`,
42
+ };
43
+ } catch (err) {
44
+ return {
45
+ ok: false,
46
+ error: err && err.message ? err.message : "bash failed",
47
+ };
48
+ }
49
+ }
50
+
51
+ module.exports = {
52
+ runBashTool,
53
+ };
@@ -0,0 +1,42 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ function normalizeWorkspaceRoot(workspaceRoot = "", cwd = process.cwd()) {
5
+ const base = String(workspaceRoot || "").trim();
6
+ return path.resolve(base || cwd || process.cwd());
7
+ }
8
+
9
+ function isPathInside(root, target) {
10
+ const normalizedRoot = path.resolve(root);
11
+ const normalizedTarget = path.resolve(target);
12
+ if (normalizedRoot === normalizedTarget) return true;
13
+ return normalizedTarget.startsWith(`${normalizedRoot}${path.sep}`);
14
+ }
15
+
16
+ function resolveWorkspacePath(workspaceRoot = "", targetPath = "", cwd = process.cwd()) {
17
+ const root = normalizeWorkspaceRoot(workspaceRoot, cwd);
18
+ const requested = String(targetPath || "").trim();
19
+ if (!requested) {
20
+ throw new Error("path is required");
21
+ }
22
+ const resolved = path.resolve(root, requested);
23
+ if (!isPathInside(root, resolved)) {
24
+ throw new Error("path escapes workspace root");
25
+ }
26
+ return {
27
+ workspaceRoot: root,
28
+ requested,
29
+ resolved,
30
+ };
31
+ }
32
+
33
+ function ensureParentDir(filePath = "") {
34
+ const dir = path.dirname(path.resolve(filePath));
35
+ fs.mkdirSync(dir, { recursive: true });
36
+ }
37
+
38
+ module.exports = {
39
+ normalizeWorkspaceRoot,
40
+ resolveWorkspacePath,
41
+ ensureParentDir,
42
+ };