fifony 0.1.26 → 0.1.27-next.84df008

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 (36) hide show
  1. package/README.md +152 -129
  2. package/app/dist/assets/{KeyboardShortcutsHelp-lTNj9GiT.js → KeyboardShortcutsHelp-By0KcDhZ.js} +1 -1
  3. package/app/dist/assets/OnboardingWizard-5cz7Onsu.js +1 -0
  4. package/app/dist/assets/{analytics.lazy-C42PFRzr.js → analytics.lazy-ArFwnOEn.js} +1 -1
  5. package/app/dist/assets/{createLucideIcon-BWC-guQt.js → createLucideIcon-DgMTp0yx.js} +1 -1
  6. package/app/dist/assets/index-59O8esMr.js +45 -0
  7. package/app/dist/assets/{index-Qr2OPvRO.css → index-DuBwUsuf.css} +1 -1
  8. package/app/dist/assets/vendor-D-IqxHHu.js +9 -0
  9. package/app/dist/dinofffaur.webp +0 -0
  10. package/app/dist/index.html +4 -4
  11. package/app/dist/service-worker.js +1 -1
  12. package/dist/agent/run-local.js +57 -144
  13. package/dist/agent-KMXNVDRO.js +74 -0
  14. package/dist/chunk-FJR4ALEN.js +847 -0
  15. package/dist/{chunk-2F3Q2MAG.js → chunk-HSGUPFTV.js} +1224 -611
  16. package/dist/chunk-O5AEQXUV.js +311 -0
  17. package/dist/chunk-OONOOWNC.js +123 -0
  18. package/dist/{chunk-NFHVAIPW.js → chunk-UYCDOH6S.js} +380 -795
  19. package/dist/chunk-XENKNHFS.js +295 -0
  20. package/dist/cli.js +6 -4
  21. package/dist/issue-runner-JJAFMHKV.js +15 -0
  22. package/dist/{issue-state-machine-OWABY5S2.js → issue-state-machine-ACMUJSXC.js} +5 -3
  23. package/dist/issues-VDFXBK3N.js +40 -0
  24. package/dist/mcp/server.js +23 -121
  25. package/dist/queue-workers-U47CVPTO.js +23 -0
  26. package/dist/scheduler-MEXEDV4M.js +21 -0
  27. package/dist/{store-WN47MDT5.js → store-AG6LLYJ7.js} +7 -5
  28. package/dist/workspace-474CCKTW.js +44 -0
  29. package/package.json +6 -6
  30. package/app/dist/assets/OnboardingWizard-B6LlJR9B.js +0 -1
  31. package/app/dist/assets/index-fVSxs9d5.js +0 -43
  32. package/app/dist/assets/vendor-BTlTWMUF.js +0 -9
  33. package/dist/chunk-AMOGDOM7.js +0 -796
  34. package/dist/chunk-IA7IMQ5F.js +0 -91
  35. package/dist/issue-runner-DA4IDLKX.js +0 -13
  36. package/dist/queue-workers-JIH5ZMNQ.js +0 -20
@@ -1,36 +1,32 @@
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
19
  cpSync,
24
- existsSync as existsSync6,
20
+ existsSync as existsSync7,
25
21
  mkdirSync,
26
22
  readdirSync,
27
- readFileSync as readFileSync3,
23
+ readFileSync as readFileSync4,
28
24
  rmSync as rmSync2,
29
25
  statSync,
30
26
  writeFileSync as writeFileSync2
31
27
  } from "fs";
32
28
  import { copyFile, mkdir, readdir, stat, writeFile } from "fs/promises";
33
- import { extname, join as join7, resolve } from "path";
29
+ import { extname as extname2, join as join7, resolve } from "path";
34
30
  import { execSync } from "child_process";
35
31
 
36
32
  // src/agents/command-executor.ts
@@ -45,15 +41,17 @@ import { spawn } from "child_process";
45
41
 
46
42
  // src/agents/providers.ts
47
43
  import { execFileSync as execFileSync2 } from "child_process";
48
- import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
44
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
49
45
  import { join as join5 } from "path";
50
46
  import { homedir as homedir2 } from "os";
51
47
 
52
48
  // src/agents/adapters/claude.ts
53
- import { existsSync } from "fs";
49
+ import { existsSync as existsSync2 } from "fs";
54
50
  import { join } from "path";
55
51
 
56
52
  // src/agents/adapters/shared.ts
53
+ import { existsSync, readFileSync } from "fs";
54
+ import { basename, extname } from "path";
57
55
  function buildPlanContextSection(plan) {
58
56
  const parts = ["## Plan Context", "", `**Summary:** ${plan.summary}`];
59
57
  if (plan.assumptions?.length) {
@@ -126,17 +124,17 @@ function buildValidationSection(plan) {
126
124
  return parts.join("\n");
127
125
  }
128
126
  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) {
127
+ const skills = plan.suggestedSkills ?? [];
128
+ const agents = plan.suggestedAgents ?? [];
129
+ if (skills.length === 0 && agents.length === 0) return "";
130
+ const parts = ["## Recommended Skills & Agents"];
131
+ if (skills.length > 0) {
134
132
  parts.push("", "**Skills to activate:**");
135
- td.skillsToUse.forEach((s) => parts.push(`- **${s.name}**: ${s.why}`));
133
+ skills.forEach((s) => parts.push(`- ${s}`));
136
134
  }
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}`));
135
+ if (agents.length > 0) {
136
+ parts.push("", "**Agents to use:**");
137
+ agents.forEach((a) => parts.push(`- ${a}`));
140
138
  }
141
139
  return parts.join("\n");
142
140
  }
@@ -181,6 +179,32 @@ function extractValidationCommands(plan) {
181
179
  }
182
180
  return { pre: [...new Set(pre)], post: [...new Set(post)] };
183
181
  }
182
+ var MIME_MAP = {
183
+ ".png": "image/png",
184
+ ".jpg": "image/jpeg",
185
+ ".jpeg": "image/jpeg",
186
+ ".gif": "image/gif",
187
+ ".webp": "image/webp",
188
+ ".svg": "image/svg+xml"
189
+ };
190
+ function buildImagePromptSection(imagePaths) {
191
+ const validPaths = imagePaths.filter((p) => existsSync(p));
192
+ if (validPaths.length === 0) return "";
193
+ const parts = ["## Attached Images", ""];
194
+ for (const imgPath of validPaths) {
195
+ const ext = extname(imgPath).toLowerCase();
196
+ const mime = MIME_MAP[ext] || "image/png";
197
+ const name = basename(imgPath);
198
+ try {
199
+ const data = readFileSync(imgPath).toString("base64");
200
+ parts.push(`### ${name}`);
201
+ parts.push(`![${name}](data:${mime};base64,${data})`);
202
+ parts.push("");
203
+ } catch {
204
+ }
205
+ }
206
+ return parts.length > 2 ? parts.join("\n") : "";
207
+ }
184
208
  function buildExecutionPayload(issue, provider, plan, workspacePath) {
185
209
  const strategy = plan.executionStrategy;
186
210
  const hasPhases = Boolean(plan.phases?.length);
@@ -191,7 +215,6 @@ function buildExecutionPayload(issue, provider, plan, workspacePath) {
191
215
  identifier: issue.identifier,
192
216
  title: issue.title,
193
217
  description: issue.description || "",
194
- priority: issue.priority,
195
218
  labels: issue.labels || [],
196
219
  paths: issue.paths || []
197
220
  },
@@ -200,14 +223,13 @@ function buildExecutionPayload(issue, provider, plan, workspacePath) {
200
223
  role: provider.role,
201
224
  model: provider.model || "default",
202
225
  effort: provider.reasoningEffort || "medium",
203
- capabilityCategory: provider.capabilityCategory || "",
204
226
  overlays: provider.overlays || []
205
227
  },
206
228
  executionIntent: {
207
229
  complexity: plan.estimatedComplexity,
208
230
  approach: strategy?.approach || "",
209
231
  rationale: strategy?.whyThisApproach || "",
210
- workPattern: hasPhases ? "phased" : plan.toolingDecision?.shouldUseSubagents ? "parallel_subtasks" : "sequential"
232
+ workPattern: hasPhases ? "phased" : "sequential"
211
233
  },
212
234
  plan: {
213
235
  summary: plan.summary,
@@ -242,8 +264,8 @@ function buildExecutionPayload(issue, provider, plan, workspacePath) {
242
264
  mitigation: r.mitigation || ""
243
265
  })),
244
266
  tooling: {
245
- skills: plan.toolingDecision?.skillsToUse || [],
246
- subagents: plan.toolingDecision?.subagentsToUse || []
267
+ skills: plan.suggestedSkills || [],
268
+ agents: plan.suggestedAgents || []
247
269
  },
248
270
  targetPaths: plan.suggestedPaths || [],
249
271
  workspacePath,
@@ -291,13 +313,18 @@ function extractPlanDirs(plan) {
291
313
  // src/agents/adapters/claude.ts
292
314
  function buildClaudeCommand(options) {
293
315
  const parts = ["claude", "--print"];
294
- if (!options.noToolAccess) {
316
+ if (options.readOnly) {
317
+ parts.push("--permission-mode plan");
318
+ } else if (!options.noToolAccess) {
295
319
  parts.push("--dangerously-skip-permissions");
296
320
  }
297
321
  parts.push("--no-session-persistence", "--output-format json");
298
322
  if (options.effort) {
299
323
  parts.push(`--effort ${options.effort}`);
300
324
  }
325
+ if (options.maxBudgetUsd && options.maxBudgetUsd > 0) {
326
+ parts.push(`--max-budget-usd ${options.maxBudgetUsd}`);
327
+ }
301
328
  if (options.jsonSchema) {
302
329
  parts.push(`--json-schema '${options.jsonSchema}'`);
303
330
  }
@@ -312,16 +339,17 @@ function buildClaudeCommand(options) {
312
339
  parts.push('< "$FIFONY_PROMPT_FILE"');
313
340
  return parts.join(" ");
314
341
  }
315
- async function compile(issue, provider, plan, config, workspacePath, skillContext) {
342
+ async function compile(issue, provider, plan, config, workspacePath, skillContext, capabilitiesManifest) {
316
343
  const effort = resolveEffortForProvider(plan, provider.role, config.defaultEffort);
317
- const prompt = await renderPrompt("compile-execution-claude", {
344
+ let prompt = await renderPrompt("compile-execution-claude", {
318
345
  isPlanner: provider.role === "planner",
319
346
  isReviewer: provider.role === "reviewer",
320
347
  profileInstructions: provider.profileInstructions || "",
321
348
  skillContext,
349
+ capabilitiesManifest: capabilitiesManifest || "",
322
350
  planPrompt: buildFullPlanPrompt(plan),
323
- subagentsToUse: plan.toolingDecision?.shouldUseSubagents ? plan.toolingDecision.subagentsToUse ?? [] : [],
324
- skillsToUse: plan.toolingDecision?.shouldUseSkills ? plan.toolingDecision.skillsToUse ?? [] : [],
351
+ suggestedSkills: plan.suggestedSkills ?? [],
352
+ suggestedAgents: plan.suggestedAgents ?? [],
325
353
  suggestedPaths: plan.suggestedPaths ?? [],
326
354
  workspacePath,
327
355
  issueIdentifier: issue.identifier,
@@ -330,13 +358,20 @@ async function compile(issue, provider, plan, config, workspacePath, skillContex
330
358
  validationItems: (plan.validation ?? []).map((value) => ({ value }))
331
359
  });
332
360
  const relativeDirs = extractPlanDirs(plan);
333
- const codePath = existsSync(join(workspacePath, "worktree")) ? join(workspacePath, "worktree") : workspacePath;
361
+ const codePath = existsSync2(join(workspacePath, "worktree")) ? join(workspacePath, "worktree") : workspacePath;
334
362
  const absoluteDirs = relativeDirs.map((d) => join(codePath, d));
363
+ if (issue.images?.length) {
364
+ const imageSection = buildImagePromptSection(issue.images);
365
+ if (imageSection) prompt = prompt + "\n\n" + imageSection;
366
+ }
367
+ const isReadOnlyRole = provider.role === "planner" || provider.role === "reviewer";
335
368
  const command = buildClaudeCommand({
336
369
  model: provider.model,
337
370
  effort,
338
371
  addDirs: absoluteDirs,
339
- jsonSchema: CLAUDE_RESULT_SCHEMA
372
+ jsonSchema: CLAUDE_RESULT_SCHEMA,
373
+ readOnly: isReadOnlyRole,
374
+ maxBudgetUsd: config.maxBudgetUsd
340
375
  });
341
376
  const env2 = {
342
377
  FIFONY_PLAN_COMPLEXITY: plan.estimatedComplexity,
@@ -344,8 +379,8 @@ async function compile(issue, provider, plan, config, workspacePath, skillContex
344
379
  FIFONY_EXECUTION_PAYLOAD_FILE: "fifony-execution-payload.json"
345
380
  };
346
381
  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(",");
382
+ if (plan.suggestedSkills?.length) {
383
+ env2.FIFONY_PLAN_SKILLS = plan.suggestedSkills.join(",");
349
384
  }
350
385
  const { pre, post } = extractValidationCommands(plan);
351
386
  return {
@@ -360,24 +395,26 @@ async function compile(issue, provider, plan, config, workspacePath, skillContex
360
395
  adapter: "claude",
361
396
  reasoningEffort: effort || "default",
362
397
  model: provider.model || "default",
363
- skillsActivated: plan.toolingDecision?.skillsToUse?.map((s) => s.name) || [],
364
- subagentsRequested: plan.toolingDecision?.subagentsToUse?.map((a) => a.name) || [],
398
+ skillsActivated: plan.suggestedSkills || [],
399
+ subagentsRequested: plan.suggestedAgents || [],
365
400
  phasesCount: plan.phases?.length || 0
366
401
  }
367
402
  };
368
403
  }
369
404
  var claudeAdapter = {
370
405
  buildCommand: buildClaudeCommand,
371
- buildReviewCommand: (reviewer) => buildClaudeCommand({
406
+ buildReviewCommand: (reviewer, config) => buildClaudeCommand({
372
407
  model: reviewer.model,
373
408
  effort: reviewer.reasoningEffort,
374
- jsonSchema: REVIEW_RESULT_SCHEMA
409
+ jsonSchema: REVIEW_RESULT_SCHEMA,
410
+ readOnly: true,
411
+ maxBudgetUsd: config?.maxBudgetUsd
375
412
  }),
376
413
  compile
377
414
  };
378
415
 
379
416
  // src/agents/adapters/codex.ts
380
- import { existsSync as existsSync2 } from "fs";
417
+ import { existsSync as existsSync3 } from "fs";
381
418
  import { join as join2 } from "path";
382
419
  var CODEX_RESULT_CONTRACT = `
383
420
  Return a JSON object with this exact schema when finished:
@@ -393,7 +430,7 @@ Return a JSON object with this exact schema when finished:
393
430
  }
394
431
  `.trim();
395
432
  function buildCodexCommand(options) {
396
- const parts = ["codex", "exec", "--dangerously-bypass-approvals-and-sandbox"];
433
+ const parts = ["codex", "exec", "--skip-git-repo-check", "--dangerously-bypass-approvals-and-sandbox"];
397
434
  if (options.model && options.model !== "codex") {
398
435
  parts.push(`--model ${options.model}`);
399
436
  }
@@ -410,16 +447,20 @@ function buildCodexCommand(options) {
410
447
  parts.push(`--image "${img}"`);
411
448
  }
412
449
  }
450
+ if (options.search) {
451
+ parts.push("--search");
452
+ }
413
453
  parts.push('< "$FIFONY_PROMPT_FILE"');
414
454
  return parts.join(" ");
415
455
  }
416
- async function compile2(issue, provider, plan, config, workspacePath, skillContext) {
456
+ async function compile2(issue, provider, plan, config, workspacePath, skillContext, capabilitiesManifest) {
417
457
  const effort = resolveEffortForProvider(plan, provider.role, config.defaultEffort) || provider.reasoningEffort;
418
458
  const prompt = await renderPrompt("compile-execution-codex", {
419
459
  isPlanner: provider.role === "planner",
420
460
  isReviewer: provider.role === "reviewer",
421
461
  profileInstructions: provider.profileInstructions || "",
422
462
  skillContext,
463
+ capabilitiesManifest: capabilitiesManifest || "",
423
464
  issueIdentifier: issue.identifier,
424
465
  title: issue.title,
425
466
  description: issue.description || "(none)",
@@ -431,17 +472,18 @@ async function compile2(issue, provider, plan, config, workspacePath, skillConte
431
472
  outputs: phase.outputs ?? []
432
473
  })),
433
474
  suggestedPaths: plan.suggestedPaths ?? [],
434
- skillsToUse: plan.toolingDecision?.shouldUseSkills ? plan.toolingDecision.skillsToUse ?? [] : [],
475
+ suggestedSkills: plan.suggestedSkills ?? [],
435
476
  validationItems: (plan.validation ?? []).map((value) => ({ value })),
436
477
  outputContract: CODEX_RESULT_CONTRACT
437
478
  });
438
479
  const relativeDirs = extractPlanDirs(plan);
439
- const codePath = existsSync2(join2(workspacePath, "worktree")) ? join2(workspacePath, "worktree") : workspacePath;
480
+ const codePath = existsSync3(join2(workspacePath, "worktree")) ? join2(workspacePath, "worktree") : workspacePath;
440
481
  const absoluteDirs = relativeDirs.map((d) => join2(codePath, d));
441
482
  const command = buildCodexCommand({
442
483
  model: provider.model,
443
484
  addDirs: absoluteDirs,
444
- effort
485
+ effort,
486
+ imagePaths: issue.images?.filter((p) => existsSync3(p))
445
487
  });
446
488
  const env2 = {
447
489
  FIFONY_PLAN_COMPLEXITY: plan.estimatedComplexity,
@@ -463,7 +505,7 @@ async function compile2(issue, provider, plan, config, workspacePath, skillConte
463
505
  adapter: "codex",
464
506
  reasoningEffort: effort || "default",
465
507
  model: provider.model || "default",
466
- skillsActivated: plan.toolingDecision?.skillsToUse?.map((s) => s.name) || [],
508
+ skillsActivated: plan.suggestedSkills || [],
467
509
  subagentsRequested: [],
468
510
  phasesCount: plan.phases?.length || 0
469
511
  }
@@ -471,15 +513,16 @@ async function compile2(issue, provider, plan, config, workspacePath, skillConte
471
513
  }
472
514
  var codexAdapter = {
473
515
  buildCommand: buildCodexCommand,
474
- buildReviewCommand: (reviewer) => buildCodexCommand({
516
+ buildReviewCommand: (reviewer, _config) => buildCodexCommand({
475
517
  model: reviewer.model,
476
518
  effort: reviewer.reasoningEffort
519
+ // Codex has no --permission-mode or --approval-mode equivalent for read-only review
477
520
  }),
478
521
  compile: compile2
479
522
  };
480
523
 
481
524
  // src/agents/adapters/gemini.ts
482
- import { existsSync as existsSync3 } from "fs";
525
+ import { existsSync as existsSync4 } from "fs";
483
526
  import { join as join3 } from "path";
484
527
  var GEMINI_RESULT_CONTRACT = `
485
528
  Return a JSON object with this exact schema when finished:
@@ -495,23 +538,30 @@ Return a JSON object with this exact schema when finished:
495
538
  }
496
539
  `.trim();
497
540
  function buildGeminiCommand(options) {
498
- const parts = ["gemini", "--yolo"];
541
+ const parts = ["gemini"];
542
+ if (options.readOnly) {
543
+ parts.push("--approval-mode plan");
544
+ } else {
545
+ parts.push("--yolo");
546
+ }
499
547
  if (options.model) {
500
548
  parts.push(`--model ${options.model}`);
501
549
  }
550
+ parts.push("--output-format json");
502
551
  if (options.addDirs?.length) {
503
552
  parts.push(`--include-directories ${options.addDirs.map((d) => `"${d}"`).join(",")}`);
504
553
  }
505
554
  parts.push('-p "" < "$FIFONY_PROMPT_FILE"');
506
555
  return parts.join(" ");
507
556
  }
508
- async function compile3(issue, provider, plan, config, workspacePath, skillContext) {
557
+ async function compile3(issue, provider, plan, config, workspacePath, skillContext, capabilitiesManifest) {
509
558
  const effort = resolveEffortForProvider(plan, provider.role, config.defaultEffort) || provider.reasoningEffort;
510
- const prompt = await renderPrompt("compile-execution-codex", {
559
+ let prompt = await renderPrompt("compile-execution-codex", {
511
560
  isPlanner: provider.role === "planner",
512
561
  isReviewer: provider.role === "reviewer",
513
562
  profileInstructions: provider.profileInstructions || "",
514
563
  skillContext,
564
+ capabilitiesManifest: capabilitiesManifest || "",
515
565
  issueIdentifier: issue.identifier,
516
566
  title: issue.title,
517
567
  description: issue.description || "(none)",
@@ -523,16 +573,22 @@ async function compile3(issue, provider, plan, config, workspacePath, skillConte
523
573
  outputs: phase.outputs ?? []
524
574
  })),
525
575
  suggestedPaths: plan.suggestedPaths ?? [],
526
- skillsToUse: plan.toolingDecision?.shouldUseSkills ? plan.toolingDecision.skillsToUse ?? [] : [],
576
+ suggestedSkills: plan.suggestedSkills ?? [],
527
577
  validationItems: (plan.validation ?? []).map((value) => ({ value })),
528
578
  outputContract: GEMINI_RESULT_CONTRACT
529
579
  });
580
+ if (issue.images?.length) {
581
+ const imageSection = buildImagePromptSection(issue.images);
582
+ if (imageSection) prompt = prompt + "\n\n" + imageSection;
583
+ }
530
584
  const relativeDirs = extractPlanDirs(plan);
531
- const codePath = existsSync3(join3(workspacePath, "worktree")) ? join3(workspacePath, "worktree") : workspacePath;
585
+ const codePath = existsSync4(join3(workspacePath, "worktree")) ? join3(workspacePath, "worktree") : workspacePath;
532
586
  const absoluteDirs = relativeDirs.map((d) => join3(codePath, d));
587
+ const isReadOnlyRole = provider.role === "planner" || provider.role === "reviewer";
533
588
  const command = buildGeminiCommand({
534
589
  model: provider.model,
535
- addDirs: absoluteDirs
590
+ addDirs: absoluteDirs,
591
+ readOnly: isReadOnlyRole
536
592
  });
537
593
  const env2 = {
538
594
  FIFONY_PLAN_COMPLEXITY: plan.estimatedComplexity,
@@ -554,7 +610,7 @@ async function compile3(issue, provider, plan, config, workspacePath, skillConte
554
610
  adapter: "gemini",
555
611
  reasoningEffort: effort || "default",
556
612
  model: provider.model || "default",
557
- skillsActivated: plan.toolingDecision?.skillsToUse?.map((s) => s.name) || [],
613
+ skillsActivated: plan.suggestedSkills || [],
558
614
  subagentsRequested: [],
559
615
  phasesCount: plan.phases?.length || 0
560
616
  }
@@ -563,7 +619,8 @@ async function compile3(issue, provider, plan, config, workspacePath, skillConte
563
619
  var geminiAdapter = {
564
620
  buildCommand: buildGeminiCommand,
565
621
  buildReviewCommand: (reviewer) => buildGeminiCommand({
566
- model: reviewer.model
622
+ model: reviewer.model,
623
+ readOnly: true
567
624
  }),
568
625
  compile: compile3
569
626
  };
@@ -577,7 +634,7 @@ var ADAPTERS = {
577
634
 
578
635
  // src/agents/model-discovery.ts
579
636
  import { execFileSync } from "child_process";
580
- import { existsSync as existsSync4, readFileSync, realpathSync } from "fs";
637
+ import { existsSync as existsSync5, readFileSync as readFileSync2, realpathSync } from "fs";
581
638
  import { join as join4, dirname } from "path";
582
639
  import { homedir } from "os";
583
640
  var modelCache = /* @__PURE__ */ new Map();
@@ -585,8 +642,8 @@ var MODEL_CACHE_TTL_MS = 5 * 60 * 1e3;
585
642
  function readClaudeConfig() {
586
643
  try {
587
644
  const settingsPath = join4(homedir(), ".claude", "settings.json");
588
- if (!existsSync4(settingsPath)) return {};
589
- const raw = readFileSync(settingsPath, "utf8");
645
+ if (!existsSync5(settingsPath)) return {};
646
+ const raw = readFileSync2(settingsPath, "utf8");
590
647
  const settings = JSON.parse(raw);
591
648
  return { model: typeof settings.model === "string" ? settings.model : void 0 };
592
649
  } catch {
@@ -600,7 +657,7 @@ function resolveGeminiModelsFile() {
600
657
  const realBin = realpathSync(binPath);
601
658
  const cliRoot = dirname(dirname(realBin));
602
659
  const modelsPath = join4(cliRoot, "node_modules", "@google", "gemini-cli-core", "dist", "src", "config", "models.js");
603
- return existsSync4(modelsPath) ? modelsPath : null;
660
+ return existsSync5(modelsPath) ? modelsPath : null;
604
661
  } catch {
605
662
  return null;
606
663
  }
@@ -609,7 +666,7 @@ async function fetchGeminiModels() {
609
666
  const modelsPath = resolveGeminiModelsFile();
610
667
  if (!modelsPath) return [];
611
668
  try {
612
- const content = readFileSync(modelsPath, "utf8");
669
+ const content = readFileSync2(modelsPath, "utf8");
613
670
  const regex = /export const ([A-Z0-9_]+)\s*=\s*'(gemini-[^']+)';/g;
614
671
  const seen = /* @__PURE__ */ new Set();
615
672
  const stable = [];
@@ -634,8 +691,8 @@ async function fetchGeminiModels() {
634
691
  async function fetchCodexModels() {
635
692
  const cachePath = join4(homedir(), ".codex", "models_cache.json");
636
693
  try {
637
- if (existsSync4(cachePath)) {
638
- const raw = readFileSync(cachePath, "utf8");
694
+ if (existsSync5(cachePath)) {
695
+ const raw = readFileSync2(cachePath, "utf8");
639
696
  const cache = JSON.parse(raw);
640
697
  if (Array.isArray(cache.models) && cache.models.length > 0) {
641
698
  return cache.models.sort((a, b) => {
@@ -728,28 +785,6 @@ async function discoverModels(providers) {
728
785
  }
729
786
 
730
787
  // 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
788
  function normalizeAgentProvider(value) {
754
789
  const normalized = value.trim().toLowerCase();
755
790
  if (normalized === "claude" || normalized === "codex" || normalized === "gemini") return normalized;
@@ -798,8 +833,8 @@ function detectAvailableProviders() {
798
833
  function readCodexConfig() {
799
834
  try {
800
835
  const configPath = join5(homedir2(), ".codex", "config.toml");
801
- if (!existsSync5(configPath)) return {};
802
- const raw = readFileSync2(configPath, "utf8");
836
+ if (!existsSync6(configPath)) return {};
837
+ const raw = readFileSync3(configPath, "utf8");
803
838
  const model = raw.match(/^model\s*=\s*"([^"]+)"/m)?.[1];
804
839
  const reasoningEffort = raw.match(/^model_reasoning_effort\s*=\s*"([^"]+)"/m)?.[1];
805
840
  return { model, reasoningEffort };
@@ -810,8 +845,8 @@ function readCodexConfig() {
810
845
  function readGeminiConfig() {
811
846
  try {
812
847
  const settingsPath = join5(homedir2(), ".gemini", "settings.json");
813
- if (!existsSync5(settingsPath)) return {};
814
- const raw = readFileSync2(settingsPath, "utf8");
848
+ if (!existsSync6(settingsPath)) return {};
849
+ const raw = readFileSync3(settingsPath, "utf8");
815
850
  const settings = JSON.parse(raw);
816
851
  return {
817
852
  model: typeof settings.model === "string" ? settings.model : void 0,
@@ -839,20 +874,6 @@ function getBaseAgentProviders(state) {
839
874
  }
840
875
  ];
841
876
  }
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
877
  function roleToStageKey(role) {
857
878
  switch (role) {
858
879
  case "planner":
@@ -884,41 +905,18 @@ function applyWorkflowConfigToProviders(providers, workflowConfig) {
884
905
  }
885
906
  function getEffectiveAgentProviders(state, issue, _workflowDefinition, workflowConfig) {
886
907
  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
- );
908
+ const providers = baseProviders.map((provider) => {
904
909
  const effort = resolveEffort(provider.role, issue.effort, state.config.defaultEffort);
905
- const command = provider.command;
906
910
  return {
907
911
  ...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
912
  reasoningEffort: effort
915
913
  };
916
914
  });
917
- return applyWorkflowConfigToProviders(merged, workflowConfig ?? null);
915
+ return applyWorkflowConfigToProviders(providers, workflowConfig ?? null);
918
916
  }
919
917
 
920
918
  // src/agents/command-executor.ts
921
- async function runCommandWithTimeout(command, workspacePath, issue, config, promptText, promptFile, extraEnv = {}) {
919
+ async function runCommandWithTimeout(command, workspacePath, issue, config, promptText, promptFile, extraEnv = {}, outputFile) {
922
920
  return new Promise((resolve2) => {
923
921
  const started = Date.now();
924
922
  const resultFile = extraEnv.FIFONY_RESULT_FILE;
@@ -929,7 +927,6 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
929
927
  FIFONY_ISSUE_ID: issue.id,
930
928
  FIFONY_ISSUE_IDENTIFIER: issue.identifier,
931
929
  FIFONY_ISSUE_TITLE: issue.title,
932
- FIFONY_ISSUE_PRIORITY: String(issue.priority),
933
930
  FIFONY_WORKSPACE_PATH: issue.worktreePath ?? workspacePath,
934
931
  FIFONY_PROMPT_FILE: promptFile
935
932
  };
@@ -974,6 +971,19 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
974
971
  let outputHeader = "";
975
972
  const liveLogFile = join6(workspacePath, "live-output.log");
976
973
  writeFileSync(liveLogFile, "", "utf8");
974
+ if (outputFile) {
975
+ try {
976
+ const header = `# fifony stdout capture
977
+ # turn: ${extraEnv.FIFONY_TURN_INDEX ?? "?"}
978
+ # provider: ${extraEnv.FIFONY_AGENT_PROVIDER ?? "?"}
979
+ # role: ${extraEnv.FIFONY_AGENT_ROLE ?? "?"}
980
+ # timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}
981
+ ---
982
+ `;
983
+ writeFileSync(outputFile, header, "utf8");
984
+ } catch {
985
+ }
986
+ }
977
987
  const onChunk = (chunk) => {
978
988
  const text = String(chunk);
979
989
  if (outputHeader.length < 2e3) outputHeader = (outputHeader + text).slice(0, 2e3);
@@ -983,6 +993,12 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
983
993
  appendFileSync(liveLogFile, text);
984
994
  } catch {
985
995
  }
996
+ if (outputFile) {
997
+ try {
998
+ appendFileSync(outputFile, text);
999
+ } catch {
1000
+ }
1001
+ }
986
1002
  issue.commandOutputTail = output;
987
1003
  };
988
1004
  child.stdout?.on("data", onChunk);
@@ -1103,6 +1119,48 @@ async function runHook(command, workspacePath, issue, hookName, extraEnv = {}) {
1103
1119
  }
1104
1120
 
1105
1121
  // src/agents/prompt-builder.ts
1122
+ function buildRetryContext(issue) {
1123
+ const summaries = issue.previousAttemptSummaries;
1124
+ if (!summaries || summaries.length === 0) return "";
1125
+ const lines = ["## Previous Attempts\n"];
1126
+ lines.push("The following previous attempts FAILED. Do NOT repeat the same approach. Try a fundamentally different strategy.\n");
1127
+ for (let i = 0; i < summaries.length; i++) {
1128
+ const s = summaries[i];
1129
+ const phaseLabel = s.phase === "review" ? "review" : s.phase === "crash" ? "crash" : s.phase === "plan" ? "plan" : "execution";
1130
+ lines.push(`### Attempt ${i + 1} \u2014 ${phaseLabel} failure (plan v${s.planVersion}, exec #${s.executeAttempt})`);
1131
+ if (s.phase === "review") {
1132
+ 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.*");
1133
+ } else if (s.phase === "crash") {
1134
+ lines.push("*The agent process crashed or timed out. Simplify the approach \u2014 break the work into smaller steps.*");
1135
+ }
1136
+ if (s.insight) {
1137
+ lines.push(`**Failure type:** ${s.insight.errorType}`);
1138
+ lines.push(`**Root cause:** ${s.insight.rootCause}`);
1139
+ if (s.insight.failedCommand) lines.push(`**Failed command:** \`${s.insight.failedCommand}\``);
1140
+ if (s.insight.filesInvolved.length > 0) {
1141
+ lines.push(`**Files involved:** ${s.insight.filesInvolved.map((f) => `\`${f}\``).join(", ")}`);
1142
+ }
1143
+ lines.push(`**What to do differently:** ${s.insight.suggestion}`);
1144
+ } else {
1145
+ lines.push(`**Error:** ${s.error}`);
1146
+ }
1147
+ if (s.outputTail) {
1148
+ lines.push(`
1149
+ <details><summary>Output tail</summary>
1150
+
1151
+ \`\`\`
1152
+ ${s.outputTail}
1153
+ \`\`\`
1154
+ </details>`);
1155
+ }
1156
+ if (s.outputFile) {
1157
+ lines.push(`*Full output saved in: outputs/${s.outputFile}*`);
1158
+ }
1159
+ lines.push("");
1160
+ }
1161
+ const full = lines.join("\n");
1162
+ return full.length > 8e3 ? full.slice(0, 8e3) + "\n[...truncated]" : full;
1163
+ }
1106
1164
  async function buildPrompt(issue, _workflowDefinition) {
1107
1165
  const rendered = await renderPrompt("workflow-default", { issue, attempt: issue.attempts || 0 });
1108
1166
  if (!issue.plan?.steps?.length) {
@@ -1133,7 +1191,7 @@ async function buildTurnPrompt(issue, basePrompt, previousOutput, turnIndex, max
1133
1191
  outputTail: previousOutput.trim() || "No previous output captured."
1134
1192
  });
1135
1193
  }
1136
- async function buildProviderBasePrompt(provider, issue, basePrompt, workspacePath, skillContext) {
1194
+ async function buildProviderBasePrompt(provider, issue, basePrompt, workspacePath, skillContext, capabilitiesManifest) {
1137
1195
  return renderPrompt("agent-provider-base", {
1138
1196
  isPlanner: provider.role === "planner",
1139
1197
  isReviewer: provider.role === "reviewer",
@@ -1141,8 +1199,9 @@ async function buildProviderBasePrompt(provider, issue, basePrompt, workspacePat
1141
1199
  hasFrontendDesignOverlay: provider.overlays?.includes("frontend-design") ?? false,
1142
1200
  profileInstructions: provider.profileInstructions || "",
1143
1201
  skillContext,
1144
- capabilityCategory: provider.capabilityCategory || "",
1145
- selectionReason: provider.selectionReason ?? "No additional routing reason.",
1202
+ capabilitiesManifest: capabilitiesManifest || "",
1203
+ capabilityCategory: "",
1204
+ selectionReason: provider.selectionReason ?? "",
1146
1205
  overlays: provider.overlays ?? [],
1147
1206
  targetPaths: issue.paths ?? [],
1148
1207
  workspacePath,
@@ -1173,10 +1232,46 @@ function shouldSkipPath(relativePath) {
1173
1232
  const parts = relativePath.split("/");
1174
1233
  if (parts.some((segment) => SKIP_DIRS.has(segment))) return true;
1175
1234
  const base = parts.at(-1) ?? "";
1176
- if (base.startsWith("map_scan_") && extname(base) === ".json") return true;
1177
- if (extname(base) === ".xlsx") return true;
1235
+ if (base.startsWith("map_scan_") && extname2(base) === ".json") return true;
1236
+ if (extname2(base) === ".xlsx") return true;
1178
1237
  return false;
1179
1238
  }
1239
+ function bootstrapSource() {
1240
+ if (existsSync7(SOURCE_MARKER)) return;
1241
+ logger.info("Creating local source snapshot for Fifony (local-only runtime)...");
1242
+ const copyRecursive = (source, target, rel = "") => {
1243
+ mkdirSync(target, { recursive: true });
1244
+ const items = readdirSync(source, { withFileTypes: true });
1245
+ for (const item of items) {
1246
+ const nextRel = rel ? `${rel}/${item.name}` : item.name;
1247
+ if (shouldSkipPath(nextRel)) continue;
1248
+ const sourcePath = `${source}/${item.name}`;
1249
+ const targetPath = `${target}/${item.name}`;
1250
+ const itemStat = statSync(sourcePath);
1251
+ if (item.isDirectory()) {
1252
+ copyRecursive(sourcePath, targetPath, nextRel);
1253
+ continue;
1254
+ }
1255
+ if (item.isSymbolicLink() || itemStat.isSymbolicLink()) continue;
1256
+ if (itemStat.isFile() || itemStat.isFIFO()) {
1257
+ try {
1258
+ const file = readFileSync4(sourcePath);
1259
+ writeFileSync2(targetPath, file);
1260
+ } catch (error) {
1261
+ if (error.code === "ENOENT") {
1262
+ logger.debug(`Skipped missing source file: ${sourcePath}`);
1263
+ } else {
1264
+ throw error;
1265
+ }
1266
+ }
1267
+ }
1268
+ }
1269
+ };
1270
+ mkdirSync(SOURCE_ROOT, { recursive: true });
1271
+ copyRecursive(TARGET_ROOT, SOURCE_ROOT);
1272
+ writeFileSync2(SOURCE_MARKER, `${now()}
1273
+ `, "utf8");
1274
+ }
1180
1275
  var sourceReadyPromise = null;
1181
1276
  var skipSourceFlag = false;
1182
1277
  function setSkipSource(skip) {
@@ -1187,7 +1282,7 @@ async function ensureSourceReady(onProgress) {
1187
1282
  onProgress?.("ready");
1188
1283
  return;
1189
1284
  }
1190
- if (existsSync6(SOURCE_MARKER)) {
1285
+ if (existsSync7(SOURCE_MARKER)) {
1191
1286
  onProgress?.("ready");
1192
1287
  return;
1193
1288
  }
@@ -1262,7 +1357,7 @@ async function createGitWorktree(issue, worktreePath, baseBranch) {
1262
1357
  stdio: "pipe"
1263
1358
  });
1264
1359
  try {
1265
- const gitFileContent = readFileSync3(join7(worktreePath, ".git"), "utf8").trim();
1360
+ const gitFileContent = readFileSync4(join7(worktreePath, ".git"), "utf8").trim();
1266
1361
  const gitDirRel = gitFileContent.replace("gitdir: ", "").trim();
1267
1362
  const gitDirPath = resolve(worktreePath, gitDirRel);
1268
1363
  mkdirSync(join7(gitDirPath, "info"), { recursive: true });
@@ -1280,7 +1375,7 @@ async function prepareWorkspace(issue, state, defaultBranch) {
1280
1375
  const safeId = idToSafePath(issue.id);
1281
1376
  const workspaceRoot = join7(WORKSPACE_ROOT, safeId);
1282
1377
  const worktreePath = join7(workspaceRoot, "worktree");
1283
- const createdNow = !existsSync6(worktreePath);
1378
+ const createdNow = !existsSync7(worktreePath);
1284
1379
  if (createdNow) {
1285
1380
  mkdirSync(workspaceRoot, { recursive: true });
1286
1381
  logger.debug({ issueId: issue.id, identifier: issue.identifier, workspacePath: workspaceRoot }, "[Agent] Creating workspace");
@@ -1316,7 +1411,7 @@ async function prepareWorkspace(issue, state, defaultBranch) {
1316
1411
  async function cleanWorkspace(issueId, issue, state) {
1317
1412
  const safeId = idToSafePath(issueId);
1318
1413
  const workspacePath = issue?.workspacePath ?? join7(WORKSPACE_ROOT, safeId);
1319
- if (!existsSync6(workspacePath)) return;
1414
+ if (!existsSync7(workspacePath)) return;
1320
1415
  if (state.config.beforeRemoveHook) {
1321
1416
  try {
1322
1417
  const dummyIssue = issue ?? { id: issueId, identifier: issueId };
@@ -1397,6 +1492,58 @@ function parseDiffStats(issue, raw) {
1397
1492
  issue.linesAdded = addMatch ? parseInt(addMatch[1], 10) : 0;
1398
1493
  issue.linesRemoved = delMatch ? parseInt(delMatch[1], 10) : 0;
1399
1494
  }
1495
+ async function syncIssueDiffStatsToStore(issue) {
1496
+ if (!issue?.id) return;
1497
+ const { getIssueStateResource } = await import("./store-AG6LLYJ7.js");
1498
+ const issueResource = getIssueStateResource();
1499
+ if (!issueResource) return;
1500
+ const toNumber = (value) => {
1501
+ const parsed = typeof value === "number" ? value : Number(value ?? 0);
1502
+ return Number.isFinite(parsed) ? parsed : 0;
1503
+ };
1504
+ const nextLinesAdded = toNumber(issue.linesAdded);
1505
+ const nextLinesRemoved = toNumber(issue.linesRemoved);
1506
+ const nextFilesChanged = toNumber(issue.filesChanged);
1507
+ if (nextLinesAdded === 0 && nextLinesRemoved === 0 && nextFilesChanged === 0 && !issue.branchName) {
1508
+ return;
1509
+ }
1510
+ const current = await issueResource.get?.(issue.id).catch(() => null);
1511
+ const previousLinesAdded = toNumber(current?.linesAdded);
1512
+ const previousLinesRemoved = toNumber(current?.linesRemoved);
1513
+ const previousFilesChanged = toNumber(current?.filesChanged);
1514
+ await issueResource.patch(issue.id, {
1515
+ linesAdded: nextLinesAdded,
1516
+ linesRemoved: nextLinesRemoved,
1517
+ filesChanged: nextFilesChanged,
1518
+ branchName: issue.branchName
1519
+ });
1520
+ const add = issueResource.add;
1521
+ const sub = issueResource.sub;
1522
+ if (typeof add !== "function" || typeof sub !== "function") {
1523
+ logger.debug({ issueId: issue.id }, "[DiffStats] resource.add/sub not available \u2014 EC plugin may not be installed");
1524
+ return;
1525
+ }
1526
+ const deltaAdded = nextLinesAdded - previousLinesAdded;
1527
+ const deltaRemoved = nextLinesRemoved - previousLinesRemoved;
1528
+ const deltaFiles = nextFilesChanged - previousFilesChanged;
1529
+ if (deltaAdded === 0 && deltaRemoved === 0 && deltaFiles === 0) {
1530
+ logger.debug({ issueId: issue.id, nextLinesAdded, previousLinesAdded }, "[DiffStats] No delta to send to EC (values already synced)");
1531
+ return;
1532
+ }
1533
+ logger.debug({ issueId: issue.id, deltaAdded, deltaRemoved, deltaFiles }, "[DiffStats] Sending deltas to EC");
1534
+ const applyDelta = async (field, delta) => {
1535
+ if (delta > 0) {
1536
+ await add.call(issueResource, issue.id, field, delta);
1537
+ } else if (delta < 0) {
1538
+ await sub.call(issueResource, issue.id, field, Math.abs(delta));
1539
+ }
1540
+ };
1541
+ await Promise.all([
1542
+ applyDelta("linesAdded", deltaAdded),
1543
+ applyDelta("linesRemoved", deltaRemoved),
1544
+ applyDelta("filesChanged", deltaFiles)
1545
+ ]);
1546
+ }
1400
1547
  function ensureWorktreeCommitted(issue) {
1401
1548
  const worktreePath = issue.worktreePath;
1402
1549
  if (!worktreePath || !issue.branchName) return;
@@ -1462,27 +1609,13 @@ function mergeWorktree(issue, worktreePath) {
1462
1609
  }
1463
1610
  return result;
1464
1611
  }
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
- }
1612
+ function shouldSkipMergePath(relativePath) {
1613
+ const parts = relativePath.split("/");
1614
+ if (parts.some((s) => s === ".git" || s === "node_modules" || s === ".fifony" || s === "dist" || s === ".tanstack")) {
1615
+ return true;
1485
1616
  }
1617
+ const base = parts.at(-1) ?? "";
1618
+ 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
1619
  }
1487
1620
  function mergeWorkspace(issue) {
1488
1621
  if (!issue.branchName || !issue.baseBranch || !issue.worktreePath) {
@@ -1490,622 +1623,98 @@ function mergeWorkspace(issue) {
1490
1623
  }
1491
1624
  return mergeWorktree(issue, issue.worktreePath);
1492
1625
  }
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
- }
1626
+ function dryMerge(issue) {
1627
+ if (!issue.branchName || !issue.baseBranch || !issue.worktreePath) {
1628
+ throw new Error(`Issue ${issue.identifier} has no git worktree \u2014 cannot preview merge.`);
1560
1629
  }
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
- };
1630
+ ensureWorktreeCommitted(issue);
1631
+ const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
1632
+ if (currentBranch !== issue.baseBranch) {
1633
+ throw new Error(`Cannot preview merge: current branch is ${currentBranch}, expected ${issue.baseBranch}.`);
1573
1634
  }
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) {
1635
+ const targetStatus = execSync("git status --porcelain", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
1636
+ if (targetStatus) {
1637
+ throw new Error(`Cannot preview merge: target repository has uncommitted changes.`);
1638
+ }
1639
+ let conflictFiles = [];
1640
+ let willConflict = false;
1641
+ try {
1642
+ execSync(
1643
+ `git merge --no-commit --no-ff "${issue.branchName}"`,
1644
+ { cwd: TARGET_ROOT, stdio: "pipe" }
1645
+ );
1646
+ } catch {
1647
+ willConflict = true;
1669
1648
  try {
1670
- fsmEventEmitter(issueId, kind, message);
1649
+ const conflictOut = execSync(
1650
+ "git diff --name-only --diff-filter=U",
1651
+ { cwd: TARGET_ROOT, encoding: "utf8" }
1652
+ );
1653
+ conflictFiles = conflictOut.trim().split("\n").filter(Boolean);
1671
1654
  } catch {
1672
1655
  }
1673
1656
  }
1674
- }
1675
- async function lazyEnqueueForPlanning(issue) {
1676
- const { enqueueForPlanning } = await import("./queue-workers-JIH5ZMNQ.js");
1677
- return enqueueForPlanning(issue);
1678
- }
1679
- async function lazyEnqueueForExecution(issue) {
1680
- const { enqueueForExecution } = await import("./queue-workers-JIH5ZMNQ.js");
1681
- return enqueueForExecution(issue);
1682
- }
1683
- async function lazyEnqueueForReview(issue) {
1684
- const { enqueueForReview } = await import("./queue-workers-JIH5ZMNQ.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) {
1657
+ try {
1658
+ execSync("git merge --abort", { cwd: TARGET_ROOT, stdio: "pipe" });
1659
+ } catch {
2030
1660
  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
- }
1661
+ execSync("git reset --hard HEAD", { cwd: TARGET_ROOT, stdio: "pipe" });
1662
+ } catch {
2066
1663
  }
2067
1664
  }
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 = "";
1665
+ let changedFiles = 0;
1666
+ try {
1667
+ const diffOut = execSync(
1668
+ `git diff --name-only "${issue.baseBranch}"..."${issue.branchName}"`,
1669
+ { cwd: TARGET_ROOT, encoding: "utf8" }
1670
+ );
1671
+ changedFiles = diffOut.trim().split("\n").filter(Boolean).length;
1672
+ } catch {
2074
1673
  }
2075
- markDirtyAndInvalidate(issue.id);
2076
- return { previousState: previous };
1674
+ return { willConflict, conflictFiles, canMerge: !willConflict, changedFiles };
2077
1675
  }
2078
- async function getIssueTransitionHistory(issueId, options) {
2079
- const resourceApi = getIssueResourceStateApi();
2080
- if (resourceApi?.history) {
1676
+ function rebaseWorktree(issue) {
1677
+ if (!issue.branchName || !issue.baseBranch || !issue.worktreePath) {
1678
+ throw new Error(`Issue ${issue.identifier} has no git worktree \u2014 cannot rebase.`);
1679
+ }
1680
+ ensureWorktreeCommitted(issue);
1681
+ try {
1682
+ execSync(
1683
+ `git rebase "${issue.baseBranch}"`,
1684
+ { cwd: issue.worktreePath, stdio: "pipe" }
1685
+ );
1686
+ return { success: true, conflictFiles: [] };
1687
+ } catch {
1688
+ let conflictFiles = [];
2081
1689
  try {
2082
- return await resourceApi.history(issueId, options);
1690
+ const conflictOut = execSync(
1691
+ "git diff --name-only --diff-filter=U",
1692
+ { cwd: issue.worktreePath, encoding: "utf8" }
1693
+ );
1694
+ conflictFiles = conflictOut.trim().split("\n").filter(Boolean);
2083
1695
  } catch {
2084
1696
  }
2085
- }
2086
- const plugin = getIssueStateMachinePlugin();
2087
- if (plugin?.getTransitionHistory) {
2088
1697
  try {
2089
- return await plugin.getTransitionHistory(ISSUE_STATE_MACHINE_ID, issueId, options);
1698
+ execSync("git rebase --abort", { cwd: issue.worktreePath, stdio: "pipe" });
2090
1699
  } catch {
2091
1700
  }
1701
+ return { success: false, conflictFiles };
2092
1702
  }
2093
- return [];
2094
1703
  }
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 {
1704
+ function hydrateIssuePathsFromWorkspace(issue) {
1705
+ const inferredPaths = inferChangedWorkspacePaths(issue.workspacePath ?? "", 32, issue);
1706
+ if (inferredPaths.length === 0) return [];
1707
+ issue.paths = [.../* @__PURE__ */ new Set([...issue.paths ?? [], ...inferredPaths])];
1708
+ return inferredPaths;
1709
+ }
1710
+ function writeVersionedArtifacts(workspacePath, prefix, planVersion, attempt, sources) {
1711
+ const { writeFileSync: _wfs, readFileSync: _rfs, existsSync: _es } = { writeFileSync: writeFileSync2, readFileSync: readFileSync4, existsSync: existsSync7 };
1712
+ for (const { srcFile, destSuffix } of sources) {
1713
+ const src = join7(workspacePath, srcFile);
1714
+ if (_es(src)) {
1715
+ _wfs(join7(workspacePath, `${prefix}.v${planVersion}a${attempt}.${destSuffix}`), _rfs(src, "utf8"), "utf8");
2101
1716
  }
2102
1717
  }
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
1718
  }
2110
1719
 
2111
1720
  export {
@@ -2119,54 +1728,30 @@ export {
2119
1728
  detectAvailableProviders,
2120
1729
  readCodexConfig,
2121
1730
  resolveDefaultProvider,
2122
- getCapabilityRoutingOptions,
2123
- applyCapabilityMetadata,
2124
1731
  getEffectiveAgentProviders,
2125
- computeMetrics,
2126
- computeCapabilityCounts,
2127
- getMetrics,
2128
1732
  runCommandWithTimeout,
2129
1733
  runHook,
1734
+ buildRetryContext,
1735
+ buildPrompt,
2130
1736
  buildTurnPrompt,
2131
1737
  buildProviderBasePrompt,
1738
+ bootstrapSource,
2132
1739
  setSkipSource,
1740
+ ensureSourceReady,
2133
1741
  detectDefaultBranch,
1742
+ createGitWorktree,
2134
1743
  prepareWorkspace,
2135
1744
  cleanWorkspace,
1745
+ inferChangedWorkspacePaths,
2136
1746
  computeDiffStats,
2137
1747
  parseDiffStats,
1748
+ syncIssueDiffStatsToStore,
2138
1749
  ensureWorktreeCommitted,
2139
- pushWorktreeBranch,
1750
+ shouldSkipMergePath,
2140
1751
  mergeWorkspace,
1752
+ dryMerge,
1753
+ rebaseWorktree,
2141
1754
  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
1755
+ writeVersionedArtifacts
2171
1756
  };
2172
- //# sourceMappingURL=chunk-NFHVAIPW.js.map
1757
+ //# sourceMappingURL=chunk-UYCDOH6S.js.map