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.
- package/README.md +152 -129
- package/app/dist/assets/{KeyboardShortcutsHelp-lTNj9GiT.js → KeyboardShortcutsHelp-By0KcDhZ.js} +1 -1
- package/app/dist/assets/OnboardingWizard-5cz7Onsu.js +1 -0
- package/app/dist/assets/{analytics.lazy-C42PFRzr.js → analytics.lazy-ArFwnOEn.js} +1 -1
- package/app/dist/assets/{createLucideIcon-BWC-guQt.js → createLucideIcon-DgMTp0yx.js} +1 -1
- package/app/dist/assets/index-59O8esMr.js +45 -0
- package/app/dist/assets/{index-Qr2OPvRO.css → index-DuBwUsuf.css} +1 -1
- package/app/dist/assets/vendor-D-IqxHHu.js +9 -0
- package/app/dist/dinofffaur.webp +0 -0
- package/app/dist/index.html +4 -4
- package/app/dist/service-worker.js +1 -1
- package/dist/agent/run-local.js +57 -144
- package/dist/agent-KMXNVDRO.js +74 -0
- package/dist/chunk-FJR4ALEN.js +847 -0
- package/dist/{chunk-2F3Q2MAG.js → chunk-HSGUPFTV.js} +1224 -611
- package/dist/chunk-O5AEQXUV.js +311 -0
- package/dist/chunk-OONOOWNC.js +123 -0
- package/dist/{chunk-NFHVAIPW.js → chunk-UYCDOH6S.js} +380 -795
- package/dist/chunk-XENKNHFS.js +295 -0
- package/dist/cli.js +6 -4
- package/dist/issue-runner-JJAFMHKV.js +15 -0
- package/dist/{issue-state-machine-OWABY5S2.js → issue-state-machine-ACMUJSXC.js} +5 -3
- package/dist/issues-VDFXBK3N.js +40 -0
- package/dist/mcp/server.js +23 -121
- package/dist/queue-workers-U47CVPTO.js +23 -0
- package/dist/scheduler-MEXEDV4M.js +21 -0
- package/dist/{store-WN47MDT5.js → store-AG6LLYJ7.js} +7 -5
- package/dist/workspace-474CCKTW.js +44 -0
- package/package.json +6 -6
- package/app/dist/assets/OnboardingWizard-B6LlJR9B.js +0 -1
- package/app/dist/assets/index-fVSxs9d5.js +0 -43
- package/app/dist/assets/vendor-BTlTWMUF.js +0 -9
- package/dist/chunk-AMOGDOM7.js +0 -796
- package/dist/chunk-IA7IMQ5F.js +0 -91
- package/dist/issue-runner-DA4IDLKX.js +0 -13
- 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
|
-
|
|
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
|
|
20
|
+
existsSync as existsSync7,
|
|
25
21
|
mkdirSync,
|
|
26
22
|
readdirSync,
|
|
27
|
-
readFileSync as
|
|
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
|
|
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
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (
|
|
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
|
-
|
|
133
|
+
skills.forEach((s) => parts.push(`- ${s}`));
|
|
136
134
|
}
|
|
137
|
-
if (
|
|
138
|
-
parts.push("", "**
|
|
139
|
-
|
|
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(``);
|
|
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" :
|
|
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.
|
|
246
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
324
|
-
|
|
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 =
|
|
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.
|
|
348
|
-
env2.FIFONY_PLAN_SKILLS = plan.
|
|
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.
|
|
364
|
-
subagentsRequested: plan.
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
|
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 (!
|
|
589
|
-
const raw =
|
|
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
|
|
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 =
|
|
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 (
|
|
638
|
-
const raw =
|
|
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 (!
|
|
802
|
-
const raw =
|
|
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 (!
|
|
814
|
-
const raw =
|
|
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
|
|
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(
|
|
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
|
-
|
|
1145
|
-
|
|
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_") &&
|
|
1177
|
-
if (
|
|
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 (
|
|
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 =
|
|
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 = !
|
|
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 (!
|
|
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
|
|
1466
|
-
|
|
1467
|
-
|
|
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
|
|
1494
|
-
|
|
1495
|
-
|
|
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
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
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
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1676
|
-
|
|
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
|
-
|
|
2032
|
-
} catch
|
|
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
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
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
|
-
|
|
2076
|
-
return { previousState: previous };
|
|
1674
|
+
return { willConflict, conflictFiles, canMerge: !willConflict, changedFiles };
|
|
2077
1675
|
}
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2096
|
-
const
|
|
2097
|
-
if (
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
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
|
-
|
|
1750
|
+
shouldSkipMergePath,
|
|
2140
1751
|
mergeWorkspace,
|
|
1752
|
+
dryMerge,
|
|
1753
|
+
rebaseWorktree,
|
|
2141
1754
|
hydrateIssuePathsFromWorkspace,
|
|
2142
|
-
|
|
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-
|
|
1757
|
+
//# sourceMappingURL=chunk-UYCDOH6S.js.map
|