fifony 0.1.27 → 0.1.28

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 (35) hide show
  1. package/README.md +51 -29
  2. package/app/dist/assets/{KeyboardShortcutsHelp-NmaeCZMn.js → KeyboardShortcutsHelp-BrI56bfa.js} +1 -1
  3. package/app/dist/assets/OnboardingWizard-7MvouAkN.js +1 -0
  4. package/app/dist/assets/{analytics.lazy-BpH26eA2.js → analytics.lazy-D99c8M-T.js} +1 -1
  5. package/app/dist/assets/{createLucideIcon-BWC-guQt.js → createLucideIcon-DgMTp0yx.js} +1 -1
  6. package/app/dist/assets/index-DHHTOl-9.js +45 -0
  7. package/app/dist/assets/{index-DntTEHv8.css → index-ZlyvZ7KI.css} +1 -1
  8. package/app/dist/assets/vendor-D-IqxHHu.js +9 -0
  9. package/app/dist/index.html +4 -4
  10. package/app/dist/service-worker.js +1 -1
  11. package/dist/agent/run-local.js +64 -144
  12. package/dist/agent-FPUYBJZD.js +74 -0
  13. package/dist/chunk-2G6SRDOC.js +847 -0
  14. package/dist/{chunk-G7W4NEOA.js → chunk-3FCJI2GK.js} +1232 -633
  15. package/dist/chunk-O5AEQXUV.js +311 -0
  16. package/dist/chunk-OONOOWNC.js +123 -0
  17. package/dist/chunk-VOQT7RVT.js +295 -0
  18. package/dist/{chunk-XN2QKKMY.js → chunk-XVF6GOVS.js} +456 -814
  19. package/dist/cli.js +6 -4
  20. package/dist/issue-runner-MRHO5ZAB.js +15 -0
  21. package/dist/{issue-state-machine-SKODQ6MG.js → issue-state-machine-V2KPUYPW.js} +5 -3
  22. package/dist/issues-3PUMY63N.js +40 -0
  23. package/dist/mcp/server.js +23 -121
  24. package/dist/queue-workers-EGHCDDLB.js +23 -0
  25. package/dist/scheduler-V4GMCBTE.js +21 -0
  26. package/dist/{store-366NGWR4.js → store-RVKQ6UEY.js} +7 -5
  27. package/dist/workspace-KEHFITYR.js +52 -0
  28. package/package.json +6 -6
  29. package/app/dist/assets/OnboardingWizard-CwW6b_X4.js +0 -1
  30. package/app/dist/assets/index-D6jtlB7h.js +0 -43
  31. package/app/dist/assets/vendor-BTlTWMUF.js +0 -9
  32. package/dist/chunk-AMOGDOM7.js +0 -796
  33. package/dist/chunk-MT3S55TM.js +0 -91
  34. package/dist/issue-runner-MTAIYNVN.js +0 -13
  35. package/dist/queue-workers-Q3IWRFLI.js +0 -20
@@ -1,36 +1,31 @@
1
1
  import {
2
- S3DB_ISSUE_RESOURCE,
3
- SOURCE_MARKER,
4
- SOURCE_ROOT,
5
- TARGET_ROOT,
6
- TERMINAL_STATES,
7
- WORKSPACE_ROOT,
8
2
  appendFileTail,
9
3
  idToSafePath,
10
- inferCapabilityPaths,
11
- isoWeek,
12
- mergeCapabilityProviders,
13
4
  now,
14
- renderPrompt,
15
- resolveTaskCapabilities
16
- } from "./chunk-AMOGDOM7.js";
5
+ renderPrompt
6
+ } from "./chunk-O5AEQXUV.js";
17
7
  import {
18
8
  logger
19
9
  } from "./chunk-DVU3CXWA.js";
10
+ import {
11
+ SOURCE_MARKER,
12
+ SOURCE_ROOT,
13
+ TARGET_ROOT,
14
+ WORKSPACE_ROOT
15
+ } from "./chunk-OONOOWNC.js";
20
16
 
21
17
  // src/domains/workspace.ts
22
18
  import {
23
- cpSync,
24
- existsSync as existsSync6,
19
+ existsSync as existsSync7,
25
20
  mkdirSync,
26
21
  readdirSync,
27
- readFileSync as readFileSync3,
22
+ readFileSync as readFileSync4,
28
23
  rmSync as rmSync2,
29
24
  statSync,
30
25
  writeFileSync as writeFileSync2
31
26
  } from "fs";
32
27
  import { copyFile, mkdir, readdir, stat, writeFile } from "fs/promises";
33
- import { extname, join as join7, resolve } from "path";
28
+ import { extname as extname2, join as join7, resolve } from "path";
34
29
  import { execSync } from "child_process";
35
30
 
36
31
  // src/agents/command-executor.ts
@@ -45,15 +40,17 @@ import { spawn } from "child_process";
45
40
 
46
41
  // src/agents/providers.ts
47
42
  import { execFileSync as execFileSync2 } from "child_process";
48
- import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
43
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
49
44
  import { join as join5 } from "path";
50
45
  import { homedir as homedir2 } from "os";
51
46
 
52
47
  // src/agents/adapters/claude.ts
53
- import { existsSync } from "fs";
48
+ import { existsSync as existsSync2 } from "fs";
54
49
  import { join } from "path";
55
50
 
56
51
  // src/agents/adapters/shared.ts
52
+ import { existsSync, readFileSync } from "fs";
53
+ import { basename, extname } from "path";
57
54
  function buildPlanContextSection(plan) {
58
55
  const parts = ["## Plan Context", "", `**Summary:** ${plan.summary}`];
59
56
  if (plan.assumptions?.length) {
@@ -126,17 +123,17 @@ function buildValidationSection(plan) {
126
123
  return parts.join("\n");
127
124
  }
128
125
  function buildToolingSection(plan) {
129
- const td = plan.toolingDecision;
130
- if (!td) return "";
131
- const parts = ["## Tooling & Delegation Strategy"];
132
- if (td.decisionSummary) parts.push("", td.decisionSummary);
133
- if (td.shouldUseSkills && td.skillsToUse?.length) {
126
+ const skills = plan.suggestedSkills ?? [];
127
+ const agents = plan.suggestedAgents ?? [];
128
+ if (skills.length === 0 && agents.length === 0) return "";
129
+ const parts = ["## Recommended Skills & Agents"];
130
+ if (skills.length > 0) {
134
131
  parts.push("", "**Skills to activate:**");
135
- td.skillsToUse.forEach((s) => parts.push(`- **${s.name}**: ${s.why}`));
132
+ skills.forEach((s) => parts.push(`- ${s}`));
136
133
  }
137
- if (td.shouldUseSubagents && td.subagentsToUse?.length) {
138
- parts.push("", "**Subagents to use:**");
139
- td.subagentsToUse.forEach((a) => parts.push(`- **${a.name}** (${a.role}): ${a.why}`));
134
+ if (agents.length > 0) {
135
+ parts.push("", "**Agents to use:**");
136
+ agents.forEach((a) => parts.push(`- ${a}`));
140
137
  }
141
138
  return parts.join("\n");
142
139
  }
@@ -181,6 +178,32 @@ function extractValidationCommands(plan) {
181
178
  }
182
179
  return { pre: [...new Set(pre)], post: [...new Set(post)] };
183
180
  }
181
+ var MIME_MAP = {
182
+ ".png": "image/png",
183
+ ".jpg": "image/jpeg",
184
+ ".jpeg": "image/jpeg",
185
+ ".gif": "image/gif",
186
+ ".webp": "image/webp",
187
+ ".svg": "image/svg+xml"
188
+ };
189
+ function buildImagePromptSection(imagePaths) {
190
+ const validPaths = imagePaths.filter((p) => existsSync(p));
191
+ if (validPaths.length === 0) return "";
192
+ const parts = ["## Attached Images", ""];
193
+ for (const imgPath of validPaths) {
194
+ const ext = extname(imgPath).toLowerCase();
195
+ const mime = MIME_MAP[ext] || "image/png";
196
+ const name = basename(imgPath);
197
+ try {
198
+ const data = readFileSync(imgPath).toString("base64");
199
+ parts.push(`### ${name}`);
200
+ parts.push(`![${name}](data:${mime};base64,${data})`);
201
+ parts.push("");
202
+ } catch {
203
+ }
204
+ }
205
+ return parts.length > 2 ? parts.join("\n") : "";
206
+ }
184
207
  function buildExecutionPayload(issue, provider, plan, workspacePath) {
185
208
  const strategy = plan.executionStrategy;
186
209
  const hasPhases = Boolean(plan.phases?.length);
@@ -191,7 +214,6 @@ function buildExecutionPayload(issue, provider, plan, workspacePath) {
191
214
  identifier: issue.identifier,
192
215
  title: issue.title,
193
216
  description: issue.description || "",
194
- priority: issue.priority,
195
217
  labels: issue.labels || [],
196
218
  paths: issue.paths || []
197
219
  },
@@ -200,14 +222,13 @@ function buildExecutionPayload(issue, provider, plan, workspacePath) {
200
222
  role: provider.role,
201
223
  model: provider.model || "default",
202
224
  effort: provider.reasoningEffort || "medium",
203
- capabilityCategory: provider.capabilityCategory || "",
204
225
  overlays: provider.overlays || []
205
226
  },
206
227
  executionIntent: {
207
228
  complexity: plan.estimatedComplexity,
208
229
  approach: strategy?.approach || "",
209
230
  rationale: strategy?.whyThisApproach || "",
210
- workPattern: hasPhases ? "phased" : plan.toolingDecision?.shouldUseSubagents ? "parallel_subtasks" : "sequential"
231
+ workPattern: hasPhases ? "phased" : "sequential"
211
232
  },
212
233
  plan: {
213
234
  summary: plan.summary,
@@ -242,8 +263,8 @@ function buildExecutionPayload(issue, provider, plan, workspacePath) {
242
263
  mitigation: r.mitigation || ""
243
264
  })),
244
265
  tooling: {
245
- skills: plan.toolingDecision?.skillsToUse || [],
246
- subagents: plan.toolingDecision?.subagentsToUse || []
266
+ skills: plan.suggestedSkills || [],
267
+ agents: plan.suggestedAgents || []
247
268
  },
248
269
  targetPaths: plan.suggestedPaths || [],
249
270
  workspacePath,
@@ -291,13 +312,18 @@ function extractPlanDirs(plan) {
291
312
  // src/agents/adapters/claude.ts
292
313
  function buildClaudeCommand(options) {
293
314
  const parts = ["claude", "--print"];
294
- if (!options.noToolAccess) {
315
+ if (options.readOnly) {
316
+ parts.push("--permission-mode plan");
317
+ } else if (!options.noToolAccess) {
295
318
  parts.push("--dangerously-skip-permissions");
296
319
  }
297
320
  parts.push("--no-session-persistence", "--output-format json");
298
321
  if (options.effort) {
299
322
  parts.push(`--effort ${options.effort}`);
300
323
  }
324
+ if (options.maxBudgetUsd && options.maxBudgetUsd > 0) {
325
+ parts.push(`--max-budget-usd ${options.maxBudgetUsd}`);
326
+ }
301
327
  if (options.jsonSchema) {
302
328
  parts.push(`--json-schema '${options.jsonSchema}'`);
303
329
  }
@@ -312,16 +338,17 @@ function buildClaudeCommand(options) {
312
338
  parts.push('< "$FIFONY_PROMPT_FILE"');
313
339
  return parts.join(" ");
314
340
  }
315
- async function compile(issue, provider, plan, config, workspacePath, skillContext) {
341
+ async function compile(issue, provider, plan, config, workspacePath, skillContext, capabilitiesManifest) {
316
342
  const effort = resolveEffortForProvider(plan, provider.role, config.defaultEffort);
317
- const prompt = await renderPrompt("compile-execution-claude", {
343
+ let prompt = await renderPrompt("compile-execution-claude", {
318
344
  isPlanner: provider.role === "planner",
319
345
  isReviewer: provider.role === "reviewer",
320
346
  profileInstructions: provider.profileInstructions || "",
321
347
  skillContext,
348
+ capabilitiesManifest: capabilitiesManifest || "",
322
349
  planPrompt: buildFullPlanPrompt(plan),
323
- subagentsToUse: plan.toolingDecision?.shouldUseSubagents ? plan.toolingDecision.subagentsToUse ?? [] : [],
324
- skillsToUse: plan.toolingDecision?.shouldUseSkills ? plan.toolingDecision.skillsToUse ?? [] : [],
350
+ suggestedSkills: plan.suggestedSkills ?? [],
351
+ suggestedAgents: plan.suggestedAgents ?? [],
325
352
  suggestedPaths: plan.suggestedPaths ?? [],
326
353
  workspacePath,
327
354
  issueIdentifier: issue.identifier,
@@ -330,13 +357,20 @@ async function compile(issue, provider, plan, config, workspacePath, skillContex
330
357
  validationItems: (plan.validation ?? []).map((value) => ({ value }))
331
358
  });
332
359
  const relativeDirs = extractPlanDirs(plan);
333
- const codePath = existsSync(join(workspacePath, "worktree")) ? join(workspacePath, "worktree") : workspacePath;
360
+ const codePath = existsSync2(join(workspacePath, "worktree")) ? join(workspacePath, "worktree") : workspacePath;
334
361
  const absoluteDirs = relativeDirs.map((d) => join(codePath, d));
362
+ if (issue.images?.length) {
363
+ const imageSection = buildImagePromptSection(issue.images);
364
+ if (imageSection) prompt = prompt + "\n\n" + imageSection;
365
+ }
366
+ const isReadOnlyRole = provider.role === "planner" || provider.role === "reviewer";
335
367
  const command = buildClaudeCommand({
336
368
  model: provider.model,
337
369
  effort,
338
370
  addDirs: absoluteDirs,
339
- jsonSchema: CLAUDE_RESULT_SCHEMA
371
+ jsonSchema: CLAUDE_RESULT_SCHEMA,
372
+ readOnly: isReadOnlyRole,
373
+ maxBudgetUsd: config.maxBudgetUsd
340
374
  });
341
375
  const env2 = {
342
376
  FIFONY_PLAN_COMPLEXITY: plan.estimatedComplexity,
@@ -344,8 +378,8 @@ async function compile(issue, provider, plan, config, workspacePath, skillContex
344
378
  FIFONY_EXECUTION_PAYLOAD_FILE: "fifony-execution-payload.json"
345
379
  };
346
380
  if (plan.suggestedPaths?.length) env2.FIFONY_PLAN_PATHS = plan.suggestedPaths.join(",");
347
- if (plan.toolingDecision?.skillsToUse?.length) {
348
- env2.FIFONY_PLAN_SKILLS = plan.toolingDecision.skillsToUse.map((s) => s.name).join(",");
381
+ if (plan.suggestedSkills?.length) {
382
+ env2.FIFONY_PLAN_SKILLS = plan.suggestedSkills.join(",");
349
383
  }
350
384
  const { pre, post } = extractValidationCommands(plan);
351
385
  return {
@@ -360,24 +394,26 @@ async function compile(issue, provider, plan, config, workspacePath, skillContex
360
394
  adapter: "claude",
361
395
  reasoningEffort: effort || "default",
362
396
  model: provider.model || "default",
363
- skillsActivated: plan.toolingDecision?.skillsToUse?.map((s) => s.name) || [],
364
- subagentsRequested: plan.toolingDecision?.subagentsToUse?.map((a) => a.name) || [],
397
+ skillsActivated: plan.suggestedSkills || [],
398
+ subagentsRequested: plan.suggestedAgents || [],
365
399
  phasesCount: plan.phases?.length || 0
366
400
  }
367
401
  };
368
402
  }
369
403
  var claudeAdapter = {
370
404
  buildCommand: buildClaudeCommand,
371
- buildReviewCommand: (reviewer) => buildClaudeCommand({
405
+ buildReviewCommand: (reviewer, config) => buildClaudeCommand({
372
406
  model: reviewer.model,
373
407
  effort: reviewer.reasoningEffort,
374
- jsonSchema: REVIEW_RESULT_SCHEMA
408
+ jsonSchema: REVIEW_RESULT_SCHEMA,
409
+ readOnly: true,
410
+ maxBudgetUsd: config?.maxBudgetUsd
375
411
  }),
376
412
  compile
377
413
  };
378
414
 
379
415
  // src/agents/adapters/codex.ts
380
- import { existsSync as existsSync2 } from "fs";
416
+ import { existsSync as existsSync3 } from "fs";
381
417
  import { join as join2 } from "path";
382
418
  var CODEX_RESULT_CONTRACT = `
383
419
  Return a JSON object with this exact schema when finished:
@@ -393,7 +429,7 @@ Return a JSON object with this exact schema when finished:
393
429
  }
394
430
  `.trim();
395
431
  function buildCodexCommand(options) {
396
- const parts = ["codex", "exec", "--dangerously-bypass-approvals-and-sandbox"];
432
+ const parts = ["codex", "exec", "--skip-git-repo-check", "--dangerously-bypass-approvals-and-sandbox"];
397
433
  if (options.model && options.model !== "codex") {
398
434
  parts.push(`--model ${options.model}`);
399
435
  }
@@ -410,16 +446,20 @@ function buildCodexCommand(options) {
410
446
  parts.push(`--image "${img}"`);
411
447
  }
412
448
  }
449
+ if (options.search) {
450
+ parts.push("--search");
451
+ }
413
452
  parts.push('< "$FIFONY_PROMPT_FILE"');
414
453
  return parts.join(" ");
415
454
  }
416
- async function compile2(issue, provider, plan, config, workspacePath, skillContext) {
455
+ async function compile2(issue, provider, plan, config, workspacePath, skillContext, capabilitiesManifest) {
417
456
  const effort = resolveEffortForProvider(plan, provider.role, config.defaultEffort) || provider.reasoningEffort;
418
457
  const prompt = await renderPrompt("compile-execution-codex", {
419
458
  isPlanner: provider.role === "planner",
420
459
  isReviewer: provider.role === "reviewer",
421
460
  profileInstructions: provider.profileInstructions || "",
422
461
  skillContext,
462
+ capabilitiesManifest: capabilitiesManifest || "",
423
463
  issueIdentifier: issue.identifier,
424
464
  title: issue.title,
425
465
  description: issue.description || "(none)",
@@ -431,17 +471,18 @@ async function compile2(issue, provider, plan, config, workspacePath, skillConte
431
471
  outputs: phase.outputs ?? []
432
472
  })),
433
473
  suggestedPaths: plan.suggestedPaths ?? [],
434
- skillsToUse: plan.toolingDecision?.shouldUseSkills ? plan.toolingDecision.skillsToUse ?? [] : [],
474
+ suggestedSkills: plan.suggestedSkills ?? [],
435
475
  validationItems: (plan.validation ?? []).map((value) => ({ value })),
436
476
  outputContract: CODEX_RESULT_CONTRACT
437
477
  });
438
478
  const relativeDirs = extractPlanDirs(plan);
439
- const codePath = existsSync2(join2(workspacePath, "worktree")) ? join2(workspacePath, "worktree") : workspacePath;
479
+ const codePath = existsSync3(join2(workspacePath, "worktree")) ? join2(workspacePath, "worktree") : workspacePath;
440
480
  const absoluteDirs = relativeDirs.map((d) => join2(codePath, d));
441
481
  const command = buildCodexCommand({
442
482
  model: provider.model,
443
483
  addDirs: absoluteDirs,
444
- effort
484
+ effort,
485
+ imagePaths: issue.images?.filter((p) => existsSync3(p))
445
486
  });
446
487
  const env2 = {
447
488
  FIFONY_PLAN_COMPLEXITY: plan.estimatedComplexity,
@@ -463,7 +504,7 @@ async function compile2(issue, provider, plan, config, workspacePath, skillConte
463
504
  adapter: "codex",
464
505
  reasoningEffort: effort || "default",
465
506
  model: provider.model || "default",
466
- skillsActivated: plan.toolingDecision?.skillsToUse?.map((s) => s.name) || [],
507
+ skillsActivated: plan.suggestedSkills || [],
467
508
  subagentsRequested: [],
468
509
  phasesCount: plan.phases?.length || 0
469
510
  }
@@ -471,15 +512,16 @@ async function compile2(issue, provider, plan, config, workspacePath, skillConte
471
512
  }
472
513
  var codexAdapter = {
473
514
  buildCommand: buildCodexCommand,
474
- buildReviewCommand: (reviewer) => buildCodexCommand({
515
+ buildReviewCommand: (reviewer, _config) => buildCodexCommand({
475
516
  model: reviewer.model,
476
517
  effort: reviewer.reasoningEffort
518
+ // Codex has no --permission-mode or --approval-mode equivalent for read-only review
477
519
  }),
478
520
  compile: compile2
479
521
  };
480
522
 
481
523
  // src/agents/adapters/gemini.ts
482
- import { existsSync as existsSync3 } from "fs";
524
+ import { existsSync as existsSync4 } from "fs";
483
525
  import { join as join3 } from "path";
484
526
  var GEMINI_RESULT_CONTRACT = `
485
527
  Return a JSON object with this exact schema when finished:
@@ -495,23 +537,30 @@ Return a JSON object with this exact schema when finished:
495
537
  }
496
538
  `.trim();
497
539
  function buildGeminiCommand(options) {
498
- const parts = ["gemini", "--yolo"];
540
+ const parts = ["gemini"];
541
+ if (options.readOnly) {
542
+ parts.push("--approval-mode plan");
543
+ } else {
544
+ parts.push("--yolo");
545
+ }
499
546
  if (options.model) {
500
547
  parts.push(`--model ${options.model}`);
501
548
  }
549
+ parts.push("--output-format json");
502
550
  if (options.addDirs?.length) {
503
551
  parts.push(`--include-directories ${options.addDirs.map((d) => `"${d}"`).join(",")}`);
504
552
  }
505
553
  parts.push('-p "" < "$FIFONY_PROMPT_FILE"');
506
554
  return parts.join(" ");
507
555
  }
508
- async function compile3(issue, provider, plan, config, workspacePath, skillContext) {
556
+ async function compile3(issue, provider, plan, config, workspacePath, skillContext, capabilitiesManifest) {
509
557
  const effort = resolveEffortForProvider(plan, provider.role, config.defaultEffort) || provider.reasoningEffort;
510
- const prompt = await renderPrompt("compile-execution-codex", {
558
+ let prompt = await renderPrompt("compile-execution-codex", {
511
559
  isPlanner: provider.role === "planner",
512
560
  isReviewer: provider.role === "reviewer",
513
561
  profileInstructions: provider.profileInstructions || "",
514
562
  skillContext,
563
+ capabilitiesManifest: capabilitiesManifest || "",
515
564
  issueIdentifier: issue.identifier,
516
565
  title: issue.title,
517
566
  description: issue.description || "(none)",
@@ -523,16 +572,22 @@ async function compile3(issue, provider, plan, config, workspacePath, skillConte
523
572
  outputs: phase.outputs ?? []
524
573
  })),
525
574
  suggestedPaths: plan.suggestedPaths ?? [],
526
- skillsToUse: plan.toolingDecision?.shouldUseSkills ? plan.toolingDecision.skillsToUse ?? [] : [],
575
+ suggestedSkills: plan.suggestedSkills ?? [],
527
576
  validationItems: (plan.validation ?? []).map((value) => ({ value })),
528
577
  outputContract: GEMINI_RESULT_CONTRACT
529
578
  });
579
+ if (issue.images?.length) {
580
+ const imageSection = buildImagePromptSection(issue.images);
581
+ if (imageSection) prompt = prompt + "\n\n" + imageSection;
582
+ }
530
583
  const relativeDirs = extractPlanDirs(plan);
531
- const codePath = existsSync3(join3(workspacePath, "worktree")) ? join3(workspacePath, "worktree") : workspacePath;
584
+ const codePath = existsSync4(join3(workspacePath, "worktree")) ? join3(workspacePath, "worktree") : workspacePath;
532
585
  const absoluteDirs = relativeDirs.map((d) => join3(codePath, d));
586
+ const isReadOnlyRole = provider.role === "planner" || provider.role === "reviewer";
533
587
  const command = buildGeminiCommand({
534
588
  model: provider.model,
535
- addDirs: absoluteDirs
589
+ addDirs: absoluteDirs,
590
+ readOnly: isReadOnlyRole
536
591
  });
537
592
  const env2 = {
538
593
  FIFONY_PLAN_COMPLEXITY: plan.estimatedComplexity,
@@ -554,7 +609,7 @@ async function compile3(issue, provider, plan, config, workspacePath, skillConte
554
609
  adapter: "gemini",
555
610
  reasoningEffort: effort || "default",
556
611
  model: provider.model || "default",
557
- skillsActivated: plan.toolingDecision?.skillsToUse?.map((s) => s.name) || [],
612
+ skillsActivated: plan.suggestedSkills || [],
558
613
  subagentsRequested: [],
559
614
  phasesCount: plan.phases?.length || 0
560
615
  }
@@ -563,7 +618,8 @@ async function compile3(issue, provider, plan, config, workspacePath, skillConte
563
618
  var geminiAdapter = {
564
619
  buildCommand: buildGeminiCommand,
565
620
  buildReviewCommand: (reviewer) => buildGeminiCommand({
566
- model: reviewer.model
621
+ model: reviewer.model,
622
+ readOnly: true
567
623
  }),
568
624
  compile: compile3
569
625
  };
@@ -577,7 +633,7 @@ var ADAPTERS = {
577
633
 
578
634
  // src/agents/model-discovery.ts
579
635
  import { execFileSync } from "child_process";
580
- import { existsSync as existsSync4, readFileSync, realpathSync } from "fs";
636
+ import { existsSync as existsSync5, readFileSync as readFileSync2, realpathSync } from "fs";
581
637
  import { join as join4, dirname } from "path";
582
638
  import { homedir } from "os";
583
639
  var modelCache = /* @__PURE__ */ new Map();
@@ -585,8 +641,8 @@ var MODEL_CACHE_TTL_MS = 5 * 60 * 1e3;
585
641
  function readClaudeConfig() {
586
642
  try {
587
643
  const settingsPath = join4(homedir(), ".claude", "settings.json");
588
- if (!existsSync4(settingsPath)) return {};
589
- const raw = readFileSync(settingsPath, "utf8");
644
+ if (!existsSync5(settingsPath)) return {};
645
+ const raw = readFileSync2(settingsPath, "utf8");
590
646
  const settings = JSON.parse(raw);
591
647
  return { model: typeof settings.model === "string" ? settings.model : void 0 };
592
648
  } catch {
@@ -600,7 +656,7 @@ function resolveGeminiModelsFile() {
600
656
  const realBin = realpathSync(binPath);
601
657
  const cliRoot = dirname(dirname(realBin));
602
658
  const modelsPath = join4(cliRoot, "node_modules", "@google", "gemini-cli-core", "dist", "src", "config", "models.js");
603
- return existsSync4(modelsPath) ? modelsPath : null;
659
+ return existsSync5(modelsPath) ? modelsPath : null;
604
660
  } catch {
605
661
  return null;
606
662
  }
@@ -609,7 +665,7 @@ async function fetchGeminiModels() {
609
665
  const modelsPath = resolveGeminiModelsFile();
610
666
  if (!modelsPath) return [];
611
667
  try {
612
- const content = readFileSync(modelsPath, "utf8");
668
+ const content = readFileSync2(modelsPath, "utf8");
613
669
  const regex = /export const ([A-Z0-9_]+)\s*=\s*'(gemini-[^']+)';/g;
614
670
  const seen = /* @__PURE__ */ new Set();
615
671
  const stable = [];
@@ -634,8 +690,8 @@ async function fetchGeminiModels() {
634
690
  async function fetchCodexModels() {
635
691
  const cachePath = join4(homedir(), ".codex", "models_cache.json");
636
692
  try {
637
- if (existsSync4(cachePath)) {
638
- const raw = readFileSync(cachePath, "utf8");
693
+ if (existsSync5(cachePath)) {
694
+ const raw = readFileSync2(cachePath, "utf8");
639
695
  const cache = JSON.parse(raw);
640
696
  if (Array.isArray(cache.models) && cache.models.length > 0) {
641
697
  return cache.models.sort((a, b) => {
@@ -728,28 +784,6 @@ async function discoverModels(providers) {
728
784
  }
729
785
 
730
786
  // src/agents/providers.ts
731
- function resolveAgentProfile(name) {
732
- const normalized = name.trim();
733
- if (!normalized) return { profilePath: "", instructions: "" };
734
- const candidates = [
735
- join5(TARGET_ROOT, ".codex", "agents", `${normalized}.md`),
736
- join5(TARGET_ROOT, ".codex", "agents", normalized, "AGENT.md"),
737
- join5(TARGET_ROOT, "agents", `${normalized}.md`),
738
- join5(TARGET_ROOT, "agents", normalized, "AGENT.md"),
739
- join5(homedir2(), ".codex", "agents", `${normalized}.md`),
740
- join5(homedir2(), ".codex", "agents", normalized, "AGENT.md"),
741
- join5(homedir2(), ".claude", "agents", `${normalized}.md`),
742
- join5(homedir2(), ".claude", "agents", normalized, "AGENT.md")
743
- ];
744
- for (const candidate of candidates) {
745
- if (!existsSync5(candidate)) continue;
746
- return {
747
- profilePath: candidate,
748
- instructions: readFileSync2(candidate, "utf8").trim()
749
- };
750
- }
751
- return { profilePath: "", instructions: "" };
752
- }
753
787
  function normalizeAgentProvider(value) {
754
788
  const normalized = value.trim().toLowerCase();
755
789
  if (normalized === "claude" || normalized === "codex" || normalized === "gemini") return normalized;
@@ -798,8 +832,8 @@ function detectAvailableProviders() {
798
832
  function readCodexConfig() {
799
833
  try {
800
834
  const configPath = join5(homedir2(), ".codex", "config.toml");
801
- if (!existsSync5(configPath)) return {};
802
- const raw = readFileSync2(configPath, "utf8");
835
+ if (!existsSync6(configPath)) return {};
836
+ const raw = readFileSync3(configPath, "utf8");
803
837
  const model = raw.match(/^model\s*=\s*"([^"]+)"/m)?.[1];
804
838
  const reasoningEffort = raw.match(/^model_reasoning_effort\s*=\s*"([^"]+)"/m)?.[1];
805
839
  return { model, reasoningEffort };
@@ -810,8 +844,8 @@ function readCodexConfig() {
810
844
  function readGeminiConfig() {
811
845
  try {
812
846
  const settingsPath = join5(homedir2(), ".gemini", "settings.json");
813
- if (!existsSync5(settingsPath)) return {};
814
- const raw = readFileSync2(settingsPath, "utf8");
847
+ if (!existsSync6(settingsPath)) return {};
848
+ const raw = readFileSync3(settingsPath, "utf8");
815
849
  const settings = JSON.parse(raw);
816
850
  return {
817
851
  model: typeof settings.model === "string" ? settings.model : void 0,
@@ -839,20 +873,6 @@ function getBaseAgentProviders(state) {
839
873
  }
840
874
  ];
841
875
  }
842
- function getCapabilityRoutingOptions() {
843
- return { enabled: true, overrides: [] };
844
- }
845
- function applyCapabilityMetadata(issue, resolution) {
846
- issue.capabilityCategory = resolution.category;
847
- issue.capabilityOverlays = [...resolution.overlays];
848
- issue.capabilityRationale = [...resolution.rationale];
849
- const baseLabels = (issue.labels ?? []).filter((label) => !label.startsWith("capability:") && !label.startsWith("overlay:"));
850
- const derivedLabels = [
851
- resolution.category ? `capability:${resolution.category}` : "",
852
- ...resolution.overlays.map((overlay) => `overlay:${overlay}`)
853
- ].filter(Boolean);
854
- issue.labels = [.../* @__PURE__ */ new Set([...baseLabels, ...derivedLabels])];
855
- }
856
876
  function roleToStageKey(role) {
857
877
  switch (role) {
858
878
  case "planner":
@@ -884,41 +904,18 @@ function applyWorkflowConfigToProviders(providers, workflowConfig) {
884
904
  }
885
905
  function getEffectiveAgentProviders(state, issue, _workflowDefinition, workflowConfig) {
886
906
  const baseProviders = getBaseAgentProviders(state);
887
- const resolution = resolveTaskCapabilities(
888
- {
889
- id: issue.id,
890
- identifier: issue.identifier,
891
- title: issue.title,
892
- description: issue.description,
893
- labels: issue.labels,
894
- paths: issue.paths
895
- },
896
- getCapabilityRoutingOptions()
897
- );
898
- applyCapabilityMetadata(issue, resolution);
899
- const merged = mergeCapabilityProviders(baseProviders, resolution).map((provider) => {
900
- const resolvedProfile = resolveAgentProfile(provider.profile ?? "");
901
- const suggestion = resolution.providers.find(
902
- (entry) => entry.provider === provider.provider && entry.role === provider.role
903
- );
907
+ const providers = baseProviders.map((provider) => {
904
908
  const effort = resolveEffort(provider.role, issue.effort, state.config.defaultEffort);
905
- const command = provider.command;
906
909
  return {
907
910
  ...provider,
908
- command,
909
- profilePath: resolvedProfile.profilePath,
910
- profileInstructions: resolvedProfile.instructions,
911
- selectionReason: suggestion?.reason ?? resolution.rationale.join(" "),
912
- overlays: resolution.overlays,
913
- capabilityCategory: resolution.category,
914
911
  reasoningEffort: effort
915
912
  };
916
913
  });
917
- return applyWorkflowConfigToProviders(merged, workflowConfig ?? null);
914
+ return applyWorkflowConfigToProviders(providers, workflowConfig ?? null);
918
915
  }
919
916
 
920
917
  // src/agents/command-executor.ts
921
- async function runCommandWithTimeout(command, workspacePath, issue, config, promptText, promptFile, extraEnv = {}) {
918
+ async function runCommandWithTimeout(command, workspacePath, issue, config, promptText, promptFile, extraEnv = {}, outputFile) {
922
919
  return new Promise((resolve2) => {
923
920
  const started = Date.now();
924
921
  const resultFile = extraEnv.FIFONY_RESULT_FILE;
@@ -929,7 +926,6 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
929
926
  FIFONY_ISSUE_ID: issue.id,
930
927
  FIFONY_ISSUE_IDENTIFIER: issue.identifier,
931
928
  FIFONY_ISSUE_TITLE: issue.title,
932
- FIFONY_ISSUE_PRIORITY: String(issue.priority),
933
929
  FIFONY_WORKSPACE_PATH: issue.worktreePath ?? workspacePath,
934
930
  FIFONY_PROMPT_FILE: promptFile
935
931
  };
@@ -974,6 +970,19 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
974
970
  let outputHeader = "";
975
971
  const liveLogFile = join6(workspacePath, "live-output.log");
976
972
  writeFileSync(liveLogFile, "", "utf8");
973
+ if (outputFile) {
974
+ try {
975
+ const header = `# fifony stdout capture
976
+ # turn: ${extraEnv.FIFONY_TURN_INDEX ?? "?"}
977
+ # provider: ${extraEnv.FIFONY_AGENT_PROVIDER ?? "?"}
978
+ # role: ${extraEnv.FIFONY_AGENT_ROLE ?? "?"}
979
+ # timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}
980
+ ---
981
+ `;
982
+ writeFileSync(outputFile, header, "utf8");
983
+ } catch {
984
+ }
985
+ }
977
986
  const onChunk = (chunk) => {
978
987
  const text = String(chunk);
979
988
  if (outputHeader.length < 2e3) outputHeader = (outputHeader + text).slice(0, 2e3);
@@ -983,6 +992,12 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
983
992
  appendFileSync(liveLogFile, text);
984
993
  } catch {
985
994
  }
995
+ if (outputFile) {
996
+ try {
997
+ appendFileSync(outputFile, text);
998
+ } catch {
999
+ }
1000
+ }
986
1001
  issue.commandOutputTail = output;
987
1002
  };
988
1003
  child.stdout?.on("data", onChunk);
@@ -1103,6 +1118,48 @@ async function runHook(command, workspacePath, issue, hookName, extraEnv = {}) {
1103
1118
  }
1104
1119
 
1105
1120
  // src/agents/prompt-builder.ts
1121
+ function buildRetryContext(issue) {
1122
+ const summaries = issue.previousAttemptSummaries;
1123
+ if (!summaries || summaries.length === 0) return "";
1124
+ const lines = ["## Previous Attempts\n"];
1125
+ lines.push("The following previous attempts FAILED. Do NOT repeat the same approach. Try a fundamentally different strategy.\n");
1126
+ for (let i = 0; i < summaries.length; i++) {
1127
+ const s = summaries[i];
1128
+ const phaseLabel = s.phase === "review" ? "review" : s.phase === "crash" ? "crash" : s.phase === "plan" ? "plan" : "execution";
1129
+ lines.push(`### Attempt ${i + 1} \u2014 ${phaseLabel} failure (plan v${s.planVersion}, exec #${s.executeAttempt})`);
1130
+ if (s.phase === "review") {
1131
+ lines.push("*The reviewer identified issues with the previous implementation. Focus on addressing the reviewer's feedback \u2014 do not redo work that was already approved.*");
1132
+ } else if (s.phase === "crash") {
1133
+ lines.push("*The agent process crashed or timed out. Simplify the approach \u2014 break the work into smaller steps.*");
1134
+ }
1135
+ if (s.insight) {
1136
+ lines.push(`**Failure type:** ${s.insight.errorType}`);
1137
+ lines.push(`**Root cause:** ${s.insight.rootCause}`);
1138
+ if (s.insight.failedCommand) lines.push(`**Failed command:** \`${s.insight.failedCommand}\``);
1139
+ if (s.insight.filesInvolved.length > 0) {
1140
+ lines.push(`**Files involved:** ${s.insight.filesInvolved.map((f) => `\`${f}\``).join(", ")}`);
1141
+ }
1142
+ lines.push(`**What to do differently:** ${s.insight.suggestion}`);
1143
+ } else {
1144
+ lines.push(`**Error:** ${s.error}`);
1145
+ }
1146
+ if (s.outputTail) {
1147
+ lines.push(`
1148
+ <details><summary>Output tail</summary>
1149
+
1150
+ \`\`\`
1151
+ ${s.outputTail}
1152
+ \`\`\`
1153
+ </details>`);
1154
+ }
1155
+ if (s.outputFile) {
1156
+ lines.push(`*Full output saved in: outputs/${s.outputFile}*`);
1157
+ }
1158
+ lines.push("");
1159
+ }
1160
+ const full = lines.join("\n");
1161
+ return full.length > 8e3 ? full.slice(0, 8e3) + "\n[...truncated]" : full;
1162
+ }
1106
1163
  async function buildPrompt(issue, _workflowDefinition) {
1107
1164
  const rendered = await renderPrompt("workflow-default", { issue, attempt: issue.attempts || 0 });
1108
1165
  if (!issue.plan?.steps?.length) {
@@ -1133,7 +1190,7 @@ async function buildTurnPrompt(issue, basePrompt, previousOutput, turnIndex, max
1133
1190
  outputTail: previousOutput.trim() || "No previous output captured."
1134
1191
  });
1135
1192
  }
1136
- async function buildProviderBasePrompt(provider, issue, basePrompt, workspacePath, skillContext) {
1193
+ async function buildProviderBasePrompt(provider, issue, basePrompt, workspacePath, skillContext, capabilitiesManifest) {
1137
1194
  return renderPrompt("agent-provider-base", {
1138
1195
  isPlanner: provider.role === "planner",
1139
1196
  isReviewer: provider.role === "reviewer",
@@ -1141,8 +1198,9 @@ async function buildProviderBasePrompt(provider, issue, basePrompt, workspacePat
1141
1198
  hasFrontendDesignOverlay: provider.overlays?.includes("frontend-design") ?? false,
1142
1199
  profileInstructions: provider.profileInstructions || "",
1143
1200
  skillContext,
1144
- capabilityCategory: provider.capabilityCategory || "",
1145
- selectionReason: provider.selectionReason ?? "No additional routing reason.",
1201
+ capabilitiesManifest: capabilitiesManifest || "",
1202
+ capabilityCategory: "",
1203
+ selectionReason: provider.selectionReason ?? "",
1146
1204
  overlays: provider.overlays ?? [],
1147
1205
  targetPaths: issue.paths ?? [],
1148
1206
  workspacePath,
@@ -1173,10 +1231,46 @@ function shouldSkipPath(relativePath) {
1173
1231
  const parts = relativePath.split("/");
1174
1232
  if (parts.some((segment) => SKIP_DIRS.has(segment))) return true;
1175
1233
  const base = parts.at(-1) ?? "";
1176
- if (base.startsWith("map_scan_") && extname(base) === ".json") return true;
1177
- if (extname(base) === ".xlsx") return true;
1234
+ if (base.startsWith("map_scan_") && extname2(base) === ".json") return true;
1235
+ if (extname2(base) === ".xlsx") return true;
1178
1236
  return false;
1179
1237
  }
1238
+ function bootstrapSource() {
1239
+ if (existsSync7(SOURCE_MARKER)) return;
1240
+ logger.info("Creating local source snapshot for Fifony (local-only runtime)...");
1241
+ const copyRecursive = (source, target, rel = "") => {
1242
+ mkdirSync(target, { recursive: true });
1243
+ const items = readdirSync(source, { withFileTypes: true });
1244
+ for (const item of items) {
1245
+ const nextRel = rel ? `${rel}/${item.name}` : item.name;
1246
+ if (shouldSkipPath(nextRel)) continue;
1247
+ const sourcePath = `${source}/${item.name}`;
1248
+ const targetPath = `${target}/${item.name}`;
1249
+ const itemStat = statSync(sourcePath);
1250
+ if (item.isDirectory()) {
1251
+ copyRecursive(sourcePath, targetPath, nextRel);
1252
+ continue;
1253
+ }
1254
+ if (item.isSymbolicLink() || itemStat.isSymbolicLink()) continue;
1255
+ if (itemStat.isFile() || itemStat.isFIFO()) {
1256
+ try {
1257
+ const file = readFileSync4(sourcePath);
1258
+ writeFileSync2(targetPath, file);
1259
+ } catch (error) {
1260
+ if (error.code === "ENOENT") {
1261
+ logger.debug(`Skipped missing source file: ${sourcePath}`);
1262
+ } else {
1263
+ throw error;
1264
+ }
1265
+ }
1266
+ }
1267
+ }
1268
+ };
1269
+ mkdirSync(SOURCE_ROOT, { recursive: true });
1270
+ copyRecursive(TARGET_ROOT, SOURCE_ROOT);
1271
+ writeFileSync2(SOURCE_MARKER, `${now()}
1272
+ `, "utf8");
1273
+ }
1180
1274
  var sourceReadyPromise = null;
1181
1275
  var skipSourceFlag = false;
1182
1276
  function setSkipSource(skip) {
@@ -1187,7 +1281,7 @@ async function ensureSourceReady(onProgress) {
1187
1281
  onProgress?.("ready");
1188
1282
  return;
1189
1283
  }
1190
- if (existsSync6(SOURCE_MARKER)) {
1284
+ if (existsSync7(SOURCE_MARKER)) {
1191
1285
  onProgress?.("ready");
1192
1286
  return;
1193
1287
  }
@@ -1231,12 +1325,76 @@ async function ensureSourceReady(onProgress) {
1231
1325
  })();
1232
1326
  return sourceReadyPromise;
1233
1327
  }
1234
- function isGitRepo(dir) {
1235
- try {
1236
- execSync("git rev-parse --git-dir", { cwd: dir, stdio: "pipe" });
1237
- return true;
1238
- } catch {
1239
- return false;
1328
+ function getGitRepoStatus(dir) {
1329
+ const isGit = (() => {
1330
+ try {
1331
+ execSync("git rev-parse --git-dir", { cwd: dir, stdio: "pipe" });
1332
+ return true;
1333
+ } catch {
1334
+ return false;
1335
+ }
1336
+ })();
1337
+ if (!isGit) {
1338
+ return { isGit: false, hasCommits: false, branch: null };
1339
+ }
1340
+ const branch = (() => {
1341
+ try {
1342
+ return execSync("git symbolic-ref --short HEAD", { cwd: dir, encoding: "utf8", stdio: "pipe" }).trim() || null;
1343
+ } catch {
1344
+ try {
1345
+ return execSync("git rev-parse --abbrev-ref HEAD", { cwd: dir, encoding: "utf8", stdio: "pipe" }).trim() || null;
1346
+ } catch {
1347
+ return null;
1348
+ }
1349
+ }
1350
+ })();
1351
+ const hasCommits = (() => {
1352
+ try {
1353
+ execSync("git rev-parse --verify HEAD", { cwd: dir, stdio: "pipe" });
1354
+ return true;
1355
+ } catch {
1356
+ return false;
1357
+ }
1358
+ })();
1359
+ return { isGit: true, hasCommits, branch };
1360
+ }
1361
+ function gitRequirementMessage(action) {
1362
+ return `fifony requires a git repository with at least one commit to ${action}. Initialize git in this project and create an initial commit, or use the onboarding Setup step.`;
1363
+ }
1364
+ function ensureGitRepoReadyForWorktrees(dir, action = "run issue worktrees") {
1365
+ const status = getGitRepoStatus(dir);
1366
+ if (!status.isGit) {
1367
+ throw new Error(gitRequirementMessage(action));
1368
+ }
1369
+ if (!status.hasCommits) {
1370
+ throw new Error(`fifony requires at least one commit to ${action} because git worktree needs a base commit. Create an initial commit, then retry.`);
1371
+ }
1372
+ return status;
1373
+ }
1374
+ function initializeGitRepoForWorktrees(dir) {
1375
+ let status = getGitRepoStatus(dir);
1376
+ if (!status.isGit) {
1377
+ try {
1378
+ execSync("git init -b main", { cwd: dir, stdio: "pipe" });
1379
+ } catch {
1380
+ execSync("git init", { cwd: dir, stdio: "pipe" });
1381
+ }
1382
+ status = getGitRepoStatus(dir);
1383
+ }
1384
+ if (!status.hasCommits) {
1385
+ execSync(
1386
+ 'git -c user.name="fifony" -c user.email="fifony@local.invalid" commit --allow-empty -m "Initial commit"',
1387
+ { cwd: dir, stdio: "pipe" }
1388
+ );
1389
+ status = getGitRepoStatus(dir);
1390
+ }
1391
+ return status;
1392
+ }
1393
+ function assertIssueHasGitWorktree(issue, action) {
1394
+ if (!issue.branchName || !issue.baseBranch || !issue.worktreePath) {
1395
+ throw new Error(
1396
+ `Issue ${issue.identifier} has no git worktree \u2014 cannot ${action}. This usually means the issue was executed before git was initialized for the project. Initialize git, then re-run the issue.`
1397
+ );
1240
1398
  }
1241
1399
  }
1242
1400
  function detectDefaultBranch(dir) {
@@ -1262,7 +1420,7 @@ async function createGitWorktree(issue, worktreePath, baseBranch) {
1262
1420
  stdio: "pipe"
1263
1421
  });
1264
1422
  try {
1265
- const gitFileContent = readFileSync3(join7(worktreePath, ".git"), "utf8").trim();
1423
+ const gitFileContent = readFileSync4(join7(worktreePath, ".git"), "utf8").trim();
1266
1424
  const gitDirRel = gitFileContent.replace("gitdir: ", "").trim();
1267
1425
  const gitDirPath = resolve(worktreePath, gitDirRel);
1268
1426
  mkdirSync(join7(gitDirPath, "info"), { recursive: true });
@@ -1280,23 +1438,16 @@ async function prepareWorkspace(issue, state, defaultBranch) {
1280
1438
  const safeId = idToSafePath(issue.id);
1281
1439
  const workspaceRoot = join7(WORKSPACE_ROOT, safeId);
1282
1440
  const worktreePath = join7(workspaceRoot, "worktree");
1283
- const createdNow = !existsSync6(worktreePath);
1441
+ const createdNow = !existsSync7(worktreePath);
1284
1442
  if (createdNow) {
1285
1443
  mkdirSync(workspaceRoot, { recursive: true });
1286
1444
  logger.debug({ issueId: issue.id, identifier: issue.identifier, workspacePath: workspaceRoot }, "[Agent] Creating workspace");
1445
+ ensureGitRepoReadyForWorktrees(TARGET_ROOT, "execute issues");
1287
1446
  if (state.config.afterCreateHook) {
1288
1447
  mkdirSync(worktreePath, { recursive: true });
1289
1448
  await runHook(state.config.afterCreateHook, worktreePath, issue, "after_create");
1290
- } else if (isGitRepo(TARGET_ROOT)) {
1291
- await createGitWorktree(issue, worktreePath, defaultBranch);
1292
1449
  } else {
1293
- await ensureSourceReady();
1294
- mkdirSync(worktreePath, { recursive: true });
1295
- cpSync(SOURCE_ROOT, worktreePath, {
1296
- recursive: true,
1297
- force: true,
1298
- filter: (sourcePath) => !sourcePath.startsWith(WORKSPACE_ROOT)
1299
- });
1450
+ await createGitWorktree(issue, worktreePath, defaultBranch);
1300
1451
  }
1301
1452
  logger.debug({ issueId: issue.id, workspacePath: workspaceRoot, worktreePath }, "[Agent] Workspace created");
1302
1453
  } else {
@@ -1316,7 +1467,7 @@ async function prepareWorkspace(issue, state, defaultBranch) {
1316
1467
  async function cleanWorkspace(issueId, issue, state) {
1317
1468
  const safeId = idToSafePath(issueId);
1318
1469
  const workspacePath = issue?.workspacePath ?? join7(WORKSPACE_ROOT, safeId);
1319
- if (!existsSync6(workspacePath)) return;
1470
+ if (!existsSync7(workspacePath)) return;
1320
1471
  if (state.config.beforeRemoveHook) {
1321
1472
  try {
1322
1473
  const dummyIssue = issue ?? { id: issueId, identifier: issueId };
@@ -1397,6 +1548,58 @@ function parseDiffStats(issue, raw) {
1397
1548
  issue.linesAdded = addMatch ? parseInt(addMatch[1], 10) : 0;
1398
1549
  issue.linesRemoved = delMatch ? parseInt(delMatch[1], 10) : 0;
1399
1550
  }
1551
+ async function syncIssueDiffStatsToStore(issue) {
1552
+ if (!issue?.id) return;
1553
+ const { getIssueStateResource } = await import("./store-RVKQ6UEY.js");
1554
+ const issueResource = getIssueStateResource();
1555
+ if (!issueResource) return;
1556
+ const toNumber = (value) => {
1557
+ const parsed = typeof value === "number" ? value : Number(value ?? 0);
1558
+ return Number.isFinite(parsed) ? parsed : 0;
1559
+ };
1560
+ const nextLinesAdded = toNumber(issue.linesAdded);
1561
+ const nextLinesRemoved = toNumber(issue.linesRemoved);
1562
+ const nextFilesChanged = toNumber(issue.filesChanged);
1563
+ if (nextLinesAdded === 0 && nextLinesRemoved === 0 && nextFilesChanged === 0 && !issue.branchName) {
1564
+ return;
1565
+ }
1566
+ const current = await issueResource.get?.(issue.id).catch(() => null);
1567
+ const previousLinesAdded = toNumber(current?.linesAdded);
1568
+ const previousLinesRemoved = toNumber(current?.linesRemoved);
1569
+ const previousFilesChanged = toNumber(current?.filesChanged);
1570
+ await issueResource.patch(issue.id, {
1571
+ linesAdded: nextLinesAdded,
1572
+ linesRemoved: nextLinesRemoved,
1573
+ filesChanged: nextFilesChanged,
1574
+ branchName: issue.branchName
1575
+ });
1576
+ const add = issueResource.add;
1577
+ const sub = issueResource.sub;
1578
+ if (typeof add !== "function" || typeof sub !== "function") {
1579
+ logger.debug({ issueId: issue.id }, "[DiffStats] resource.add/sub not available \u2014 EC plugin may not be installed");
1580
+ return;
1581
+ }
1582
+ const deltaAdded = nextLinesAdded - previousLinesAdded;
1583
+ const deltaRemoved = nextLinesRemoved - previousLinesRemoved;
1584
+ const deltaFiles = nextFilesChanged - previousFilesChanged;
1585
+ if (deltaAdded === 0 && deltaRemoved === 0 && deltaFiles === 0) {
1586
+ logger.debug({ issueId: issue.id, nextLinesAdded, previousLinesAdded }, "[DiffStats] No delta to send to EC (values already synced)");
1587
+ return;
1588
+ }
1589
+ logger.debug({ issueId: issue.id, deltaAdded, deltaRemoved, deltaFiles }, "[DiffStats] Sending deltas to EC");
1590
+ const applyDelta = async (field, delta) => {
1591
+ if (delta > 0) {
1592
+ await add.call(issueResource, issue.id, field, delta);
1593
+ } else if (delta < 0) {
1594
+ await sub.call(issueResource, issue.id, field, Math.abs(delta));
1595
+ }
1596
+ };
1597
+ await Promise.all([
1598
+ applyDelta("linesAdded", deltaAdded),
1599
+ applyDelta("linesRemoved", deltaRemoved),
1600
+ applyDelta("filesChanged", deltaFiles)
1601
+ ]);
1602
+ }
1400
1603
  function ensureWorktreeCommitted(issue) {
1401
1604
  const worktreePath = issue.worktreePath;
1402
1605
  if (!worktreePath || !issue.branchName) return;
@@ -1462,650 +1665,109 @@ function mergeWorktree(issue, worktreePath) {
1462
1665
  }
1463
1666
  return result;
1464
1667
  }
1465
- function pushWorktreeBranch(issue) {
1466
- if (!issue.branchName || !issue.baseBranch || !issue.worktreePath) {
1467
- throw new Error(`Issue ${issue.identifier} has no git worktree \u2014 cannot push.`);
1468
- }
1469
- ensureWorktreeCommitted(issue);
1470
- execSync(`git push -u origin "${issue.branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
1471
- try {
1472
- const prUrl = execSync(
1473
- `gh pr create --head "${issue.branchName}" --base "${issue.baseBranch}" --title "${issue.title.replace(/"/g, '\\"')}" --body "Automated by fifony"`,
1474
- { cwd: TARGET_ROOT, encoding: "utf8" }
1475
- ).trim();
1476
- return prUrl;
1477
- } catch {
1478
- try {
1479
- const remote = execSync("git remote get-url origin", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
1480
- const cleanRemote = remote.replace(/\.git$/, "");
1481
- return `${cleanRemote}/compare/${issue.baseBranch}...${issue.branchName}`;
1482
- } catch {
1483
- return `(branch: ${issue.branchName})`;
1484
- }
1668
+ function shouldSkipMergePath(relativePath) {
1669
+ const parts = relativePath.split("/");
1670
+ if (parts.some((s) => s === ".git" || s === "node_modules" || s === ".fifony" || s === "dist" || s === ".tanstack")) {
1671
+ return true;
1485
1672
  }
1673
+ const base = parts.at(-1) ?? "";
1674
+ 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_");
1486
1675
  }
1487
1676
  function mergeWorkspace(issue) {
1488
- if (!issue.branchName || !issue.baseBranch || !issue.worktreePath) {
1489
- throw new Error(`Issue ${issue.identifier} has no git worktree \u2014 cannot merge.`);
1490
- }
1677
+ ensureGitRepoReadyForWorktrees(TARGET_ROOT, "merge issues");
1678
+ assertIssueHasGitWorktree(issue, "merge");
1491
1679
  return mergeWorktree(issue, issue.worktreePath);
1492
1680
  }
1493
- function hydrateIssuePathsFromWorkspace(issue) {
1494
- const inferredPaths = inferChangedWorkspacePaths(issue.workspacePath ?? "", 32, issue);
1495
- if (inferredPaths.length === 0) return [];
1496
- issue.paths = [.../* @__PURE__ */ new Set([...issue.paths ?? [], ...inferredPaths])];
1497
- issue.inferredPaths = [.../* @__PURE__ */ new Set([...issue.inferredPaths ?? [], ...inferredPaths])];
1498
- return inferredPaths;
1499
- }
1500
- function describeRoutingSignals(issue, workspaceDerivedPaths) {
1501
- const explicitPaths = issue.paths ?? [];
1502
- const textDerivedPaths = inferCapabilityPaths({
1503
- id: issue.id,
1504
- identifier: issue.identifier,
1505
- title: issue.title,
1506
- description: issue.description,
1507
- labels: issue.labels
1508
- }).filter((path) => !explicitPaths.includes(path));
1509
- const parts = [];
1510
- if (explicitPaths.length > 0) parts.push(`payload paths=${explicitPaths.join(", ")}`);
1511
- if (textDerivedPaths.length > 0) parts.push(`text hints=${textDerivedPaths.join(", ")}`);
1512
- if (workspaceDerivedPaths.length > 0) parts.push(`workspace diff=${workspaceDerivedPaths.join(", ")}`);
1513
- return parts.join(" | ");
1514
- }
1515
-
1516
- // src/domains/metrics.ts
1517
- function computeMetrics(issues) {
1518
- let planning = 0;
1519
- let queued = 0;
1520
- let inProgress = 0;
1521
- let blocked = 0;
1522
- let done = 0;
1523
- let merged = 0;
1524
- let cancelled = 0;
1525
- const completionTimes = [];
1526
- for (const issue of issues) {
1527
- if (issue.state === "Merged") {
1528
- const duration = issue.durationMs;
1529
- const candidate = typeof duration === "number" && Number.isFinite(duration) ? duration : Number.isFinite(Date.parse(issue.startedAt ?? "")) && Number.isFinite(Date.parse(issue.completedAt ?? "")) ? Date.parse(issue.completedAt) - Date.parse(issue.startedAt) : NaN;
1530
- if (Number.isFinite(candidate) && candidate >= 0) {
1531
- completionTimes.push(candidate);
1532
- }
1533
- }
1534
- switch (issue.state) {
1535
- case "Planning":
1536
- planning += 1;
1537
- break;
1538
- case "Planned":
1539
- queued += 1;
1540
- break;
1541
- case "Queued":
1542
- case "Running":
1543
- case "Reviewing":
1544
- case "Reviewed":
1545
- inProgress += 1;
1546
- break;
1547
- case "Blocked":
1548
- blocked += 1;
1549
- break;
1550
- case "Done":
1551
- done += 1;
1552
- break;
1553
- case "Merged":
1554
- merged += 1;
1555
- break;
1556
- case "Cancelled":
1557
- cancelled += 1;
1558
- break;
1559
- }
1681
+ function dryMerge(issue) {
1682
+ ensureGitRepoReadyForWorktrees(TARGET_ROOT, "preview merges");
1683
+ assertIssueHasGitWorktree(issue, "preview merge");
1684
+ ensureWorktreeCommitted(issue);
1685
+ const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
1686
+ if (currentBranch !== issue.baseBranch) {
1687
+ throw new Error(`Cannot preview merge: current branch is ${currentBranch}, expected ${issue.baseBranch}.`);
1560
1688
  }
1561
- if (completionTimes.length === 0) {
1562
- return {
1563
- total: issues.length,
1564
- planning,
1565
- queued,
1566
- inProgress,
1567
- blocked,
1568
- done,
1569
- merged,
1570
- cancelled,
1571
- activeWorkers: 0
1572
- };
1689
+ const targetStatus = execSync("git status --porcelain", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
1690
+ if (targetStatus) {
1691
+ throw new Error(`Cannot preview merge: target repository has uncommitted changes.`);
1573
1692
  }
1574
- const sortedCompletionTimes = completionTimes.slice().sort((a, b) => a - b);
1575
- const totalCompletionMs = sortedCompletionTimes.reduce((acc, value) => acc + value, 0);
1576
- const mid = Math.floor(sortedCompletionTimes.length / 2);
1577
- const medianCompletionMs = sortedCompletionTimes.length % 2 === 1 ? sortedCompletionTimes[mid] : Math.round((sortedCompletionTimes[mid - 1] + sortedCompletionTimes[mid]) / 2);
1578
- return {
1579
- total: issues.length,
1580
- planning,
1581
- queued,
1582
- inProgress,
1583
- blocked,
1584
- done,
1585
- merged,
1586
- cancelled,
1587
- activeWorkers: 0,
1588
- avgCompletionMs: Math.round(totalCompletionMs / completionTimes.length),
1589
- medianCompletionMs,
1590
- fastestCompletionMs: sortedCompletionTimes[0],
1591
- slowestCompletionMs: sortedCompletionTimes[sortedCompletionTimes.length - 1]
1592
- };
1593
- }
1594
- function computeCapabilityCounts(issues) {
1595
- return issues.reduce((accumulator, issue) => {
1596
- const key = issue.capabilityCategory?.trim() || "default";
1597
- accumulator[key] = (accumulator[key] ?? 0) + 1;
1598
- return accumulator;
1599
- }, {});
1600
- }
1601
-
1602
- // src/persistence/metrics-cache.ts
1603
- var cachedMetrics = null;
1604
- var metricsStale = true;
1605
- function invalidateMetrics() {
1606
- metricsStale = true;
1607
- }
1608
- function getMetrics(issues) {
1609
- if (!metricsStale && cachedMetrics) return cachedMetrics;
1610
- cachedMetrics = computeMetrics(issues);
1611
- metricsStale = false;
1612
- return cachedMetrics;
1613
- }
1614
-
1615
- // src/persistence/dirty-tracker.ts
1616
- var dirtyIssueIds = /* @__PURE__ */ new Set();
1617
- var dirtyIssuePlanIds = /* @__PURE__ */ new Set();
1618
- var dirtyEventIds = /* @__PURE__ */ new Set();
1619
- function markIssueDirty(id) {
1620
- dirtyIssueIds.add(id);
1621
- }
1622
- function markIssuePlanDirty(id) {
1623
- dirtyIssuePlanIds.add(id);
1624
- }
1625
- function markEventDirty(id) {
1626
- dirtyEventIds.add(id);
1627
- }
1628
- function hasDirtyState() {
1629
- return dirtyIssueIds.size > 0 || dirtyEventIds.size > 0;
1630
- }
1631
- function getDirtyIssueIds() {
1632
- return dirtyIssueIds;
1633
- }
1634
- function getDirtyEventIds() {
1635
- return dirtyEventIds;
1636
- }
1637
- function snapshotAndClearDirtyIssueIds() {
1638
- const snapshot = new Set(dirtyIssueIds);
1639
- for (const id of snapshot) dirtyIssueIds.delete(id);
1640
- return snapshot;
1641
- }
1642
- function snapshotAndClearDirtyIssuePlanIds() {
1643
- const snapshot = new Set(dirtyIssuePlanIds);
1644
- for (const id of snapshot) dirtyIssuePlanIds.delete(id);
1645
- return snapshot;
1646
- }
1647
- function snapshotAndClearDirtyEventIds() {
1648
- const snapshot = new Set(dirtyEventIds);
1649
- for (const id of snapshot) dirtyEventIds.delete(id);
1650
- return snapshot;
1651
- }
1652
- function markAllIssuesDirty(ids) {
1653
- for (const id of ids) dirtyIssueIds.add(id);
1654
- }
1655
- function markAllIssuePlansDirty(ids) {
1656
- for (const id of ids) dirtyIssuePlanIds.add(id);
1657
- }
1658
- function markAllEventsDirty(ids) {
1659
- for (const id of ids) dirtyEventIds.add(id);
1660
- }
1661
-
1662
- // src/persistence/plugins/issue-state-machine.ts
1663
- var fsmEventEmitter = null;
1664
- function setFsmEventEmitter(emitter) {
1665
- fsmEventEmitter = emitter;
1666
- }
1667
- function emitFsmEvent(issueId, kind, message) {
1668
- if (fsmEventEmitter) {
1693
+ let conflictFiles = [];
1694
+ let willConflict = false;
1695
+ try {
1696
+ execSync(
1697
+ `git merge --no-commit --no-ff "${issue.branchName}"`,
1698
+ { cwd: TARGET_ROOT, stdio: "pipe" }
1699
+ );
1700
+ } catch {
1701
+ willConflict = true;
1669
1702
  try {
1670
- fsmEventEmitter(issueId, kind, message);
1703
+ const conflictOut = execSync(
1704
+ "git diff --name-only --diff-filter=U",
1705
+ { cwd: TARGET_ROOT, encoding: "utf8" }
1706
+ );
1707
+ conflictFiles = conflictOut.trim().split("\n").filter(Boolean);
1671
1708
  } catch {
1672
1709
  }
1673
1710
  }
1674
- }
1675
- async function lazyEnqueueForPlanning(issue) {
1676
- const { enqueueForPlanning } = await import("./queue-workers-Q3IWRFLI.js");
1677
- return enqueueForPlanning(issue);
1678
- }
1679
- async function lazyEnqueueForExecution(issue) {
1680
- const { enqueueForExecution } = await import("./queue-workers-Q3IWRFLI.js");
1681
- return enqueueForExecution(issue);
1682
- }
1683
- async function lazyEnqueueForReview(issue) {
1684
- const { enqueueForReview } = await import("./queue-workers-Q3IWRFLI.js");
1685
- return enqueueForReview(issue);
1686
- }
1687
- var ISSUE_STATE_MACHINE_ID = "issue-lifecycle";
1688
- function markDirtyAndInvalidate(issueId) {
1689
- markIssueDirty(issueId);
1690
- invalidateMetrics();
1691
- }
1692
- function resolveIssue(context) {
1693
- return context.issue ?? null;
1694
- }
1695
- function issueResource(machine) {
1696
- return machine.database?.resources?.[S3DB_ISSUE_RESOURCE];
1697
- }
1698
- var STALE_TIMEOUT_MS = 24e5;
1699
- async function isStaleIssue(context, _entityId) {
1700
- const issue = resolveIssue(context);
1701
- if (!issue) return false;
1702
- return Date.now() - Date.parse(issue.updatedAt) > STALE_TIMEOUT_MS;
1703
- }
1704
- var issueStateMachineConfig = {
1705
- persistTransitions: true,
1706
- workerId: `fifony-${process.pid}`,
1707
- lockTimeout: 5e3,
1708
- lockTTL: 30,
1709
- stateMachines: {
1710
- [ISSUE_STATE_MACHINE_ID]: {
1711
- resource: S3DB_ISSUE_RESOURCE,
1712
- stateField: "state",
1713
- initialState: "Planning",
1714
- autoCleanup: false,
1715
- states: {
1716
- Planning: {
1717
- on: { PLANNED: "Planned", CANCEL: "Cancelled" },
1718
- entry: "onEnterPlanning"
1719
- },
1720
- Planned: {
1721
- on: { QUEUE: "Queued", REPLAN: "Planning", CANCEL: "Cancelled" },
1722
- entry: "onEnterPlanned"
1723
- },
1724
- Queued: {
1725
- on: { RUN: "Running" },
1726
- entry: "onEnterQueued"
1727
- },
1728
- Running: {
1729
- on: { REVIEW: "Reviewing", REQUEUE: "Queued", BLOCK: "Blocked" },
1730
- guards: { BLOCK: "requireBlockReason" },
1731
- triggers: [{
1732
- type: "cron",
1733
- cron: "*/10 * * * *",
1734
- sendEvent: "BLOCK",
1735
- condition: isStaleIssue
1736
- }]
1737
- },
1738
- Reviewing: {
1739
- on: { REVIEWED: "Reviewed", REQUEUE: "Queued", BLOCK: "Blocked" },
1740
- entry: "onEnterReviewing",
1741
- guards: { BLOCK: "requireBlockReason" },
1742
- triggers: [{
1743
- type: "cron",
1744
- cron: "*/10 * * * *",
1745
- sendEvent: "BLOCK",
1746
- condition: isStaleIssue
1747
- }]
1748
- },
1749
- Reviewed: {
1750
- on: { DONE: "Done", REQUEUE: "Queued", REPLAN: "Planning", CANCEL: "Cancelled" }
1751
- },
1752
- Blocked: {
1753
- on: { UNBLOCK: "Queued", REPLAN: "Planning", CANCEL: "Cancelled" },
1754
- entry: "onEnterBlocked"
1755
- },
1756
- Done: {
1757
- on: { MERGE: "Merged", REOPEN: "Planning" },
1758
- entry: "onEnterDone"
1759
- },
1760
- Merged: {
1761
- on: { REOPEN: "Planning" },
1762
- type: "final",
1763
- entry: "onEnterMerged"
1764
- },
1765
- Cancelled: {
1766
- on: { REOPEN: "Planning" },
1767
- type: "final",
1768
- entry: "onEnterCancelled"
1769
- }
1770
- }
1771
- }
1772
- },
1773
- // ── Actions: (context, event, machine) ──────────────────────────────────
1774
- // context = payload from send()
1775
- // event = event name ("PLANNED", "BLOCK", etc.)
1776
- // machine = { database, machineId, entityId }
1777
- //
1778
- // Actions only mutate the in-memory issue + fire side effects (enqueue, s3db patch).
1779
- // Dirty tracking + metrics invalidation is done once in executeTransition() after send().
1780
- actions: {
1781
- onEnterPlanning: async (context, _event, _machine) => {
1782
- const issue = resolveIssue(context);
1783
- if (issue) {
1784
- issue.planningStatus = "idle";
1785
- issue.planningError = void 0;
1786
- issue.nextRetryAt = void 0;
1787
- issue.lastError = void 0;
1788
- emitFsmEvent(issue.id, "state", `${issue.identifier} entered Planning.`);
1789
- lazyEnqueueForPlanning(issue).catch(() => {
1790
- });
1791
- }
1792
- },
1793
- onEnterPlanned: async (context, _event, _machine) => {
1794
- const issue = resolveIssue(context);
1795
- if (issue) {
1796
- issue.nextRetryAt = void 0;
1797
- issue.lastError = void 0;
1798
- emitFsmEvent(issue.id, "state", `Plan approved \u2014 ${issue.identifier} moved to Planned.`);
1799
- }
1800
- },
1801
- onEnterQueued: async (context, _event, _machine) => {
1802
- const issue = resolveIssue(context);
1803
- if (issue) {
1804
- issue.nextRetryAt = void 0;
1805
- issue.lastError = void 0;
1806
- logger.info({ issueId: issue.id, identifier: issue.identifier }, "[FSM] onEnterQueued \u2014 enqueuing for execution");
1807
- emitFsmEvent(issue.id, "state", `${issue.identifier} queued for execution.`);
1808
- lazyEnqueueForExecution(issue).catch((err) => {
1809
- logger.error({ err, issueId: issue.id }, "[FSM] onEnterQueued \u2014 enqueue FAILED");
1810
- });
1811
- }
1812
- },
1813
- onEnterReviewing: async (context, _event, machine) => {
1814
- const issue = resolveIssue(context);
1815
- const ts = (/* @__PURE__ */ new Date()).toISOString();
1816
- if (issue) {
1817
- issue.reviewingAt = ts;
1818
- issue.lastError = void 0;
1819
- emitFsmEvent(issue.id, "state", `${issue.identifier} moved to Reviewing.`);
1820
- lazyEnqueueForReview(issue).catch(() => {
1821
- });
1822
- }
1823
- const res = issueResource(machine);
1824
- if (res) {
1825
- res.patch(machine.entityId, { reviewingAt: ts }).catch(() => {
1826
- });
1827
- }
1828
- },
1829
- onEnterBlocked: async (context, _event, _machine) => {
1830
- const issue = resolveIssue(context);
1831
- const note = typeof context.note === "string" ? context.note : "Blocked";
1832
- if (issue) {
1833
- issue.lastError = note;
1834
- emitFsmEvent(issue.id, "error", `${issue.identifier} blocked: ${note}`);
1835
- }
1836
- },
1837
- onEnterDone: async (context, _event, _machine) => {
1838
- const issue = resolveIssue(context);
1839
- if (issue) {
1840
- issue.nextRetryAt = void 0;
1841
- issue.lastError = void 0;
1842
- if (!issue.linesAdded && !issue.linesRemoved && issue.baseBranch && issue.branchName) {
1843
- computeDiffStats(issue);
1844
- }
1845
- emitFsmEvent(issue.id, "state", `${issue.identifier} approved \u2014 waiting for merge.`);
1846
- }
1847
- },
1848
- onEnterMerged: async (context, _event, machine) => {
1849
- const issue = resolveIssue(context);
1850
- const ts = (/* @__PURE__ */ new Date()).toISOString();
1851
- const week = isoWeek();
1852
- if (issue) {
1853
- if (!issue.linesAdded && !issue.linesRemoved && issue.baseBranch && issue.branchName) {
1854
- computeDiffStats(issue);
1855
- }
1856
- issue.completedAt = ts;
1857
- issue.terminalWeek = week;
1858
- if (!issue.mergedAt) issue.mergedAt = ts;
1859
- issue.nextRetryAt = void 0;
1860
- issue.lastError = void 0;
1861
- emitFsmEvent(issue.id, "state", `${issue.identifier} merged.`);
1862
- }
1863
- const res = issueResource(machine);
1864
- if (res) {
1865
- res.patch(machine.entityId, {
1866
- completedAt: ts,
1867
- terminalWeek: week,
1868
- mergedAt: issue?.mergedAt ?? ts,
1869
- nextRetryAt: void 0,
1870
- lastError: void 0,
1871
- linesAdded: issue?.linesAdded,
1872
- linesRemoved: issue?.linesRemoved,
1873
- filesChanged: issue?.filesChanged,
1874
- branchName: issue?.branchName,
1875
- workspacePath: issue?.workspacePath,
1876
- worktreePath: issue?.worktreePath
1877
- }).catch(() => {
1878
- });
1879
- const add = res.add;
1880
- if (typeof add === "function" && issue) {
1881
- try {
1882
- if (issue.linesAdded) await add.call(res, machine.entityId, "linesAdded", issue.linesAdded);
1883
- if (issue.linesRemoved) await add.call(res, machine.entityId, "linesRemoved", issue.linesRemoved);
1884
- if (issue.filesChanged) await add.call(res, machine.entityId, "filesChanged", issue.filesChanged);
1885
- logger.info({ issueId: issue.id, linesAdded: issue.linesAdded, linesRemoved: issue.linesRemoved, filesChanged: issue.filesChanged }, "[FSM] EC add() for diff stats on merge");
1886
- } catch (err) {
1887
- logger.warn({ err: String(err), issueId: issue?.id }, "[FSM] EC add() failed for diff stats");
1888
- }
1889
- }
1890
- }
1891
- },
1892
- onEnterCancelled: async (context, _event, machine) => {
1893
- const issue = resolveIssue(context);
1894
- const ts = (/* @__PURE__ */ new Date()).toISOString();
1895
- const week = isoWeek();
1896
- if (issue) {
1897
- issue.completedAt = ts;
1898
- issue.terminalWeek = week;
1899
- issue.nextRetryAt = void 0;
1900
- emitFsmEvent(issue.id, "state", `${issue.identifier} cancelled.`);
1901
- }
1902
- const res = issueResource(machine);
1903
- if (res) {
1904
- res.patch(machine.entityId, {
1905
- completedAt: ts,
1906
- terminalWeek: week,
1907
- nextRetryAt: void 0
1908
- }).catch(() => {
1909
- });
1910
- }
1911
- }
1912
- },
1913
- // ── Guards: (context, event, machine) ───────────────────────────────────
1914
- guards: {
1915
- requireBlockReason: async (context, _event, _machine) => {
1916
- return typeof context.note === "string" && context.note.trim().length > 0;
1917
- }
1918
- }
1919
- };
1920
- var EVENT_TO_STATE = {
1921
- PLANNED: "Planned",
1922
- QUEUE: "Queued",
1923
- RUN: "Running",
1924
- REVIEW: "Reviewing",
1925
- REVIEWED: "Reviewed",
1926
- DONE: "Done",
1927
- MERGE: "Merged",
1928
- CANCEL: "Cancelled",
1929
- BLOCK: "Blocked",
1930
- UNBLOCK: "Queued",
1931
- REPLAN: "Planning",
1932
- REQUEUE: "Queued",
1933
- REOPEN: "Planning"
1934
- };
1935
- function eventToTargetState(event) {
1936
- return EVENT_TO_STATE[event];
1937
- }
1938
- function getStatesFromConfig() {
1939
- const machine = issueStateMachineConfig.stateMachines[ISSUE_STATE_MACHINE_ID];
1940
- const result = {};
1941
- for (const [state, def] of Object.entries(machine.states)) {
1942
- result[state] = def.on ?? {};
1943
- }
1944
- return result;
1945
- }
1946
- function getStateMachineTransitions() {
1947
- const edges = getStatesFromConfig();
1948
- const result = {};
1949
- for (const [state, events] of Object.entries(edges)) {
1950
- const targets = [...new Set(Object.values(events))];
1951
- result[state] = targets;
1952
- }
1953
- return result;
1954
- }
1955
- function findIssueStateMachineTransitionPath(_machineDefinition, from, to) {
1956
- if (from === to) return [];
1957
- const edges = getStatesFromConfig();
1958
- if (!edges[from] || !edges[to]) return null;
1959
- const queue = [from];
1960
- const previousState = /* @__PURE__ */ new Map();
1961
- const previousEvent = /* @__PURE__ */ new Map();
1962
- previousState.set(from, "");
1963
- for (let i = 0; i < queue.length; i += 1) {
1964
- const current = queue[i];
1965
- const transitions = edges[current];
1966
- if (!transitions) continue;
1967
- for (const [evt, next] of Object.entries(transitions)) {
1968
- if (previousState.has(next)) continue;
1969
- previousState.set(next, current);
1970
- previousEvent.set(next, evt);
1971
- if (next === to) {
1972
- const events = [];
1973
- let cursor = next;
1974
- while (cursor !== from) {
1975
- const prev = previousState.get(cursor);
1976
- const e = previousEvent.get(cursor);
1977
- if (!prev || !e) return null;
1978
- events.unshift(e);
1979
- cursor = prev;
1980
- }
1981
- return events;
1982
- }
1983
- queue.push(next);
1984
- }
1985
- }
1986
- return null;
1987
- }
1988
- var issueResourceStateApi = null;
1989
- function setIssueResourceStateApi(api) {
1990
- issueResourceStateApi = api;
1991
- }
1992
- function getIssueResourceStateApi() {
1993
- return issueResourceStateApi;
1994
- }
1995
- var issueStateMachinePlugin = null;
1996
- function setIssueStateMachinePlugin(plugin) {
1997
- issueStateMachinePlugin = plugin;
1998
- }
1999
- function getIssueStateMachinePlugin() {
2000
- return issueStateMachinePlugin;
2001
- }
2002
- function getIssueStateMachineDefinition() {
2003
- return issueStateMachinePlugin?.getMachineDefinition?.(ISSUE_STATE_MACHINE_ID) ?? issueStateMachineConfig.stateMachines[ISSUE_STATE_MACHINE_ID];
2004
- }
2005
- function getIssueStateMachineInitialState() {
2006
- return issueStateMachineConfig.stateMachines[ISSUE_STATE_MACHINE_ID].initialState;
2007
- }
2008
- async function executeTransition(issue, event, context = {}) {
2009
- const ts = (/* @__PURE__ */ new Date()).toISOString();
2010
- const previous = issue.state;
2011
- const targetState = eventToTargetState(event);
2012
- if (!targetState) {
2013
- throw new Error(`Unknown FSM event '${event}' for issue ${issue.id}.`);
2014
- }
2015
- const resourceApi = getIssueResourceStateApi();
2016
- const plugin = getIssueStateMachinePlugin();
2017
- const sendContext = { ...context, issue };
2018
- if (resourceApi) {
2019
- try {
2020
- await resourceApi.send(issue.id, event, sendContext);
2021
- } catch (err) {
2022
- if (String(err).includes("not found") || String(err).includes("not initialized")) {
2023
- await resourceApi.initialize(issue.id, { issue, state: previous });
2024
- await resourceApi.send(issue.id, event, sendContext);
2025
- } else {
2026
- throw err;
2027
- }
2028
- }
2029
- } else if (plugin?.send) {
1711
+ try {
1712
+ execSync("git merge --abort", { cwd: TARGET_ROOT, stdio: "pipe" });
1713
+ } catch {
2030
1714
  try {
2031
- await plugin.send(ISSUE_STATE_MACHINE_ID, issue.id, event, sendContext);
2032
- } catch (err) {
2033
- if (plugin.initializeEntity && String(err).includes("not found")) {
2034
- await plugin.initializeEntity(ISSUE_STATE_MACHINE_ID, issue.id, { issue, state: previous });
2035
- await plugin.send(ISSUE_STATE_MACHINE_ID, issue.id, event, sendContext);
2036
- } else {
2037
- throw err;
2038
- }
2039
- }
2040
- } else {
2041
- if (previous !== targetState) {
2042
- const edges = getStatesFromConfig();
2043
- const stateTransitions = edges[previous];
2044
- if (!stateTransitions || !stateTransitions[event]) {
2045
- throw new Error(`State machine does not allow event '${event}' from '${previous}' for issue ${issue.id}.`);
2046
- }
2047
- }
2048
- const stateDef = issueStateMachineConfig.stateMachines[ISSUE_STATE_MACHINE_ID].states[previous];
2049
- if (stateDef && "guards" in stateDef && stateDef.guards?.[event]) {
2050
- const guardName = stateDef.guards[event];
2051
- const guardFn = issueStateMachineConfig.guards[guardName];
2052
- if (guardFn) {
2053
- const allowed = await guardFn(sendContext, event, { database: null, machineId: ISSUE_STATE_MACHINE_ID, entityId: issue.id });
2054
- if (!allowed) {
2055
- throw new Error(`Guard '${guardName}' rejected event '${event}' for issue ${issue.id}.`);
2056
- }
2057
- }
2058
- }
2059
- const targetDef = issueStateMachineConfig.stateMachines[ISSUE_STATE_MACHINE_ID].states[targetState];
2060
- if (targetDef && "entry" in targetDef && typeof targetDef.entry === "string") {
2061
- const actionName = targetDef.entry;
2062
- const actionFn = issueStateMachineConfig.actions[actionName];
2063
- if (actionFn) {
2064
- await actionFn(sendContext, event, { database: null, machineId: ISSUE_STATE_MACHINE_ID, entityId: issue.id });
2065
- }
1715
+ execSync("git reset --hard HEAD", { cwd: TARGET_ROOT, stdio: "pipe" });
1716
+ } catch {
2066
1717
  }
2067
1718
  }
2068
- issue.state = targetState;
2069
- issue.updatedAt = ts;
2070
- const note = typeof context.note === "string" ? context.note : `${event}: ${previous} \u2192 ${targetState}`;
2071
- issue.history.push(`[${ts}] ${note}`);
2072
- if (TERMINAL_STATES.has(previous) && !TERMINAL_STATES.has(targetState)) {
2073
- issue.terminalWeek = "";
1719
+ let changedFiles = 0;
1720
+ try {
1721
+ const diffOut = execSync(
1722
+ `git diff --name-only "${issue.baseBranch}"..."${issue.branchName}"`,
1723
+ { cwd: TARGET_ROOT, encoding: "utf8" }
1724
+ );
1725
+ changedFiles = diffOut.trim().split("\n").filter(Boolean).length;
1726
+ } catch {
2074
1727
  }
2075
- markDirtyAndInvalidate(issue.id);
2076
- return { previousState: previous };
1728
+ return { willConflict, conflictFiles, canMerge: !willConflict, changedFiles };
2077
1729
  }
2078
- async function getIssueTransitionHistory(issueId, options) {
2079
- const resourceApi = getIssueResourceStateApi();
2080
- if (resourceApi?.history) {
1730
+ function rebaseWorktree(issue) {
1731
+ ensureGitRepoReadyForWorktrees(TARGET_ROOT, "rebase worktrees");
1732
+ assertIssueHasGitWorktree(issue, "rebase");
1733
+ ensureWorktreeCommitted(issue);
1734
+ try {
1735
+ execSync(
1736
+ `git rebase "${issue.baseBranch}"`,
1737
+ { cwd: issue.worktreePath, stdio: "pipe" }
1738
+ );
1739
+ return { success: true, conflictFiles: [] };
1740
+ } catch {
1741
+ let conflictFiles = [];
2081
1742
  try {
2082
- return await resourceApi.history(issueId, options);
1743
+ const conflictOut = execSync(
1744
+ "git diff --name-only --diff-filter=U",
1745
+ { cwd: issue.worktreePath, encoding: "utf8" }
1746
+ );
1747
+ conflictFiles = conflictOut.trim().split("\n").filter(Boolean);
2083
1748
  } catch {
2084
1749
  }
2085
- }
2086
- const plugin = getIssueStateMachinePlugin();
2087
- if (plugin?.getTransitionHistory) {
2088
1750
  try {
2089
- return await plugin.getTransitionHistory(ISSUE_STATE_MACHINE_ID, issueId, options);
1751
+ execSync("git rebase --abort", { cwd: issue.worktreePath, stdio: "pipe" });
2090
1752
  } catch {
2091
1753
  }
1754
+ return { success: false, conflictFiles };
2092
1755
  }
2093
- return [];
2094
1756
  }
2095
- async function canTransitionIssue(issueId, event) {
2096
- const resourceApi = getIssueResourceStateApi();
2097
- if (resourceApi?.canTransition) {
2098
- try {
2099
- return await resourceApi.canTransition(issueId, event);
2100
- } catch {
1757
+ function hydrateIssuePathsFromWorkspace(issue) {
1758
+ const inferredPaths = inferChangedWorkspacePaths(issue.workspacePath ?? "", 32, issue);
1759
+ if (inferredPaths.length === 0) return [];
1760
+ issue.paths = [.../* @__PURE__ */ new Set([...issue.paths ?? [], ...inferredPaths])];
1761
+ return inferredPaths;
1762
+ }
1763
+ function writeVersionedArtifacts(workspacePath, prefix, planVersion, attempt, sources) {
1764
+ const { writeFileSync: _wfs, readFileSync: _rfs, existsSync: _es } = { writeFileSync: writeFileSync2, readFileSync: readFileSync4, existsSync: existsSync7 };
1765
+ for (const { srcFile, destSuffix } of sources) {
1766
+ const src = join7(workspacePath, srcFile);
1767
+ if (_es(src)) {
1768
+ _wfs(join7(workspacePath, `${prefix}.v${planVersion}a${attempt}.${destSuffix}`), _rfs(src, "utf8"), "utf8");
2101
1769
  }
2102
1770
  }
2103
- return false;
2104
- }
2105
- function visualizeStateMachine() {
2106
- const plugin = getIssueStateMachinePlugin();
2107
- if (!plugin?.visualize) return null;
2108
- return plugin.visualize(ISSUE_STATE_MACHINE_ID);
2109
1771
  }
2110
1772
 
2111
1773
  export {
@@ -2119,54 +1781,34 @@ export {
2119
1781
  detectAvailableProviders,
2120
1782
  readCodexConfig,
2121
1783
  resolveDefaultProvider,
2122
- getCapabilityRoutingOptions,
2123
- applyCapabilityMetadata,
2124
1784
  getEffectiveAgentProviders,
2125
- computeMetrics,
2126
- computeCapabilityCounts,
2127
- getMetrics,
2128
1785
  runCommandWithTimeout,
2129
1786
  runHook,
1787
+ buildRetryContext,
1788
+ buildPrompt,
2130
1789
  buildTurnPrompt,
2131
1790
  buildProviderBasePrompt,
1791
+ bootstrapSource,
2132
1792
  setSkipSource,
1793
+ ensureSourceReady,
1794
+ getGitRepoStatus,
1795
+ ensureGitRepoReadyForWorktrees,
1796
+ initializeGitRepoForWorktrees,
1797
+ assertIssueHasGitWorktree,
2133
1798
  detectDefaultBranch,
1799
+ createGitWorktree,
2134
1800
  prepareWorkspace,
2135
1801
  cleanWorkspace,
1802
+ inferChangedWorkspacePaths,
2136
1803
  computeDiffStats,
2137
1804
  parseDiffStats,
1805
+ syncIssueDiffStatsToStore,
2138
1806
  ensureWorktreeCommitted,
2139
- pushWorktreeBranch,
1807
+ shouldSkipMergePath,
2140
1808
  mergeWorkspace,
1809
+ dryMerge,
1810
+ rebaseWorktree,
2141
1811
  hydrateIssuePathsFromWorkspace,
2142
- describeRoutingSignals,
2143
- markIssueDirty,
2144
- markIssuePlanDirty,
2145
- markEventDirty,
2146
- hasDirtyState,
2147
- getDirtyIssueIds,
2148
- getDirtyEventIds,
2149
- snapshotAndClearDirtyIssueIds,
2150
- snapshotAndClearDirtyIssuePlanIds,
2151
- snapshotAndClearDirtyEventIds,
2152
- markAllIssuesDirty,
2153
- markAllIssuePlansDirty,
2154
- markAllEventsDirty,
2155
- setFsmEventEmitter,
2156
- ISSUE_STATE_MACHINE_ID,
2157
- issueStateMachineConfig,
2158
- eventToTargetState,
2159
- getStateMachineTransitions,
2160
- findIssueStateMachineTransitionPath,
2161
- setIssueResourceStateApi,
2162
- getIssueResourceStateApi,
2163
- setIssueStateMachinePlugin,
2164
- getIssueStateMachinePlugin,
2165
- getIssueStateMachineDefinition,
2166
- getIssueStateMachineInitialState,
2167
- executeTransition,
2168
- getIssueTransitionHistory,
2169
- canTransitionIssue,
2170
- visualizeStateMachine
1812
+ writeVersionedArtifacts
2171
1813
  };
2172
- //# sourceMappingURL=chunk-XN2QKKMY.js.map
1814
+ //# sourceMappingURL=chunk-XVF6GOVS.js.map