fifony 0.1.21 → 0.1.22-next.4ab1d2e

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 (51) hide show
  1. package/README.md +40 -9
  2. package/app/dist/assets/KeyboardShortcutsHelp-BB5jLK_E.js +1 -0
  3. package/app/dist/assets/OnboardingWizard-xyM3Okjv.js +1 -0
  4. package/app/dist/assets/analytics.lazy-CfJXsh6r.js +1 -0
  5. package/app/dist/assets/{createLucideIcon-DtZs0TX0.js → createLucideIcon-BWC-guQt.js} +1 -1
  6. package/app/dist/assets/index-C1QEwHZG.js +43 -0
  7. package/app/dist/assets/index-DjmUHXd1.css +1 -0
  8. package/app/dist/assets/vendor-BTlTWMUF.js +9 -0
  9. package/app/dist/dinofffaur.png +0 -0
  10. package/app/dist/index.html +4 -5
  11. package/app/dist/service-worker.js +1 -1
  12. package/app/public/dinofffaur.png +0 -0
  13. package/bin/fifony-wrap.js +53 -0
  14. package/dist/agent/cli-wrapper.js +78 -0
  15. package/dist/agent/cli-wrapper.js.map +1 -0
  16. package/dist/agent/run-local.js +228 -7894
  17. package/dist/agent/run-local.js.map +1 -1
  18. package/dist/chunk-3QSBGJMT.js +2190 -0
  19. package/dist/chunk-3QSBGJMT.js.map +1 -0
  20. package/dist/chunk-4OLABTVH.js +7083 -0
  21. package/dist/chunk-4OLABTVH.js.map +1 -0
  22. package/dist/chunk-D564G33G.js +91 -0
  23. package/dist/chunk-D564G33G.js.map +1 -0
  24. package/dist/{chunk-SMGXYOWU.js → chunk-DD5BE2W6.js} +430 -31
  25. package/dist/chunk-DD5BE2W6.js.map +1 -0
  26. package/dist/chunk-DVU3CXWA.js +75 -0
  27. package/dist/chunk-DVU3CXWA.js.map +1 -0
  28. package/dist/cli.js +187 -1
  29. package/dist/cli.js.map +1 -1
  30. package/dist/issue-runner-4WL4EK6R.js +13 -0
  31. package/dist/issue-runner-4WL4EK6R.js.map +1 -0
  32. package/dist/issue-state-machine-IWLKOTPI.js +39 -0
  33. package/dist/issue-state-machine-IWLKOTPI.js.map +1 -0
  34. package/dist/mcp/server.js +592 -605
  35. package/dist/mcp/server.js.map +1 -1
  36. package/dist/queue-workers-2I7VRZA7.js +20 -0
  37. package/dist/queue-workers-2I7VRZA7.js.map +1 -0
  38. package/dist/store-3JLC6EXY.js +56 -0
  39. package/dist/store-3JLC6EXY.js.map +1 -0
  40. package/package.json +10 -9
  41. package/FIFONY.md +0 -173
  42. package/app/dist/assets/KeyboardShortcutsHelp-BTjiQe_Y.js +0 -1
  43. package/app/dist/assets/OnboardingWizard-BALlquG0.js +0 -1
  44. package/app/dist/assets/analytics.lazy-DjSzXIey.js +0 -1
  45. package/app/dist/assets/index-BV11ScVl.js +0 -42
  46. package/app/dist/assets/index-DWbxgKSd.css +0 -1
  47. package/app/dist/assets/vendor-BoGBoEwT.js +0 -9
  48. package/app/dist/assets/zap-DpjdVd1i.js +0 -1
  49. package/dist/chunk-SMGXYOWU.js.map +0 -1
  50. package/src/fixtures/agent-catalog.json +0 -208
  51. package/src/fixtures/skill-catalog.json +0 -67
@@ -0,0 +1,2190 @@
1
+ import {
2
+ S3DB_ISSUE_RESOURCE,
3
+ SOURCE_MARKER,
4
+ SOURCE_ROOT,
5
+ TARGET_ROOT,
6
+ TERMINAL_STATES,
7
+ WORKSPACE_ROOT,
8
+ appendFileTail,
9
+ idToSafePath,
10
+ inferCapabilityPaths,
11
+ isoWeek,
12
+ mergeCapabilityProviders,
13
+ now,
14
+ renderPrompt,
15
+ resolveTaskCapabilities
16
+ } from "./chunk-DD5BE2W6.js";
17
+ import {
18
+ logger
19
+ } from "./chunk-DVU3CXWA.js";
20
+
21
+ // src/domains/workspace.ts
22
+ import {
23
+ cpSync,
24
+ existsSync as existsSync6,
25
+ mkdirSync,
26
+ readdirSync,
27
+ readFileSync as readFileSync3,
28
+ rmSync as rmSync2,
29
+ statSync,
30
+ writeFileSync as writeFileSync2
31
+ } from "fs";
32
+ import { copyFile, mkdir, readdir, stat, writeFile } from "fs/promises";
33
+ import { extname, join as join7, resolve } from "path";
34
+ import { execSync } from "child_process";
35
+
36
+ // src/agents/command-executor.ts
37
+ import {
38
+ appendFileSync,
39
+ rmSync,
40
+ writeFileSync
41
+ } from "fs";
42
+ import { join as join6 } from "path";
43
+ import { env } from "process";
44
+ import { spawn } from "child_process";
45
+
46
+ // src/agents/providers.ts
47
+ import { execFileSync as execFileSync2 } from "child_process";
48
+ import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
49
+ import { join as join5 } from "path";
50
+ import { homedir as homedir2 } from "os";
51
+
52
+ // src/agents/adapters/claude.ts
53
+ import { existsSync } from "fs";
54
+ import { join } from "path";
55
+
56
+ // src/agents/adapters/shared.ts
57
+ function buildPlanContextSection(plan) {
58
+ const parts = ["## Plan Context", "", `**Summary:** ${plan.summary}`];
59
+ if (plan.assumptions?.length) {
60
+ parts.push("", "**Assumptions:**");
61
+ plan.assumptions.forEach((a) => parts.push(`- ${a}`));
62
+ }
63
+ if (plan.constraints?.length) {
64
+ parts.push("", "**Constraints:**");
65
+ plan.constraints.forEach((c) => parts.push(`- ${c}`));
66
+ }
67
+ if (plan.unknowns?.length) {
68
+ parts.push("", "**Unknowns to investigate:**");
69
+ plan.unknowns.forEach((u) => {
70
+ parts.push(`- **${u.question}**`);
71
+ if (u.whyItMatters) parts.push(` Why it matters: ${u.whyItMatters}`);
72
+ if (u.howToResolve) parts.push(` How to resolve: ${u.howToResolve}`);
73
+ });
74
+ }
75
+ return parts.join("\n");
76
+ }
77
+ function buildStepsSection(plan) {
78
+ const parts = ["## Execution Steps"];
79
+ if (plan.phases?.length) {
80
+ for (const phase of plan.phases) {
81
+ parts.push("", `### Phase: ${phase.phaseName}`, `Goal: ${phase.goal}`);
82
+ if (phase.dependencies?.length) parts.push(`Dependencies: ${phase.dependencies.join(", ")}`);
83
+ for (const task of phase.tasks) {
84
+ parts.push(`${task.step}. **${task.action}**${task.ownerType ? ` [${task.ownerType}]` : ""}`);
85
+ if (task.details) parts.push(` ${task.details}`);
86
+ if (task.doneWhen) parts.push(` Done when: ${task.doneWhen}`);
87
+ if (task.files?.length) parts.push(` Files: ${task.files.join(", ")}`);
88
+ }
89
+ if (phase.outputs?.length) parts.push(`Outputs: ${phase.outputs.join(", ")}`);
90
+ }
91
+ } else {
92
+ parts.push("");
93
+ for (const step of plan.steps) {
94
+ parts.push(`${step.step}. **${step.action}**${step.ownerType ? ` [${step.ownerType}]` : ""}`);
95
+ if (step.details) parts.push(` ${step.details}`);
96
+ if (step.doneWhen) parts.push(` Done when: ${step.doneWhen}`);
97
+ if (step.files?.length) parts.push(` Files: ${step.files.join(", ")}`);
98
+ }
99
+ }
100
+ parts.push("", "Follow this plan. Complete each step in order.");
101
+ return parts.join("\n");
102
+ }
103
+ function buildRiskSection(plan) {
104
+ if (!plan.risks?.length) return "";
105
+ const parts = ["## Risks"];
106
+ for (const r of plan.risks) {
107
+ parts.push(`- **${r.risk}** \u2014 Impact: ${r.impact}. Mitigation: ${r.mitigation}`);
108
+ }
109
+ return parts.join("\n");
110
+ }
111
+ function buildValidationSection(plan) {
112
+ const parts = [];
113
+ if (plan.successCriteria?.length) {
114
+ parts.push("## Success Criteria");
115
+ plan.successCriteria.forEach((c) => parts.push(`- ${c}`));
116
+ }
117
+ if (plan.validation?.length) {
118
+ parts.push("", "## Validation Checks");
119
+ parts.push("Run these before marking as done:");
120
+ plan.validation.forEach((v) => parts.push(`- ${v}`));
121
+ }
122
+ if (plan.deliverables?.length) {
123
+ parts.push("", "## Deliverables");
124
+ plan.deliverables.forEach((d) => parts.push(`- ${d}`));
125
+ }
126
+ return parts.join("\n");
127
+ }
128
+ 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) {
134
+ parts.push("", "**Skills to activate:**");
135
+ td.skillsToUse.forEach((s) => parts.push(`- **${s.name}**: ${s.why}`));
136
+ }
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}`));
140
+ }
141
+ return parts.join("\n");
142
+ }
143
+ function buildStrategySection(plan) {
144
+ const es = plan.executionStrategy;
145
+ if (!es) return "";
146
+ const parts = [
147
+ "## Execution Strategy",
148
+ "",
149
+ `**Approach:** ${es.approach}`,
150
+ `**Rationale:** ${es.whyThisApproach}`
151
+ ];
152
+ if (es.alternativesConsidered?.length) {
153
+ parts.push("", "Alternatives considered:");
154
+ es.alternativesConsidered.forEach((a) => parts.push(`- ${a}`));
155
+ }
156
+ return parts.join("\n");
157
+ }
158
+ function resolveEffortForProvider(plan, role, globalEffort) {
159
+ const planEffort = plan?.suggestedEffort;
160
+ const roleKey = role;
161
+ return planEffort?.[roleKey] || planEffort?.default || globalEffort?.[roleKey] || globalEffort?.default || void 0;
162
+ }
163
+ function buildFullPlanPrompt(plan) {
164
+ return [
165
+ buildPlanContextSection(plan),
166
+ buildStrategySection(plan),
167
+ buildToolingSection(plan),
168
+ buildStepsSection(plan),
169
+ buildRiskSection(plan),
170
+ buildValidationSection(plan)
171
+ ].filter(Boolean).join("\n\n");
172
+ }
173
+ function extractValidationCommands(plan) {
174
+ const pre = [];
175
+ const post = [];
176
+ for (const v of plan.validation || []) {
177
+ const lower = v.toLowerCase();
178
+ if (lower.includes("lint")) post.push("pnpm lint --quiet 2>/dev/null || true");
179
+ if (lower.includes("typecheck") || lower.includes("tsc")) post.push("pnpm tsc --noEmit 2>/dev/null || true");
180
+ if (lower.includes("test")) post.push("pnpm test 2>/dev/null || true");
181
+ }
182
+ return { pre: [...new Set(pre)], post: [...new Set(post)] };
183
+ }
184
+ function buildExecutionPayload(issue, provider, plan, workspacePath) {
185
+ const strategy = plan.executionStrategy;
186
+ const hasPhases = Boolean(plan.phases?.length);
187
+ return {
188
+ version: 1,
189
+ issue: {
190
+ id: issue.id,
191
+ identifier: issue.identifier,
192
+ title: issue.title,
193
+ description: issue.description || "",
194
+ priority: issue.priority,
195
+ labels: issue.labels || [],
196
+ paths: issue.paths || []
197
+ },
198
+ provider: {
199
+ name: provider.provider,
200
+ role: provider.role,
201
+ model: provider.model || "default",
202
+ effort: provider.reasoningEffort || "medium",
203
+ capabilityCategory: provider.capabilityCategory || "",
204
+ overlays: provider.overlays || []
205
+ },
206
+ executionIntent: {
207
+ complexity: plan.estimatedComplexity,
208
+ approach: strategy?.approach || "",
209
+ rationale: strategy?.whyThisApproach || "",
210
+ workPattern: hasPhases ? "phased" : plan.toolingDecision?.shouldUseSubagents ? "parallel_subtasks" : "sequential"
211
+ },
212
+ plan: {
213
+ summary: plan.summary,
214
+ steps: plan.steps.map((s) => ({
215
+ step: s.step,
216
+ action: s.action,
217
+ files: s.files || [],
218
+ ownerType: s.ownerType || "agent",
219
+ doneWhen: s.doneWhen || ""
220
+ })),
221
+ phases: (plan.phases || []).map((p) => ({
222
+ name: p.phaseName,
223
+ goal: p.goal,
224
+ tasks: p.tasks.map((t) => t.step),
225
+ dependencies: p.dependencies || [],
226
+ outputs: p.outputs || []
227
+ }))
228
+ },
229
+ constraints: plan.constraints || [],
230
+ successCriteria: plan.successCriteria || [],
231
+ validation: plan.validation || [],
232
+ deliverables: plan.deliverables || [],
233
+ assumptions: plan.assumptions || [],
234
+ unknowns: (plan.unknowns || []).map((u) => ({
235
+ question: u.question,
236
+ whyItMatters: u.whyItMatters || "",
237
+ howToResolve: u.howToResolve || ""
238
+ })),
239
+ risks: (plan.risks || []).map((r) => ({
240
+ risk: r.risk,
241
+ impact: r.impact || "",
242
+ mitigation: r.mitigation || ""
243
+ })),
244
+ tooling: {
245
+ skills: plan.toolingDecision?.skillsToUse || [],
246
+ subagents: plan.toolingDecision?.subagentsToUse || []
247
+ },
248
+ targetPaths: plan.suggestedPaths || [],
249
+ workspacePath,
250
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
251
+ };
252
+ }
253
+
254
+ // src/agents/adapters/commands.ts
255
+ var CLAUDE_RESULT_SCHEMA = JSON.stringify({
256
+ type: "object",
257
+ properties: {
258
+ status: { type: "string", enum: ["done", "continue", "blocked", "failed"] },
259
+ summary: { type: "string" },
260
+ nextPrompt: { type: "string" }
261
+ },
262
+ required: ["status"]
263
+ });
264
+ var REVIEW_RESULT_SCHEMA = JSON.stringify({
265
+ type: "object",
266
+ properties: {
267
+ status: { type: "string", enum: ["done", "continue", "blocked", "failed"] },
268
+ summary: { type: "string" },
269
+ nextPrompt: { type: "string" },
270
+ criteriaResults: {
271
+ type: "array",
272
+ items: {
273
+ type: "object",
274
+ properties: { criterion: { type: "string" }, met: { type: "boolean" }, note: { type: "string" } }
275
+ }
276
+ }
277
+ },
278
+ required: ["status"]
279
+ });
280
+ function extractPlanDirs(plan) {
281
+ if (!plan.suggestedPaths?.length) return [];
282
+ const dirs = /* @__PURE__ */ new Set();
283
+ for (const p of plan.suggestedPaths) {
284
+ const lastSlash = p.lastIndexOf("/");
285
+ if (lastSlash > 0) dirs.add(p.slice(0, lastSlash));
286
+ else if (!p.includes(".")) dirs.add(p);
287
+ }
288
+ return [...dirs];
289
+ }
290
+
291
+ // src/agents/adapters/claude.ts
292
+ function buildClaudeCommand(options) {
293
+ const parts = ["claude", "--print"];
294
+ if (!options.noToolAccess) {
295
+ parts.push("--dangerously-skip-permissions");
296
+ }
297
+ parts.push("--no-session-persistence", "--output-format json");
298
+ if (options.effort) {
299
+ parts.push(`--effort ${options.effort}`);
300
+ }
301
+ if (options.jsonSchema) {
302
+ parts.push(`--json-schema '${options.jsonSchema}'`);
303
+ }
304
+ if (options.addDirs?.length) {
305
+ for (const dir of options.addDirs) {
306
+ parts.push(`--add-dir "${dir}"`);
307
+ }
308
+ }
309
+ if (options.model && options.model !== "claude") {
310
+ parts.splice(1, 0, `--model ${options.model}`);
311
+ }
312
+ parts.push('< "$FIFONY_PROMPT_FILE"');
313
+ return parts.join(" ");
314
+ }
315
+ async function compile(issue, provider, plan, config, workspacePath, skillContext) {
316
+ const effort = resolveEffortForProvider(plan, provider.role, config.defaultEffort);
317
+ const prompt = await renderPrompt("compile-execution-claude", {
318
+ isPlanner: provider.role === "planner",
319
+ isReviewer: provider.role === "reviewer",
320
+ profileInstructions: provider.profileInstructions || "",
321
+ skillContext,
322
+ planPrompt: buildFullPlanPrompt(plan),
323
+ subagentsToUse: plan.toolingDecision?.shouldUseSubagents ? plan.toolingDecision.subagentsToUse ?? [] : [],
324
+ skillsToUse: plan.toolingDecision?.shouldUseSkills ? plan.toolingDecision.skillsToUse ?? [] : [],
325
+ suggestedPaths: plan.suggestedPaths ?? [],
326
+ workspacePath,
327
+ issueIdentifier: issue.identifier,
328
+ title: issue.title,
329
+ description: issue.description || "(none)",
330
+ validationItems: (plan.validation ?? []).map((value) => ({ value }))
331
+ });
332
+ const relativeDirs = extractPlanDirs(plan);
333
+ const codePath = existsSync(join(workspacePath, "worktree")) ? join(workspacePath, "worktree") : workspacePath;
334
+ const absoluteDirs = relativeDirs.map((d) => join(codePath, d));
335
+ const command = buildClaudeCommand({
336
+ model: provider.model,
337
+ effort,
338
+ addDirs: absoluteDirs,
339
+ jsonSchema: CLAUDE_RESULT_SCHEMA
340
+ });
341
+ const env2 = {
342
+ FIFONY_PLAN_COMPLEXITY: plan.estimatedComplexity,
343
+ FIFONY_PLAN_STEPS: String(plan.steps.length),
344
+ FIFONY_EXECUTION_PAYLOAD_FILE: "fifony-execution-payload.json"
345
+ };
346
+ 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(",");
349
+ }
350
+ const { pre, post } = extractValidationCommands(plan);
351
+ return {
352
+ prompt,
353
+ command,
354
+ env: env2,
355
+ preHooks: pre,
356
+ postHooks: post,
357
+ outputSchema: CLAUDE_RESULT_SCHEMA,
358
+ payload: null,
359
+ meta: {
360
+ adapter: "claude",
361
+ reasoningEffort: effort || "default",
362
+ model: provider.model || "default",
363
+ skillsActivated: plan.toolingDecision?.skillsToUse?.map((s) => s.name) || [],
364
+ subagentsRequested: plan.toolingDecision?.subagentsToUse?.map((a) => a.name) || [],
365
+ phasesCount: plan.phases?.length || 0
366
+ }
367
+ };
368
+ }
369
+ var claudeAdapter = {
370
+ buildCommand: buildClaudeCommand,
371
+ buildReviewCommand: (reviewer) => buildClaudeCommand({
372
+ model: reviewer.model,
373
+ effort: reviewer.reasoningEffort,
374
+ jsonSchema: REVIEW_RESULT_SCHEMA
375
+ }),
376
+ compile
377
+ };
378
+
379
+ // src/agents/adapters/codex.ts
380
+ import { existsSync as existsSync2 } from "fs";
381
+ import { join as join2 } from "path";
382
+ var CODEX_RESULT_CONTRACT = `
383
+ Return a JSON object with this exact schema when finished:
384
+ {
385
+ "status": "done" | "continue" | "blocked" | "failed",
386
+ "summary": "one paragraph summary of what was done",
387
+ "root_cause": ["list of root causes found"],
388
+ "changes_made": ["list of files/changes"],
389
+ "validation": { "commands_run": ["..."], "result": "pass" | "partial" | "fail" },
390
+ "open_questions": ["..."],
391
+ "followups": ["..."],
392
+ "nextPrompt": "guidance for next turn if status is continue"
393
+ }
394
+ `.trim();
395
+ function buildCodexCommand(options) {
396
+ const parts = ["codex", "exec", "--dangerously-bypass-approvals-and-sandbox"];
397
+ if (options.model && options.model !== "codex") {
398
+ parts.push(`--model ${options.model}`);
399
+ }
400
+ if (options.effort) {
401
+ parts.push(`-c reasoning_effort="${options.effort}"`);
402
+ }
403
+ if (options.addDirs?.length) {
404
+ for (const dir of options.addDirs) {
405
+ parts.push(`--add-dir "${dir}"`);
406
+ }
407
+ }
408
+ if (options.imagePaths?.length) {
409
+ for (const img of options.imagePaths) {
410
+ parts.push(`--image "${img}"`);
411
+ }
412
+ }
413
+ parts.push('< "$FIFONY_PROMPT_FILE"');
414
+ return parts.join(" ");
415
+ }
416
+ async function compile2(issue, provider, plan, config, workspacePath, skillContext) {
417
+ const effort = resolveEffortForProvider(plan, provider.role, config.defaultEffort) || provider.reasoningEffort;
418
+ const prompt = await renderPrompt("compile-execution-codex", {
419
+ isPlanner: provider.role === "planner",
420
+ isReviewer: provider.role === "reviewer",
421
+ profileInstructions: provider.profileInstructions || "",
422
+ skillContext,
423
+ issueIdentifier: issue.identifier,
424
+ title: issue.title,
425
+ description: issue.description || "(none)",
426
+ workspacePath,
427
+ planPrompt: buildFullPlanPrompt(plan),
428
+ phases: (plan.phases ?? []).map((phase) => ({
429
+ phaseName: phase.phaseName,
430
+ goal: phase.goal,
431
+ outputs: phase.outputs ?? []
432
+ })),
433
+ suggestedPaths: plan.suggestedPaths ?? [],
434
+ skillsToUse: plan.toolingDecision?.shouldUseSkills ? plan.toolingDecision.skillsToUse ?? [] : [],
435
+ validationItems: (plan.validation ?? []).map((value) => ({ value })),
436
+ outputContract: CODEX_RESULT_CONTRACT
437
+ });
438
+ const relativeDirs = extractPlanDirs(plan);
439
+ const codePath = existsSync2(join2(workspacePath, "worktree")) ? join2(workspacePath, "worktree") : workspacePath;
440
+ const absoluteDirs = relativeDirs.map((d) => join2(codePath, d));
441
+ const command = buildCodexCommand({
442
+ model: provider.model,
443
+ addDirs: absoluteDirs,
444
+ effort
445
+ });
446
+ const env2 = {
447
+ FIFONY_PLAN_COMPLEXITY: plan.estimatedComplexity,
448
+ FIFONY_PLAN_STEPS: String(plan.steps.length),
449
+ FIFONY_PLAN_PHASES: String(plan.phases?.length || 0),
450
+ FIFONY_EXECUTION_PAYLOAD_FILE: "execution-payload.json"
451
+ };
452
+ if (plan.suggestedPaths?.length) env2.FIFONY_PLAN_PATHS = plan.suggestedPaths.join(",");
453
+ const { pre, post } = extractValidationCommands(plan);
454
+ return {
455
+ prompt,
456
+ command,
457
+ env: env2,
458
+ preHooks: pre,
459
+ postHooks: post,
460
+ outputSchema: "",
461
+ payload: null,
462
+ meta: {
463
+ adapter: "codex",
464
+ reasoningEffort: effort || "default",
465
+ model: provider.model || "default",
466
+ skillsActivated: plan.toolingDecision?.skillsToUse?.map((s) => s.name) || [],
467
+ subagentsRequested: [],
468
+ phasesCount: plan.phases?.length || 0
469
+ }
470
+ };
471
+ }
472
+ var codexAdapter = {
473
+ buildCommand: buildCodexCommand,
474
+ buildReviewCommand: (reviewer) => buildCodexCommand({
475
+ model: reviewer.model,
476
+ effort: reviewer.reasoningEffort
477
+ }),
478
+ compile: compile2
479
+ };
480
+
481
+ // src/agents/adapters/gemini.ts
482
+ import { existsSync as existsSync3 } from "fs";
483
+ import { join as join3 } from "path";
484
+ var GEMINI_RESULT_CONTRACT = `
485
+ Return a JSON object with this exact schema when finished:
486
+ {
487
+ "status": "done" | "continue" | "blocked" | "failed",
488
+ "summary": "one paragraph summary of what was done",
489
+ "root_cause": ["list of root causes found"],
490
+ "changes_made": ["list of files/changes"],
491
+ "validation": { "commands_run": ["..."], "result": "pass" | "partial" | "fail" },
492
+ "open_questions": ["..."],
493
+ "followups": ["..."],
494
+ "nextPrompt": "guidance for next turn if status is continue"
495
+ }
496
+ `.trim();
497
+ function buildGeminiCommand(options) {
498
+ const parts = ["gemini", "--yolo"];
499
+ if (options.model) {
500
+ parts.push(`--model ${options.model}`);
501
+ }
502
+ if (options.addDirs?.length) {
503
+ parts.push(`--include-directories ${options.addDirs.map((d) => `"${d}"`).join(",")}`);
504
+ }
505
+ parts.push('-p "" < "$FIFONY_PROMPT_FILE"');
506
+ return parts.join(" ");
507
+ }
508
+ async function compile3(issue, provider, plan, config, workspacePath, skillContext) {
509
+ const effort = resolveEffortForProvider(plan, provider.role, config.defaultEffort) || provider.reasoningEffort;
510
+ const prompt = await renderPrompt("compile-execution-codex", {
511
+ isPlanner: provider.role === "planner",
512
+ isReviewer: provider.role === "reviewer",
513
+ profileInstructions: provider.profileInstructions || "",
514
+ skillContext,
515
+ issueIdentifier: issue.identifier,
516
+ title: issue.title,
517
+ description: issue.description || "(none)",
518
+ workspacePath,
519
+ planPrompt: buildFullPlanPrompt(plan),
520
+ phases: (plan.phases ?? []).map((phase) => ({
521
+ phaseName: phase.phaseName,
522
+ goal: phase.goal,
523
+ outputs: phase.outputs ?? []
524
+ })),
525
+ suggestedPaths: plan.suggestedPaths ?? [],
526
+ skillsToUse: plan.toolingDecision?.shouldUseSkills ? plan.toolingDecision.skillsToUse ?? [] : [],
527
+ validationItems: (plan.validation ?? []).map((value) => ({ value })),
528
+ outputContract: GEMINI_RESULT_CONTRACT
529
+ });
530
+ const relativeDirs = extractPlanDirs(plan);
531
+ const codePath = existsSync3(join3(workspacePath, "worktree")) ? join3(workspacePath, "worktree") : workspacePath;
532
+ const absoluteDirs = relativeDirs.map((d) => join3(codePath, d));
533
+ const command = buildGeminiCommand({
534
+ model: provider.model,
535
+ addDirs: absoluteDirs
536
+ });
537
+ const env2 = {
538
+ FIFONY_PLAN_COMPLEXITY: plan.estimatedComplexity,
539
+ FIFONY_PLAN_STEPS: String(plan.steps.length),
540
+ FIFONY_PLAN_PHASES: String(plan.phases?.length || 0),
541
+ FIFONY_EXECUTION_PAYLOAD_FILE: "execution-payload.json"
542
+ };
543
+ if (plan.suggestedPaths?.length) env2.FIFONY_PLAN_PATHS = plan.suggestedPaths.join(",");
544
+ const { pre, post } = extractValidationCommands(plan);
545
+ return {
546
+ prompt,
547
+ command,
548
+ env: env2,
549
+ preHooks: pre,
550
+ postHooks: post,
551
+ outputSchema: "",
552
+ payload: null,
553
+ meta: {
554
+ adapter: "gemini",
555
+ reasoningEffort: effort || "default",
556
+ model: provider.model || "default",
557
+ skillsActivated: plan.toolingDecision?.skillsToUse?.map((s) => s.name) || [],
558
+ subagentsRequested: [],
559
+ phasesCount: plan.phases?.length || 0
560
+ }
561
+ };
562
+ }
563
+ var geminiAdapter = {
564
+ buildCommand: buildGeminiCommand,
565
+ buildReviewCommand: (reviewer) => buildGeminiCommand({
566
+ model: reviewer.model
567
+ }),
568
+ compile: compile3
569
+ };
570
+
571
+ // src/agents/adapters/registry.ts
572
+ var ADAPTERS = {
573
+ claude: claudeAdapter,
574
+ codex: codexAdapter,
575
+ gemini: geminiAdapter
576
+ };
577
+
578
+ // src/agents/model-discovery.ts
579
+ import { execFileSync } from "child_process";
580
+ import { existsSync as existsSync4, readFileSync, realpathSync } from "fs";
581
+ import { join as join4, dirname } from "path";
582
+ import { homedir } from "os";
583
+ var modelCache = /* @__PURE__ */ new Map();
584
+ var MODEL_CACHE_TTL_MS = 5 * 60 * 1e3;
585
+ function readClaudeConfig() {
586
+ try {
587
+ const settingsPath = join4(homedir(), ".claude", "settings.json");
588
+ if (!existsSync4(settingsPath)) return {};
589
+ const raw = readFileSync(settingsPath, "utf8");
590
+ const settings = JSON.parse(raw);
591
+ return { model: typeof settings.model === "string" ? settings.model : void 0 };
592
+ } catch {
593
+ return {};
594
+ }
595
+ }
596
+ function resolveGeminiModelsFile() {
597
+ try {
598
+ const binPath = execFileSync("which", ["gemini"], { encoding: "utf8", timeout: 3e3 }).trim();
599
+ if (!binPath) return null;
600
+ const realBin = realpathSync(binPath);
601
+ const cliRoot = dirname(dirname(realBin));
602
+ const modelsPath = join4(cliRoot, "node_modules", "@google", "gemini-cli-core", "dist", "src", "config", "models.js");
603
+ return existsSync4(modelsPath) ? modelsPath : null;
604
+ } catch {
605
+ return null;
606
+ }
607
+ }
608
+ async function fetchGeminiModels() {
609
+ const modelsPath = resolveGeminiModelsFile();
610
+ if (!modelsPath) return [];
611
+ try {
612
+ const content = readFileSync(modelsPath, "utf8");
613
+ const regex = /export const ([A-Z0-9_]+)\s*=\s*'(gemini-[^']+)';/g;
614
+ const seen = /* @__PURE__ */ new Set();
615
+ const stable = [];
616
+ const preview = [];
617
+ let match;
618
+ while ((match = regex.exec(content)) !== null) {
619
+ const [, constName, modelId] = match;
620
+ if (seen.has(modelId)) continue;
621
+ if (modelId.includes("embedding")) continue;
622
+ seen.add(modelId);
623
+ const isPreview = constName.startsWith("PREVIEW_");
624
+ const tier = isPreview ? "Preview" : "Stable";
625
+ const model = { id: modelId, provider: "gemini", label: modelId, tier };
626
+ if (isPreview) preview.push(model);
627
+ else stable.push(model);
628
+ }
629
+ return [...stable, ...preview];
630
+ } catch {
631
+ return [];
632
+ }
633
+ }
634
+ async function fetchCodexModels() {
635
+ const cachePath = join4(homedir(), ".codex", "models_cache.json");
636
+ try {
637
+ if (existsSync4(cachePath)) {
638
+ const raw = readFileSync(cachePath, "utf8");
639
+ const cache = JSON.parse(raw);
640
+ if (Array.isArray(cache.models) && cache.models.length > 0) {
641
+ return cache.models.sort((a, b) => {
642
+ const visA = a.visibility === "list" ? 0 : 1;
643
+ const visB = b.visibility === "list" ? 0 : 1;
644
+ if (visA !== visB) return visA - visB;
645
+ return (a.priority ?? 99) - (b.priority ?? 99);
646
+ }).map((m) => ({
647
+ id: m.slug,
648
+ provider: "codex",
649
+ label: m.slug,
650
+ tier: m.description || (m.visibility === "list" ? "Supported" : "Legacy")
651
+ }));
652
+ }
653
+ }
654
+ } catch {
655
+ }
656
+ return [];
657
+ }
658
+ async function fetchAnthropicModels() {
659
+ try {
660
+ execFileSync("which", ["claude"], { encoding: "utf8", timeout: 3e3 });
661
+ } catch {
662
+ return [];
663
+ }
664
+ return [
665
+ { id: "opus", provider: "claude", label: "claude/opus (latest)", tier: "Most capable" },
666
+ { id: "sonnet", provider: "claude", label: "claude/sonnet (latest)", tier: "Balanced" },
667
+ { id: "haiku", provider: "claude", label: "claude/haiku (latest)", tier: "Fast" }
668
+ ];
669
+ }
670
+ async function discoverModels(providers) {
671
+ const result = {};
672
+ const tasks = [];
673
+ for (const p of providers) {
674
+ if (!p.available) continue;
675
+ const cached = modelCache.get(p.name);
676
+ if (cached && Date.now() - cached.fetchedAt < MODEL_CACHE_TTL_MS) {
677
+ result[p.name] = cached.models;
678
+ continue;
679
+ }
680
+ if (p.name === "codex") tasks.push({ name: "codex", fetch: fetchCodexModels });
681
+ if (p.name === "claude") tasks.push({ name: "claude", fetch: fetchAnthropicModels });
682
+ if (p.name === "gemini") tasks.push({ name: "gemini", fetch: fetchGeminiModels });
683
+ }
684
+ const settled = await Promise.allSettled(tasks.map((t) => t.fetch()));
685
+ for (let i = 0; i < tasks.length; i++) {
686
+ const res = settled[i];
687
+ let models = res.status === "fulfilled" ? res.value : [];
688
+ if (tasks[i].name === "codex") {
689
+ const { model: configuredModel } = readCodexConfig();
690
+ if (configuredModel) {
691
+ const idx = models.findIndex((m) => m.id === configuredModel);
692
+ if (idx > 0) {
693
+ models = [models[idx], ...models.slice(0, idx), ...models.slice(idx + 1)];
694
+ } else if (idx === -1) {
695
+ models = [{ id: configuredModel, provider: "codex", label: configuredModel, tier: "Configured default" }, ...models];
696
+ }
697
+ }
698
+ }
699
+ if (tasks[i].name === "claude") {
700
+ const { model: configuredModel } = readClaudeConfig();
701
+ if (configuredModel) {
702
+ const idx = models.findIndex((m) => m.id === configuredModel || m.id.includes(configuredModel));
703
+ if (idx > 0) {
704
+ models = [models[idx], ...models.slice(0, idx), ...models.slice(idx + 1)];
705
+ }
706
+ }
707
+ }
708
+ if (tasks[i].name === "gemini") {
709
+ const { model: configuredModel, previewFeatures } = readGeminiConfig();
710
+ if (configuredModel) {
711
+ const idx = models.findIndex((m) => m.id === configuredModel);
712
+ if (idx > 0) {
713
+ models = [models[idx], ...models.slice(0, idx), ...models.slice(idx + 1)];
714
+ } else if (idx === -1) {
715
+ models = [{ id: configuredModel, provider: "gemini", label: configuredModel, tier: "Configured default" }, ...models];
716
+ }
717
+ } else if (previewFeatures) {
718
+ const previewIdx = models.findIndex((m) => m.tier === "Preview");
719
+ if (previewIdx > 0) {
720
+ models = [models[previewIdx], ...models.slice(0, previewIdx), ...models.slice(previewIdx + 1)];
721
+ }
722
+ }
723
+ }
724
+ result[tasks[i].name] = models;
725
+ modelCache.set(tasks[i].name, { models, fetchedAt: Date.now() });
726
+ }
727
+ return result;
728
+ }
729
+
730
+ // 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
+ function normalizeAgentProvider(value) {
754
+ const normalized = value.trim().toLowerCase();
755
+ if (normalized === "claude" || normalized === "codex" || normalized === "gemini") return normalized;
756
+ if (!normalized) return "codex";
757
+ return normalized;
758
+ }
759
+ function resolveAgentCommand(provider, explicitCommand, codexCommand, claudeCommand, reasoningEffort) {
760
+ if (explicitCommand.trim()) return explicitCommand.trim();
761
+ if (provider === "claude" && claudeCommand.trim()) return claudeCommand.trim();
762
+ if (provider === "codex" && codexCommand.trim()) return codexCommand.trim();
763
+ return getProviderDefaultCommand(provider, reasoningEffort);
764
+ }
765
+ function resolveEffort(role, issueEffort, globalEffort) {
766
+ const roleKey = role;
767
+ if (issueEffort?.[roleKey]) return issueEffort[roleKey];
768
+ if (issueEffort?.default) return issueEffort.default;
769
+ if (globalEffort?.[roleKey]) return globalEffort[roleKey];
770
+ return globalEffort?.default;
771
+ }
772
+ function getProviderDefaultCommand(provider, reasoningEffort, model) {
773
+ const adapter = ADAPTERS[provider];
774
+ if (!adapter) return "";
775
+ const jsonSchema = provider === "claude" ? CLAUDE_RESULT_SCHEMA : void 0;
776
+ return adapter.buildCommand({ model, effort: reasoningEffort, jsonSchema });
777
+ }
778
+ var cachedProviders = null;
779
+ var providersCachedAt = 0;
780
+ var PROVIDER_CACHE_TTL = 6e4;
781
+ function detectAvailableProviders() {
782
+ if (cachedProviders && Date.now() - providersCachedAt < PROVIDER_CACHE_TTL) {
783
+ return cachedProviders;
784
+ }
785
+ const providers = [];
786
+ for (const name of ["claude", "codex", "gemini"]) {
787
+ try {
788
+ const path = execFileSync2("which", [name], { encoding: "utf8", timeout: 5e3 }).trim();
789
+ providers.push({ name, available: true, path });
790
+ } catch {
791
+ providers.push({ name, available: false, path: "" });
792
+ }
793
+ }
794
+ cachedProviders = providers;
795
+ providersCachedAt = Date.now();
796
+ return providers;
797
+ }
798
+ function readCodexConfig() {
799
+ try {
800
+ const configPath = join5(homedir2(), ".codex", "config.toml");
801
+ if (!existsSync5(configPath)) return {};
802
+ const raw = readFileSync2(configPath, "utf8");
803
+ const model = raw.match(/^model\s*=\s*"([^"]+)"/m)?.[1];
804
+ const reasoningEffort = raw.match(/^model_reasoning_effort\s*=\s*"([^"]+)"/m)?.[1];
805
+ return { model, reasoningEffort };
806
+ } catch {
807
+ return {};
808
+ }
809
+ }
810
+ function readGeminiConfig() {
811
+ try {
812
+ const settingsPath = join5(homedir2(), ".gemini", "settings.json");
813
+ if (!existsSync5(settingsPath)) return {};
814
+ const raw = readFileSync2(settingsPath, "utf8");
815
+ const settings = JSON.parse(raw);
816
+ return {
817
+ model: typeof settings.model === "string" ? settings.model : void 0,
818
+ previewFeatures: settings.general?.previewFeatures === true
819
+ };
820
+ } catch {
821
+ return {};
822
+ }
823
+ }
824
+ function resolveDefaultProvider(detected) {
825
+ const available = detected.filter((p) => p.available);
826
+ if (available.length === 0) return "";
827
+ if (available.some((p) => p.name === "codex")) return "codex";
828
+ return available[0].name;
829
+ }
830
+ function getBaseAgentProviders(state) {
831
+ return [
832
+ {
833
+ provider: state.config.agentProvider,
834
+ role: "executor",
835
+ command: state.config.agentCommand,
836
+ profile: "",
837
+ profilePath: "",
838
+ profileInstructions: ""
839
+ }
840
+ ];
841
+ }
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
+ function roleToStageKey(role) {
857
+ switch (role) {
858
+ case "planner":
859
+ return "plan";
860
+ case "executor":
861
+ return "execute";
862
+ case "reviewer":
863
+ return "review";
864
+ }
865
+ }
866
+ function applyWorkflowConfigToProviders(providers, workflowConfig) {
867
+ if (!workflowConfig) return providers;
868
+ return providers.map((provider) => {
869
+ const stageKey = roleToStageKey(provider.role);
870
+ const stageConfig = workflowConfig[stageKey];
871
+ if (!stageConfig) return provider;
872
+ const newProvider = stageConfig.provider || provider.provider;
873
+ const newModel = stageConfig.model || void 0;
874
+ const newEffort = stageConfig.effort || provider.reasoningEffort;
875
+ const command = getProviderDefaultCommand(newProvider, newEffort, newModel);
876
+ return {
877
+ ...provider,
878
+ provider: newProvider,
879
+ model: newModel,
880
+ command: command || provider.command,
881
+ reasoningEffort: newEffort
882
+ };
883
+ });
884
+ }
885
+ function getEffectiveAgentProviders(state, issue, _workflowDefinition, workflowConfig) {
886
+ 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
+ );
904
+ const effort = resolveEffort(provider.role, issue.effort, state.config.defaultEffort);
905
+ const command = provider.command;
906
+ return {
907
+ ...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
+ reasoningEffort: effort
915
+ };
916
+ });
917
+ return applyWorkflowConfigToProviders(merged, workflowConfig ?? null);
918
+ }
919
+
920
+ // src/agents/command-executor.ts
921
+ async function runCommandWithTimeout(command, workspacePath, issue, config, promptText, promptFile, extraEnv = {}) {
922
+ return new Promise((resolve2) => {
923
+ const started = Date.now();
924
+ const resultFile = extraEnv.FIFONY_RESULT_FILE;
925
+ if (resultFile && extraEnv.FIFONY_PRESERVE_RESULT_FILE !== "1") {
926
+ rmSync(resultFile, { force: true });
927
+ }
928
+ const allVars = {
929
+ FIFONY_ISSUE_ID: issue.id,
930
+ FIFONY_ISSUE_IDENTIFIER: issue.identifier,
931
+ FIFONY_ISSUE_TITLE: issue.title,
932
+ FIFONY_ISSUE_PRIORITY: String(issue.priority),
933
+ FIFONY_WORKSPACE_PATH: issue.worktreePath ?? workspacePath,
934
+ FIFONY_PROMPT_FILE: promptFile
935
+ };
936
+ for (const [key, value] of Object.entries(extraEnv)) {
937
+ if (value.length > 4e3) {
938
+ const valFile = join6(workspacePath, `${key.toLowerCase()}.txt`);
939
+ writeFileSync(valFile, value, "utf8");
940
+ allVars[`${key}_FILE`] = valFile;
941
+ } else {
942
+ allVars[key] = value;
943
+ }
944
+ }
945
+ const envFilePath = join6(workspacePath, ".env.sh");
946
+ const envFileLines = Object.entries(allVars).map(([k, v]) => `export ${k}='${String(v).replace(/'/g, "'\\''")}'`).join("\n");
947
+ writeFileSync(envFilePath, envFileLines, "utf8");
948
+ const wrappedCommand = `. "${envFilePath}" && ${command}`;
949
+ const child = spawn(wrappedCommand, {
950
+ shell: true,
951
+ cwd: issue.worktreePath ?? workspacePath,
952
+ detached: true,
953
+ // Survive parent death
954
+ stdio: ["pipe", "pipe", "pipe"]
955
+ });
956
+ child.unref();
957
+ if (child.stdin) {
958
+ child.stdin.end();
959
+ }
960
+ const pidFile = join6(workspacePath, "agent.pid");
961
+ const pid = child.pid;
962
+ if (pid) {
963
+ logger.debug({ issueId: issue.id, pid, command: command.slice(0, 120), cwd: workspacePath }, "[Agent] Process spawned");
964
+ writeFileSync(pidFile, JSON.stringify({
965
+ pid,
966
+ issueId: issue.id,
967
+ startedAt: new Date(started).toISOString(),
968
+ command: command.slice(0, 200)
969
+ }), "utf8");
970
+ }
971
+ let output = "";
972
+ let timedOut = false;
973
+ let outputBytes = 0;
974
+ let outputHeader = "";
975
+ const liveLogFile = join6(workspacePath, "live-output.log");
976
+ writeFileSync(liveLogFile, "", "utf8");
977
+ const onChunk = (chunk) => {
978
+ const text = String(chunk);
979
+ if (outputHeader.length < 2e3) outputHeader = (outputHeader + text).slice(0, 2e3);
980
+ output = appendFileTail(output, text, config.logLinesTail);
981
+ outputBytes += text.length;
982
+ try {
983
+ appendFileSync(liveLogFile, text);
984
+ } catch {
985
+ }
986
+ issue.commandOutputTail = output;
987
+ };
988
+ child.stdout?.on("data", onChunk);
989
+ child.stderr?.on("data", onChunk);
990
+ const AGENT_STALE_OUTPUT_MS = 3e5;
991
+ const timer = setTimeout(() => {
992
+ timedOut = true;
993
+ if (pid) {
994
+ try {
995
+ process.kill(-pid, "SIGTERM");
996
+ } catch {
997
+ }
998
+ } else {
999
+ child.kill("SIGTERM");
1000
+ }
1001
+ }, config.commandTimeoutMs);
1002
+ let lastWatchdogBytes = 0;
1003
+ let lastOutputGrowthAt = Date.now();
1004
+ let watchdogKilled = false;
1005
+ const watchdog = setInterval(() => {
1006
+ if (pid) {
1007
+ try {
1008
+ process.kill(pid, 0);
1009
+ } catch {
1010
+ clearInterval(watchdog);
1011
+ clearTimeout(timer);
1012
+ watchdogKilled = true;
1013
+ try {
1014
+ rmSync(pidFile, { force: true });
1015
+ } catch {
1016
+ }
1017
+ resolve2({ success: false, code: null, output: appendFileTail(output, `
1018
+ Agent process died unexpectedly (PID ${pid}).`, config.logLinesTail) });
1019
+ return;
1020
+ }
1021
+ }
1022
+ if (outputBytes > lastWatchdogBytes) {
1023
+ lastWatchdogBytes = outputBytes;
1024
+ lastOutputGrowthAt = Date.now();
1025
+ } else if (Date.now() - lastOutputGrowthAt > AGENT_STALE_OUTPUT_MS) {
1026
+ clearInterval(watchdog);
1027
+ clearTimeout(timer);
1028
+ timedOut = true;
1029
+ watchdogKilled = true;
1030
+ if (pid) {
1031
+ try {
1032
+ process.kill(-pid, "SIGTERM");
1033
+ } catch {
1034
+ }
1035
+ } else {
1036
+ child.kill("SIGTERM");
1037
+ }
1038
+ try {
1039
+ rmSync(pidFile, { force: true });
1040
+ } catch {
1041
+ }
1042
+ resolve2({ success: false, code: null, output: appendFileTail(output, `
1043
+ Agent process stuck \u2014 no output for ${Math.round(AGENT_STALE_OUTPUT_MS / 6e4)} minutes.`, config.logLinesTail) });
1044
+ }
1045
+ }, 3e4);
1046
+ const cleanup = () => {
1047
+ clearInterval(watchdog);
1048
+ try {
1049
+ rmSync(pidFile, { force: true });
1050
+ } catch {
1051
+ }
1052
+ };
1053
+ child.on("error", () => {
1054
+ clearTimeout(timer);
1055
+ cleanup();
1056
+ if (watchdogKilled) return;
1057
+ resolve2({ success: false, code: null, output: `Command execution failed for issue ${issue.id}.` });
1058
+ });
1059
+ child.on("close", (code) => {
1060
+ clearTimeout(timer);
1061
+ cleanup();
1062
+ if (watchdogKilled) return;
1063
+ const buildOutput = (suffix) => {
1064
+ const tail = appendFileTail(output, suffix, config.logLinesTail);
1065
+ return outputHeader.length > 0 && !tail.startsWith(outputHeader.slice(0, 80)) ? `${outputHeader}
1066
+ ${tail}` : tail;
1067
+ };
1068
+ if (timedOut) {
1069
+ resolve2({ success: false, code: null, output: buildOutput(`
1070
+ Execution timeout after ${config.commandTimeoutMs}ms.`) });
1071
+ return;
1072
+ }
1073
+ const duration = Math.max(0, Date.now() - started);
1074
+ if (code === 0) {
1075
+ resolve2({ success: true, code, output: buildOutput(`
1076
+ Execution succeeded in ${duration}ms.`) });
1077
+ return;
1078
+ }
1079
+ resolve2({ success: false, code, output: buildOutput(`
1080
+ Command exit code ${code ?? "unknown"} after ${duration}ms.`) });
1081
+ });
1082
+ });
1083
+ }
1084
+ async function runHook(command, workspacePath, issue, hookName, extraEnv = {}) {
1085
+ if (!command.trim()) return;
1086
+ const result = await runCommandWithTimeout(command, workspacePath, issue, {
1087
+ pollIntervalMs: 0,
1088
+ workerConcurrency: 1,
1089
+ maxConcurrentByState: {},
1090
+ commandTimeoutMs: 3e5,
1091
+ maxAttemptsDefault: 1,
1092
+ retryDelayMs: 0,
1093
+ staleInProgressTimeoutMs: 0,
1094
+ logLinesTail: 12e3,
1095
+ agentProvider: normalizeAgentProvider(env.FIFONY_AGENT_PROVIDER ?? "codex"),
1096
+ agentCommand: command,
1097
+ maxTurns: 1,
1098
+ runMode: "filesystem"
1099
+ }, "", "", { FIFONY_HOOK_NAME: hookName, ...extraEnv });
1100
+ if (!result.success) {
1101
+ throw new Error(`${hookName} hook failed: ${result.output}`);
1102
+ }
1103
+ }
1104
+
1105
+ // src/agents/prompt-builder.ts
1106
+ async function buildPrompt(issue, _workflowDefinition) {
1107
+ const rendered = await renderPrompt("workflow-default", { issue, attempt: issue.attempts || 0 });
1108
+ if (!issue.plan?.steps?.length) {
1109
+ return rendered;
1110
+ }
1111
+ const planSection = await renderPrompt("workflow-plan-section", {
1112
+ estimatedComplexity: issue.plan.estimatedComplexity,
1113
+ summary: issue.plan.summary,
1114
+ steps: issue.plan.steps.map((step) => ({
1115
+ step: step.step,
1116
+ action: step.action,
1117
+ files: step.files ?? [],
1118
+ details: step.details ?? ""
1119
+ }))
1120
+ });
1121
+ return `${rendered}
1122
+
1123
+ ${planSection}`;
1124
+ }
1125
+ async function buildTurnPrompt(issue, basePrompt, previousOutput, turnIndex, maxTurns, nextPrompt) {
1126
+ if (turnIndex === 1) return basePrompt;
1127
+ return renderPrompt("agent-turn", {
1128
+ issueIdentifier: issue.identifier,
1129
+ turnIndex,
1130
+ maxTurns,
1131
+ basePrompt,
1132
+ continuation: nextPrompt.trim() || "Continue the work, inspect the workspace, and move the issue toward completion.",
1133
+ outputTail: previousOutput.trim() || "No previous output captured."
1134
+ });
1135
+ }
1136
+ async function buildProviderBasePrompt(provider, issue, basePrompt, workspacePath, skillContext) {
1137
+ return renderPrompt("agent-provider-base", {
1138
+ isPlanner: provider.role === "planner",
1139
+ isReviewer: provider.role === "reviewer",
1140
+ hasImpeccableOverlay: provider.overlays?.includes("impeccable") ?? false,
1141
+ hasFrontendDesignOverlay: provider.overlays?.includes("frontend-design") ?? false,
1142
+ profileInstructions: provider.profileInstructions || "",
1143
+ skillContext,
1144
+ capabilityCategory: provider.capabilityCategory || "",
1145
+ selectionReason: provider.selectionReason ?? "No additional routing reason.",
1146
+ overlays: provider.overlays ?? [],
1147
+ targetPaths: issue.paths ?? [],
1148
+ workspacePath,
1149
+ basePrompt
1150
+ });
1151
+ }
1152
+
1153
+ // src/domains/workspace.ts
1154
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
1155
+ ".git",
1156
+ ".fifony",
1157
+ "node_modules",
1158
+ ".venv",
1159
+ "data",
1160
+ "dist",
1161
+ "build",
1162
+ ".turbo",
1163
+ ".next",
1164
+ ".nuxt",
1165
+ ".tanstack",
1166
+ "coverage",
1167
+ "artifacts",
1168
+ "captures",
1169
+ "tmp",
1170
+ "temp"
1171
+ ]);
1172
+ function shouldSkipPath(relativePath) {
1173
+ const parts = relativePath.split("/");
1174
+ if (parts.some((segment) => SKIP_DIRS.has(segment))) return true;
1175
+ const base = parts.at(-1) ?? "";
1176
+ if (base.startsWith("map_scan_") && extname(base) === ".json") return true;
1177
+ if (extname(base) === ".xlsx") return true;
1178
+ return false;
1179
+ }
1180
+ var sourceReadyPromise = null;
1181
+ var skipSourceFlag = false;
1182
+ function setSkipSource(skip) {
1183
+ skipSourceFlag = skip;
1184
+ }
1185
+ async function ensureSourceReady(onProgress) {
1186
+ if (skipSourceFlag) {
1187
+ onProgress?.("ready");
1188
+ return;
1189
+ }
1190
+ if (existsSync6(SOURCE_MARKER)) {
1191
+ onProgress?.("ready");
1192
+ return;
1193
+ }
1194
+ if (sourceReadyPromise) return sourceReadyPromise;
1195
+ sourceReadyPromise = (async () => {
1196
+ onProgress?.("copying");
1197
+ logger.info("Creating local source snapshot (async) for Fifony...");
1198
+ const copyRecursiveAsync = async (source, target, rel = "") => {
1199
+ await mkdir(target, { recursive: true });
1200
+ const items = await readdir(source, { withFileTypes: true });
1201
+ for (const item of items) {
1202
+ const nextRel = rel ? `${rel}/${item.name}` : item.name;
1203
+ if (shouldSkipPath(nextRel)) continue;
1204
+ const sourcePath = `${source}/${item.name}`;
1205
+ const targetPath = `${target}/${item.name}`;
1206
+ const itemStat = await stat(sourcePath);
1207
+ if (item.isDirectory()) {
1208
+ await copyRecursiveAsync(sourcePath, targetPath, nextRel);
1209
+ continue;
1210
+ }
1211
+ if (item.isSymbolicLink() || itemStat.isSymbolicLink()) continue;
1212
+ if (itemStat.isFile() || itemStat.isFIFO()) {
1213
+ try {
1214
+ await copyFile(sourcePath, targetPath);
1215
+ } catch (error) {
1216
+ if (error.code === "ENOENT") {
1217
+ logger.debug(`Skipped missing source file: ${sourcePath}`);
1218
+ } else {
1219
+ throw error;
1220
+ }
1221
+ }
1222
+ }
1223
+ }
1224
+ };
1225
+ await mkdir(SOURCE_ROOT, { recursive: true });
1226
+ await copyRecursiveAsync(TARGET_ROOT, SOURCE_ROOT);
1227
+ await writeFile(SOURCE_MARKER, `${now()}
1228
+ `, "utf8");
1229
+ onProgress?.("ready");
1230
+ logger.info("Source snapshot ready (async).");
1231
+ })();
1232
+ return sourceReadyPromise;
1233
+ }
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;
1240
+ }
1241
+ }
1242
+ function detectDefaultBranch(dir) {
1243
+ try {
1244
+ const current = execSync("git rev-parse --abbrev-ref HEAD", { cwd: dir, encoding: "utf8" }).trim();
1245
+ if (current && current !== "HEAD") return current;
1246
+ const remote = execSync("git symbolic-ref refs/remotes/origin/HEAD", { cwd: dir, encoding: "utf8" }).trim();
1247
+ return remote.replace("refs/remotes/origin/", "");
1248
+ } catch {
1249
+ return "main";
1250
+ }
1251
+ }
1252
+ async function createGitWorktree(issue, worktreePath, baseBranch) {
1253
+ let headCommitAtStart = "";
1254
+ const resolvedBaseBranch = baseBranch ?? detectDefaultBranch(TARGET_ROOT);
1255
+ try {
1256
+ headCommitAtStart = execSync("git rev-parse HEAD", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
1257
+ } catch {
1258
+ }
1259
+ const branchName = `fifony/${issue.id}`;
1260
+ execSync(`git worktree add "${worktreePath}" -B "${branchName}"`, {
1261
+ cwd: TARGET_ROOT,
1262
+ stdio: "pipe"
1263
+ });
1264
+ try {
1265
+ const gitFileContent = readFileSync3(join7(worktreePath, ".git"), "utf8").trim();
1266
+ const gitDirRel = gitFileContent.replace("gitdir: ", "").trim();
1267
+ const gitDirPath = resolve(worktreePath, gitDirRel);
1268
+ mkdirSync(join7(gitDirPath, "info"), { recursive: true });
1269
+ writeFileSync2(join7(gitDirPath, "info", "exclude"), "fifony-*\n.fifony-*\nfifony_*\n", "utf8");
1270
+ } catch (err) {
1271
+ logger.warn({ err: String(err) }, "[Agent] Failed to write worktree excludes");
1272
+ }
1273
+ issue.branchName = branchName;
1274
+ issue.baseBranch = resolvedBaseBranch;
1275
+ issue.headCommitAtStart = headCommitAtStart;
1276
+ issue.worktreePath = worktreePath;
1277
+ logger.debug({ issueId: issue.id, branchName, baseBranch: resolvedBaseBranch, worktreePath }, "[Agent] Git worktree created");
1278
+ }
1279
+ async function prepareWorkspace(issue, state, defaultBranch) {
1280
+ const safeId = idToSafePath(issue.id);
1281
+ const workspaceRoot = join7(WORKSPACE_ROOT, safeId);
1282
+ const worktreePath = join7(workspaceRoot, "worktree");
1283
+ const createdNow = !existsSync6(worktreePath);
1284
+ if (createdNow) {
1285
+ mkdirSync(workspaceRoot, { recursive: true });
1286
+ logger.debug({ issueId: issue.id, identifier: issue.identifier, workspacePath: workspaceRoot }, "[Agent] Creating workspace");
1287
+ if (state.config.afterCreateHook) {
1288
+ mkdirSync(worktreePath, { recursive: true });
1289
+ await runHook(state.config.afterCreateHook, worktreePath, issue, "after_create");
1290
+ } else if (isGitRepo(TARGET_ROOT)) {
1291
+ await createGitWorktree(issue, worktreePath, defaultBranch);
1292
+ } 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
+ });
1300
+ }
1301
+ logger.debug({ issueId: issue.id, workspacePath: workspaceRoot, worktreePath }, "[Agent] Workspace created");
1302
+ } else {
1303
+ logger.debug({ issueId: issue.id, workspacePath: workspaceRoot }, "[Agent] Reusing existing workspace");
1304
+ }
1305
+ const metaPath = join7(workspaceRoot, "issue.json");
1306
+ const promptText = await buildPrompt(issue, null);
1307
+ const promptFile = join7(workspaceRoot, "prompt.md");
1308
+ writeFileSync2(metaPath, JSON.stringify({ ...issue, runtimeSource: SOURCE_ROOT, bootstrapAt: now() }, null, 2), "utf8");
1309
+ writeFileSync2(promptFile, `${promptText}
1310
+ `, "utf8");
1311
+ issue.workspacePath = workspaceRoot;
1312
+ issue.worktreePath = worktreePath;
1313
+ issue.workspacePreparedAt = now();
1314
+ return { workspacePath: workspaceRoot, promptText, promptFile };
1315
+ }
1316
+ async function cleanWorkspace(issueId, issue, state) {
1317
+ const safeId = idToSafePath(issueId);
1318
+ const workspacePath = issue?.workspacePath ?? join7(WORKSPACE_ROOT, safeId);
1319
+ if (!existsSync6(workspacePath)) return;
1320
+ if (state.config.beforeRemoveHook) {
1321
+ try {
1322
+ const dummyIssue = issue ?? { id: issueId, identifier: issueId };
1323
+ await runHook(state.config.beforeRemoveHook, workspacePath, dummyIssue, "before_remove");
1324
+ } catch (error) {
1325
+ logger.warn(`before_remove hook failed for ${issueId}: ${String(error)}`);
1326
+ }
1327
+ }
1328
+ if (issue?.branchName && issue.worktreePath) {
1329
+ try {
1330
+ execSync(`git worktree remove --force "${issue.worktreePath}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
1331
+ logger.info(`Removed worktree for ${issueId}: ${issue.worktreePath}`);
1332
+ } catch (error) {
1333
+ logger.warn(`Failed to remove worktree for ${issueId}: ${String(error)}`);
1334
+ try {
1335
+ rmSync2(issue.worktreePath, { recursive: true, force: true });
1336
+ } catch {
1337
+ }
1338
+ }
1339
+ try {
1340
+ execSync(`git branch -D "${issue.branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
1341
+ } catch {
1342
+ }
1343
+ try {
1344
+ rmSync2(workspacePath, { recursive: true, force: true });
1345
+ } catch {
1346
+ }
1347
+ return;
1348
+ }
1349
+ try {
1350
+ rmSync2(workspacePath, { recursive: true, force: true });
1351
+ logger.info(`Cleaned workspace for ${issueId}: ${workspacePath}`);
1352
+ } catch (error) {
1353
+ logger.warn(`Failed to clean workspace for ${issueId}: ${String(error)}`);
1354
+ }
1355
+ }
1356
+ function inferChangedWorkspacePaths(workspacePath, limit = 32, issue) {
1357
+ if (!issue?.baseBranch || !issue.branchName) return [];
1358
+ try {
1359
+ const output = execSync(
1360
+ `git diff --name-only "${issue.baseBranch}"..."${issue.branchName}"`,
1361
+ { cwd: TARGET_ROOT, encoding: "utf8", timeout: 1e4, stdio: "pipe" }
1362
+ );
1363
+ return output.trim().split("\n").filter(Boolean).slice(0, limit);
1364
+ } catch {
1365
+ return [];
1366
+ }
1367
+ }
1368
+ function computeDiffStats(issue) {
1369
+ if (!issue.baseBranch || !issue.branchName) return;
1370
+ try {
1371
+ let raw = "";
1372
+ try {
1373
+ raw = execSync(
1374
+ `git diff --stat "${issue.baseBranch}"..."${issue.branchName}"`,
1375
+ { cwd: TARGET_ROOT, encoding: "utf8", maxBuffer: 512e3, timeout: 1e4, stdio: "pipe" }
1376
+ );
1377
+ } catch (err) {
1378
+ raw = err.stdout || "";
1379
+ }
1380
+ if (raw) parseDiffStats(issue, raw);
1381
+ } catch {
1382
+ }
1383
+ }
1384
+ function parseDiffStats(issue, raw) {
1385
+ const lines = raw.trim().split("\n");
1386
+ const summary = lines[lines.length - 1] || "";
1387
+ const filesMatch = summary.match(/(\d+)\s+files?\s+changed/);
1388
+ const addMatch = summary.match(/(\d+)\s+insertions?\(\+\)/);
1389
+ const delMatch = summary.match(/(\d+)\s+deletions?\(-\)/);
1390
+ const internalRe = /fifony[-_]|\.fifony-|WORKFLOW\.local/;
1391
+ const fileLines = lines.slice(0, -1).filter((l) => {
1392
+ const name = l.trim().split("|")[0]?.trim().split("/").pop() || "";
1393
+ return !internalRe.test(name);
1394
+ });
1395
+ const regexFiles = filesMatch ? parseInt(filesMatch[1], 10) : 0;
1396
+ issue.filesChanged = fileLines.length > 0 ? fileLines.length : regexFiles;
1397
+ issue.linesAdded = addMatch ? parseInt(addMatch[1], 10) : 0;
1398
+ issue.linesRemoved = delMatch ? parseInt(delMatch[1], 10) : 0;
1399
+ }
1400
+ async function syncIssueDiffStatsToStore(issue) {
1401
+ if (!issue?.id) return;
1402
+ const { getIssueStateResource } = await import("./store-3JLC6EXY.js");
1403
+ const issueResource2 = getIssueStateResource();
1404
+ if (!issueResource2) return;
1405
+ const toNumber = (value) => {
1406
+ const parsed = typeof value === "number" ? value : Number(value ?? 0);
1407
+ return Number.isFinite(parsed) ? parsed : 0;
1408
+ };
1409
+ const nextLinesAdded = toNumber(issue.linesAdded);
1410
+ const nextLinesRemoved = toNumber(issue.linesRemoved);
1411
+ const nextFilesChanged = toNumber(issue.filesChanged);
1412
+ if (nextLinesAdded === 0 && nextLinesRemoved === 0 && nextFilesChanged === 0 && !issue.branchName) {
1413
+ return;
1414
+ }
1415
+ const current = await issueResource2.get?.(issue.id).catch(() => null);
1416
+ const previousLinesAdded = toNumber(current?.linesAdded);
1417
+ const previousLinesRemoved = toNumber(current?.linesRemoved);
1418
+ const previousFilesChanged = toNumber(current?.filesChanged);
1419
+ await issueResource2.patch(issue.id, {
1420
+ linesAdded: nextLinesAdded,
1421
+ linesRemoved: nextLinesRemoved,
1422
+ filesChanged: nextFilesChanged,
1423
+ branchName: issue.branchName
1424
+ });
1425
+ const add = issueResource2.add;
1426
+ const sub = issueResource2.sub;
1427
+ if (typeof add !== "function" || typeof sub !== "function") {
1428
+ logger.debug({ issueId: issue.id }, "[DiffStats] resource.add/sub not available \u2014 EC plugin may not be installed");
1429
+ return;
1430
+ }
1431
+ const deltaAdded = nextLinesAdded - previousLinesAdded;
1432
+ const deltaRemoved = nextLinesRemoved - previousLinesRemoved;
1433
+ const deltaFiles = nextFilesChanged - previousFilesChanged;
1434
+ if (deltaAdded === 0 && deltaRemoved === 0 && deltaFiles === 0) {
1435
+ logger.debug({ issueId: issue.id, nextLinesAdded, previousLinesAdded }, "[DiffStats] No delta to send to EC (values already synced)");
1436
+ return;
1437
+ }
1438
+ logger.debug({ issueId: issue.id, deltaAdded, deltaRemoved, deltaFiles }, "[DiffStats] Sending deltas to EC");
1439
+ const applyDelta = async (field, delta) => {
1440
+ if (delta > 0) {
1441
+ await add.call(issueResource2, issue.id, field, delta);
1442
+ } else if (delta < 0) {
1443
+ await sub.call(issueResource2, issue.id, field, Math.abs(delta));
1444
+ }
1445
+ };
1446
+ await Promise.all([
1447
+ applyDelta("linesAdded", deltaAdded),
1448
+ applyDelta("linesRemoved", deltaRemoved),
1449
+ applyDelta("filesChanged", deltaFiles)
1450
+ ]);
1451
+ }
1452
+ function ensureWorktreeCommitted(issue) {
1453
+ const worktreePath = issue.worktreePath;
1454
+ if (!worktreePath || !issue.branchName) return;
1455
+ execSync("git add -A", { cwd: worktreePath, stdio: "pipe" });
1456
+ const statusBeforeCommit = execSync("git status --porcelain", { cwd: worktreePath, encoding: "utf8" }).trim();
1457
+ if (!statusBeforeCommit) return;
1458
+ try {
1459
+ execSync(`git commit -m "fifony: agent changes for ${issue.identifier}"`, { cwd: worktreePath, stdio: "pipe" });
1460
+ } catch (error) {
1461
+ const remaining = execSync("git status --porcelain", { cwd: worktreePath, encoding: "utf8" }).trim();
1462
+ if (remaining) {
1463
+ throw new Error(`Failed to commit agent changes for ${issue.identifier}: ${String(error)}`);
1464
+ }
1465
+ }
1466
+ const statusAfterCommit = execSync("git status --porcelain", { cwd: worktreePath, encoding: "utf8" }).trim();
1467
+ if (statusAfterCommit) {
1468
+ throw new Error(`Worktree for ${issue.identifier} still has uncommitted changes after commit.`);
1469
+ }
1470
+ }
1471
+ function mergeWorktree(issue, worktreePath) {
1472
+ const result = { copied: [], deleted: [], skipped: [], conflicts: [] };
1473
+ ensureWorktreeCommitted(issue);
1474
+ const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
1475
+ if (currentBranch !== issue.baseBranch) {
1476
+ throw new Error(`Cannot merge ${issue.identifier}: current branch is ${currentBranch}, expected ${issue.baseBranch}.`);
1477
+ }
1478
+ const targetStatus = execSync("git status --porcelain", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
1479
+ if (targetStatus) {
1480
+ throw new Error(`Cannot merge ${issue.identifier}: target repository has uncommitted changes.`);
1481
+ }
1482
+ try {
1483
+ const diffOut = execSync(
1484
+ `git diff --name-status "${issue.baseBranch}"..."${issue.branchName}"`,
1485
+ { cwd: TARGET_ROOT, encoding: "utf8" }
1486
+ );
1487
+ for (const line of diffOut.trim().split("\n").filter(Boolean)) {
1488
+ const [statusChar, ...parts] = line.split(" ");
1489
+ const filePath = parts.join(" ");
1490
+ if (statusChar === "D") result.deleted.push(filePath);
1491
+ else result.copied.push(filePath);
1492
+ }
1493
+ } catch {
1494
+ }
1495
+ try {
1496
+ execSync(
1497
+ `git merge --no-ff "${issue.branchName}" -m "fifony: merge ${issue.identifier}"`,
1498
+ { cwd: TARGET_ROOT, stdio: "pipe" }
1499
+ );
1500
+ } catch (err) {
1501
+ try {
1502
+ const conflictOut = execSync(
1503
+ "git diff --name-only --diff-filter=U",
1504
+ { cwd: TARGET_ROOT, encoding: "utf8" }
1505
+ );
1506
+ result.conflicts.push(...conflictOut.trim().split("\n").filter(Boolean));
1507
+ } catch {
1508
+ }
1509
+ try {
1510
+ execSync("git merge --abort", { cwd: TARGET_ROOT, stdio: "pipe" });
1511
+ } catch {
1512
+ }
1513
+ logger.warn({ issueId: issue.id, err: String(err) }, "[Agent] Git merge failed, aborted");
1514
+ }
1515
+ return result;
1516
+ }
1517
+ function pushWorktreeBranch(issue) {
1518
+ if (!issue.branchName || !issue.baseBranch || !issue.worktreePath) {
1519
+ throw new Error(`Issue ${issue.identifier} has no git worktree \u2014 cannot push.`);
1520
+ }
1521
+ ensureWorktreeCommitted(issue);
1522
+ execSync(`git push -u origin "${issue.branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
1523
+ try {
1524
+ const prUrl = execSync(
1525
+ `gh pr create --head "${issue.branchName}" --base "${issue.baseBranch}" --title "${issue.title.replace(/"/g, '\\"')}" --body "Automated by fifony"`,
1526
+ { cwd: TARGET_ROOT, encoding: "utf8" }
1527
+ ).trim();
1528
+ return prUrl;
1529
+ } catch {
1530
+ try {
1531
+ const remote = execSync("git remote get-url origin", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
1532
+ const cleanRemote = remote.replace(/\.git$/, "");
1533
+ return `${cleanRemote}/compare/${issue.baseBranch}...${issue.branchName}`;
1534
+ } catch {
1535
+ return `(branch: ${issue.branchName})`;
1536
+ }
1537
+ }
1538
+ }
1539
+ function mergeWorkspace(issue) {
1540
+ if (!issue.branchName || !issue.baseBranch || !issue.worktreePath) {
1541
+ throw new Error(`Issue ${issue.identifier} has no git worktree \u2014 cannot merge.`);
1542
+ }
1543
+ return mergeWorktree(issue, issue.worktreePath);
1544
+ }
1545
+ function hydrateIssuePathsFromWorkspace(issue) {
1546
+ const inferredPaths = inferChangedWorkspacePaths(issue.workspacePath ?? "", 32, issue);
1547
+ if (inferredPaths.length === 0) return [];
1548
+ issue.paths = [.../* @__PURE__ */ new Set([...issue.paths ?? [], ...inferredPaths])];
1549
+ issue.inferredPaths = [.../* @__PURE__ */ new Set([...issue.inferredPaths ?? [], ...inferredPaths])];
1550
+ return inferredPaths;
1551
+ }
1552
+ function describeRoutingSignals(issue, workspaceDerivedPaths) {
1553
+ const explicitPaths = issue.paths ?? [];
1554
+ const textDerivedPaths = inferCapabilityPaths({
1555
+ id: issue.id,
1556
+ identifier: issue.identifier,
1557
+ title: issue.title,
1558
+ description: issue.description,
1559
+ labels: issue.labels
1560
+ }).filter((path) => !explicitPaths.includes(path));
1561
+ const parts = [];
1562
+ if (explicitPaths.length > 0) parts.push(`payload paths=${explicitPaths.join(", ")}`);
1563
+ if (textDerivedPaths.length > 0) parts.push(`text hints=${textDerivedPaths.join(", ")}`);
1564
+ if (workspaceDerivedPaths.length > 0) parts.push(`workspace diff=${workspaceDerivedPaths.join(", ")}`);
1565
+ return parts.join(" | ");
1566
+ }
1567
+
1568
+ // src/domains/metrics.ts
1569
+ function computeMetrics(issues) {
1570
+ let planning = 0;
1571
+ let queued = 0;
1572
+ let inProgress = 0;
1573
+ let blocked = 0;
1574
+ let done = 0;
1575
+ let cancelled = 0;
1576
+ const completionTimes = [];
1577
+ for (const issue of issues) {
1578
+ const duration = issue.durationMs;
1579
+ if (issue.state === "Done") {
1580
+ 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;
1581
+ if (Number.isFinite(candidate) && candidate >= 0) {
1582
+ completionTimes.push(candidate);
1583
+ }
1584
+ }
1585
+ switch (issue.state) {
1586
+ case "Planning":
1587
+ planning += 1;
1588
+ break;
1589
+ case "Planned":
1590
+ queued += 1;
1591
+ break;
1592
+ case "Queued":
1593
+ case "Running":
1594
+ case "Reviewing":
1595
+ case "Reviewed":
1596
+ inProgress += 1;
1597
+ break;
1598
+ case "Blocked":
1599
+ blocked += 1;
1600
+ break;
1601
+ case "Done":
1602
+ done += 1;
1603
+ break;
1604
+ case "Cancelled":
1605
+ cancelled += 1;
1606
+ break;
1607
+ }
1608
+ }
1609
+ if (completionTimes.length === 0) {
1610
+ return {
1611
+ total: issues.length,
1612
+ planning,
1613
+ queued,
1614
+ inProgress,
1615
+ blocked,
1616
+ done,
1617
+ cancelled,
1618
+ activeWorkers: 0
1619
+ };
1620
+ }
1621
+ const sortedCompletionTimes = completionTimes.slice().sort((a, b) => a - b);
1622
+ const totalCompletionMs = sortedCompletionTimes.reduce((acc, value) => acc + value, 0);
1623
+ const mid = Math.floor(sortedCompletionTimes.length / 2);
1624
+ const medianCompletionMs = sortedCompletionTimes.length % 2 === 1 ? sortedCompletionTimes[mid] : Math.round((sortedCompletionTimes[mid - 1] + sortedCompletionTimes[mid]) / 2);
1625
+ return {
1626
+ total: issues.length,
1627
+ planning,
1628
+ queued,
1629
+ inProgress,
1630
+ blocked,
1631
+ done,
1632
+ cancelled,
1633
+ activeWorkers: 0,
1634
+ avgCompletionMs: Math.round(totalCompletionMs / completionTimes.length),
1635
+ medianCompletionMs,
1636
+ fastestCompletionMs: sortedCompletionTimes[0],
1637
+ slowestCompletionMs: sortedCompletionTimes[sortedCompletionTimes.length - 1]
1638
+ };
1639
+ }
1640
+ function computeCapabilityCounts(issues) {
1641
+ return issues.reduce((accumulator, issue) => {
1642
+ const key = issue.capabilityCategory?.trim() || "default";
1643
+ accumulator[key] = (accumulator[key] ?? 0) + 1;
1644
+ return accumulator;
1645
+ }, {});
1646
+ }
1647
+
1648
+ // src/persistence/metrics-cache.ts
1649
+ var cachedMetrics = null;
1650
+ var metricsStale = true;
1651
+ function invalidateMetrics() {
1652
+ metricsStale = true;
1653
+ }
1654
+ function getMetrics(issues) {
1655
+ if (!metricsStale && cachedMetrics) return cachedMetrics;
1656
+ cachedMetrics = computeMetrics(issues);
1657
+ metricsStale = false;
1658
+ return cachedMetrics;
1659
+ }
1660
+
1661
+ // src/persistence/dirty-tracker.ts
1662
+ var dirtyIssueIds = /* @__PURE__ */ new Set();
1663
+ var dirtyIssuePlanIds = /* @__PURE__ */ new Set();
1664
+ var dirtyEventIds = /* @__PURE__ */ new Set();
1665
+ function markIssueDirty(id) {
1666
+ dirtyIssueIds.add(id);
1667
+ }
1668
+ function markIssuePlanDirty(id) {
1669
+ dirtyIssuePlanIds.add(id);
1670
+ }
1671
+ function markEventDirty(id) {
1672
+ dirtyEventIds.add(id);
1673
+ }
1674
+ function hasDirtyState() {
1675
+ return dirtyIssueIds.size > 0 || dirtyEventIds.size > 0;
1676
+ }
1677
+ function getDirtyIssueIds() {
1678
+ return dirtyIssueIds;
1679
+ }
1680
+ function getDirtyEventIds() {
1681
+ return dirtyEventIds;
1682
+ }
1683
+ function snapshotAndClearDirtyIssueIds() {
1684
+ const snapshot = new Set(dirtyIssueIds);
1685
+ for (const id of snapshot) dirtyIssueIds.delete(id);
1686
+ return snapshot;
1687
+ }
1688
+ function snapshotAndClearDirtyIssuePlanIds() {
1689
+ const snapshot = new Set(dirtyIssuePlanIds);
1690
+ for (const id of snapshot) dirtyIssuePlanIds.delete(id);
1691
+ return snapshot;
1692
+ }
1693
+ function snapshotAndClearDirtyEventIds() {
1694
+ const snapshot = new Set(dirtyEventIds);
1695
+ for (const id of snapshot) dirtyEventIds.delete(id);
1696
+ return snapshot;
1697
+ }
1698
+ function markAllIssuesDirty(ids) {
1699
+ for (const id of ids) dirtyIssueIds.add(id);
1700
+ }
1701
+ function markAllIssuePlansDirty(ids) {
1702
+ for (const id of ids) dirtyIssuePlanIds.add(id);
1703
+ }
1704
+ function markAllEventsDirty(ids) {
1705
+ for (const id of ids) dirtyEventIds.add(id);
1706
+ }
1707
+
1708
+ // src/persistence/plugins/issue-state-machine.ts
1709
+ var fsmEventEmitter = null;
1710
+ function setFsmEventEmitter(emitter) {
1711
+ fsmEventEmitter = emitter;
1712
+ }
1713
+ function emitFsmEvent(issueId, kind, message) {
1714
+ if (fsmEventEmitter) {
1715
+ try {
1716
+ fsmEventEmitter(issueId, kind, message);
1717
+ } catch {
1718
+ }
1719
+ }
1720
+ }
1721
+ async function lazyEnqueueForPlanning(issue) {
1722
+ const { enqueueForPlanning } = await import("./queue-workers-2I7VRZA7.js");
1723
+ return enqueueForPlanning(issue);
1724
+ }
1725
+ async function lazyEnqueueForExecution(issue) {
1726
+ const { enqueueForExecution } = await import("./queue-workers-2I7VRZA7.js");
1727
+ return enqueueForExecution(issue);
1728
+ }
1729
+ async function lazyEnqueueForReview(issue) {
1730
+ const { enqueueForReview } = await import("./queue-workers-2I7VRZA7.js");
1731
+ return enqueueForReview(issue);
1732
+ }
1733
+ var ISSUE_STATE_MACHINE_ID = "issue-lifecycle";
1734
+ function markDirtyAndInvalidate(issueId) {
1735
+ markIssueDirty(issueId);
1736
+ invalidateMetrics();
1737
+ }
1738
+ function resolveIssue(context) {
1739
+ return context.issue ?? null;
1740
+ }
1741
+ function issueResource(machine) {
1742
+ return machine.database?.resources?.[S3DB_ISSUE_RESOURCE];
1743
+ }
1744
+ var STALE_TIMEOUT_MS = 24e5;
1745
+ async function isStaleIssue(context, _entityId) {
1746
+ const issue = resolveIssue(context);
1747
+ if (!issue) return false;
1748
+ return Date.now() - Date.parse(issue.updatedAt) > STALE_TIMEOUT_MS;
1749
+ }
1750
+ var issueStateMachineConfig = {
1751
+ persistTransitions: true,
1752
+ workerId: `fifony-${process.pid}`,
1753
+ lockTimeout: 5e3,
1754
+ lockTTL: 30,
1755
+ stateMachines: {
1756
+ [ISSUE_STATE_MACHINE_ID]: {
1757
+ resource: S3DB_ISSUE_RESOURCE,
1758
+ stateField: "state",
1759
+ initialState: "Planning",
1760
+ autoCleanup: false,
1761
+ states: {
1762
+ Planning: {
1763
+ on: { PLANNED: "Planned", CANCEL: "Cancelled" },
1764
+ entry: "onEnterPlanning"
1765
+ },
1766
+ Planned: {
1767
+ on: { QUEUE: "Queued", REPLAN: "Planning", CANCEL: "Cancelled" },
1768
+ entry: "onEnterPlanned"
1769
+ },
1770
+ Queued: {
1771
+ on: { RUN: "Running" },
1772
+ entry: "onEnterQueued"
1773
+ },
1774
+ Running: {
1775
+ on: { REVIEW: "Reviewing", REQUEUE: "Queued", BLOCK: "Blocked" },
1776
+ guards: { BLOCK: "requireBlockReason" },
1777
+ triggers: [{
1778
+ type: "cron",
1779
+ cron: "*/10 * * * *",
1780
+ sendEvent: "BLOCK",
1781
+ condition: isStaleIssue
1782
+ }]
1783
+ },
1784
+ Reviewing: {
1785
+ on: { REVIEWED: "Reviewed", REQUEUE: "Queued", BLOCK: "Blocked" },
1786
+ entry: "onEnterReviewing",
1787
+ guards: { BLOCK: "requireBlockReason" },
1788
+ triggers: [{
1789
+ type: "cron",
1790
+ cron: "*/10 * * * *",
1791
+ sendEvent: "BLOCK",
1792
+ condition: isStaleIssue
1793
+ }]
1794
+ },
1795
+ Reviewed: {
1796
+ on: { DONE: "Done", REQUEUE: "Queued", REPLAN: "Planning", CANCEL: "Cancelled" }
1797
+ },
1798
+ Blocked: {
1799
+ on: { UNBLOCK: "Queued", REPLAN: "Planning", CANCEL: "Cancelled" },
1800
+ entry: "onEnterBlocked"
1801
+ },
1802
+ Done: {
1803
+ on: { REOPEN: "Planning" },
1804
+ type: "final",
1805
+ entry: "onEnterDone"
1806
+ },
1807
+ Cancelled: {
1808
+ on: { REOPEN: "Planning" },
1809
+ type: "final",
1810
+ entry: "onEnterCancelled"
1811
+ }
1812
+ }
1813
+ }
1814
+ },
1815
+ // ── Actions: (context, event, machine) ──────────────────────────────────
1816
+ // context = payload from send()
1817
+ // event = event name ("PLANNED", "BLOCK", etc.)
1818
+ // machine = { database, machineId, entityId }
1819
+ //
1820
+ // Actions only mutate the in-memory issue + fire side effects (enqueue, s3db patch).
1821
+ // Dirty tracking + metrics invalidation is done once in executeTransition() after send().
1822
+ actions: {
1823
+ onEnterPlanning: async (context, _event, _machine) => {
1824
+ const issue = resolveIssue(context);
1825
+ if (issue) {
1826
+ issue.planningStatus = "idle";
1827
+ issue.planningError = void 0;
1828
+ issue.nextRetryAt = void 0;
1829
+ issue.lastError = void 0;
1830
+ emitFsmEvent(issue.id, "state", `${issue.identifier} entered Planning.`);
1831
+ lazyEnqueueForPlanning(issue).catch(() => {
1832
+ });
1833
+ }
1834
+ },
1835
+ onEnterPlanned: async (context, _event, _machine) => {
1836
+ const issue = resolveIssue(context);
1837
+ if (issue) {
1838
+ issue.nextRetryAt = void 0;
1839
+ issue.lastError = void 0;
1840
+ emitFsmEvent(issue.id, "state", `Plan approved \u2014 ${issue.identifier} moved to Planned.`);
1841
+ }
1842
+ },
1843
+ onEnterQueued: async (context, _event, _machine) => {
1844
+ const issue = resolveIssue(context);
1845
+ if (issue) {
1846
+ issue.nextRetryAt = void 0;
1847
+ issue.lastError = void 0;
1848
+ logger.info({ issueId: issue.id, identifier: issue.identifier }, "[FSM] onEnterQueued \u2014 enqueuing for execution");
1849
+ emitFsmEvent(issue.id, "state", `${issue.identifier} queued for execution.`);
1850
+ lazyEnqueueForExecution(issue).catch((err) => {
1851
+ logger.error({ err, issueId: issue.id }, "[FSM] onEnterQueued \u2014 enqueue FAILED");
1852
+ });
1853
+ }
1854
+ },
1855
+ onEnterReviewing: async (context, _event, machine) => {
1856
+ const issue = resolveIssue(context);
1857
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1858
+ if (issue) {
1859
+ issue.reviewingAt = ts;
1860
+ issue.lastError = void 0;
1861
+ emitFsmEvent(issue.id, "state", `${issue.identifier} moved to Reviewing.`);
1862
+ lazyEnqueueForReview(issue).catch(() => {
1863
+ });
1864
+ }
1865
+ const res = issueResource(machine);
1866
+ if (res) {
1867
+ res.patch(machine.entityId, { reviewingAt: ts }).catch(() => {
1868
+ });
1869
+ }
1870
+ },
1871
+ onEnterBlocked: async (context, _event, _machine) => {
1872
+ const issue = resolveIssue(context);
1873
+ const note = typeof context.note === "string" ? context.note : "Blocked";
1874
+ if (issue) {
1875
+ issue.lastError = note;
1876
+ emitFsmEvent(issue.id, "error", `${issue.identifier} blocked: ${note}`);
1877
+ }
1878
+ },
1879
+ onEnterDone: async (context, _event, machine) => {
1880
+ const issue = resolveIssue(context);
1881
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1882
+ const week = isoWeek();
1883
+ if (issue) {
1884
+ if (!issue.linesAdded && !issue.linesRemoved && issue.baseBranch && issue.branchName) {
1885
+ computeDiffStats(issue);
1886
+ }
1887
+ issue.completedAt = ts;
1888
+ issue.terminalWeek = week;
1889
+ issue.nextRetryAt = void 0;
1890
+ issue.lastError = void 0;
1891
+ emitFsmEvent(issue.id, "state", `${issue.identifier} completed.`);
1892
+ }
1893
+ const res = issueResource(machine);
1894
+ if (res) {
1895
+ res.patch(machine.entityId, {
1896
+ completedAt: ts,
1897
+ terminalWeek: week,
1898
+ nextRetryAt: void 0,
1899
+ lastError: void 0,
1900
+ linesAdded: issue?.linesAdded,
1901
+ linesRemoved: issue?.linesRemoved,
1902
+ filesChanged: issue?.filesChanged,
1903
+ branchName: issue?.branchName,
1904
+ workspacePath: issue?.workspacePath,
1905
+ worktreePath: issue?.worktreePath
1906
+ }).catch(() => {
1907
+ });
1908
+ }
1909
+ },
1910
+ onEnterCancelled: async (context, _event, machine) => {
1911
+ const issue = resolveIssue(context);
1912
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1913
+ const week = isoWeek();
1914
+ if (issue) {
1915
+ issue.completedAt = ts;
1916
+ issue.terminalWeek = week;
1917
+ issue.nextRetryAt = void 0;
1918
+ emitFsmEvent(issue.id, "state", `${issue.identifier} cancelled.`);
1919
+ }
1920
+ const res = issueResource(machine);
1921
+ if (res) {
1922
+ res.patch(machine.entityId, {
1923
+ completedAt: ts,
1924
+ terminalWeek: week,
1925
+ nextRetryAt: void 0
1926
+ }).catch(() => {
1927
+ });
1928
+ }
1929
+ }
1930
+ },
1931
+ // ── Guards: (context, event, machine) ───────────────────────────────────
1932
+ guards: {
1933
+ requireBlockReason: async (context, _event, _machine) => {
1934
+ return typeof context.note === "string" && context.note.trim().length > 0;
1935
+ }
1936
+ }
1937
+ };
1938
+ var EVENT_TO_STATE = {
1939
+ PLANNED: "Planned",
1940
+ QUEUE: "Queued",
1941
+ RUN: "Running",
1942
+ REVIEW: "Reviewing",
1943
+ REVIEWED: "Reviewed",
1944
+ DONE: "Done",
1945
+ CANCEL: "Cancelled",
1946
+ BLOCK: "Blocked",
1947
+ UNBLOCK: "Queued",
1948
+ REPLAN: "Planning",
1949
+ REQUEUE: "Queued",
1950
+ REOPEN: "Planning"
1951
+ };
1952
+ function eventToTargetState(event) {
1953
+ return EVENT_TO_STATE[event];
1954
+ }
1955
+ function getStatesFromConfig() {
1956
+ const machine = issueStateMachineConfig.stateMachines[ISSUE_STATE_MACHINE_ID];
1957
+ const result = {};
1958
+ for (const [state, def] of Object.entries(machine.states)) {
1959
+ result[state] = def.on ?? {};
1960
+ }
1961
+ return result;
1962
+ }
1963
+ function getStateMachineTransitions() {
1964
+ const edges = getStatesFromConfig();
1965
+ const result = {};
1966
+ for (const [state, events] of Object.entries(edges)) {
1967
+ const targets = [...new Set(Object.values(events))];
1968
+ result[state] = targets;
1969
+ }
1970
+ return result;
1971
+ }
1972
+ function findIssueStateMachineTransitionPath(_machineDefinition, from, to) {
1973
+ if (from === to) return [];
1974
+ const edges = getStatesFromConfig();
1975
+ if (!edges[from] || !edges[to]) return null;
1976
+ const queue = [from];
1977
+ const previousState = /* @__PURE__ */ new Map();
1978
+ const previousEvent = /* @__PURE__ */ new Map();
1979
+ previousState.set(from, "");
1980
+ for (let i = 0; i < queue.length; i += 1) {
1981
+ const current = queue[i];
1982
+ const transitions = edges[current];
1983
+ if (!transitions) continue;
1984
+ for (const [evt, next] of Object.entries(transitions)) {
1985
+ if (previousState.has(next)) continue;
1986
+ previousState.set(next, current);
1987
+ previousEvent.set(next, evt);
1988
+ if (next === to) {
1989
+ const events = [];
1990
+ let cursor = next;
1991
+ while (cursor !== from) {
1992
+ const prev = previousState.get(cursor);
1993
+ const e = previousEvent.get(cursor);
1994
+ if (!prev || !e) return null;
1995
+ events.unshift(e);
1996
+ cursor = prev;
1997
+ }
1998
+ return events;
1999
+ }
2000
+ queue.push(next);
2001
+ }
2002
+ }
2003
+ return null;
2004
+ }
2005
+ var issueResourceStateApi = null;
2006
+ function setIssueResourceStateApi(api) {
2007
+ issueResourceStateApi = api;
2008
+ }
2009
+ function getIssueResourceStateApi() {
2010
+ return issueResourceStateApi;
2011
+ }
2012
+ var issueStateMachinePlugin = null;
2013
+ function setIssueStateMachinePlugin(plugin) {
2014
+ issueStateMachinePlugin = plugin;
2015
+ }
2016
+ function getIssueStateMachinePlugin() {
2017
+ return issueStateMachinePlugin;
2018
+ }
2019
+ function getIssueStateMachineDefinition() {
2020
+ return issueStateMachinePlugin?.getMachineDefinition?.(ISSUE_STATE_MACHINE_ID) ?? issueStateMachineConfig.stateMachines[ISSUE_STATE_MACHINE_ID];
2021
+ }
2022
+ function getIssueStateMachineInitialState() {
2023
+ return issueStateMachineConfig.stateMachines[ISSUE_STATE_MACHINE_ID].initialState;
2024
+ }
2025
+ async function executeTransition(issue, event, context = {}) {
2026
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
2027
+ const previous = issue.state;
2028
+ const targetState = eventToTargetState(event);
2029
+ if (!targetState) {
2030
+ throw new Error(`Unknown FSM event '${event}' for issue ${issue.id}.`);
2031
+ }
2032
+ const resourceApi = getIssueResourceStateApi();
2033
+ const plugin = getIssueStateMachinePlugin();
2034
+ const sendContext = { ...context, issue };
2035
+ if (resourceApi) {
2036
+ try {
2037
+ await resourceApi.send(issue.id, event, sendContext);
2038
+ } catch (err) {
2039
+ if (String(err).includes("not found") || String(err).includes("not initialized")) {
2040
+ await resourceApi.initialize(issue.id, { issue, state: previous });
2041
+ await resourceApi.send(issue.id, event, sendContext);
2042
+ } else {
2043
+ throw err;
2044
+ }
2045
+ }
2046
+ } else if (plugin?.send) {
2047
+ try {
2048
+ await plugin.send(ISSUE_STATE_MACHINE_ID, issue.id, event, sendContext);
2049
+ } catch (err) {
2050
+ if (plugin.initializeEntity && String(err).includes("not found")) {
2051
+ await plugin.initializeEntity(ISSUE_STATE_MACHINE_ID, issue.id, { issue, state: previous });
2052
+ await plugin.send(ISSUE_STATE_MACHINE_ID, issue.id, event, sendContext);
2053
+ } else {
2054
+ throw err;
2055
+ }
2056
+ }
2057
+ } else {
2058
+ if (previous !== targetState) {
2059
+ const edges = getStatesFromConfig();
2060
+ const stateTransitions = edges[previous];
2061
+ if (!stateTransitions || !stateTransitions[event]) {
2062
+ throw new Error(`State machine does not allow event '${event}' from '${previous}' for issue ${issue.id}.`);
2063
+ }
2064
+ }
2065
+ const stateDef = issueStateMachineConfig.stateMachines[ISSUE_STATE_MACHINE_ID].states[previous];
2066
+ if (stateDef && "guards" in stateDef && stateDef.guards?.[event]) {
2067
+ const guardName = stateDef.guards[event];
2068
+ const guardFn = issueStateMachineConfig.guards[guardName];
2069
+ if (guardFn) {
2070
+ const allowed = await guardFn(sendContext, event, { database: null, machineId: ISSUE_STATE_MACHINE_ID, entityId: issue.id });
2071
+ if (!allowed) {
2072
+ throw new Error(`Guard '${guardName}' rejected event '${event}' for issue ${issue.id}.`);
2073
+ }
2074
+ }
2075
+ }
2076
+ const targetDef = issueStateMachineConfig.stateMachines[ISSUE_STATE_MACHINE_ID].states[targetState];
2077
+ if (targetDef && "entry" in targetDef && typeof targetDef.entry === "string") {
2078
+ const actionName = targetDef.entry;
2079
+ const actionFn = issueStateMachineConfig.actions[actionName];
2080
+ if (actionFn) {
2081
+ await actionFn(sendContext, event, { database: null, machineId: ISSUE_STATE_MACHINE_ID, entityId: issue.id });
2082
+ }
2083
+ }
2084
+ }
2085
+ issue.state = targetState;
2086
+ issue.updatedAt = ts;
2087
+ const note = typeof context.note === "string" ? context.note : `${event}: ${previous} \u2192 ${targetState}`;
2088
+ issue.history.push(`[${ts}] ${note}`);
2089
+ if (TERMINAL_STATES.has(previous) && !TERMINAL_STATES.has(targetState)) {
2090
+ issue.terminalWeek = "";
2091
+ }
2092
+ markDirtyAndInvalidate(issue.id);
2093
+ return { previousState: previous };
2094
+ }
2095
+ async function getIssueTransitionHistory(issueId, options) {
2096
+ const resourceApi = getIssueResourceStateApi();
2097
+ if (resourceApi?.history) {
2098
+ try {
2099
+ return await resourceApi.history(issueId, options);
2100
+ } catch {
2101
+ }
2102
+ }
2103
+ const plugin = getIssueStateMachinePlugin();
2104
+ if (plugin?.getTransitionHistory) {
2105
+ try {
2106
+ return await plugin.getTransitionHistory(ISSUE_STATE_MACHINE_ID, issueId, options);
2107
+ } catch {
2108
+ }
2109
+ }
2110
+ return [];
2111
+ }
2112
+ async function canTransitionIssue(issueId, event) {
2113
+ const resourceApi = getIssueResourceStateApi();
2114
+ if (resourceApi?.canTransition) {
2115
+ try {
2116
+ return await resourceApi.canTransition(issueId, event);
2117
+ } catch {
2118
+ }
2119
+ }
2120
+ return false;
2121
+ }
2122
+ function visualizeStateMachine() {
2123
+ const plugin = getIssueStateMachinePlugin();
2124
+ if (!plugin?.visualize) return null;
2125
+ return plugin.visualize(ISSUE_STATE_MACHINE_ID);
2126
+ }
2127
+
2128
+ export {
2129
+ buildFullPlanPrompt,
2130
+ buildExecutionPayload,
2131
+ ADAPTERS,
2132
+ discoverModels,
2133
+ normalizeAgentProvider,
2134
+ resolveAgentCommand,
2135
+ getProviderDefaultCommand,
2136
+ detectAvailableProviders,
2137
+ readCodexConfig,
2138
+ resolveDefaultProvider,
2139
+ getCapabilityRoutingOptions,
2140
+ applyCapabilityMetadata,
2141
+ getEffectiveAgentProviders,
2142
+ computeMetrics,
2143
+ computeCapabilityCounts,
2144
+ getMetrics,
2145
+ runCommandWithTimeout,
2146
+ runHook,
2147
+ buildTurnPrompt,
2148
+ buildProviderBasePrompt,
2149
+ setSkipSource,
2150
+ detectDefaultBranch,
2151
+ prepareWorkspace,
2152
+ cleanWorkspace,
2153
+ computeDiffStats,
2154
+ parseDiffStats,
2155
+ syncIssueDiffStatsToStore,
2156
+ ensureWorktreeCommitted,
2157
+ pushWorktreeBranch,
2158
+ mergeWorkspace,
2159
+ hydrateIssuePathsFromWorkspace,
2160
+ describeRoutingSignals,
2161
+ markIssueDirty,
2162
+ markIssuePlanDirty,
2163
+ markEventDirty,
2164
+ hasDirtyState,
2165
+ getDirtyIssueIds,
2166
+ getDirtyEventIds,
2167
+ snapshotAndClearDirtyIssueIds,
2168
+ snapshotAndClearDirtyIssuePlanIds,
2169
+ snapshotAndClearDirtyEventIds,
2170
+ markAllIssuesDirty,
2171
+ markAllIssuePlansDirty,
2172
+ markAllEventsDirty,
2173
+ setFsmEventEmitter,
2174
+ ISSUE_STATE_MACHINE_ID,
2175
+ issueStateMachineConfig,
2176
+ eventToTargetState,
2177
+ getStateMachineTransitions,
2178
+ findIssueStateMachineTransitionPath,
2179
+ setIssueResourceStateApi,
2180
+ getIssueResourceStateApi,
2181
+ setIssueStateMachinePlugin,
2182
+ getIssueStateMachinePlugin,
2183
+ getIssueStateMachineDefinition,
2184
+ getIssueStateMachineInitialState,
2185
+ executeTransition,
2186
+ getIssueTransitionHistory,
2187
+ canTransitionIssue,
2188
+ visualizeStateMachine
2189
+ };
2190
+ //# sourceMappingURL=chunk-3QSBGJMT.js.map