fifony 0.1.43 → 0.1.47
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/app/dist/assets/{CommandPalette-M4VAMxCU.js → CommandPalette-CL8p78lG.js} +1 -1
- package/app/dist/assets/{KeyboardShortcutsHelp-DkvPUXQq.js → KeyboardShortcutsHelp-CqEFfGcE.js} +1 -1
- package/app/dist/assets/OnboardingWizard-BmI50ZUv.js +1 -0
- package/app/dist/assets/analytics.lazy-CXGjZabc.js +1 -0
- package/app/dist/assets/{api-CkVfYg_m.js → api-CEr_D4e5.js} +1 -1
- package/app/dist/assets/{createLucideIcon-Dfk_Hxud.js → createLucideIcon-luywpIq4.js} +1 -1
- package/app/dist/assets/index-CEaccpYh.js +96 -0
- package/app/dist/assets/index-CzzWGzux.css +1 -0
- package/app/dist/assets/vendor-uqBx3VSC.js +9 -0
- package/app/dist/index.html +12 -12
- package/app/dist/service-worker.js +15 -5
- package/dist/agent/pty-daemon.js +3 -2
- package/dist/agent/run-local.js +71 -52
- package/dist/{agent-RMQTTUEC.js → agent-DFSFG6DG.js} +18 -12
- package/dist/{analytics-broadcaster-O6YBP66L.js → analytics-broadcaster-O4AE3RUK.js} +21 -14
- package/dist/approve-plan.command-QGQZZXTQ.js +17 -0
- package/dist/{chunk-E2EWEYA4.js → chunk-2PRRKBG6.js} +20 -10
- package/dist/chunk-5AMWD66T.js +38 -0
- package/dist/{chunk-QQQLP3PL.js → chunk-7TXZYZR5.js} +9 -37
- package/dist/chunk-AAVROEQC.js +859 -0
- package/dist/{chunk-ESWHDHH6.js → chunk-AAZKYWOY.js} +4 -4
- package/dist/chunk-EBCSQFPR.js +682 -0
- package/dist/{chunk-BRSR26VK.js → chunk-FH7HUPZX.js} +2 -2
- package/dist/chunk-HOIOVUHI.js +35 -0
- package/dist/{chunk-AILXZ2TD.js → chunk-JRLWLZOD.js} +20 -13
- package/dist/{chunk-YRSH2CLW.js → chunk-K36BWMUV.js} +1741 -1216
- package/dist/chunk-N4KFNX2G.js +370 -0
- package/dist/chunk-PACI3T4I.js +125 -0
- package/dist/{chunk-FJNH3G2Z.js → chunk-PI7Y77R3.js} +38 -663
- package/dist/{chunk-DVU3CXWA.js → chunk-PXTIWKLQ.js} +2 -1
- package/dist/{chunk-SOBLO4YZ.js → chunk-QH6VCTET.js} +316 -127
- package/dist/{chunk-MVTGAKQK.js → chunk-QHISYRXJ.js} +2 -2
- package/dist/{chunk-42AMQAJG.js → chunk-VM5QAYP5.js} +2 -2
- package/dist/cli.js +17 -11
- package/dist/create-issue.command-VAKYRECC.js +24 -0
- package/dist/{fsm-issue-YGGF7SIL.js → fsm-issue-EHTSKMFN.js} +9 -8
- package/dist/fsm-service-7O4AJG2R.js +32 -0
- package/dist/{helpers-L7NYO5XS.js → helpers-ON2S7UEF.js} +2 -2
- package/dist/{issue-log-broadcaster-WZAHISYB.js → issue-log-broadcaster-FZGVEEIX.js} +20 -13
- package/dist/{issues-3QRR7KM6.js → issues-3YNNTB4U.js} +10 -7
- package/dist/{log-analyzer-K7MXQB4T.js → log-analyzer-EIX6R6PP.js} +82 -18
- package/dist/logger-IFLXTQPS.js +11 -0
- package/dist/mcp/server.js +2 -2
- package/dist/merge-workspace.command-T2NIGR4M.js +24 -0
- package/dist/{parallel-executor-6INE6NDO.js → parallel-executor-DWESCNX3.js} +20 -14
- package/dist/queue-workers-V57BYXAY.js +38 -0
- package/dist/replan-issue.command-2GQ3QXCR.js +17 -0
- package/dist/retry-issue.command-GJBUUYDJ.js +17 -0
- package/dist/scheduler-KYILMWLD.js +32 -0
- package/dist/{settings-ZAWDCFP2.js → settings-SOTIS6ZD.js} +32 -12
- package/dist/settings.resource-JMD3JQOS.js +30 -0
- package/dist/{store-M6NCKMZY.js → store-S3NAYZ3S.js} +18 -12
- package/dist/{web-push-AX5IIK3P.js → web-push-QCTLS7EJ.js} +3 -3
- package/dist/websocket-T2Y3BY4B.js +61 -0
- package/dist/{workspace-CJTWFWTJ.js → workspace-OS7GPMCN.js} +7 -6
- package/package.json +8 -5
- package/app/dist/assets/OnboardingWizard-B7V9hoCR.js +0 -1
- package/app/dist/assets/analytics.lazy-zVJdF880.js +0 -1
- package/app/dist/assets/index-BpiCi7Ew.css +0 -1
- package/app/dist/assets/index-D2INW0zc.js +0 -47
- package/app/dist/assets/vendor-BEoYbFV1.js +0 -9
- package/dist/queue-workers-XFZK3TT5.js +0 -32
- package/dist/replan-issue.command-4UCWYHGZ.js +0 -15
- package/dist/scheduler-ZP7GOZDW.js +0 -26
- package/dist/settings.resource-5CW456AZ.js +0 -24
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
markIssueDirty,
|
|
3
3
|
normalizeAgentProvider
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import {
|
|
6
|
-
logger
|
|
7
|
-
} from "./chunk-DVU3CXWA.js";
|
|
4
|
+
} from "./chunk-PI7Y77R3.js";
|
|
8
5
|
import {
|
|
9
6
|
renderPrompt
|
|
10
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-AAZKYWOY.js";
|
|
11
8
|
import {
|
|
12
9
|
SOURCE_MARKER,
|
|
13
10
|
SOURCE_ROOT,
|
|
@@ -16,12 +13,15 @@ import {
|
|
|
16
13
|
appendFileTail,
|
|
17
14
|
idToSafePath,
|
|
18
15
|
now
|
|
19
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-VM5QAYP5.js";
|
|
17
|
+
import {
|
|
18
|
+
logger
|
|
19
|
+
} from "./chunk-PXTIWKLQ.js";
|
|
20
20
|
|
|
21
21
|
// src/domains/workspace.ts
|
|
22
22
|
import {
|
|
23
|
-
existsSync as
|
|
24
|
-
mkdirSync as
|
|
23
|
+
existsSync as existsSync5,
|
|
24
|
+
mkdirSync as mkdirSync3,
|
|
25
25
|
readdirSync as readdirSync2,
|
|
26
26
|
readFileSync as readFileSync3,
|
|
27
27
|
rmSync as rmSync2,
|
|
@@ -29,18 +29,18 @@ import {
|
|
|
29
29
|
writeFileSync as writeFileSync3
|
|
30
30
|
} from "fs";
|
|
31
31
|
import { copyFile, mkdir, readdir, stat, writeFile } from "fs/promises";
|
|
32
|
-
import { extname, join as
|
|
33
|
-
import { execSync } from "child_process";
|
|
32
|
+
import { extname, join as join4, resolve } from "path";
|
|
33
|
+
import { execSync as execSync2 } from "child_process";
|
|
34
34
|
|
|
35
35
|
// src/agents/command-executor.ts
|
|
36
36
|
import {
|
|
37
37
|
appendFileSync,
|
|
38
|
-
existsSync as
|
|
38
|
+
existsSync as existsSync3,
|
|
39
39
|
readFileSync,
|
|
40
40
|
rmSync,
|
|
41
41
|
writeFileSync
|
|
42
42
|
} from "fs";
|
|
43
|
-
import { join } from "path";
|
|
43
|
+
import { join as join2 } from "path";
|
|
44
44
|
import { env, execPath } from "process";
|
|
45
45
|
import { spawn } from "child_process";
|
|
46
46
|
import { createConnection } from "net";
|
|
@@ -115,16 +115,142 @@ function buildDockerPlanCommand(innerCommand, tempDir, image) {
|
|
|
115
115
|
].join(" ");
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// src/domains/sandbox.ts
|
|
119
|
+
import { existsSync as existsSync2, chmodSync, mkdirSync } from "fs";
|
|
120
|
+
import { homedir as homedir2, platform, arch } from "os";
|
|
121
|
+
import { join } from "path";
|
|
122
|
+
import { execSync } from "child_process";
|
|
123
|
+
var BIN_DIR = join(homedir2(), ".fifony", "bin");
|
|
124
|
+
var AI_JAIL_BIN = join(BIN_DIR, "ai-jail");
|
|
125
|
+
var BWRAP_BIN = join(BIN_DIR, "bwrap");
|
|
126
|
+
function resolveLinuxArch() {
|
|
127
|
+
const cpu = arch();
|
|
128
|
+
if (cpu === "x64") return "x86_64";
|
|
129
|
+
if (cpu === "arm64") return "aarch64";
|
|
130
|
+
throw new Error(`Unsupported Linux architecture: ${cpu}`);
|
|
131
|
+
}
|
|
132
|
+
function resolveAiJailSuffix() {
|
|
133
|
+
const os = platform();
|
|
134
|
+
const cpu = arch();
|
|
135
|
+
if (os === "linux" && cpu === "x64") return "linux-x86_64";
|
|
136
|
+
if (os === "darwin" && cpu === "arm64") return "macos-aarch64";
|
|
137
|
+
if (os === "darwin" && cpu === "x64") return "macos-x86_64";
|
|
138
|
+
throw new Error(`Unsupported platform for ai-jail: ${os}-${cpu}`);
|
|
139
|
+
}
|
|
140
|
+
function downloadTarball(url, destDir, binaryName) {
|
|
141
|
+
mkdirSync(destDir, { recursive: true });
|
|
142
|
+
execSync(
|
|
143
|
+
`curl -fsSL "${url}" | tar xz -C "${destDir}"`,
|
|
144
|
+
{ stdio: "pipe", timeout: 12e4 }
|
|
145
|
+
);
|
|
146
|
+
const binPath = join(destDir, binaryName);
|
|
147
|
+
if (!existsSync2(binPath)) {
|
|
148
|
+
throw new Error(`Binary ${binaryName} not found at ${binPath} after extraction`);
|
|
149
|
+
}
|
|
150
|
+
chmodSync(binPath, 493);
|
|
151
|
+
}
|
|
152
|
+
function downloadRawBinary(url, destPath) {
|
|
153
|
+
mkdirSync(join(destPath, ".."), { recursive: true });
|
|
154
|
+
execSync(
|
|
155
|
+
`curl -fsSL -o "${destPath}" "${url}"`,
|
|
156
|
+
{ stdio: "pipe", timeout: 12e4 }
|
|
157
|
+
);
|
|
158
|
+
if (!existsSync2(destPath)) {
|
|
159
|
+
throw new Error(`Binary not found at ${destPath} after download`);
|
|
160
|
+
}
|
|
161
|
+
chmodSync(destPath, 493);
|
|
162
|
+
}
|
|
163
|
+
function isBwrapAvailable() {
|
|
164
|
+
if (platform() !== "linux") return true;
|
|
165
|
+
if (existsSync2(BWRAP_BIN)) {
|
|
166
|
+
try {
|
|
167
|
+
execSync(`"${BWRAP_BIN}" --version`, { stdio: "pipe", timeout: 3e3 });
|
|
168
|
+
return true;
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
execSync("which bwrap", { stdio: "pipe", timeout: 3e3 });
|
|
174
|
+
return true;
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
function downloadBwrap() {
|
|
180
|
+
const linuxArch = resolveLinuxArch();
|
|
181
|
+
const url = `https://github.com/forattini-dev/bubblewrap/releases/latest/download/bwrap-linux-${linuxArch}`;
|
|
182
|
+
logger.info({ url, dest: BWRAP_BIN }, "[Sandbox] Downloading bubblewrap");
|
|
183
|
+
downloadRawBinary(url, BWRAP_BIN);
|
|
184
|
+
logger.info("[Sandbox] bubblewrap installed successfully");
|
|
185
|
+
}
|
|
186
|
+
function ensureBwrap() {
|
|
187
|
+
if (platform() !== "linux") return;
|
|
188
|
+
if (isBwrapAvailable()) return;
|
|
189
|
+
downloadBwrap();
|
|
190
|
+
if (!isBwrapAvailable()) {
|
|
191
|
+
throw new Error("Failed to install bubblewrap. Sandbox requires bwrap on Linux.");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function getBwrapEnv() {
|
|
195
|
+
if (existsSync2(BWRAP_BIN)) return { BWRAP_BIN };
|
|
196
|
+
return {};
|
|
197
|
+
}
|
|
198
|
+
function isAiJailInstalled() {
|
|
199
|
+
if (!existsSync2(AI_JAIL_BIN)) return false;
|
|
200
|
+
try {
|
|
201
|
+
execSync(`"${AI_JAIL_BIN}" --version`, { stdio: "pipe", timeout: 5e3 });
|
|
202
|
+
return true;
|
|
203
|
+
} catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function getAiJailVersion() {
|
|
208
|
+
try {
|
|
209
|
+
return execSync(`"${AI_JAIL_BIN}" --version`, { stdio: "pipe", timeout: 5e3 }).toString().trim() || null;
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function downloadAiJail() {
|
|
215
|
+
const suffix = resolveAiJailSuffix();
|
|
216
|
+
const url = `https://github.com/akitaonrails/ai-jail/releases/latest/download/ai-jail-${suffix}.tar.gz`;
|
|
217
|
+
logger.info({ url, dest: AI_JAIL_BIN }, "[Sandbox] Downloading ai-jail");
|
|
218
|
+
downloadTarball(url, BIN_DIR, "ai-jail");
|
|
219
|
+
const version = getAiJailVersion();
|
|
220
|
+
logger.info({ version }, "[Sandbox] ai-jail installed successfully");
|
|
221
|
+
}
|
|
222
|
+
async function ensureAiJail() {
|
|
223
|
+
ensureBwrap();
|
|
224
|
+
if (!isAiJailInstalled()) downloadAiJail();
|
|
225
|
+
return AI_JAIL_BIN;
|
|
226
|
+
}
|
|
227
|
+
function buildSandboxCommand(command, worktreePath, extraRwPaths) {
|
|
228
|
+
const rwMaps = [...extraRwPaths ?? []];
|
|
229
|
+
const rwFlags = rwMaps.map((p) => `--rw-map "${p}"`).join(" ");
|
|
230
|
+
const escapedCommand = command.replace(/'/g, "'\\''");
|
|
231
|
+
const flags = [
|
|
232
|
+
"--exec",
|
|
233
|
+
"--no-docker",
|
|
234
|
+
"--no-display",
|
|
235
|
+
"--no-gpu",
|
|
236
|
+
"--no-status-bar",
|
|
237
|
+
rwFlags
|
|
238
|
+
].filter(Boolean).join(" ");
|
|
239
|
+
const bwrapEnv = getBwrapEnv();
|
|
240
|
+
const envPrefix = bwrapEnv.BWRAP_BIN ? `BWRAP_BIN="${bwrapEnv.BWRAP_BIN}" ` : "";
|
|
241
|
+
return `cd "${worktreePath}" && ${envPrefix}"${AI_JAIL_BIN}" ${flags} -- sh -c '${escapedCommand}'`;
|
|
242
|
+
}
|
|
243
|
+
|
|
118
244
|
// src/agents/command-executor.ts
|
|
119
245
|
function resolveDaemonScript() {
|
|
120
246
|
const pkgRoot = process.env.FIFONY_PKG_ROOT;
|
|
121
247
|
if (!pkgRoot) return null;
|
|
122
|
-
const compiled =
|
|
123
|
-
if (
|
|
248
|
+
const compiled = join2(pkgRoot, "dist", "agent", "pty-daemon.js");
|
|
249
|
+
if (existsSync3(compiled)) {
|
|
124
250
|
return { command: execPath, args: [compiled] };
|
|
125
251
|
}
|
|
126
|
-
const source =
|
|
127
|
-
if (
|
|
252
|
+
const source = join2(pkgRoot, "src", "agents", "pty-daemon.ts");
|
|
253
|
+
if (existsSync3(source)) {
|
|
128
254
|
try {
|
|
129
255
|
const require2 = createRequire(fileURLToPath(import.meta.url));
|
|
130
256
|
const tsxCli = require2.resolve("tsx/cli");
|
|
@@ -154,6 +280,7 @@ var HOOK_RUNTIME_CONFIG = {
|
|
|
154
280
|
autoReviewApproval: true,
|
|
155
281
|
dockerExecution: false,
|
|
156
282
|
dockerImage: "fifony-agent:latest",
|
|
283
|
+
sandboxExecution: false,
|
|
157
284
|
afterCreateHook: "",
|
|
158
285
|
beforeRunHook: "",
|
|
159
286
|
afterRunHook: "",
|
|
@@ -162,7 +289,7 @@ var HOOK_RUNTIME_CONFIG = {
|
|
|
162
289
|
async function waitForSocket(socketPath, timeoutMs) {
|
|
163
290
|
const deadline = Date.now() + timeoutMs;
|
|
164
291
|
while (Date.now() < deadline) {
|
|
165
|
-
if (
|
|
292
|
+
if (existsSync3(socketPath)) return true;
|
|
166
293
|
await new Promise((r) => setTimeout(r, 50));
|
|
167
294
|
}
|
|
168
295
|
return false;
|
|
@@ -179,7 +306,7 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
|
|
|
179
306
|
};
|
|
180
307
|
for (const [key, value] of Object.entries(extraEnv)) {
|
|
181
308
|
if (value.length > 4e3) {
|
|
182
|
-
const valFile =
|
|
309
|
+
const valFile = join2(workspacePath, `${key.toLowerCase()}.txt`);
|
|
183
310
|
writeFileSync(valFile, value, "utf8");
|
|
184
311
|
allVars[`${key}_FILE`] = valFile;
|
|
185
312
|
} else {
|
|
@@ -191,7 +318,7 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
|
|
|
191
318
|
allVars[key] = translatePaths(allVars[key], workspacePath);
|
|
192
319
|
}
|
|
193
320
|
}
|
|
194
|
-
const envFilePath =
|
|
321
|
+
const envFilePath = join2(workspacePath, ".env.sh");
|
|
195
322
|
const envFileLines = Object.entries(allVars).map(([k, v]) => `export ${k}='${String(v).replace(/'/g, "'\\''")}'`).join("\n");
|
|
196
323
|
writeFileSync(envFilePath, envFileLines, "utf8");
|
|
197
324
|
let effectiveCommand;
|
|
@@ -204,10 +331,15 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
|
|
|
204
331
|
TARGET_ROOT,
|
|
205
332
|
config.dockerImage
|
|
206
333
|
);
|
|
334
|
+
} else if (config.sandboxExecution) {
|
|
335
|
+
await ensureAiJail();
|
|
336
|
+
const innerCommand = `. "${envFilePath}" && ${command}`;
|
|
337
|
+
const worktree = issue.worktreePath ?? workspacePath;
|
|
338
|
+
effectiveCommand = buildSandboxCommand(innerCommand, worktree, [workspacePath]);
|
|
207
339
|
} else {
|
|
208
340
|
effectiveCommand = `. "${envFilePath}" && ${command}`;
|
|
209
341
|
}
|
|
210
|
-
const liveLogFile =
|
|
342
|
+
const liveLogFile = join2(workspacePath, "live-output.log");
|
|
211
343
|
if (outputFile) {
|
|
212
344
|
try {
|
|
213
345
|
const header = `# fifony stdout capture
|
|
@@ -222,8 +354,8 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
|
|
|
222
354
|
}
|
|
223
355
|
}
|
|
224
356
|
if (!config.dockerExecution && DAEMON_SCRIPT) {
|
|
225
|
-
const socketPath =
|
|
226
|
-
if (
|
|
357
|
+
const socketPath = join2(workspacePath, "agent.sock");
|
|
358
|
+
if (existsSync3(socketPath)) {
|
|
227
359
|
const { isDaemonAlive } = await import("./pid-manager-UBWXVSMD.js");
|
|
228
360
|
if (isDaemonAlive(workspacePath)) {
|
|
229
361
|
logger.info({ issueId: issue.id }, "[Agent] Live PTY daemon detected \u2014 reattaching to existing session");
|
|
@@ -241,17 +373,19 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
|
|
|
241
373
|
const daemonArgs = JSON.stringify({
|
|
242
374
|
command: effectiveCommand,
|
|
243
375
|
workspacePath,
|
|
376
|
+
codePath: issue.worktreePath ?? workspacePath,
|
|
244
377
|
issueId: issue.id,
|
|
245
378
|
startedAt: new Date(started).toISOString(),
|
|
246
379
|
commandSlice: command.slice(0, 200)
|
|
247
380
|
});
|
|
381
|
+
const effectiveCwd = issue.worktreePath ?? workspacePath;
|
|
248
382
|
const daemonProcess = spawn(DAEMON_SCRIPT.command, [...DAEMON_SCRIPT.args, daemonArgs], {
|
|
249
383
|
detached: true,
|
|
250
384
|
stdio: "ignore",
|
|
251
|
-
cwd:
|
|
385
|
+
cwd: effectiveCwd
|
|
252
386
|
});
|
|
253
387
|
daemonProcess.unref();
|
|
254
|
-
logger.debug({ issueId: issue.id, daemonPid: daemonProcess.pid, command: command.slice(0, 120), cwd:
|
|
388
|
+
logger.debug({ issueId: issue.id, daemonPid: daemonProcess.pid, command: command.slice(0, 120), cwd: effectiveCwd }, "[Agent] PTY daemon spawned");
|
|
255
389
|
const socketReady = await waitForSocket(socketPath, 1e4);
|
|
256
390
|
if (!socketReady) {
|
|
257
391
|
logger.warn({ issueId: issue.id }, "[Agent] PTY daemon socket not ready \u2014 falling back to inline PTY");
|
|
@@ -272,17 +406,18 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
|
|
|
272
406
|
}
|
|
273
407
|
writeFileSync(liveLogFile, "", "utf8");
|
|
274
408
|
return new Promise((resolve2) => {
|
|
409
|
+
const ptyEffectiveCwd = issue.worktreePath ?? workspacePath;
|
|
275
410
|
const ptyProcess = nodePty.spawn("sh", ["-c", effectiveCommand], {
|
|
276
411
|
name: "xterm-256color",
|
|
277
412
|
cols: 220,
|
|
278
413
|
rows: 50,
|
|
279
|
-
cwd:
|
|
414
|
+
cwd: ptyEffectiveCwd,
|
|
280
415
|
env: process.env
|
|
281
416
|
});
|
|
282
417
|
const pid = ptyProcess.pid;
|
|
283
|
-
const pidFile =
|
|
418
|
+
const pidFile = join2(workspacePath, "agent.pid");
|
|
284
419
|
if (pid) {
|
|
285
|
-
logger.debug({ issueId: issue.id, pid, command: command.slice(0, 120), cwd:
|
|
420
|
+
logger.debug({ issueId: issue.id, pid, command: command.slice(0, 120), cwd: ptyEffectiveCwd }, "[Agent] Process spawned (PTY)");
|
|
286
421
|
writeFileSync(pidFile, JSON.stringify({
|
|
287
422
|
pid,
|
|
288
423
|
issueId: issue.id,
|
|
@@ -409,7 +544,7 @@ Command exit code ${exitCode ?? "unknown"} after ${duration}ms.`) });
|
|
|
409
544
|
if (child.stdin) {
|
|
410
545
|
child.stdin.end();
|
|
411
546
|
}
|
|
412
|
-
const pidFile =
|
|
547
|
+
const pidFile = join2(workspacePath, "agent.pid");
|
|
413
548
|
const pid = child.pid;
|
|
414
549
|
if (pid) {
|
|
415
550
|
logger.debug({ issueId: issue.id, pid, command: command.slice(0, 120), cwd: workspacePath }, "[Agent] Process spawned");
|
|
@@ -539,7 +674,7 @@ Command exit code ${code ?? "unknown"} after ${duration}ms.`) });
|
|
|
539
674
|
}
|
|
540
675
|
function attachToDaemon(socketPath, workspacePath, issue, config, started, outputFile, resultFile) {
|
|
541
676
|
return new Promise((resolve2) => {
|
|
542
|
-
const daemonExitFile =
|
|
677
|
+
const daemonExitFile = join2(workspacePath, "daemon.exit.json");
|
|
543
678
|
let output = "";
|
|
544
679
|
let outputHeader = "";
|
|
545
680
|
let outputBytes = 0;
|
|
@@ -651,8 +786,8 @@ Agent process stuck \u2014 no output for ${Math.round(AGENT_STALE_OUTPUT_MS / 6e
|
|
|
651
786
|
});
|
|
652
787
|
}
|
|
653
788
|
async function writeToDaemon(workspacePath, text) {
|
|
654
|
-
const socketPath =
|
|
655
|
-
if (!
|
|
789
|
+
const socketPath = join2(workspacePath, "agent.sock");
|
|
790
|
+
if (!existsSync3(socketPath)) return;
|
|
656
791
|
return new Promise((resolve2) => {
|
|
657
792
|
const sock = createConnection(socketPath);
|
|
658
793
|
const cleanup = () => {
|
|
@@ -957,13 +1092,13 @@ async function buildProviderBasePrompt(provider, issue, basePrompt, workspacePat
|
|
|
957
1092
|
|
|
958
1093
|
// src/agents/memory-engine.ts
|
|
959
1094
|
import {
|
|
960
|
-
existsSync as
|
|
961
|
-
mkdirSync,
|
|
1095
|
+
existsSync as existsSync4,
|
|
1096
|
+
mkdirSync as mkdirSync2,
|
|
962
1097
|
readdirSync,
|
|
963
1098
|
readFileSync as readFileSync2,
|
|
964
1099
|
writeFileSync as writeFileSync2
|
|
965
1100
|
} from "fs";
|
|
966
|
-
import { join as
|
|
1101
|
+
import { join as join3 } from "path";
|
|
967
1102
|
var MEMORY_DIRNAME = "memory";
|
|
968
1103
|
var WORKFLOW_FILE = "WORKFLOW.md";
|
|
969
1104
|
var MEMORY_FILE = "MEMORY.md";
|
|
@@ -972,19 +1107,19 @@ function resolveTodayDate(value = now()) {
|
|
|
972
1107
|
return value.slice(0, 10);
|
|
973
1108
|
}
|
|
974
1109
|
function resolvePaths(workspacePath, date = resolveTodayDate()) {
|
|
975
|
-
const memoryDir =
|
|
1110
|
+
const memoryDir = join3(workspacePath, MEMORY_DIRNAME);
|
|
976
1111
|
return {
|
|
977
1112
|
root: workspacePath,
|
|
978
1113
|
memoryDir,
|
|
979
|
-
workflowFile:
|
|
980
|
-
memoryFile:
|
|
981
|
-
heartbeatFile:
|
|
982
|
-
dailyFile:
|
|
1114
|
+
workflowFile: join3(workspacePath, WORKFLOW_FILE),
|
|
1115
|
+
memoryFile: join3(workspacePath, MEMORY_FILE),
|
|
1116
|
+
heartbeatFile: join3(workspacePath, HEARTBEAT_FILE),
|
|
1117
|
+
dailyFile: join3(memoryDir, `${date}.md`)
|
|
983
1118
|
};
|
|
984
1119
|
}
|
|
985
1120
|
function readText(filePath) {
|
|
986
1121
|
try {
|
|
987
|
-
return
|
|
1122
|
+
return existsSync4(filePath) ? readFileSync2(filePath, "utf8") : "";
|
|
988
1123
|
} catch {
|
|
989
1124
|
return "";
|
|
990
1125
|
}
|
|
@@ -996,7 +1131,7 @@ function writeIfChanged(filePath, next) {
|
|
|
996
1131
|
return true;
|
|
997
1132
|
}
|
|
998
1133
|
function ensureFile(filePath, initial) {
|
|
999
|
-
if (
|
|
1134
|
+
if (existsSync4(filePath)) return false;
|
|
1000
1135
|
writeFileSync2(filePath, initial, "utf8");
|
|
1001
1136
|
return true;
|
|
1002
1137
|
}
|
|
@@ -1185,13 +1320,13 @@ function appendUniqueEntry(filePath, entry) {
|
|
|
1185
1320
|
return true;
|
|
1186
1321
|
}
|
|
1187
1322
|
function listRecentDailyFiles(memoryDir) {
|
|
1188
|
-
if (!
|
|
1189
|
-
return readdirSync(memoryDir).filter((entry) => entry.endsWith(".md")).sort((left, right) => right.localeCompare(left)).slice(0, 3).map((entry) =>
|
|
1323
|
+
if (!existsSync4(memoryDir)) return [];
|
|
1324
|
+
return readdirSync(memoryDir).filter((entry) => entry.endsWith(".md")).sort((left, right) => right.localeCompare(left)).slice(0, 3).map((entry) => join3(memoryDir, entry));
|
|
1190
1325
|
}
|
|
1191
1326
|
function ensureWorkspaceMemoryFiles(issue, workspacePath) {
|
|
1192
1327
|
const paths = resolvePaths(workspacePath);
|
|
1193
|
-
|
|
1194
|
-
|
|
1328
|
+
mkdirSync2(paths.root, { recursive: true });
|
|
1329
|
+
mkdirSync2(paths.memoryDir, { recursive: true });
|
|
1195
1330
|
ensureFile(paths.workflowFile, renderWorkflowDocument(issue));
|
|
1196
1331
|
ensureFile(paths.memoryFile, renderMemoryHeader(issue));
|
|
1197
1332
|
ensureFile(paths.heartbeatFile, renderHeartbeatDocument(issue));
|
|
@@ -1207,7 +1342,7 @@ function flushWorkspaceMemory(issue, workspacePath, reason) {
|
|
|
1207
1342
|
let promotedEntries = 0;
|
|
1208
1343
|
if (writeIfChanged(paths.workflowFile, renderWorkflowDocument(issue))) changedFiles.push(paths.workflowFile);
|
|
1209
1344
|
if (writeIfChanged(paths.heartbeatFile, renderHeartbeatDocument(issue))) changedFiles.push(paths.heartbeatFile);
|
|
1210
|
-
if (!
|
|
1345
|
+
if (!existsSync4(paths.memoryFile)) {
|
|
1211
1346
|
writeFileSync2(paths.memoryFile, renderMemoryHeader(issue), "utf8");
|
|
1212
1347
|
changedFiles.push(paths.memoryFile);
|
|
1213
1348
|
}
|
|
@@ -1308,10 +1443,10 @@ function shouldSkipPath(relativePath) {
|
|
|
1308
1443
|
return false;
|
|
1309
1444
|
}
|
|
1310
1445
|
function bootstrapSource() {
|
|
1311
|
-
if (
|
|
1446
|
+
if (existsSync5(SOURCE_MARKER)) return;
|
|
1312
1447
|
logger.info("Creating local source snapshot for Fifony (local-only runtime)...");
|
|
1313
1448
|
const copyRecursive = (source, target, rel = "") => {
|
|
1314
|
-
|
|
1449
|
+
mkdirSync3(target, { recursive: true });
|
|
1315
1450
|
const items = readdirSync2(source, { withFileTypes: true });
|
|
1316
1451
|
for (const item of items) {
|
|
1317
1452
|
const nextRel = rel ? `${rel}/${item.name}` : item.name;
|
|
@@ -1338,7 +1473,7 @@ function bootstrapSource() {
|
|
|
1338
1473
|
}
|
|
1339
1474
|
}
|
|
1340
1475
|
};
|
|
1341
|
-
|
|
1476
|
+
mkdirSync3(SOURCE_ROOT, { recursive: true });
|
|
1342
1477
|
copyRecursive(TARGET_ROOT, SOURCE_ROOT);
|
|
1343
1478
|
writeFileSync3(SOURCE_MARKER, `${now()}
|
|
1344
1479
|
`, "utf8");
|
|
@@ -1353,7 +1488,7 @@ async function ensureSourceReady(onProgress) {
|
|
|
1353
1488
|
onProgress?.("ready");
|
|
1354
1489
|
return;
|
|
1355
1490
|
}
|
|
1356
|
-
if (
|
|
1491
|
+
if (existsSync5(SOURCE_MARKER)) {
|
|
1357
1492
|
onProgress?.("ready");
|
|
1358
1493
|
return;
|
|
1359
1494
|
}
|
|
@@ -1400,7 +1535,7 @@ async function ensureSourceReady(onProgress) {
|
|
|
1400
1535
|
function getGitRepoStatus(dir) {
|
|
1401
1536
|
const isGit = (() => {
|
|
1402
1537
|
try {
|
|
1403
|
-
|
|
1538
|
+
execSync2("git rev-parse --git-dir", { cwd: dir, stdio: "pipe" });
|
|
1404
1539
|
return true;
|
|
1405
1540
|
} catch {
|
|
1406
1541
|
return false;
|
|
@@ -1411,10 +1546,10 @@ function getGitRepoStatus(dir) {
|
|
|
1411
1546
|
}
|
|
1412
1547
|
const branch = (() => {
|
|
1413
1548
|
try {
|
|
1414
|
-
return
|
|
1549
|
+
return execSync2("git symbolic-ref --short HEAD", { cwd: dir, encoding: "utf8", stdio: "pipe" }).trim() || null;
|
|
1415
1550
|
} catch {
|
|
1416
1551
|
try {
|
|
1417
|
-
return
|
|
1552
|
+
return execSync2("git rev-parse --abbrev-ref HEAD", { cwd: dir, encoding: "utf8", stdio: "pipe" }).trim() || null;
|
|
1418
1553
|
} catch {
|
|
1419
1554
|
return null;
|
|
1420
1555
|
}
|
|
@@ -1422,7 +1557,7 @@ function getGitRepoStatus(dir) {
|
|
|
1422
1557
|
})();
|
|
1423
1558
|
const hasCommits = (() => {
|
|
1424
1559
|
try {
|
|
1425
|
-
|
|
1560
|
+
execSync2("git rev-parse --verify HEAD", { cwd: dir, stdio: "pipe" });
|
|
1426
1561
|
return true;
|
|
1427
1562
|
} catch {
|
|
1428
1563
|
return false;
|
|
@@ -1432,7 +1567,7 @@ function getGitRepoStatus(dir) {
|
|
|
1432
1567
|
let untrackedCount = 0;
|
|
1433
1568
|
if (hasCommits) {
|
|
1434
1569
|
try {
|
|
1435
|
-
const porcelain =
|
|
1570
|
+
const porcelain = execSync2("git status --porcelain", { cwd: dir, encoding: "utf8", timeout: 5e3 }).trim();
|
|
1436
1571
|
isClean = porcelain.length === 0;
|
|
1437
1572
|
untrackedCount = porcelain.split("\n").filter((l) => l.startsWith("??")).length;
|
|
1438
1573
|
} catch {
|
|
@@ -1457,14 +1592,14 @@ function initializeGitRepoForWorktrees(dir) {
|
|
|
1457
1592
|
let status = getGitRepoStatus(dir);
|
|
1458
1593
|
if (!status.isGit) {
|
|
1459
1594
|
try {
|
|
1460
|
-
|
|
1595
|
+
execSync2("git init -b main", { cwd: dir, stdio: "pipe" });
|
|
1461
1596
|
} catch {
|
|
1462
|
-
|
|
1597
|
+
execSync2("git init", { cwd: dir, stdio: "pipe" });
|
|
1463
1598
|
}
|
|
1464
1599
|
status = getGitRepoStatus(dir);
|
|
1465
1600
|
}
|
|
1466
1601
|
if (!status.hasCommits) {
|
|
1467
|
-
|
|
1602
|
+
execSync2(
|
|
1468
1603
|
'git -c user.name="fifony" -c user.email="fifony@local.invalid" commit --allow-empty -m "Initial commit"',
|
|
1469
1604
|
{ cwd: dir, stdio: "pipe" }
|
|
1470
1605
|
);
|
|
@@ -1481,9 +1616,9 @@ function assertIssueHasGitWorktree(issue, action) {
|
|
|
1481
1616
|
}
|
|
1482
1617
|
function detectDefaultBranch(dir) {
|
|
1483
1618
|
try {
|
|
1484
|
-
const current =
|
|
1619
|
+
const current = execSync2("git rev-parse --abbrev-ref HEAD", { cwd: dir, encoding: "utf8" }).trim();
|
|
1485
1620
|
if (current && current !== "HEAD") return current;
|
|
1486
|
-
const remote =
|
|
1621
|
+
const remote = execSync2("git symbolic-ref refs/remotes/origin/HEAD", { cwd: dir, encoding: "utf8" }).trim();
|
|
1487
1622
|
return remote.replace("refs/remotes/origin/", "");
|
|
1488
1623
|
} catch {
|
|
1489
1624
|
return "main";
|
|
@@ -1493,11 +1628,11 @@ var CLI_CONFIG_DIRS = [".claude", ".codex", ".gemini"];
|
|
|
1493
1628
|
var CLI_CONFIG_FILES = ["CLAUDE.md"];
|
|
1494
1629
|
function copyCliConfigDirs(sourceRoot, worktreePath) {
|
|
1495
1630
|
for (const dir of CLI_CONFIG_DIRS) {
|
|
1496
|
-
const src =
|
|
1497
|
-
const dst =
|
|
1498
|
-
if (
|
|
1631
|
+
const src = join4(sourceRoot, dir);
|
|
1632
|
+
const dst = join4(worktreePath, dir);
|
|
1633
|
+
if (existsSync5(src) && statSync(src).isDirectory() && !existsSync5(dst)) {
|
|
1499
1634
|
try {
|
|
1500
|
-
|
|
1635
|
+
execSync2(`cp -R "${src}" "${dst}"`, { stdio: "pipe", timeout: 1e4 });
|
|
1501
1636
|
logger.debug({ dir, worktreePath }, "[Workspace] Copied CLI config dir to worktree");
|
|
1502
1637
|
} catch (err) {
|
|
1503
1638
|
logger.warn({ err: String(err), dir }, "[Workspace] Failed to copy CLI config dir");
|
|
@@ -1505,11 +1640,11 @@ function copyCliConfigDirs(sourceRoot, worktreePath) {
|
|
|
1505
1640
|
}
|
|
1506
1641
|
}
|
|
1507
1642
|
for (const file of CLI_CONFIG_FILES) {
|
|
1508
|
-
const src =
|
|
1509
|
-
const dst =
|
|
1510
|
-
if (
|
|
1643
|
+
const src = join4(sourceRoot, file);
|
|
1644
|
+
const dst = join4(worktreePath, file);
|
|
1645
|
+
if (existsSync5(src) && !existsSync5(dst)) {
|
|
1511
1646
|
try {
|
|
1512
|
-
|
|
1647
|
+
execSync2(`cp "${src}" "${dst}"`, { stdio: "pipe", timeout: 5e3 });
|
|
1513
1648
|
logger.debug({ file, worktreePath }, "[Workspace] Copied CLI config file to worktree");
|
|
1514
1649
|
} catch (err) {
|
|
1515
1650
|
logger.warn({ err: String(err), file }, "[Workspace] Failed to copy CLI config file");
|
|
@@ -1519,23 +1654,23 @@ function copyCliConfigDirs(sourceRoot, worktreePath) {
|
|
|
1519
1654
|
}
|
|
1520
1655
|
function isGitWorkingTree(dir) {
|
|
1521
1656
|
try {
|
|
1522
|
-
|
|
1657
|
+
execSync2("git rev-parse --git-dir", { cwd: dir, stdio: "pipe", timeout: 5e3 });
|
|
1523
1658
|
return true;
|
|
1524
1659
|
} catch {
|
|
1525
1660
|
return false;
|
|
1526
1661
|
}
|
|
1527
1662
|
}
|
|
1528
1663
|
function resolveTestWorkspacePath(issue) {
|
|
1529
|
-
const workspaceRoot = issue.workspacePath ??
|
|
1530
|
-
return
|
|
1664
|
+
const workspaceRoot = issue.workspacePath ?? join4(WORKSPACE_ROOT, idToSafePath(issue.id));
|
|
1665
|
+
return join4(workspaceRoot, "test-worktree");
|
|
1531
1666
|
}
|
|
1532
1667
|
function createTestWorkspace(issue) {
|
|
1533
1668
|
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "create isolated test workspaces");
|
|
1534
1669
|
assertIssueHasGitWorktree(issue, "create a test workspace");
|
|
1535
|
-
const workspaceRoot = issue.workspacePath ??
|
|
1670
|
+
const workspaceRoot = issue.workspacePath ?? join4(WORKSPACE_ROOT, idToSafePath(issue.id));
|
|
1536
1671
|
const testWorkspacePath = issue.testWorkspacePath ?? resolveTestWorkspacePath(issue);
|
|
1537
|
-
|
|
1538
|
-
if (
|
|
1672
|
+
mkdirSync3(workspaceRoot, { recursive: true });
|
|
1673
|
+
if (existsSync5(testWorkspacePath)) {
|
|
1539
1674
|
if (isGitWorkingTree(testWorkspacePath)) {
|
|
1540
1675
|
issue.testWorkspacePath = testWorkspacePath;
|
|
1541
1676
|
issue.testApplied = true;
|
|
@@ -1544,7 +1679,7 @@ function createTestWorkspace(issue) {
|
|
|
1544
1679
|
rmSync2(testWorkspacePath, { recursive: true, force: true });
|
|
1545
1680
|
}
|
|
1546
1681
|
try {
|
|
1547
|
-
|
|
1682
|
+
execSync2(`git worktree add --detach "${testWorkspacePath}" "${issue.branchName}"`, {
|
|
1548
1683
|
cwd: TARGET_ROOT,
|
|
1549
1684
|
stdio: "pipe",
|
|
1550
1685
|
timeout: 3e4
|
|
@@ -1564,7 +1699,7 @@ function removeTestWorkspace(issue) {
|
|
|
1564
1699
|
issue.testWorkspacePath = void 0;
|
|
1565
1700
|
if (!testWorkspacePath) return;
|
|
1566
1701
|
try {
|
|
1567
|
-
|
|
1702
|
+
execSync2(`git worktree remove --force "${testWorkspacePath}"`, {
|
|
1568
1703
|
cwd: TARGET_ROOT,
|
|
1569
1704
|
stdio: "pipe",
|
|
1570
1705
|
timeout: 3e4
|
|
@@ -1584,20 +1719,55 @@ async function createGitWorktree(issue, worktreePath, baseBranch) {
|
|
|
1584
1719
|
let headCommitAtStart = "";
|
|
1585
1720
|
const resolvedBaseBranch = baseBranch ?? detectDefaultBranch(TARGET_ROOT);
|
|
1586
1721
|
try {
|
|
1587
|
-
headCommitAtStart =
|
|
1722
|
+
headCommitAtStart = execSync2("git rev-parse HEAD", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
|
|
1588
1723
|
} catch {
|
|
1589
1724
|
}
|
|
1590
1725
|
const branchName = `fifony/${issue.id}`;
|
|
1591
|
-
|
|
1726
|
+
try {
|
|
1727
|
+
execSync2("git worktree prune", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
1728
|
+
} catch {
|
|
1729
|
+
}
|
|
1730
|
+
if (existsSync5(worktreePath)) {
|
|
1731
|
+
try {
|
|
1732
|
+
execSync2(`git worktree remove --force "${worktreePath}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
1733
|
+
} catch {
|
|
1734
|
+
rmSync2(worktreePath, { recursive: true, force: true });
|
|
1735
|
+
try {
|
|
1736
|
+
execSync2("git worktree prune", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
1737
|
+
} catch {
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
try {
|
|
1742
|
+
const wtList = execSync2("git worktree list --porcelain", { cwd: TARGET_ROOT, encoding: "utf8" });
|
|
1743
|
+
for (const block of wtList.split("\n\n").filter(Boolean)) {
|
|
1744
|
+
if (block.includes(`branch refs/heads/${branchName}`)) {
|
|
1745
|
+
const m = block.match(/^worktree (.+)$/m);
|
|
1746
|
+
if (m) {
|
|
1747
|
+
try {
|
|
1748
|
+
execSync2(`git worktree remove --force "${m[1]}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
1749
|
+
} catch {
|
|
1750
|
+
rmSync2(m[1], { recursive: true, force: true });
|
|
1751
|
+
try {
|
|
1752
|
+
execSync2("git worktree prune", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
1753
|
+
} catch {
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
} catch {
|
|
1760
|
+
}
|
|
1761
|
+
execSync2(`git worktree add "${worktreePath}" -B "${branchName}"`, {
|
|
1592
1762
|
cwd: TARGET_ROOT,
|
|
1593
1763
|
stdio: "pipe"
|
|
1594
1764
|
});
|
|
1595
1765
|
try {
|
|
1596
|
-
const gitFileContent = readFileSync3(
|
|
1766
|
+
const gitFileContent = readFileSync3(join4(worktreePath, ".git"), "utf8").trim();
|
|
1597
1767
|
const gitDirRel = gitFileContent.replace("gitdir: ", "").trim();
|
|
1598
1768
|
const gitDirPath = resolve(worktreePath, gitDirRel);
|
|
1599
|
-
|
|
1600
|
-
writeFileSync3(
|
|
1769
|
+
mkdirSync3(join4(gitDirPath, "info"), { recursive: true });
|
|
1770
|
+
writeFileSync3(join4(gitDirPath, "info", "exclude"), "fifony-*\n.fifony-*\nfifony_*\n", "utf8");
|
|
1601
1771
|
} catch (err) {
|
|
1602
1772
|
logger.warn({ err: String(err) }, "[Agent] Failed to write worktree excludes");
|
|
1603
1773
|
}
|
|
@@ -1610,15 +1780,15 @@ async function createGitWorktree(issue, worktreePath, baseBranch) {
|
|
|
1610
1780
|
}
|
|
1611
1781
|
async function prepareWorkspace(issue, state, defaultBranch) {
|
|
1612
1782
|
const safeId = idToSafePath(issue.id);
|
|
1613
|
-
const workspaceRoot =
|
|
1614
|
-
const worktreePath =
|
|
1615
|
-
const createdNow = !
|
|
1783
|
+
const workspaceRoot = join4(WORKSPACE_ROOT, safeId);
|
|
1784
|
+
const worktreePath = join4(workspaceRoot, "worktree");
|
|
1785
|
+
const createdNow = !existsSync5(worktreePath);
|
|
1616
1786
|
if (createdNow) {
|
|
1617
|
-
|
|
1787
|
+
mkdirSync3(workspaceRoot, { recursive: true });
|
|
1618
1788
|
logger.debug({ issueId: issue.id, identifier: issue.identifier, workspacePath: workspaceRoot }, "[Agent] Creating workspace");
|
|
1619
1789
|
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "execute issues");
|
|
1620
1790
|
if (state.config.afterCreateHook) {
|
|
1621
|
-
|
|
1791
|
+
mkdirSync3(worktreePath, { recursive: true });
|
|
1622
1792
|
await runHook(state.config.afterCreateHook, worktreePath, issue, "after_create");
|
|
1623
1793
|
} else {
|
|
1624
1794
|
await createGitWorktree(issue, worktreePath, defaultBranch);
|
|
@@ -1627,9 +1797,9 @@ async function prepareWorkspace(issue, state, defaultBranch) {
|
|
|
1627
1797
|
} else {
|
|
1628
1798
|
logger.debug({ issueId: issue.id, workspacePath: workspaceRoot }, "[Agent] Reusing existing workspace");
|
|
1629
1799
|
}
|
|
1630
|
-
const metaPath =
|
|
1800
|
+
const metaPath = join4(workspaceRoot, "issue.json");
|
|
1631
1801
|
const promptText = await buildPrompt(issue, null);
|
|
1632
|
-
const promptFile =
|
|
1802
|
+
const promptFile = join4(workspaceRoot, "prompt.md");
|
|
1633
1803
|
ensureWorkspaceMemoryFiles(issue, workspaceRoot);
|
|
1634
1804
|
if (createdNow) {
|
|
1635
1805
|
recordWorkspaceMemoryEvent(issue, workspaceRoot, {
|
|
@@ -1656,8 +1826,8 @@ async function prepareWorkspace(issue, state, defaultBranch) {
|
|
|
1656
1826
|
}
|
|
1657
1827
|
async function cleanWorkspace(issueId, issue, state) {
|
|
1658
1828
|
const safeId = idToSafePath(issueId);
|
|
1659
|
-
const workspacePath = issue?.workspacePath ??
|
|
1660
|
-
if (!
|
|
1829
|
+
const workspacePath = issue?.workspacePath ?? join4(WORKSPACE_ROOT, safeId);
|
|
1830
|
+
if (!existsSync5(workspacePath)) return;
|
|
1661
1831
|
if (state.config.beforeRemoveHook) {
|
|
1662
1832
|
try {
|
|
1663
1833
|
const dummyIssue = issue ?? { id: issueId, identifier: issueId };
|
|
@@ -1671,7 +1841,7 @@ async function cleanWorkspace(issueId, issue, state) {
|
|
|
1671
1841
|
}
|
|
1672
1842
|
if (issue?.branchName && issue.worktreePath) {
|
|
1673
1843
|
try {
|
|
1674
|
-
|
|
1844
|
+
execSync2(`git worktree remove --force "${issue.worktreePath}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
1675
1845
|
logger.info(`Removed worktree for ${issueId}: ${issue.worktreePath}`);
|
|
1676
1846
|
} catch (error) {
|
|
1677
1847
|
logger.warn(`Failed to remove worktree for ${issueId}: ${String(error)}`);
|
|
@@ -1681,7 +1851,7 @@ async function cleanWorkspace(issueId, issue, state) {
|
|
|
1681
1851
|
}
|
|
1682
1852
|
}
|
|
1683
1853
|
try {
|
|
1684
|
-
|
|
1854
|
+
execSync2(`git branch -D "${issue.branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
1685
1855
|
} catch {
|
|
1686
1856
|
}
|
|
1687
1857
|
try {
|
|
@@ -1700,7 +1870,7 @@ async function cleanWorkspace(issueId, issue, state) {
|
|
|
1700
1870
|
function inferChangedWorkspacePaths(_workspacePath, limit = 32, issue) {
|
|
1701
1871
|
if (!issue?.baseBranch || !issue.branchName) return [];
|
|
1702
1872
|
try {
|
|
1703
|
-
const output =
|
|
1873
|
+
const output = execSync2(
|
|
1704
1874
|
`git diff --name-only "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
1705
1875
|
{ cwd: TARGET_ROOT, encoding: "utf8", timeout: 1e4, stdio: "pipe" }
|
|
1706
1876
|
);
|
|
@@ -1714,7 +1884,7 @@ function computeDiffStats(issue) {
|
|
|
1714
1884
|
try {
|
|
1715
1885
|
let raw = "";
|
|
1716
1886
|
try {
|
|
1717
|
-
raw =
|
|
1887
|
+
raw = execSync2(
|
|
1718
1888
|
`git diff --stat "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
1719
1889
|
{ cwd: TARGET_ROOT, encoding: "utf8", maxBuffer: 512e3, timeout: 1e4, stdio: "pipe" }
|
|
1720
1890
|
);
|
|
@@ -1743,7 +1913,7 @@ function parseDiffStats(issue, raw) {
|
|
|
1743
1913
|
}
|
|
1744
1914
|
async function syncIssueDiffStatsToStore(issue) {
|
|
1745
1915
|
if (!issue?.id) return;
|
|
1746
|
-
const { getIssueStateResource } = await import("./store-
|
|
1916
|
+
const { getIssueStateResource } = await import("./store-S3NAYZ3S.js");
|
|
1747
1917
|
const issueResource = getIssueStateResource();
|
|
1748
1918
|
if (!issueResource) return;
|
|
1749
1919
|
const toNumber = (value) => {
|
|
@@ -1796,35 +1966,50 @@ async function syncIssueDiffStatsToStore(issue) {
|
|
|
1796
1966
|
function ensureWorktreeCommitted(issue) {
|
|
1797
1967
|
const worktreePath = issue.worktreePath;
|
|
1798
1968
|
if (!worktreePath || !issue.branchName) return;
|
|
1799
|
-
|
|
1800
|
-
const statusBeforeCommit =
|
|
1969
|
+
execSync2("git add -A", { cwd: worktreePath, stdio: "pipe" });
|
|
1970
|
+
const statusBeforeCommit = execSync2("git status --porcelain", { cwd: worktreePath, encoding: "utf8" }).trim();
|
|
1801
1971
|
if (!statusBeforeCommit) return;
|
|
1802
1972
|
try {
|
|
1803
|
-
|
|
1973
|
+
execSync2(`git commit -m "fifony: agent changes for ${issue.identifier}"`, { cwd: worktreePath, stdio: "pipe" });
|
|
1804
1974
|
} catch (error) {
|
|
1805
|
-
const remaining =
|
|
1975
|
+
const remaining = execSync2("git status --porcelain", { cwd: worktreePath, encoding: "utf8" }).trim();
|
|
1806
1976
|
if (remaining) {
|
|
1807
1977
|
throw new Error(`Failed to commit agent changes for ${issue.identifier}: ${String(error)}`);
|
|
1808
1978
|
}
|
|
1809
1979
|
}
|
|
1810
|
-
const statusAfterCommit =
|
|
1980
|
+
const statusAfterCommit = execSync2("git status --porcelain", { cwd: worktreePath, encoding: "utf8" }).trim();
|
|
1811
1981
|
if (statusAfterCommit) {
|
|
1812
1982
|
throw new Error(`Worktree for ${issue.identifier} still has uncommitted changes after commit.`);
|
|
1813
1983
|
}
|
|
1814
1984
|
}
|
|
1815
|
-
function
|
|
1985
|
+
function autoCommitTargetIfDirty() {
|
|
1986
|
+
const status = execSync2("git status --porcelain", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
|
|
1987
|
+
if (!status) return;
|
|
1988
|
+
try {
|
|
1989
|
+
execSync2("git add -A", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
1990
|
+
execSync2('git commit -m "chore: auto-commit uncommitted changes before fifony merge"', { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
1991
|
+
logger.info("[Workspace] Auto-committed dirty TARGET_ROOT before merge");
|
|
1992
|
+
} catch (err) {
|
|
1993
|
+
logger.warn({ err: String(err) }, "[Workspace] Failed to auto-commit TARGET_ROOT");
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
function mergeWorktree(issue, abortOnConflict = true, autoCommit = true) {
|
|
1816
1997
|
const result = { copied: [], deleted: [], skipped: [], conflicts: [] };
|
|
1817
1998
|
ensureWorktreeCommitted(issue);
|
|
1818
|
-
const currentBranch =
|
|
1999
|
+
const currentBranch = execSync2("git rev-parse --abbrev-ref HEAD", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
|
|
1819
2000
|
if (currentBranch !== issue.baseBranch) {
|
|
1820
2001
|
throw new Error(`Cannot merge ${issue.identifier}: current branch is ${currentBranch}, expected ${issue.baseBranch}.`);
|
|
1821
2002
|
}
|
|
1822
|
-
const targetStatus =
|
|
2003
|
+
const targetStatus = execSync2("git status --porcelain", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
|
|
1823
2004
|
if (targetStatus) {
|
|
1824
|
-
|
|
2005
|
+
if (autoCommit) {
|
|
2006
|
+
autoCommitTargetIfDirty();
|
|
2007
|
+
} else {
|
|
2008
|
+
throw new Error(`Cannot merge ${issue.identifier}: target repository has uncommitted changes. Enable 'Auto-commit before merge' in Settings or commit manually.`);
|
|
2009
|
+
}
|
|
1825
2010
|
}
|
|
1826
2011
|
try {
|
|
1827
|
-
const diffOut =
|
|
2012
|
+
const diffOut = execSync2(
|
|
1828
2013
|
`git diff --name-status "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
1829
2014
|
{ cwd: TARGET_ROOT, encoding: "utf8" }
|
|
1830
2015
|
);
|
|
@@ -1837,13 +2022,13 @@ function mergeWorktree(issue, abortOnConflict = true) {
|
|
|
1837
2022
|
} catch {
|
|
1838
2023
|
}
|
|
1839
2024
|
try {
|
|
1840
|
-
|
|
2025
|
+
execSync2(
|
|
1841
2026
|
`git merge --no-ff "${issue.branchName}" -m "fifony: merge ${issue.identifier}"`,
|
|
1842
2027
|
{ cwd: TARGET_ROOT, stdio: "pipe" }
|
|
1843
2028
|
);
|
|
1844
2029
|
} catch (err) {
|
|
1845
2030
|
try {
|
|
1846
|
-
const conflictOut =
|
|
2031
|
+
const conflictOut = execSync2(
|
|
1847
2032
|
"git diff --name-only --diff-filter=U",
|
|
1848
2033
|
{ cwd: TARGET_ROOT, encoding: "utf8" }
|
|
1849
2034
|
);
|
|
@@ -1852,7 +2037,7 @@ function mergeWorktree(issue, abortOnConflict = true) {
|
|
|
1852
2037
|
}
|
|
1853
2038
|
if (abortOnConflict) {
|
|
1854
2039
|
try {
|
|
1855
|
-
|
|
2040
|
+
execSync2("git merge --abort", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
1856
2041
|
} catch {
|
|
1857
2042
|
}
|
|
1858
2043
|
logger.warn({ issueId: issue.id, err: String(err) }, "[Agent] Git merge failed, aborted");
|
|
@@ -1870,34 +2055,38 @@ function shouldSkipMergePath(relativePath) {
|
|
|
1870
2055
|
const base = parts.at(-1) ?? "";
|
|
1871
2056
|
return base === "WORKFLOW.local.md" || base === ".fifony-env.sh" || base === ".fifony-compiled-env.sh" || base === ".fifony-local-source-ready" || base.startsWith("fifony-") || base.startsWith("fifony_");
|
|
1872
2057
|
}
|
|
1873
|
-
function mergeWorkspace(issue, abortOnConflict = true) {
|
|
2058
|
+
function mergeWorkspace(issue, abortOnConflict = true, autoCommit = true) {
|
|
1874
2059
|
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "merge issues");
|
|
1875
2060
|
assertIssueHasGitWorktree(issue, "merge");
|
|
1876
|
-
return mergeWorktree(issue, abortOnConflict);
|
|
2061
|
+
return mergeWorktree(issue, abortOnConflict, autoCommit);
|
|
1877
2062
|
}
|
|
1878
|
-
function dryMerge(issue) {
|
|
2063
|
+
function dryMerge(issue, autoCommit = true) {
|
|
1879
2064
|
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "preview merges");
|
|
1880
2065
|
assertIssueHasGitWorktree(issue, "preview merge");
|
|
1881
2066
|
ensureWorktreeCommitted(issue);
|
|
1882
|
-
const currentBranch =
|
|
2067
|
+
const currentBranch = execSync2("git rev-parse --abbrev-ref HEAD", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
|
|
1883
2068
|
if (currentBranch !== issue.baseBranch) {
|
|
1884
2069
|
throw new Error(`Cannot preview merge: current branch is ${currentBranch}, expected ${issue.baseBranch}.`);
|
|
1885
2070
|
}
|
|
1886
|
-
const targetStatus =
|
|
2071
|
+
const targetStatus = execSync2("git status --porcelain", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
|
|
1887
2072
|
if (targetStatus) {
|
|
1888
|
-
|
|
2073
|
+
if (autoCommit) {
|
|
2074
|
+
autoCommitTargetIfDirty();
|
|
2075
|
+
} else {
|
|
2076
|
+
throw new Error(`Cannot preview merge: target repository has uncommitted changes. Enable 'Auto-commit before merge' in Settings or commit manually.`);
|
|
2077
|
+
}
|
|
1889
2078
|
}
|
|
1890
2079
|
let conflictFiles = [];
|
|
1891
2080
|
let willConflict = false;
|
|
1892
2081
|
try {
|
|
1893
|
-
|
|
2082
|
+
execSync2(
|
|
1894
2083
|
`git merge --no-commit --no-ff "${issue.branchName}"`,
|
|
1895
2084
|
{ cwd: TARGET_ROOT, stdio: "pipe" }
|
|
1896
2085
|
);
|
|
1897
2086
|
} catch {
|
|
1898
2087
|
willConflict = true;
|
|
1899
2088
|
try {
|
|
1900
|
-
const conflictOut =
|
|
2089
|
+
const conflictOut = execSync2(
|
|
1901
2090
|
"git diff --name-only --diff-filter=U",
|
|
1902
2091
|
{ cwd: TARGET_ROOT, encoding: "utf8" }
|
|
1903
2092
|
);
|
|
@@ -1906,13 +2095,13 @@ function dryMerge(issue) {
|
|
|
1906
2095
|
}
|
|
1907
2096
|
}
|
|
1908
2097
|
try {
|
|
1909
|
-
|
|
2098
|
+
execSync2("git merge --abort", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
1910
2099
|
} catch {
|
|
1911
2100
|
try {
|
|
1912
|
-
|
|
2101
|
+
execSync2("git reset --merge ORIG_HEAD", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
1913
2102
|
} catch {
|
|
1914
2103
|
try {
|
|
1915
|
-
|
|
2104
|
+
execSync2("git reset --merge", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
1916
2105
|
} catch (error) {
|
|
1917
2106
|
logger.warn({ issueId: issue.id, err: String(error) }, "[Workspace] Failed to safely clean dry-merge state");
|
|
1918
2107
|
}
|
|
@@ -1920,7 +2109,7 @@ function dryMerge(issue) {
|
|
|
1920
2109
|
}
|
|
1921
2110
|
let changedFiles = 0;
|
|
1922
2111
|
try {
|
|
1923
|
-
const diffOut =
|
|
2112
|
+
const diffOut = execSync2(
|
|
1924
2113
|
`git diff --name-only "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
1925
2114
|
{ cwd: TARGET_ROOT, encoding: "utf8" }
|
|
1926
2115
|
);
|
|
@@ -1934,7 +2123,7 @@ function rebaseWorktree(issue) {
|
|
|
1934
2123
|
assertIssueHasGitWorktree(issue, "rebase");
|
|
1935
2124
|
ensureWorktreeCommitted(issue);
|
|
1936
2125
|
try {
|
|
1937
|
-
|
|
2126
|
+
execSync2(
|
|
1938
2127
|
`git rebase "${issue.baseBranch}"`,
|
|
1939
2128
|
{ cwd: issue.worktreePath, stdio: "pipe" }
|
|
1940
2129
|
);
|
|
@@ -1942,7 +2131,7 @@ function rebaseWorktree(issue) {
|
|
|
1942
2131
|
} catch {
|
|
1943
2132
|
let conflictFiles = [];
|
|
1944
2133
|
try {
|
|
1945
|
-
const conflictOut =
|
|
2134
|
+
const conflictOut = execSync2(
|
|
1946
2135
|
"git diff --name-only --diff-filter=U",
|
|
1947
2136
|
{ cwd: issue.worktreePath, encoding: "utf8" }
|
|
1948
2137
|
);
|
|
@@ -1950,7 +2139,7 @@ function rebaseWorktree(issue) {
|
|
|
1950
2139
|
} catch {
|
|
1951
2140
|
}
|
|
1952
2141
|
try {
|
|
1953
|
-
|
|
2142
|
+
execSync2("git rebase --abort", { cwd: issue.worktreePath, stdio: "pipe" });
|
|
1954
2143
|
} catch {
|
|
1955
2144
|
}
|
|
1956
2145
|
return { success: false, conflictFiles };
|
|
@@ -1963,11 +2152,11 @@ function hydrateIssuePathsFromWorkspace(issue) {
|
|
|
1963
2152
|
return inferredPaths;
|
|
1964
2153
|
}
|
|
1965
2154
|
function writeVersionedArtifacts(workspacePath, prefix, planVersion, attempt, sources) {
|
|
1966
|
-
const { writeFileSync: _wfs, readFileSync: _rfs, existsSync: _es } = { writeFileSync: writeFileSync3, readFileSync: readFileSync3, existsSync:
|
|
2155
|
+
const { writeFileSync: _wfs, readFileSync: _rfs, existsSync: _es } = { writeFileSync: writeFileSync3, readFileSync: readFileSync3, existsSync: existsSync5 };
|
|
1967
2156
|
for (const { srcFile, destSuffix } of sources) {
|
|
1968
|
-
const src =
|
|
2157
|
+
const src = join4(workspacePath, srcFile);
|
|
1969
2158
|
if (_es(src)) {
|
|
1970
|
-
_wfs(
|
|
2159
|
+
_wfs(join4(workspacePath, `${prefix}.v${planVersion}a${attempt}.${destSuffix}`), _rfs(src, "utf8"), "utf8");
|
|
1971
2160
|
}
|
|
1972
2161
|
}
|
|
1973
2162
|
}
|
|
@@ -2013,4 +2202,4 @@ export {
|
|
|
2013
2202
|
hydrateIssuePathsFromWorkspace,
|
|
2014
2203
|
writeVersionedArtifacts
|
|
2015
2204
|
};
|
|
2016
|
-
//# sourceMappingURL=chunk-
|
|
2205
|
+
//# sourceMappingURL=chunk-QH6VCTET.js.map
|