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