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.
Files changed (65) hide show
  1. package/app/dist/assets/{CommandPalette-M4VAMxCU.js → CommandPalette-CL8p78lG.js} +1 -1
  2. package/app/dist/assets/{KeyboardShortcutsHelp-DkvPUXQq.js → KeyboardShortcutsHelp-CqEFfGcE.js} +1 -1
  3. package/app/dist/assets/OnboardingWizard-BmI50ZUv.js +1 -0
  4. package/app/dist/assets/analytics.lazy-CXGjZabc.js +1 -0
  5. package/app/dist/assets/{api-CkVfYg_m.js → api-CEr_D4e5.js} +1 -1
  6. package/app/dist/assets/{createLucideIcon-Dfk_Hxud.js → createLucideIcon-luywpIq4.js} +1 -1
  7. package/app/dist/assets/index-CEaccpYh.js +96 -0
  8. package/app/dist/assets/index-CzzWGzux.css +1 -0
  9. package/app/dist/assets/vendor-uqBx3VSC.js +9 -0
  10. package/app/dist/index.html +12 -12
  11. package/app/dist/service-worker.js +15 -5
  12. package/dist/agent/pty-daemon.js +3 -2
  13. package/dist/agent/run-local.js +71 -52
  14. package/dist/{agent-RMQTTUEC.js → agent-DFSFG6DG.js} +18 -12
  15. package/dist/{analytics-broadcaster-O6YBP66L.js → analytics-broadcaster-O4AE3RUK.js} +21 -14
  16. package/dist/approve-plan.command-QGQZZXTQ.js +17 -0
  17. package/dist/{chunk-E2EWEYA4.js → chunk-2PRRKBG6.js} +20 -10
  18. package/dist/chunk-5AMWD66T.js +38 -0
  19. package/dist/{chunk-QQQLP3PL.js → chunk-7TXZYZR5.js} +9 -37
  20. package/dist/chunk-AAVROEQC.js +859 -0
  21. package/dist/{chunk-ESWHDHH6.js → chunk-AAZKYWOY.js} +4 -4
  22. package/dist/chunk-EBCSQFPR.js +682 -0
  23. package/dist/{chunk-BRSR26VK.js → chunk-FH7HUPZX.js} +2 -2
  24. package/dist/chunk-HOIOVUHI.js +35 -0
  25. package/dist/{chunk-AILXZ2TD.js → chunk-JRLWLZOD.js} +20 -13
  26. package/dist/{chunk-YRSH2CLW.js → chunk-K36BWMUV.js} +1741 -1216
  27. package/dist/chunk-N4KFNX2G.js +370 -0
  28. package/dist/chunk-PACI3T4I.js +125 -0
  29. package/dist/{chunk-FJNH3G2Z.js → chunk-PI7Y77R3.js} +38 -663
  30. package/dist/{chunk-DVU3CXWA.js → chunk-PXTIWKLQ.js} +2 -1
  31. package/dist/{chunk-SOBLO4YZ.js → chunk-QH6VCTET.js} +316 -127
  32. package/dist/{chunk-MVTGAKQK.js → chunk-QHISYRXJ.js} +2 -2
  33. package/dist/{chunk-42AMQAJG.js → chunk-VM5QAYP5.js} +2 -2
  34. package/dist/cli.js +17 -11
  35. package/dist/create-issue.command-VAKYRECC.js +24 -0
  36. package/dist/{fsm-issue-YGGF7SIL.js → fsm-issue-EHTSKMFN.js} +9 -8
  37. package/dist/fsm-service-7O4AJG2R.js +32 -0
  38. package/dist/{helpers-L7NYO5XS.js → helpers-ON2S7UEF.js} +2 -2
  39. package/dist/{issue-log-broadcaster-WZAHISYB.js → issue-log-broadcaster-FZGVEEIX.js} +20 -13
  40. package/dist/{issues-3QRR7KM6.js → issues-3YNNTB4U.js} +10 -7
  41. package/dist/{log-analyzer-K7MXQB4T.js → log-analyzer-EIX6R6PP.js} +82 -18
  42. package/dist/logger-IFLXTQPS.js +11 -0
  43. package/dist/mcp/server.js +2 -2
  44. package/dist/merge-workspace.command-T2NIGR4M.js +24 -0
  45. package/dist/{parallel-executor-6INE6NDO.js → parallel-executor-DWESCNX3.js} +20 -14
  46. package/dist/queue-workers-V57BYXAY.js +38 -0
  47. package/dist/replan-issue.command-2GQ3QXCR.js +17 -0
  48. package/dist/retry-issue.command-GJBUUYDJ.js +17 -0
  49. package/dist/scheduler-KYILMWLD.js +32 -0
  50. package/dist/{settings-ZAWDCFP2.js → settings-SOTIS6ZD.js} +32 -12
  51. package/dist/settings.resource-JMD3JQOS.js +30 -0
  52. package/dist/{store-M6NCKMZY.js → store-S3NAYZ3S.js} +18 -12
  53. package/dist/{web-push-AX5IIK3P.js → web-push-QCTLS7EJ.js} +3 -3
  54. package/dist/websocket-T2Y3BY4B.js +61 -0
  55. package/dist/{workspace-CJTWFWTJ.js → workspace-OS7GPMCN.js} +7 -6
  56. package/package.json +8 -5
  57. package/app/dist/assets/OnboardingWizard-B7V9hoCR.js +0 -1
  58. package/app/dist/assets/analytics.lazy-zVJdF880.js +0 -1
  59. package/app/dist/assets/index-BpiCi7Ew.css +0 -1
  60. package/app/dist/assets/index-D2INW0zc.js +0 -47
  61. package/app/dist/assets/vendor-BEoYbFV1.js +0 -9
  62. package/dist/queue-workers-XFZK3TT5.js +0 -32
  63. package/dist/replan-issue.command-4UCWYHGZ.js +0 -15
  64. package/dist/scheduler-ZP7GOZDW.js +0 -26
  65. package/dist/settings.resource-5CW456AZ.js +0 -24
@@ -1,13 +1,10 @@
1
1
  import {
2
2
  markIssueDirty,
3
3
  normalizeAgentProvider
4
- } from "./chunk-FJNH3G2Z.js";
5
- import {
6
- logger
7
- } from "./chunk-DVU3CXWA.js";
4
+ } from "./chunk-PI7Y77R3.js";
8
5
  import {
9
6
  renderPrompt
10
- } from "./chunk-ESWHDHH6.js";
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-42AMQAJG.js";
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 existsSync4,
24
- mkdirSync as mkdirSync2,
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 join3, resolve } from "path";
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 existsSync2,
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 = join(pkgRoot, "dist", "agent", "pty-daemon.js");
123
- if (existsSync2(compiled)) {
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 = join(pkgRoot, "src", "agents", "pty-daemon.ts");
127
- if (existsSync2(source)) {
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 (existsSync2(socketPath)) return true;
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 = join(workspacePath, `${key.toLowerCase()}.txt`);
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 = join(workspacePath, ".env.sh");
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 = join(workspacePath, "live-output.log");
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 = join(workspacePath, "agent.sock");
226
- if (existsSync2(socketPath)) {
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: workspacePath
385
+ cwd: effectiveCwd
252
386
  });
253
387
  daemonProcess.unref();
254
- logger.debug({ issueId: issue.id, daemonPid: daemonProcess.pid, command: command.slice(0, 120), cwd: workspacePath }, "[Agent] PTY daemon spawned");
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: workspacePath,
414
+ cwd: ptyEffectiveCwd,
280
415
  env: process.env
281
416
  });
282
417
  const pid = ptyProcess.pid;
283
- const pidFile = join(workspacePath, "agent.pid");
418
+ const pidFile = join2(workspacePath, "agent.pid");
284
419
  if (pid) {
285
- logger.debug({ issueId: issue.id, pid, command: command.slice(0, 120), cwd: workspacePath }, "[Agent] Process spawned (PTY)");
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 = join(workspacePath, "agent.pid");
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 = join(workspacePath, "daemon.exit.json");
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 = join(workspacePath, "agent.sock");
655
- if (!existsSync2(socketPath)) return;
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 existsSync3,
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 join2 } from "path";
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 = join2(workspacePath, MEMORY_DIRNAME);
1110
+ const memoryDir = join3(workspacePath, MEMORY_DIRNAME);
976
1111
  return {
977
1112
  root: workspacePath,
978
1113
  memoryDir,
979
- workflowFile: join2(workspacePath, WORKFLOW_FILE),
980
- memoryFile: join2(workspacePath, MEMORY_FILE),
981
- heartbeatFile: join2(workspacePath, HEARTBEAT_FILE),
982
- dailyFile: join2(memoryDir, `${date}.md`)
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 existsSync3(filePath) ? readFileSync2(filePath, "utf8") : "";
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 (existsSync3(filePath)) return false;
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 (!existsSync3(memoryDir)) return [];
1189
- return readdirSync(memoryDir).filter((entry) => entry.endsWith(".md")).sort((left, right) => right.localeCompare(left)).slice(0, 3).map((entry) => join2(memoryDir, 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
- mkdirSync(paths.root, { recursive: true });
1194
- mkdirSync(paths.memoryDir, { recursive: true });
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 (!existsSync3(paths.memoryFile)) {
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 (existsSync4(SOURCE_MARKER)) return;
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
- mkdirSync2(target, { recursive: true });
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
- mkdirSync2(SOURCE_ROOT, { recursive: true });
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 (existsSync4(SOURCE_MARKER)) {
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
- execSync("git rev-parse --git-dir", { cwd: dir, stdio: "pipe" });
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 execSync("git symbolic-ref --short HEAD", { cwd: dir, encoding: "utf8", stdio: "pipe" }).trim() || null;
1549
+ return execSync2("git symbolic-ref --short HEAD", { cwd: dir, encoding: "utf8", stdio: "pipe" }).trim() || null;
1415
1550
  } catch {
1416
1551
  try {
1417
- return execSync("git rev-parse --abbrev-ref HEAD", { cwd: dir, encoding: "utf8", stdio: "pipe" }).trim() || null;
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
- execSync("git rev-parse --verify HEAD", { cwd: dir, stdio: "pipe" });
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 = execSync("git status --porcelain", { cwd: dir, encoding: "utf8", timeout: 5e3 }).trim();
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
- execSync("git init -b main", { cwd: dir, stdio: "pipe" });
1595
+ execSync2("git init -b main", { cwd: dir, stdio: "pipe" });
1461
1596
  } catch {
1462
- execSync("git init", { cwd: dir, stdio: "pipe" });
1597
+ execSync2("git init", { cwd: dir, stdio: "pipe" });
1463
1598
  }
1464
1599
  status = getGitRepoStatus(dir);
1465
1600
  }
1466
1601
  if (!status.hasCommits) {
1467
- execSync(
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 = execSync("git rev-parse --abbrev-ref HEAD", { cwd: dir, encoding: "utf8" }).trim();
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 = execSync("git symbolic-ref refs/remotes/origin/HEAD", { cwd: dir, encoding: "utf8" }).trim();
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 = join3(sourceRoot, dir);
1497
- const dst = join3(worktreePath, dir);
1498
- if (existsSync4(src) && statSync(src).isDirectory() && !existsSync4(dst)) {
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
- execSync(`cp -R "${src}" "${dst}"`, { stdio: "pipe", timeout: 1e4 });
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 = join3(sourceRoot, file);
1509
- const dst = join3(worktreePath, file);
1510
- if (existsSync4(src) && !existsSync4(dst)) {
1643
+ const src = join4(sourceRoot, file);
1644
+ const dst = join4(worktreePath, file);
1645
+ if (existsSync5(src) && !existsSync5(dst)) {
1511
1646
  try {
1512
- execSync(`cp "${src}" "${dst}"`, { stdio: "pipe", timeout: 5e3 });
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
- execSync("git rev-parse --git-dir", { cwd: dir, stdio: "pipe", timeout: 5e3 });
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 ?? join3(WORKSPACE_ROOT, idToSafePath(issue.id));
1530
- return join3(workspaceRoot, "test-worktree");
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 ?? join3(WORKSPACE_ROOT, idToSafePath(issue.id));
1670
+ const workspaceRoot = issue.workspacePath ?? join4(WORKSPACE_ROOT, idToSafePath(issue.id));
1536
1671
  const testWorkspacePath = issue.testWorkspacePath ?? resolveTestWorkspacePath(issue);
1537
- mkdirSync2(workspaceRoot, { recursive: true });
1538
- if (existsSync4(testWorkspacePath)) {
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
- execSync(`git worktree add --detach "${testWorkspacePath}" "${issue.branchName}"`, {
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
- execSync(`git worktree remove --force "${testWorkspacePath}"`, {
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 = execSync("git rev-parse HEAD", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
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
- execSync(`git worktree add "${worktreePath}" -B "${branchName}"`, {
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(join3(worktreePath, ".git"), "utf8").trim();
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
- mkdirSync2(join3(gitDirPath, "info"), { recursive: true });
1600
- writeFileSync3(join3(gitDirPath, "info", "exclude"), "fifony-*\n.fifony-*\nfifony_*\n", "utf8");
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 = join3(WORKSPACE_ROOT, safeId);
1614
- const worktreePath = join3(workspaceRoot, "worktree");
1615
- const createdNow = !existsSync4(worktreePath);
1783
+ const workspaceRoot = join4(WORKSPACE_ROOT, safeId);
1784
+ const worktreePath = join4(workspaceRoot, "worktree");
1785
+ const createdNow = !existsSync5(worktreePath);
1616
1786
  if (createdNow) {
1617
- mkdirSync2(workspaceRoot, { recursive: true });
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
- mkdirSync2(worktreePath, { recursive: true });
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 = join3(workspaceRoot, "issue.json");
1800
+ const metaPath = join4(workspaceRoot, "issue.json");
1631
1801
  const promptText = await buildPrompt(issue, null);
1632
- const promptFile = join3(workspaceRoot, "prompt.md");
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 ?? join3(WORKSPACE_ROOT, safeId);
1660
- if (!existsSync4(workspacePath)) return;
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
- execSync(`git worktree remove --force "${issue.worktreePath}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
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
- execSync(`git branch -D "${issue.branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
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 = execSync(
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 = execSync(
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-M6NCKMZY.js");
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
- execSync("git add -A", { cwd: worktreePath, stdio: "pipe" });
1800
- const statusBeforeCommit = execSync("git status --porcelain", { cwd: worktreePath, encoding: "utf8" }).trim();
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
- execSync(`git commit -m "fifony: agent changes for ${issue.identifier}"`, { cwd: worktreePath, stdio: "pipe" });
1973
+ execSync2(`git commit -m "fifony: agent changes for ${issue.identifier}"`, { cwd: worktreePath, stdio: "pipe" });
1804
1974
  } catch (error) {
1805
- const remaining = execSync("git status --porcelain", { cwd: worktreePath, encoding: "utf8" }).trim();
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 = execSync("git status --porcelain", { cwd: worktreePath, encoding: "utf8" }).trim();
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 mergeWorktree(issue, abortOnConflict = true) {
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 = execSync("git rev-parse --abbrev-ref HEAD", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
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 = execSync("git status --porcelain", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
2003
+ const targetStatus = execSync2("git status --porcelain", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
1823
2004
  if (targetStatus) {
1824
- throw new Error(`Cannot merge ${issue.identifier}: target repository has uncommitted changes.`);
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 = execSync(
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
- execSync(
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 = execSync(
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
- execSync("git merge --abort", { cwd: TARGET_ROOT, stdio: "pipe" });
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 = execSync("git rev-parse --abbrev-ref HEAD", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
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 = execSync("git status --porcelain", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
2071
+ const targetStatus = execSync2("git status --porcelain", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
1887
2072
  if (targetStatus) {
1888
- throw new Error(`Cannot preview merge: target repository has uncommitted changes.`);
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
- execSync(
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 = execSync(
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
- execSync("git merge --abort", { cwd: TARGET_ROOT, stdio: "pipe" });
2098
+ execSync2("git merge --abort", { cwd: TARGET_ROOT, stdio: "pipe" });
1910
2099
  } catch {
1911
2100
  try {
1912
- execSync("git reset --merge ORIG_HEAD", { cwd: TARGET_ROOT, stdio: "pipe" });
2101
+ execSync2("git reset --merge ORIG_HEAD", { cwd: TARGET_ROOT, stdio: "pipe" });
1913
2102
  } catch {
1914
2103
  try {
1915
- execSync("git reset --merge", { cwd: TARGET_ROOT, stdio: "pipe" });
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 = execSync(
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
- execSync(
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 = execSync(
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
- execSync("git rebase --abort", { cwd: issue.worktreePath, stdio: "pipe" });
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: existsSync4 };
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 = join3(workspacePath, srcFile);
2157
+ const src = join4(workspacePath, srcFile);
1969
2158
  if (_es(src)) {
1970
- _wfs(join3(workspacePath, `${prefix}.v${planVersion}a${attempt}.${destSuffix}`), _rfs(src, "utf8"), "utf8");
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-SOBLO4YZ.js.map
2205
+ //# sourceMappingURL=chunk-QH6VCTET.js.map