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.
- package/README.md +44 -4
- package/SKILLS/ufoo/SKILL.md +17 -2
- package/SKILLS/uinit/SKILL.md +8 -3
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +4 -0
- package/modules/AGENTS.template.md +14 -4
- package/modules/bus/README.md +8 -5
- package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
- package/modules/context/SKILLS/uctx/SKILL.md +3 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +12 -3
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +20 -49
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +524 -31
- package/src/agent/internalRunner.js +76 -9
- package/src/agent/launcher.js +97 -45
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +144 -4
- package/src/agent/ptyRunner.js +480 -10
- package/src/agent/ptyWrapper.js +28 -3
- package/src/agent/readyDetector.js +16 -0
- package/src/agent/ucode.js +443 -0
- package/src/agent/ucodeBootstrap.js +113 -0
- package/src/agent/ucodeBuild.js +67 -0
- package/src/agent/ucodeDoctor.js +184 -0
- package/src/agent/ucodeRuntimeConfig.js +129 -0
- package/src/agent/ufooAgent.js +11 -2
- package/src/assistant/agent.js +260 -0
- package/src/assistant/bridge.js +172 -0
- package/src/assistant/engine.js +252 -0
- package/src/assistant/stdio.js +58 -0
- package/src/assistant/ufooEngineCli.js +306 -0
- package/src/bus/activate.js +27 -11
- package/src/bus/daemon.js +133 -5
- package/src/bus/index.js +137 -80
- package/src/bus/inject.js +47 -17
- package/src/bus/message.js +145 -17
- package/src/bus/nickname.js +3 -1
- package/src/bus/queue.js +6 -1
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +20 -4
- package/src/bus/utils.js +9 -3
- package/src/chat/agentBar.js +117 -0
- package/src/chat/agentDirectory.js +88 -0
- package/src/chat/agentSockets.js +225 -0
- package/src/chat/agentViewController.js +298 -0
- package/src/chat/chatLogController.js +115 -0
- package/src/chat/commandExecutor.js +700 -0
- package/src/chat/commands.js +132 -0
- package/src/chat/completionController.js +414 -0
- package/src/chat/cronScheduler.js +160 -0
- package/src/chat/daemonConnection.js +166 -0
- package/src/chat/daemonCoordinator.js +64 -0
- package/src/chat/daemonMessageRouter.js +257 -0
- package/src/chat/daemonReconnect.js +41 -0
- package/src/chat/daemonTransport.js +36 -0
- package/src/chat/daemonTransportDefaults.js +10 -0
- package/src/chat/dashboardKeyController.js +480 -0
- package/src/chat/dashboardView.js +154 -0
- package/src/chat/index.js +935 -2909
- package/src/chat/inputHistoryController.js +105 -0
- package/src/chat/inputListenerController.js +304 -0
- package/src/chat/inputMath.js +104 -0
- package/src/chat/inputSubmitHandler.js +171 -0
- package/src/chat/layout.js +165 -0
- package/src/chat/pasteController.js +81 -0
- package/src/chat/rawKeyMap.js +42 -0
- package/src/chat/settingsController.js +132 -0
- package/src/chat/statusLineController.js +177 -0
- package/src/chat/streamTracker.js +138 -0
- package/src/chat/text.js +70 -0
- package/src/chat/transport.js +61 -0
- package/src/cli/busCoreCommands.js +59 -0
- package/src/cli/ctxCoreCommands.js +199 -0
- package/src/cli/onlineCoreCommands.js +379 -0
- package/src/cli.js +741 -238
- package/src/code/README.md +29 -0
- package/src/code/UCODE_PROMPT.md +32 -0
- package/src/code/agent.js +1651 -0
- package/src/code/cli.js +158 -0
- package/src/code/config +0 -0
- package/src/code/dispatch.js +42 -0
- package/src/code/index.js +70 -0
- package/src/code/nativeRunner.js +1213 -0
- package/src/code/runtime.js +154 -0
- package/src/code/sessionStore.js +162 -0
- package/src/code/taskDecomposer.js +269 -0
- package/src/code/tools/bash.js +53 -0
- package/src/code/tools/common.js +42 -0
- package/src/code/tools/edit.js +70 -0
- package/src/code/tools/read.js +44 -0
- package/src/code/tools/write.js +35 -0
- package/src/code/tui.js +1580 -0
- package/src/config.js +47 -1
- package/src/context/decisions.js +12 -2
- package/src/context/index.js +18 -1
- package/src/context/sync.js +127 -0
- package/src/daemon/agentProcessManager.js +74 -0
- package/src/daemon/cronOps.js +241 -0
- package/src/daemon/index.js +661 -488
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +417 -179
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +32 -17
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +2 -5
- package/src/daemon/status.js +24 -1
- package/src/init/index.js +68 -14
- package/src/online/bridge.js +663 -0
- package/src/online/client.js +245 -0
- package/src/online/runner.js +253 -0
- package/src/online/server.js +992 -0
- package/src/online/tokens.js +103 -0
- package/src/report/store.js +331 -0
- package/src/shared/eventContract.js +35 -0
- package/src/shared/ptySocketContract.js +21 -0
- package/src/status/index.js +50 -17
- package/src/terminal/adapterContract.js +87 -0
- package/src/terminal/adapterRouter.js +84 -0
- package/src/terminal/adapters/externalAdapter.js +14 -0
- package/src/terminal/adapters/internalAdapter.js +13 -0
- package/src/terminal/adapters/internalPtyAdapter.js +42 -0
- package/src/terminal/adapters/internalQueueAdapter.js +37 -0
- package/src/terminal/adapters/terminalAdapter.js +31 -0
- package/src/terminal/adapters/tmuxAdapter.js +30 -0
- package/src/ufoo/agentsStore.js +69 -3
- package/src/utils/banner.js +5 -2
- package/scripts/.archived/bash-to-js-migration/README.md +0 -46
- package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
- package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
- package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
- package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
- package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
- package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
- package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
- package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
- package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
- package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
- package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
- package/scripts/banner.sh +0 -2
- 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
|
+
};
|