fifony 0.1.42 → 0.1.43
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/app/dist/assets/{CommandPalette-DNR5umI1.js → CommandPalette-M4VAMxCU.js} +1 -1
- package/app/dist/assets/{KeyboardShortcutsHelp-Dpl19F20.js → KeyboardShortcutsHelp-DkvPUXQq.js} +1 -1
- package/app/dist/assets/OnboardingWizard-B7V9hoCR.js +1 -0
- package/app/dist/assets/analytics.lazy-zVJdF880.js +1 -0
- package/app/dist/assets/{api-ChEctgc5.js → api-CkVfYg_m.js} +1 -1
- package/app/dist/assets/{createLucideIcon-R47sXufx.js → createLucideIcon-Dfk_Hxud.js} +1 -1
- package/app/dist/assets/index-BpiCi7Ew.css +1 -0
- package/app/dist/assets/index-D2INW0zc.js +47 -0
- package/app/dist/assets/vendor-BEoYbFV1.js +9 -0
- package/app/dist/index.html +5 -5
- package/app/dist/service-worker.js +9 -4
- package/bin/fifony.js +3 -0
- package/dist/agent/pty-daemon.js +177 -0
- package/dist/agent/run-local.js +177 -43
- package/dist/{agent-NNGZEKZH.js → agent-RMQTTUEC.js} +37 -16
- package/dist/analytics-broadcaster-O6YBP66L.js +145 -0
- package/dist/chunk-3NE23NYW.js +82 -0
- package/dist/chunk-42AMQAJG.js +404 -0
- package/dist/{chunk-H5N7O5NP.js → chunk-AILXZ2TD.js} +79 -147
- package/dist/{chunk-I2UHVKHS.js → chunk-BRSR26VK.js} +2 -2
- package/dist/chunk-E2EWEYA4.js +1302 -0
- package/dist/chunk-ESWHDHH6.js +102 -0
- package/dist/{chunk-NB44PCD2.js → chunk-FJNH3G2Z.js} +1061 -1138
- package/dist/chunk-MVTGAKQK.js +493 -0
- package/dist/chunk-QQQLP3PL.js +155 -0
- package/dist/chunk-SOBLO4YZ.js +2016 -0
- package/dist/chunk-YRSH2CLW.js +13784 -0
- package/dist/cli.js +335 -44
- package/dist/{issue-state-machine-GPQNZYUZ.js → fsm-issue-YGGF7SIL.js} +9 -5
- package/dist/helpers-L7NYO5XS.js +53 -0
- package/dist/issue-log-broadcaster-WZAHISYB.js +84 -0
- package/dist/{issues-MZLRSXD6.js → issues-3QRR7KM6.js} +10 -8
- package/dist/log-analyzer-K7MXQB4T.js +287 -0
- package/dist/mcp/server.js +109 -137
- package/dist/parallel-executor-6INE6NDO.js +118 -0
- package/dist/pid-manager-UBWXVSMD.js +21 -0
- package/dist/queue-workers-XFZK3TT5.js +32 -0
- package/dist/replan-issue.command-4UCWYHGZ.js +15 -0
- package/dist/scheduler-ZP7GOZDW.js +26 -0
- package/dist/{settings-NGY33WQE.js → settings-ZAWDCFP2.js} +32 -8
- package/dist/settings.resource-5CW456AZ.js +24 -0
- package/dist/store-M6NCKMZY.js +97 -0
- package/dist/{web-push-CRVDJKWR.js → web-push-AX5IIK3P.js} +2 -2
- package/dist/{workspace-D3F3XGSI.js → workspace-CJTWFWTJ.js} +5 -4
- package/package.json +8 -7
- package/app/dist/assets/OnboardingWizard-CijMhJDW.js +0 -1
- package/app/dist/assets/analytics.lazy-Dq90a756.js +0 -1
- package/app/dist/assets/index-Dy_fM427.js +0 -54
- package/app/dist/assets/index-Q9jBP0Pz.css +0 -1
- package/app/dist/assets/vendor-DkWeBvNl.js +0 -9
- package/dist/chunk-2CVTK5F2.js +0 -288
- package/dist/chunk-37N5OFHM.js +0 -125
- package/dist/chunk-JTKUWIQD.js +0 -8406
- package/dist/chunk-RBDBGU2C.js +0 -303
- package/dist/issue-runner-CMZPSVC7.js +0 -16
- package/dist/queue-workers-XZ6DGH4W.js +0 -23
- package/dist/scheduler-NVE6L3P7.js +0 -22
- package/dist/store-4HCGBN4L.js +0 -65
|
@@ -1,48 +1,74 @@
|
|
|
1
|
-
import {
|
|
2
|
-
appendFileTail,
|
|
3
|
-
idToSafePath,
|
|
4
|
-
now,
|
|
5
|
-
renderPrompt
|
|
6
|
-
} from "./chunk-2CVTK5F2.js";
|
|
7
|
-
import {
|
|
8
|
-
SOURCE_MARKER,
|
|
9
|
-
SOURCE_ROOT,
|
|
10
|
-
TARGET_ROOT,
|
|
11
|
-
WORKSPACE_ROOT
|
|
12
|
-
} from "./chunk-37N5OFHM.js";
|
|
13
1
|
import {
|
|
14
2
|
logger
|
|
15
3
|
} from "./chunk-DVU3CXWA.js";
|
|
16
|
-
|
|
17
|
-
// src/domains/workspace.ts
|
|
18
4
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
readdirSync,
|
|
22
|
-
readFileSync as readFileSync4,
|
|
23
|
-
rmSync as rmSync2,
|
|
24
|
-
statSync,
|
|
25
|
-
writeFileSync as writeFileSync2
|
|
26
|
-
} from "fs";
|
|
27
|
-
import { copyFile, mkdir, readdir, stat, writeFile } from "fs/promises";
|
|
28
|
-
import { extname as extname2, join as join7, resolve } from "path";
|
|
29
|
-
import { execSync } from "child_process";
|
|
30
|
-
|
|
31
|
-
// src/agents/command-executor.ts
|
|
5
|
+
renderPrompt
|
|
6
|
+
} from "./chunk-ESWHDHH6.js";
|
|
32
7
|
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
writeFileSync
|
|
36
|
-
} from "fs";
|
|
37
|
-
import { join as join6 } from "path";
|
|
38
|
-
import { env as env2 } from "process";
|
|
39
|
-
import { spawn } from "child_process";
|
|
8
|
+
sleep
|
|
9
|
+
} from "./chunk-42AMQAJG.js";
|
|
40
10
|
|
|
41
|
-
// src/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
11
|
+
// src/persistence/dirty-tracker.ts
|
|
12
|
+
var dirtyIssueIds = /* @__PURE__ */ new Set();
|
|
13
|
+
var dirtyMilestoneIds = /* @__PURE__ */ new Set();
|
|
14
|
+
var dirtyIssuePlanIds = /* @__PURE__ */ new Set();
|
|
15
|
+
var dirtyEventIds = /* @__PURE__ */ new Set();
|
|
16
|
+
function markIssueDirty(id) {
|
|
17
|
+
dirtyIssueIds.add(id);
|
|
18
|
+
}
|
|
19
|
+
function markMilestoneDirty(id) {
|
|
20
|
+
dirtyMilestoneIds.add(id);
|
|
21
|
+
}
|
|
22
|
+
function markIssuePlanDirty(id) {
|
|
23
|
+
dirtyIssuePlanIds.add(id);
|
|
24
|
+
}
|
|
25
|
+
function markEventDirty(id) {
|
|
26
|
+
dirtyEventIds.add(id);
|
|
27
|
+
}
|
|
28
|
+
function hasDirtyState() {
|
|
29
|
+
return dirtyIssueIds.size > 0 || dirtyMilestoneIds.size > 0 || dirtyEventIds.size > 0;
|
|
30
|
+
}
|
|
31
|
+
function getDirtyIssueIds() {
|
|
32
|
+
return dirtyIssueIds;
|
|
33
|
+
}
|
|
34
|
+
function getDirtyMilestoneIds() {
|
|
35
|
+
return dirtyMilestoneIds;
|
|
36
|
+
}
|
|
37
|
+
function getDirtyEventIds() {
|
|
38
|
+
return dirtyEventIds;
|
|
39
|
+
}
|
|
40
|
+
function snapshotAndClearDirtyIssueIds() {
|
|
41
|
+
const snapshot = new Set(dirtyIssueIds);
|
|
42
|
+
for (const id of snapshot) dirtyIssueIds.delete(id);
|
|
43
|
+
return snapshot;
|
|
44
|
+
}
|
|
45
|
+
function snapshotAndClearDirtyMilestoneIds() {
|
|
46
|
+
const snapshot = new Set(dirtyMilestoneIds);
|
|
47
|
+
for (const id of snapshot) dirtyMilestoneIds.delete(id);
|
|
48
|
+
return snapshot;
|
|
49
|
+
}
|
|
50
|
+
function snapshotAndClearDirtyIssuePlanIds() {
|
|
51
|
+
const snapshot = new Set(dirtyIssuePlanIds);
|
|
52
|
+
for (const id of snapshot) dirtyIssuePlanIds.delete(id);
|
|
53
|
+
return snapshot;
|
|
54
|
+
}
|
|
55
|
+
function snapshotAndClearDirtyEventIds() {
|
|
56
|
+
const snapshot = new Set(dirtyEventIds);
|
|
57
|
+
for (const id of snapshot) dirtyEventIds.delete(id);
|
|
58
|
+
return snapshot;
|
|
59
|
+
}
|
|
60
|
+
function markAllIssuesDirty(ids) {
|
|
61
|
+
for (const id of ids) dirtyIssueIds.add(id);
|
|
62
|
+
}
|
|
63
|
+
function markAllMilestonesDirty(ids) {
|
|
64
|
+
for (const id of ids) dirtyMilestoneIds.add(id);
|
|
65
|
+
}
|
|
66
|
+
function markAllIssuePlansDirty(ids) {
|
|
67
|
+
for (const id of ids) dirtyIssuePlanIds.add(id);
|
|
68
|
+
}
|
|
69
|
+
function markAllEventsDirty(ids) {
|
|
70
|
+
for (const id of ids) dirtyEventIds.add(id);
|
|
71
|
+
}
|
|
46
72
|
|
|
47
73
|
// src/agents/adapters/claude.ts
|
|
48
74
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -52,7 +78,7 @@ import { join } from "path";
|
|
|
52
78
|
import { existsSync, readFileSync } from "fs";
|
|
53
79
|
import { basename, extname } from "path";
|
|
54
80
|
function buildPlanContextSection(plan) {
|
|
55
|
-
const parts = ["## Plan Context", "", `**Summary:** ${plan.summary}`];
|
|
81
|
+
const parts = ["## Plan Context", "", `**Summary:** ${plan.summary}`, `**Harness mode:** ${plan.harnessMode}`];
|
|
56
82
|
if (plan.assumptions?.length) {
|
|
57
83
|
parts.push("", "**Assumptions:**");
|
|
58
84
|
plan.assumptions.forEach((a) => parts.push(`- ${a}`));
|
|
@@ -105,11 +131,43 @@ function buildRiskSection(plan) {
|
|
|
105
131
|
}
|
|
106
132
|
return parts.join("\n");
|
|
107
133
|
}
|
|
134
|
+
function normalizeAcceptanceCriteria(plan) {
|
|
135
|
+
return plan.acceptanceCriteria.map((criterion, index) => ({
|
|
136
|
+
id: criterion.id || `AC-${index + 1}`,
|
|
137
|
+
description: criterion.description,
|
|
138
|
+
category: criterion.category,
|
|
139
|
+
verificationMethod: criterion.verificationMethod,
|
|
140
|
+
evidenceExpected: criterion.evidenceExpected,
|
|
141
|
+
blocking: criterion.blocking,
|
|
142
|
+
weight: criterion.weight
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
function deriveExecutionContract(plan) {
|
|
146
|
+
const ec = plan.executionContract;
|
|
147
|
+
return {
|
|
148
|
+
summary: ec.summary,
|
|
149
|
+
deliverables: Array.isArray(ec.deliverables) ? ec.deliverables.slice() : [],
|
|
150
|
+
requiredChecks: Array.isArray(ec.requiredChecks) ? ec.requiredChecks.slice() : [],
|
|
151
|
+
requiredEvidence: Array.isArray(ec.requiredEvidence) ? ec.requiredEvidence.slice() : [],
|
|
152
|
+
focusAreas: Array.isArray(ec.focusAreas) ? ec.focusAreas.slice() : [],
|
|
153
|
+
checkpointPolicy: ec.checkpointPolicy === "checkpointed" ? "checkpointed" : "final_only",
|
|
154
|
+
blueprintId: ec.blueprintId,
|
|
155
|
+
delegationPolicy: ec.delegationPolicy,
|
|
156
|
+
budgetPolicy: ec.budgetPolicy
|
|
157
|
+
};
|
|
158
|
+
}
|
|
108
159
|
function buildValidationSection(plan) {
|
|
109
160
|
const parts = [];
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
161
|
+
const acceptanceCriteria = normalizeAcceptanceCriteria(plan);
|
|
162
|
+
const executionContract = deriveExecutionContract(plan);
|
|
163
|
+
if (acceptanceCriteria.length) {
|
|
164
|
+
parts.push("## Acceptance Criteria");
|
|
165
|
+
acceptanceCriteria.forEach((criterion) => {
|
|
166
|
+
parts.push(`- **${criterion.id}** [${criterion.category}]${criterion.blocking ? " blocking" : " advisory"} \u2014 ${criterion.description}`);
|
|
167
|
+
parts.push(` Verify via: ${criterion.verificationMethod}`);
|
|
168
|
+
parts.push(` Evidence expected: ${criterion.evidenceExpected}`);
|
|
169
|
+
parts.push(` Weight: ${criterion.weight}`);
|
|
170
|
+
});
|
|
113
171
|
}
|
|
114
172
|
if (plan.validation?.length) {
|
|
115
173
|
parts.push("", "## Validation Checks");
|
|
@@ -120,6 +178,25 @@ function buildValidationSection(plan) {
|
|
|
120
178
|
parts.push("", "## Deliverables");
|
|
121
179
|
plan.deliverables.forEach((d) => parts.push(`- ${d}`));
|
|
122
180
|
}
|
|
181
|
+
parts.push("", "## Execution Contract");
|
|
182
|
+
parts.push(`Summary: ${executionContract.summary}`);
|
|
183
|
+
parts.push(`Checkpoint policy: ${executionContract.checkpointPolicy}`);
|
|
184
|
+
if (executionContract.blueprintId) parts.push(`Blueprint: ${executionContract.blueprintId}`);
|
|
185
|
+
if (executionContract.focusAreas.length) parts.push(`Focus areas: ${executionContract.focusAreas.join(", ")}`);
|
|
186
|
+
if (executionContract.delegationPolicy) {
|
|
187
|
+
parts.push(`Delegation policy: ${executionContract.delegationPolicy.mode} (max fanout ${executionContract.delegationPolicy.maxFanout})`);
|
|
188
|
+
}
|
|
189
|
+
if (executionContract.budgetPolicy) {
|
|
190
|
+
parts.push(`Budget policy: local retries=${executionContract.budgetPolicy.maxLocalRetries}, remote rounds=${executionContract.budgetPolicy.maxRemoteRounds}, wall clock=${executionContract.budgetPolicy.maxWallClockMinutes}m`);
|
|
191
|
+
}
|
|
192
|
+
if (executionContract.requiredChecks.length) {
|
|
193
|
+
parts.push("Required checks:");
|
|
194
|
+
executionContract.requiredChecks.forEach((check) => parts.push(`- ${check}`));
|
|
195
|
+
}
|
|
196
|
+
if (executionContract.requiredEvidence.length) {
|
|
197
|
+
parts.push("Required evidence:");
|
|
198
|
+
executionContract.requiredEvidence.forEach((evidence) => parts.push(`- ${evidence}`));
|
|
199
|
+
}
|
|
123
200
|
return parts.join("\n");
|
|
124
201
|
}
|
|
125
202
|
function buildToolingSection(plan) {
|
|
@@ -207,6 +284,8 @@ function buildImagePromptSection(imagePaths) {
|
|
|
207
284
|
function buildExecutionPayload(issue, provider, plan, workspacePath) {
|
|
208
285
|
const strategy = plan.executionStrategy;
|
|
209
286
|
const hasPhases = Boolean(plan.phases?.length);
|
|
287
|
+
const acceptanceCriteria = normalizeAcceptanceCriteria(plan);
|
|
288
|
+
const executionContract = deriveExecutionContract(plan);
|
|
210
289
|
return {
|
|
211
290
|
version: 1,
|
|
212
291
|
issue: {
|
|
@@ -226,6 +305,7 @@ function buildExecutionPayload(issue, provider, plan, workspacePath) {
|
|
|
226
305
|
},
|
|
227
306
|
executionIntent: {
|
|
228
307
|
complexity: plan.estimatedComplexity,
|
|
308
|
+
harnessMode: plan.harnessMode,
|
|
229
309
|
approach: strategy?.approach || "",
|
|
230
310
|
rationale: strategy?.whyThisApproach || "",
|
|
231
311
|
workPattern: hasPhases ? "phased" : "sequential"
|
|
@@ -248,9 +328,10 @@ function buildExecutionPayload(issue, provider, plan, workspacePath) {
|
|
|
248
328
|
}))
|
|
249
329
|
},
|
|
250
330
|
constraints: plan.constraints || [],
|
|
251
|
-
|
|
331
|
+
acceptanceCriteria,
|
|
252
332
|
validation: plan.validation || [],
|
|
253
333
|
deliverables: plan.deliverables || [],
|
|
334
|
+
executionContract,
|
|
254
335
|
assumptions: plan.assumptions || [],
|
|
255
336
|
unknowns: (plan.unknowns || []).map((u) => ({
|
|
256
337
|
question: u.question,
|
|
@@ -322,9 +403,6 @@ function extractPlanDirs(plan) {
|
|
|
322
403
|
|
|
323
404
|
// src/agents/adapters/usage.ts
|
|
324
405
|
import { cwd, env } from "process";
|
|
325
|
-
function sleep(ms) {
|
|
326
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
327
|
-
}
|
|
328
406
|
async function createPtyProcess(command, args) {
|
|
329
407
|
try {
|
|
330
408
|
const nodePty = await import("node-pty");
|
|
@@ -415,7 +493,7 @@ function parseResetTime(value) {
|
|
|
415
493
|
function parseResetDateFromText(raw) {
|
|
416
494
|
const text = normalizeResetText(raw);
|
|
417
495
|
if (!text) return null;
|
|
418
|
-
const
|
|
496
|
+
const now = /* @__PURE__ */ new Date();
|
|
419
497
|
const explicitDateMatch = text.match(/([A-Za-z]{3,9})\s*(\d{1,2})(?:,?|\s+)(\d{1,2}:\d{2}(?:\s*[AaPp][Mm])?)/i) || text.match(/(\d{1,2})\s+([A-Za-z]{3,9})(?:,\s*)?(\d{1,2}:\d{2}(?:\s*[AaPp][Mm])?)/i);
|
|
420
498
|
if (explicitDateMatch) {
|
|
421
499
|
const [, monthRaw, dayRaw, timeRaw] = explicitDateMatch;
|
|
@@ -423,12 +501,12 @@ function parseResetDateFromText(raw) {
|
|
|
423
501
|
const monthIndex = toMonthIndex(monthRaw);
|
|
424
502
|
const time2 = parseResetTime(timeRaw);
|
|
425
503
|
if (!Number.isNaN(day) && monthIndex !== null && time2) {
|
|
426
|
-
const candidate = new Date(
|
|
504
|
+
const candidate = new Date(now);
|
|
427
505
|
candidate.setMonth(monthIndex);
|
|
428
506
|
candidate.setDate(day);
|
|
429
507
|
candidate.setHours(time2.hour, time2.minute, 0, 0);
|
|
430
|
-
if (candidate.getTime() <=
|
|
431
|
-
candidate.setFullYear(
|
|
508
|
+
if (candidate.getTime() <= now.getTime()) {
|
|
509
|
+
candidate.setFullYear(now.getFullYear() + 1);
|
|
432
510
|
}
|
|
433
511
|
return candidate.toISOString();
|
|
434
512
|
}
|
|
@@ -436,9 +514,9 @@ function parseResetDateFromText(raw) {
|
|
|
436
514
|
const timeMatch = text.match(/(\d{1,2}:\d{2}(?:\s*[AaPp][Mm])?)/i) || text.match(/(\d{1,2})(?:\s*([AaPp][Mm]))/i);
|
|
437
515
|
const time = timeMatch?.[1] ? parseResetTime(timeMatch[1]) : null;
|
|
438
516
|
if (time) {
|
|
439
|
-
const candidate = new Date(
|
|
517
|
+
const candidate = new Date(now);
|
|
440
518
|
candidate.setHours(time.hour, time.minute, 0, 0);
|
|
441
|
-
if (candidate.getTime() <=
|
|
519
|
+
if (candidate.getTime() <= now.getTime()) {
|
|
442
520
|
candidate.setDate(candidate.getDate() + 1);
|
|
443
521
|
}
|
|
444
522
|
return candidate.toISOString();
|
|
@@ -476,10 +554,10 @@ function initSnapshot(raw) {
|
|
|
476
554
|
}
|
|
477
555
|
function waitForOutput(getOutput, pattern, timeoutMs, pollMs = 100) {
|
|
478
556
|
const deadline = Date.now() + timeoutMs;
|
|
479
|
-
return new Promise((
|
|
557
|
+
return new Promise((resolve) => {
|
|
480
558
|
const check = () => {
|
|
481
|
-
if (pattern.test(getOutput())) return
|
|
482
|
-
if (Date.now() >= deadline) return
|
|
559
|
+
if (pattern.test(getOutput())) return resolve(true);
|
|
560
|
+
if (Date.now() >= deadline) return resolve(false);
|
|
483
561
|
setTimeout(check, pollMs);
|
|
484
562
|
};
|
|
485
563
|
check();
|
|
@@ -552,7 +630,6 @@ function parseClaudeUsageFromStatus(raw) {
|
|
|
552
630
|
const lines = raw.split(/[\r\n]+/).map((line) => line.trim()).filter(Boolean);
|
|
553
631
|
let currentHeading = null;
|
|
554
632
|
let lastPercentSection = null;
|
|
555
|
-
let lastPercentUsed = null;
|
|
556
633
|
for (const line of lines) {
|
|
557
634
|
const normalized = line.toLowerCase();
|
|
558
635
|
if (/^esc to cancel/i.test(normalized)) continue;
|
|
@@ -584,7 +661,6 @@ function parseClaudeUsageFromStatus(raw) {
|
|
|
584
661
|
if (percentMatch?.[1] && currentHeading) {
|
|
585
662
|
const used = parseInt(percentMatch[1], 10);
|
|
586
663
|
lastPercentSection = currentHeading;
|
|
587
|
-
lastPercentUsed = used;
|
|
588
664
|
if (currentHeading.section === "current-week-all") {
|
|
589
665
|
base.weeklyPercentUsed = keepLargest(base.weeklyPercentUsed, used);
|
|
590
666
|
}
|
|
@@ -613,7 +689,6 @@ function parseClaudeUsageFromStatus(raw) {
|
|
|
613
689
|
last.nextResetAt = nextResetAt;
|
|
614
690
|
}
|
|
615
691
|
lastPercentSection = null;
|
|
616
|
-
lastPercentUsed = null;
|
|
617
692
|
}
|
|
618
693
|
}
|
|
619
694
|
const allModelsLine = lines.find((line) => parseClaudeUsageHeading(line)?.section === "current-week-all");
|
|
@@ -839,6 +914,16 @@ function collectProviderUsageSnapshotFromCli(command, usageCommand, parseSnapsho
|
|
|
839
914
|
}
|
|
840
915
|
|
|
841
916
|
// src/agents/adapters/claude.ts
|
|
917
|
+
var CLAUDE_CAPABILITIES = {
|
|
918
|
+
readOnlyExecution: "plan",
|
|
919
|
+
structuredOutput: {
|
|
920
|
+
mode: "json-schema",
|
|
921
|
+
requiresToolDisable: true
|
|
922
|
+
},
|
|
923
|
+
imageInput: "prompt-inline",
|
|
924
|
+
usageReporting: "cli-command",
|
|
925
|
+
nativeSubagents: "native"
|
|
926
|
+
};
|
|
842
927
|
var CLAUDE_USAGE_COMMAND = "/usage";
|
|
843
928
|
var collectClaudeUsageFromCli = () => collectProviderUsageSnapshotFromCli("claude", CLAUDE_USAGE_COMMAND, parseClaudeUsageFromStatus, [
|
|
844
929
|
"--dangerously-skip-permissions"
|
|
@@ -852,7 +937,8 @@ function buildClaudeCommand(options) {
|
|
|
852
937
|
}
|
|
853
938
|
parts.push("--no-session-persistence", "--output-format json");
|
|
854
939
|
if (options.effort) {
|
|
855
|
-
|
|
940
|
+
const claudeEffort = options.effort === "extra-high" ? "max" : options.effort;
|
|
941
|
+
parts.push(`--effort ${claudeEffort}`);
|
|
856
942
|
}
|
|
857
943
|
if (options.maxBudgetUsd && options.maxBudgetUsd > 0) {
|
|
858
944
|
parts.push(`--max-budget-usd ${options.maxBudgetUsd}`);
|
|
@@ -882,6 +968,7 @@ async function compile(issue, provider, plan, config, workspacePath, skillContex
|
|
|
882
968
|
planPrompt: buildFullPlanPrompt(plan),
|
|
883
969
|
suggestedSkills: plan.suggestedSkills ?? [],
|
|
884
970
|
suggestedAgents: plan.suggestedAgents ?? [],
|
|
971
|
+
hasNativeSubagents: CLAUDE_CAPABILITIES.nativeSubagents === "native",
|
|
885
972
|
suggestedPaths: plan.suggestedPaths ?? [],
|
|
886
973
|
workspacePath,
|
|
887
974
|
issueIdentifier: issue.identifier,
|
|
@@ -905,20 +992,20 @@ async function compile(issue, provider, plan, config, workspacePath, skillContex
|
|
|
905
992
|
readOnly: isReadOnlyRole,
|
|
906
993
|
maxBudgetUsd: config.maxBudgetUsd
|
|
907
994
|
});
|
|
908
|
-
const
|
|
995
|
+
const env2 = {
|
|
909
996
|
FIFONY_PLAN_COMPLEXITY: plan.estimatedComplexity,
|
|
910
997
|
FIFONY_PLAN_STEPS: String(plan.steps.length),
|
|
911
998
|
FIFONY_EXECUTION_PAYLOAD_FILE: "execution-payload.json"
|
|
912
999
|
};
|
|
913
|
-
if (plan.suggestedPaths?.length)
|
|
1000
|
+
if (plan.suggestedPaths?.length) env2.FIFONY_PLAN_PATHS = plan.suggestedPaths.join(",");
|
|
914
1001
|
if (plan.suggestedSkills?.length) {
|
|
915
|
-
|
|
1002
|
+
env2.FIFONY_PLAN_SKILLS = plan.suggestedSkills.join(",");
|
|
916
1003
|
}
|
|
917
1004
|
const { pre, post } = extractValidationCommands(plan);
|
|
918
1005
|
return {
|
|
919
1006
|
prompt,
|
|
920
1007
|
command,
|
|
921
|
-
env:
|
|
1008
|
+
env: env2,
|
|
922
1009
|
preHooks: pre,
|
|
923
1010
|
postHooks: post,
|
|
924
1011
|
outputSchema: CLAUDE_RESULT_SCHEMA,
|
|
@@ -927,6 +1014,7 @@ async function compile(issue, provider, plan, config, workspacePath, skillContex
|
|
|
927
1014
|
adapter: "claude",
|
|
928
1015
|
reasoningEffort: effort || "default",
|
|
929
1016
|
model: provider.model || "default",
|
|
1017
|
+
providerCapabilities: CLAUDE_CAPABILITIES,
|
|
930
1018
|
skillsActivated: plan.suggestedSkills || [],
|
|
931
1019
|
subagentsRequested: plan.suggestedAgents || [],
|
|
932
1020
|
phasesCount: plan.phases?.length || 0
|
|
@@ -934,6 +1022,7 @@ async function compile(issue, provider, plan, config, workspacePath, skillContex
|
|
|
934
1022
|
};
|
|
935
1023
|
}
|
|
936
1024
|
var claudeAdapter = {
|
|
1025
|
+
capabilities: CLAUDE_CAPABILITIES,
|
|
937
1026
|
buildCommand: buildClaudeCommand,
|
|
938
1027
|
buildReviewCommand: (reviewer, config) => buildClaudeCommand({
|
|
939
1028
|
model: reviewer.model,
|
|
@@ -948,6 +1037,16 @@ var claudeAdapter = {
|
|
|
948
1037
|
// src/agents/adapters/codex.ts
|
|
949
1038
|
import { existsSync as existsSync3 } from "fs";
|
|
950
1039
|
import { join as join2 } from "path";
|
|
1040
|
+
var CODEX_CAPABILITIES = {
|
|
1041
|
+
readOnlyExecution: "none",
|
|
1042
|
+
structuredOutput: {
|
|
1043
|
+
mode: "prompt-contract",
|
|
1044
|
+
requiresToolDisable: false
|
|
1045
|
+
},
|
|
1046
|
+
imageInput: "cli-flag",
|
|
1047
|
+
usageReporting: "cli-command",
|
|
1048
|
+
nativeSubagents: "runtime-only"
|
|
1049
|
+
};
|
|
951
1050
|
var CODEX_USAGE_COMMAND = "/status";
|
|
952
1051
|
var collectCodexUsageFromCli = () => collectProviderUsageSnapshotFromCli("codex", CODEX_USAGE_COMMAND, parseCodexUsageFromStatus, [
|
|
953
1052
|
"--dangerously-bypass-approvals-and-sandbox"
|
|
@@ -1016,6 +1115,8 @@ async function compile2(issue, provider, plan, config, workspacePath, skillConte
|
|
|
1016
1115
|
goal: phase.goal,
|
|
1017
1116
|
outputs: phase.outputs ?? []
|
|
1018
1117
|
})),
|
|
1118
|
+
suggestedAgents: plan.suggestedAgents ?? [],
|
|
1119
|
+
hasNativeSubagents: CODEX_CAPABILITIES.nativeSubagents === "native",
|
|
1019
1120
|
suggestedPaths: plan.suggestedPaths ?? [],
|
|
1020
1121
|
suggestedSkills: plan.suggestedSkills ?? [],
|
|
1021
1122
|
validationItems: (plan.validation ?? []).map((value) => ({ value })),
|
|
@@ -1030,18 +1131,18 @@ async function compile2(issue, provider, plan, config, workspacePath, skillConte
|
|
|
1030
1131
|
effort,
|
|
1031
1132
|
imagePaths: issue.images?.filter((p) => existsSync3(p))
|
|
1032
1133
|
});
|
|
1033
|
-
const
|
|
1134
|
+
const env2 = {
|
|
1034
1135
|
FIFONY_PLAN_COMPLEXITY: plan.estimatedComplexity,
|
|
1035
1136
|
FIFONY_PLAN_STEPS: String(plan.steps.length),
|
|
1036
1137
|
FIFONY_PLAN_PHASES: String(plan.phases?.length || 0),
|
|
1037
1138
|
FIFONY_EXECUTION_PAYLOAD_FILE: "execution-payload.json"
|
|
1038
1139
|
};
|
|
1039
|
-
if (plan.suggestedPaths?.length)
|
|
1140
|
+
if (plan.suggestedPaths?.length) env2.FIFONY_PLAN_PATHS = plan.suggestedPaths.join(",");
|
|
1040
1141
|
const { pre, post } = extractValidationCommands(plan);
|
|
1041
1142
|
return {
|
|
1042
1143
|
prompt,
|
|
1043
1144
|
command,
|
|
1044
|
-
env:
|
|
1145
|
+
env: env2,
|
|
1045
1146
|
preHooks: pre,
|
|
1046
1147
|
postHooks: post,
|
|
1047
1148
|
outputSchema: "",
|
|
@@ -1050,13 +1151,15 @@ async function compile2(issue, provider, plan, config, workspacePath, skillConte
|
|
|
1050
1151
|
adapter: "codex",
|
|
1051
1152
|
reasoningEffort: effort || "default",
|
|
1052
1153
|
model: provider.model || "default",
|
|
1154
|
+
providerCapabilities: CODEX_CAPABILITIES,
|
|
1053
1155
|
skillsActivated: plan.suggestedSkills || [],
|
|
1054
|
-
subagentsRequested: [],
|
|
1156
|
+
subagentsRequested: plan.suggestedAgents || [],
|
|
1055
1157
|
phasesCount: plan.phases?.length || 0
|
|
1056
1158
|
}
|
|
1057
1159
|
};
|
|
1058
1160
|
}
|
|
1059
1161
|
var codexAdapter = {
|
|
1162
|
+
capabilities: CODEX_CAPABILITIES,
|
|
1060
1163
|
buildCommand: buildCodexCommand,
|
|
1061
1164
|
buildReviewCommand: (reviewer, _config) => buildCodexCommand({
|
|
1062
1165
|
model: reviewer.model,
|
|
@@ -1069,6 +1172,16 @@ var codexAdapter = {
|
|
|
1069
1172
|
// src/agents/adapters/gemini.ts
|
|
1070
1173
|
import { existsSync as existsSync4 } from "fs";
|
|
1071
1174
|
import { join as join3 } from "path";
|
|
1175
|
+
var GEMINI_CAPABILITIES = {
|
|
1176
|
+
readOnlyExecution: "approval",
|
|
1177
|
+
structuredOutput: {
|
|
1178
|
+
mode: "prompt-contract",
|
|
1179
|
+
requiresToolDisable: false
|
|
1180
|
+
},
|
|
1181
|
+
imageInput: "prompt-inline",
|
|
1182
|
+
usageReporting: "cli-command",
|
|
1183
|
+
nativeSubagents: "runtime-only"
|
|
1184
|
+
};
|
|
1072
1185
|
var GEMINI_USAGE_COMMAND = "/stats session";
|
|
1073
1186
|
var collectGeminiUsageFromCli = () => collectProviderUsageSnapshotFromCli("gemini", GEMINI_USAGE_COMMAND, parseGeminiUsageFromStatus);
|
|
1074
1187
|
var GEMINI_RESULT_CONTRACT = `
|
|
@@ -1124,6 +1237,8 @@ async function compile3(issue, provider, plan, config, workspacePath, skillConte
|
|
|
1124
1237
|
goal: phase.goal,
|
|
1125
1238
|
outputs: phase.outputs ?? []
|
|
1126
1239
|
})),
|
|
1240
|
+
suggestedAgents: plan.suggestedAgents ?? [],
|
|
1241
|
+
hasNativeSubagents: GEMINI_CAPABILITIES.nativeSubagents === "native",
|
|
1127
1242
|
suggestedPaths: plan.suggestedPaths ?? [],
|
|
1128
1243
|
suggestedSkills: plan.suggestedSkills ?? [],
|
|
1129
1244
|
validationItems: (plan.validation ?? []).map((value) => ({ value })),
|
|
@@ -1142,18 +1257,18 @@ async function compile3(issue, provider, plan, config, workspacePath, skillConte
|
|
|
1142
1257
|
addDirs: absoluteDirs,
|
|
1143
1258
|
readOnly: isReadOnlyRole
|
|
1144
1259
|
});
|
|
1145
|
-
const
|
|
1260
|
+
const env2 = {
|
|
1146
1261
|
FIFONY_PLAN_COMPLEXITY: plan.estimatedComplexity,
|
|
1147
1262
|
FIFONY_PLAN_STEPS: String(plan.steps.length),
|
|
1148
1263
|
FIFONY_PLAN_PHASES: String(plan.phases?.length || 0),
|
|
1149
1264
|
FIFONY_EXECUTION_PAYLOAD_FILE: "execution-payload.json"
|
|
1150
1265
|
};
|
|
1151
|
-
if (plan.suggestedPaths?.length)
|
|
1266
|
+
if (plan.suggestedPaths?.length) env2.FIFONY_PLAN_PATHS = plan.suggestedPaths.join(",");
|
|
1152
1267
|
const { pre, post } = extractValidationCommands(plan);
|
|
1153
1268
|
return {
|
|
1154
1269
|
prompt,
|
|
1155
1270
|
command,
|
|
1156
|
-
env:
|
|
1271
|
+
env: env2,
|
|
1157
1272
|
preHooks: pre,
|
|
1158
1273
|
postHooks: post,
|
|
1159
1274
|
outputSchema: "",
|
|
@@ -1162,13 +1277,15 @@ async function compile3(issue, provider, plan, config, workspacePath, skillConte
|
|
|
1162
1277
|
adapter: "gemini",
|
|
1163
1278
|
reasoningEffort: effort || "default",
|
|
1164
1279
|
model: provider.model || "default",
|
|
1280
|
+
providerCapabilities: GEMINI_CAPABILITIES,
|
|
1165
1281
|
skillsActivated: plan.suggestedSkills || [],
|
|
1166
|
-
subagentsRequested: [],
|
|
1282
|
+
subagentsRequested: plan.suggestedAgents || [],
|
|
1167
1283
|
phasesCount: plan.phases?.length || 0
|
|
1168
1284
|
}
|
|
1169
1285
|
};
|
|
1170
1286
|
}
|
|
1171
1287
|
var geminiAdapter = {
|
|
1288
|
+
capabilities: GEMINI_CAPABILITIES,
|
|
1172
1289
|
buildCommand: buildGeminiCommand,
|
|
1173
1290
|
buildReviewCommand: (reviewer) => buildGeminiCommand({
|
|
1174
1291
|
model: reviewer.model,
|
|
@@ -1183,6 +1300,700 @@ var ADAPTERS = {
|
|
|
1183
1300
|
codex: codexAdapter,
|
|
1184
1301
|
gemini: geminiAdapter
|
|
1185
1302
|
};
|
|
1303
|
+
var UNSUPPORTED_CAPABILITIES = {
|
|
1304
|
+
readOnlyExecution: "none",
|
|
1305
|
+
structuredOutput: {
|
|
1306
|
+
mode: "none",
|
|
1307
|
+
requiresToolDisable: false
|
|
1308
|
+
},
|
|
1309
|
+
imageInput: "none",
|
|
1310
|
+
usageReporting: "none",
|
|
1311
|
+
nativeSubagents: "runtime-only"
|
|
1312
|
+
};
|
|
1313
|
+
function getProviderCapabilities(provider, overrides) {
|
|
1314
|
+
if (overrides) return overrides;
|
|
1315
|
+
return ADAPTERS[provider]?.capabilities ?? UNSUPPORTED_CAPABILITIES;
|
|
1316
|
+
}
|
|
1317
|
+
function supportsReadOnlyExecution(capabilities) {
|
|
1318
|
+
return capabilities.readOnlyExecution !== "none";
|
|
1319
|
+
}
|
|
1320
|
+
function usesNativeStructuredOutput(capabilities) {
|
|
1321
|
+
return capabilities.structuredOutput.mode === "json-schema";
|
|
1322
|
+
}
|
|
1323
|
+
function supportsNativeSubagents(capabilities) {
|
|
1324
|
+
return capabilities.nativeSubagents === "native";
|
|
1325
|
+
}
|
|
1326
|
+
function describeProviderCapabilityWarnings(provider, capabilities) {
|
|
1327
|
+
const warnings = [];
|
|
1328
|
+
if (!supportsReadOnlyExecution(capabilities)) {
|
|
1329
|
+
warnings.push(`${provider} does not expose CLI-enforced read-only execution; planner/reviewer runs fall back to prompt/runtime discipline.`);
|
|
1330
|
+
}
|
|
1331
|
+
if (!usesNativeStructuredOutput(capabilities)) {
|
|
1332
|
+
warnings.push(`${provider} does not expose native JSON schema enforcement; structured output falls back to prompt-contract parsing.`);
|
|
1333
|
+
}
|
|
1334
|
+
if (!supportsNativeSubagents(capabilities)) {
|
|
1335
|
+
warnings.push(`${provider} does not expose native subagents; delegation will use Fifony runtime orchestration instead.`);
|
|
1336
|
+
}
|
|
1337
|
+
if (capabilities.usageReporting === "none") {
|
|
1338
|
+
warnings.push(`${provider} does not expose usage reporting; provider budget telemetry may be incomplete.`);
|
|
1339
|
+
}
|
|
1340
|
+
return warnings;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// src/agents/providers.ts
|
|
1344
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1345
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
|
|
1346
|
+
import { join as join5 } from "path";
|
|
1347
|
+
import { homedir as homedir2 } from "os";
|
|
1348
|
+
|
|
1349
|
+
// src/agents/review-profile.ts
|
|
1350
|
+
var UI_EXTENSIONS = [".jsx", ".tsx", ".css", ".scss", ".vue", ".svelte", ".html"];
|
|
1351
|
+
function collectCandidatePaths(issue) {
|
|
1352
|
+
const planPaths = issue.plan?.suggestedPaths ?? [];
|
|
1353
|
+
const issuePaths = issue.paths ?? [];
|
|
1354
|
+
const contractAreas = issue.plan?.executionContract?.focusAreas ?? [];
|
|
1355
|
+
return [.../* @__PURE__ */ new Set([...issuePaths, ...planPaths, ...contractAreas])];
|
|
1356
|
+
}
|
|
1357
|
+
function collectCandidateText(issue) {
|
|
1358
|
+
return [
|
|
1359
|
+
issue.title,
|
|
1360
|
+
issue.description,
|
|
1361
|
+
issue.issueType,
|
|
1362
|
+
...issue.labels ?? [],
|
|
1363
|
+
...issue.plan?.suggestedPaths ?? []
|
|
1364
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
1365
|
+
}
|
|
1366
|
+
function hasPath(paths, pattern) {
|
|
1367
|
+
return paths.some((path) => pattern.test(path));
|
|
1368
|
+
}
|
|
1369
|
+
function hasCategory(criteria, category) {
|
|
1370
|
+
return criteria.some((criterion) => criterion.category === category);
|
|
1371
|
+
}
|
|
1372
|
+
function includesAny(text, terms) {
|
|
1373
|
+
return terms.some((term) => text.includes(term));
|
|
1374
|
+
}
|
|
1375
|
+
function buildFocusAreas(paths, fallback) {
|
|
1376
|
+
const combined = [...paths, ...fallback].filter(Boolean);
|
|
1377
|
+
return [...new Set(combined)].slice(0, 6);
|
|
1378
|
+
}
|
|
1379
|
+
function deriveReviewProfile(issue) {
|
|
1380
|
+
const paths = collectCandidatePaths(issue);
|
|
1381
|
+
const text = collectCandidateText(issue);
|
|
1382
|
+
const criteria = issue.plan?.acceptanceCriteria ?? [];
|
|
1383
|
+
const scores = [
|
|
1384
|
+
{ name: "general-quality", score: 1, rationale: ["Fallback profile for broad correctness, regression risk, and code quality review."] },
|
|
1385
|
+
{ name: "ui-polish", score: 0, rationale: [] },
|
|
1386
|
+
{ name: "workflow-fsm", score: 0, rationale: [] },
|
|
1387
|
+
{ name: "integration-safety", score: 0, rationale: [] },
|
|
1388
|
+
{ name: "api-contract", score: 0, rationale: [] },
|
|
1389
|
+
{ name: "security-hardening", score: 0, rationale: [] }
|
|
1390
|
+
];
|
|
1391
|
+
const uiScore = scores.find((entry) => entry.name === "ui-polish");
|
|
1392
|
+
if (paths.some((path) => UI_EXTENSIONS.some((ext) => path.endsWith(ext)))) {
|
|
1393
|
+
uiScore.score += 4;
|
|
1394
|
+
uiScore.rationale.push("Touched frontend files that can regress visual polish, interaction flow, or responsiveness.");
|
|
1395
|
+
}
|
|
1396
|
+
if (hasCategory(criteria, "design") || includesAny(text, ["frontend", "ui", "ux", "drawer", "onboarding", "layout", "mobile"])) {
|
|
1397
|
+
uiScore.score += 3;
|
|
1398
|
+
uiScore.rationale.push("Issue signals UI/UX work that needs stronger product-behavior and visual scrutiny.");
|
|
1399
|
+
}
|
|
1400
|
+
const workflowScore = scores.find((entry) => entry.name === "workflow-fsm");
|
|
1401
|
+
if (hasPath(paths, /src\/persistence\/plugins\/fsm-|src\/commands\/|src\/domains\/issues\.ts|src\/agents\//)) {
|
|
1402
|
+
workflowScore.score += 5;
|
|
1403
|
+
workflowScore.rationale.push("Touched workflow/FSM/orchestration code where lifecycle invariants and retry semantics are fragile.");
|
|
1404
|
+
}
|
|
1405
|
+
if (includesAny(text, ["fsm", "workflow", "queue", "review gate", "lifecycle", "orchestration", "agent"])) {
|
|
1406
|
+
workflowScore.score += 3;
|
|
1407
|
+
workflowScore.rationale.push("Issue description or labels indicate orchestration semantics rather than isolated implementation.");
|
|
1408
|
+
}
|
|
1409
|
+
const integrationScore = scores.find((entry) => entry.name === "integration-safety");
|
|
1410
|
+
if (hasPath(paths, /workspace|merge|push|rebase|git|dirty-tracker|services?|store\.ts/)) {
|
|
1411
|
+
integrationScore.score += 5;
|
|
1412
|
+
integrationScore.rationale.push("Touched integration or git/workspace code where destructive behavior and state drift must be caught.");
|
|
1413
|
+
}
|
|
1414
|
+
if (hasCategory(criteria, "integration") || hasCategory(criteria, "regression")) {
|
|
1415
|
+
integrationScore.score += 2;
|
|
1416
|
+
integrationScore.rationale.push("Acceptance criteria explicitly call out integration or regression guarantees.");
|
|
1417
|
+
}
|
|
1418
|
+
const apiScore = scores.find((entry) => entry.name === "api-contract");
|
|
1419
|
+
if (hasPath(paths, /src\/routes\/|src\/persistence\/resources\/|src\/mcp\//)) {
|
|
1420
|
+
apiScore.score += 4;
|
|
1421
|
+
apiScore.rationale.push("Touched API/resource surface that can drift from contract or persistence schema.");
|
|
1422
|
+
}
|
|
1423
|
+
if (includesAny(text, ["api", "route", "http", "endpoint", "resource", "schema"])) {
|
|
1424
|
+
apiScore.score += 2;
|
|
1425
|
+
apiScore.rationale.push("Issue language implies request/response or schema contract changes.");
|
|
1426
|
+
}
|
|
1427
|
+
const securityScore = scores.find((entry) => entry.name === "security-hardening");
|
|
1428
|
+
if (hasCategory(criteria, "security") || includesAny(text, ["auth", "security", "token", "permission", "secret"])) {
|
|
1429
|
+
securityScore.score += 5;
|
|
1430
|
+
securityScore.rationale.push("Security-sensitive behavior or criteria are present and should be treated as blocking by default.");
|
|
1431
|
+
}
|
|
1432
|
+
if (hasPath(paths, /auth|permission|secret|credential|shell|command-executor/)) {
|
|
1433
|
+
securityScore.score += 3;
|
|
1434
|
+
securityScore.rationale.push("Touched code paths that can introduce auth, privilege, or command-execution risk.");
|
|
1435
|
+
}
|
|
1436
|
+
const ranked = [...scores].sort((a, b) => b.score - a.score);
|
|
1437
|
+
const primary = ranked[0];
|
|
1438
|
+
const secondary = ranked.filter((entry) => entry.name !== primary.name && entry.score >= 3).slice(0, 2).map((entry) => entry.name);
|
|
1439
|
+
const byName = {
|
|
1440
|
+
"general-quality": {
|
|
1441
|
+
focusAreas: buildFocusAreas(paths, ["Correctness under real usage", "Regression risk", "Code quality and maintainability"]),
|
|
1442
|
+
failureModes: [
|
|
1443
|
+
"Partial implementations that look complete but leave core behavior stubbed",
|
|
1444
|
+
"Missing validation, tests, or evidence for blocking criteria",
|
|
1445
|
+
"Code that technically works but introduces obvious maintainability debt"
|
|
1446
|
+
],
|
|
1447
|
+
evidencePriorities: [
|
|
1448
|
+
"Run or inspect the most relevant validation commands",
|
|
1449
|
+
"Trace the dominant code path end to end",
|
|
1450
|
+
"Call out unverified assumptions explicitly instead of hand-waving them"
|
|
1451
|
+
],
|
|
1452
|
+
severityBias: "Bias toward FAIL when behavior is only implied rather than demonstrated."
|
|
1453
|
+
},
|
|
1454
|
+
"ui-polish": {
|
|
1455
|
+
focusAreas: buildFocusAreas(paths, ["Primary interaction flow", "Responsive layout", "Accessibility and clarity of actions"]),
|
|
1456
|
+
failureModes: [
|
|
1457
|
+
"Broken or unintuitive interaction flow, especially onboarding, drawers, and primary actions",
|
|
1458
|
+
"Visual regressions, overflow, spacing collapse, or inaccessible controls",
|
|
1459
|
+
"Interfaces that technically render but feel unfinished or confusing in use"
|
|
1460
|
+
],
|
|
1461
|
+
evidencePriorities: [
|
|
1462
|
+
"Navigate the affected UI and describe what users can and cannot do",
|
|
1463
|
+
"Verify mobile-width and edge-state behavior, not just the happy path",
|
|
1464
|
+
"Use Playwright evidence when visible behavior is part of the contract"
|
|
1465
|
+
],
|
|
1466
|
+
severityBias: "Treat usability breaks and visually misleading states as blocking defects, not polish nits."
|
|
1467
|
+
},
|
|
1468
|
+
"workflow-fsm": {
|
|
1469
|
+
focusAreas: buildFocusAreas(paths, ["State transitions", "Retry semantics", "Lifecycle invariants", "Counter reset behavior"]),
|
|
1470
|
+
failureModes: [
|
|
1471
|
+
"Illegal transitions that bypass approval, review, or terminal-state rules",
|
|
1472
|
+
"Retry and checkpoint flows that jump to the wrong phase or double-increment counters",
|
|
1473
|
+
"State cleanup/reset bugs that leave stale error, checkpoint, or lifecycle metadata behind"
|
|
1474
|
+
],
|
|
1475
|
+
evidencePriorities: [
|
|
1476
|
+
"Trace the exact state path for the critical scenario, including failure paths",
|
|
1477
|
+
"Verify counters and lifecycle fields are reset or preserved intentionally",
|
|
1478
|
+
"Treat ambiguous transition behavior as a defect until proven safe"
|
|
1479
|
+
],
|
|
1480
|
+
severityBias: "Any lifecycle inconsistency that can misroute an issue or bypass a gate is blocking."
|
|
1481
|
+
},
|
|
1482
|
+
"integration-safety": {
|
|
1483
|
+
focusAreas: buildFocusAreas(paths, ["Git/worktree operations", "Persistence side effects", "Idempotency and cleanup"]),
|
|
1484
|
+
failureModes: [
|
|
1485
|
+
"Destructive workspace behavior that can delete user work or dirty target branches",
|
|
1486
|
+
"Cross-system drift between runtime state, resources, and filesystem artifacts",
|
|
1487
|
+
"Merge/push/service-management flows that work only in the happy path and break under dirty state"
|
|
1488
|
+
],
|
|
1489
|
+
evidencePriorities: [
|
|
1490
|
+
"Verify failure handling, not just success path behavior",
|
|
1491
|
+
"Check idempotency and cleanup paths explicitly",
|
|
1492
|
+
"Call out any command or filesystem side effect that is not safely guarded"
|
|
1493
|
+
],
|
|
1494
|
+
severityBias: "Prefer FAIL when integration code assumes a clean environment or safe side effects without enforcing them."
|
|
1495
|
+
},
|
|
1496
|
+
"api-contract": {
|
|
1497
|
+
focusAreas: buildFocusAreas(paths, ["Route handlers", "Resource schema", "Input/output contract"]),
|
|
1498
|
+
failureModes: [
|
|
1499
|
+
"HTTP/API behavior that no longer matches route or resource contract",
|
|
1500
|
+
"Schema drift between persisted fields, normalization, and route responses",
|
|
1501
|
+
"Missing validation or status-code mismatches that break downstream callers"
|
|
1502
|
+
],
|
|
1503
|
+
evidencePriorities: [
|
|
1504
|
+
"Read the route/resource code and trace request-to-response behavior",
|
|
1505
|
+
"Verify persisted fields, normalization, and response shape stay aligned",
|
|
1506
|
+
"Treat silent contract drift as blocking even if the implementation compiles"
|
|
1507
|
+
],
|
|
1508
|
+
severityBias: "Contract drift is blocking because it breaks automation and downstream clients silently."
|
|
1509
|
+
},
|
|
1510
|
+
"security-hardening": {
|
|
1511
|
+
focusAreas: buildFocusAreas(paths, ["Authorization boundaries", "Secret handling", "Shell/command safety"]),
|
|
1512
|
+
failureModes: [
|
|
1513
|
+
"Authorization bypass, over-broad permissions, or unsafe defaults",
|
|
1514
|
+
"Leaked secrets, credentials, or unsafe command composition",
|
|
1515
|
+
"Security-sensitive criteria marked as effectively optional or unverified"
|
|
1516
|
+
],
|
|
1517
|
+
evidencePriorities: [
|
|
1518
|
+
"Look for privilege escalation and shell/filepath injection opportunities",
|
|
1519
|
+
"Verify security checks with concrete evidence, not inference alone",
|
|
1520
|
+
"Escalate uncertainty instead of allowing a soft PASS on security-sensitive paths"
|
|
1521
|
+
],
|
|
1522
|
+
severityBias: "Security uncertainty should fail closed; do not grant benefit of the doubt."
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
return {
|
|
1526
|
+
primary: primary.name,
|
|
1527
|
+
secondary,
|
|
1528
|
+
rationale: primary.rationale.length ? primary.rationale : ["Selected as the highest-risk profile based on touched code and acceptance criteria."],
|
|
1529
|
+
...byName[primary.name]
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// src/agents/harness-policy.ts
|
|
1534
|
+
var HIGH_RISK_PROFILES = /* @__PURE__ */ new Set([
|
|
1535
|
+
"workflow-fsm",
|
|
1536
|
+
"integration-safety",
|
|
1537
|
+
"api-contract",
|
|
1538
|
+
"security-hardening"
|
|
1539
|
+
]);
|
|
1540
|
+
var HIGH_CHECKPOINT_PROFILES = /* @__PURE__ */ new Set([
|
|
1541
|
+
"workflow-fsm",
|
|
1542
|
+
"integration-safety",
|
|
1543
|
+
"security-hardening"
|
|
1544
|
+
]);
|
|
1545
|
+
var ROUTE_AFFINITY = {
|
|
1546
|
+
"general-quality": { claude: 2.4, codex: 1.8, gemini: 1.4 },
|
|
1547
|
+
"ui-polish": { claude: 3.2, codex: 1.8, gemini: 1.6 },
|
|
1548
|
+
"workflow-fsm": { codex: 3.1, claude: 2, gemini: 1 },
|
|
1549
|
+
"integration-safety": { codex: 3, claude: 2, gemini: 1 },
|
|
1550
|
+
"api-contract": { codex: 3.1, claude: 1.9, gemini: 1.2 },
|
|
1551
|
+
"security-hardening": { claude: 2.6, codex: 2.6, gemini: 0.8 }
|
|
1552
|
+
};
|
|
1553
|
+
function rate(numerator, denominator) {
|
|
1554
|
+
return denominator > 0 ? numerator / denominator : null;
|
|
1555
|
+
}
|
|
1556
|
+
function isCompletedIssue(issue) {
|
|
1557
|
+
return issue.state === "Approved" || issue.state === "Merged";
|
|
1558
|
+
}
|
|
1559
|
+
function hadReviewRework(issue) {
|
|
1560
|
+
return (issue.previousAttemptSummaries ?? []).some((summary) => summary.phase === "review");
|
|
1561
|
+
}
|
|
1562
|
+
function resolveEffectiveReviewProfile(issue) {
|
|
1563
|
+
return issue.reviewProfile ?? deriveReviewProfile(issue);
|
|
1564
|
+
}
|
|
1565
|
+
function serializeReviewRouteSnapshot(route) {
|
|
1566
|
+
const providerLabel = `${route.provider}${route.model ? `/${route.model}` : ""}`;
|
|
1567
|
+
const effortLabel = route.reasoningEffort ? `[${route.reasoningEffort}]` : "";
|
|
1568
|
+
const overlayLabel = route.overlays?.length ? `overlays:${[...route.overlays].sort().join(",")}` : "";
|
|
1569
|
+
return [providerLabel, effortLabel, overlayLabel].filter(Boolean).join(" | ");
|
|
1570
|
+
}
|
|
1571
|
+
function buildReviewRouteKey(candidate) {
|
|
1572
|
+
return serializeReviewRouteSnapshot({
|
|
1573
|
+
provider: candidate.provider,
|
|
1574
|
+
model: candidate.model,
|
|
1575
|
+
reasoningEffort: candidate.reasoningEffort,
|
|
1576
|
+
overlays: candidate.overlays ?? []
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
function applyHarnessModeToPlan(plan, mode) {
|
|
1580
|
+
plan.harnessMode = mode;
|
|
1581
|
+
if (mode !== "contractual") {
|
|
1582
|
+
plan.executionContract.checkpointPolicy = "final_only";
|
|
1583
|
+
} else if (plan.executionContract.checkpointPolicy !== "checkpointed") {
|
|
1584
|
+
plan.executionContract.checkpointPolicy = "final_only";
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
function applyCheckpointPolicyToPlan(plan, checkpointPolicy) {
|
|
1588
|
+
plan.executionContract.checkpointPolicy = plan.harnessMode === "contractual" ? checkpointPolicy : "final_only";
|
|
1589
|
+
}
|
|
1590
|
+
function resolveLatestCompletedReviewRun(issue, scope = "final") {
|
|
1591
|
+
const reviewRuns = Array.isArray(issue.reviewRuns) ? issue.reviewRuns : [];
|
|
1592
|
+
const completed = reviewRuns.filter((entry) => entry.status === "completed");
|
|
1593
|
+
const matchingScope = completed.filter((entry) => entry.scope === scope);
|
|
1594
|
+
const pool = matchingScope.length > 0 ? matchingScope : completed;
|
|
1595
|
+
if (pool.length === 0) return null;
|
|
1596
|
+
return [...pool].sort((left, right) => {
|
|
1597
|
+
const leftAt = Date.parse(left.completedAt ?? left.startedAt);
|
|
1598
|
+
const rightAt = Date.parse(right.completedAt ?? right.startedAt);
|
|
1599
|
+
if (!Number.isNaN(leftAt) && !Number.isNaN(rightAt) && leftAt !== rightAt) return rightAt - leftAt;
|
|
1600
|
+
if ((left.planVersion ?? 0) !== (right.planVersion ?? 0)) return (right.planVersion ?? 0) - (left.planVersion ?? 0);
|
|
1601
|
+
return (right.attempt ?? 0) - (left.attempt ?? 0);
|
|
1602
|
+
})[0] ?? null;
|
|
1603
|
+
}
|
|
1604
|
+
function resolveLatestCompletedContractNegotiationRuns(issue) {
|
|
1605
|
+
const runs = Array.isArray(issue.contractNegotiationRuns) ? issue.contractNegotiationRuns : [];
|
|
1606
|
+
const completed = runs.filter((entry) => entry.status === "completed");
|
|
1607
|
+
if (completed.length === 0) return [];
|
|
1608
|
+
const latestPlanVersion = completed.reduce((maxPlanVersion, entry) => Math.max(maxPlanVersion, entry.planVersion ?? 0), 0);
|
|
1609
|
+
return completed.filter((entry) => (entry.planVersion ?? 0) === latestPlanVersion).sort((left, right) => {
|
|
1610
|
+
if ((left.attempt ?? 0) !== (right.attempt ?? 0)) return (left.attempt ?? 0) - (right.attempt ?? 0);
|
|
1611
|
+
const leftAt = Date.parse(left.completedAt ?? left.startedAt);
|
|
1612
|
+
const rightAt = Date.parse(right.completedAt ?? right.startedAt);
|
|
1613
|
+
if (!Number.isNaN(leftAt) && !Number.isNaN(rightAt) && leftAt !== rightAt) return leftAt - rightAt;
|
|
1614
|
+
return left.id.localeCompare(right.id);
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
function resolveLatestCompletedScopedReviewRuns(issue, scope) {
|
|
1618
|
+
const reviewRuns = Array.isArray(issue.reviewRuns) ? issue.reviewRuns : [];
|
|
1619
|
+
const completed = reviewRuns.filter((entry) => entry.status === "completed" && entry.scope === scope);
|
|
1620
|
+
if (completed.length === 0) return [];
|
|
1621
|
+
const latestPlanVersion = completed.reduce((maxPlanVersion, entry) => Math.max(maxPlanVersion, entry.planVersion ?? 0), 0);
|
|
1622
|
+
return completed.filter((entry) => (entry.planVersion ?? 0) === latestPlanVersion).sort((left, right) => {
|
|
1623
|
+
if ((left.attempt ?? 0) !== (right.attempt ?? 0)) return (left.attempt ?? 0) - (right.attempt ?? 0);
|
|
1624
|
+
const leftAt = Date.parse(left.completedAt ?? left.startedAt);
|
|
1625
|
+
const rightAt = Date.parse(right.completedAt ?? right.startedAt);
|
|
1626
|
+
if (!Number.isNaN(leftAt) && !Number.isNaN(rightAt) && leftAt !== rightAt) return leftAt - rightAt;
|
|
1627
|
+
return left.id.localeCompare(right.id);
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
function computeHarnessModeStats(issues, profileName) {
|
|
1631
|
+
const buckets = {
|
|
1632
|
+
solo: { reviewedIssues: 0, completedReviewedIssues: 0, gatePasses: 0, firstPassPasses: 0, reworkIssues: 0 },
|
|
1633
|
+
standard: { reviewedIssues: 0, completedReviewedIssues: 0, gatePasses: 0, firstPassPasses: 0, reworkIssues: 0 },
|
|
1634
|
+
contractual: { reviewedIssues: 0, completedReviewedIssues: 0, gatePasses: 0, firstPassPasses: 0, reworkIssues: 0 }
|
|
1635
|
+
};
|
|
1636
|
+
for (const issue of issues) {
|
|
1637
|
+
const reviewRun = resolveLatestCompletedReviewRun(issue, "final");
|
|
1638
|
+
if (!reviewRun) continue;
|
|
1639
|
+
const effectiveProfile = reviewRun.reviewProfile ?? resolveEffectiveReviewProfile(issue);
|
|
1640
|
+
if (effectiveProfile.primary !== profileName) continue;
|
|
1641
|
+
const mode = issue.plan?.harnessMode ?? "standard";
|
|
1642
|
+
const bucket = buckets[mode];
|
|
1643
|
+
bucket.reviewedIssues += 1;
|
|
1644
|
+
if (isCompletedIssue(issue)) bucket.completedReviewedIssues += 1;
|
|
1645
|
+
if (reviewRun.blockingVerdict === "PASS") bucket.gatePasses += 1;
|
|
1646
|
+
if ((issue.reviewAttempt ?? 0) <= 1 && !hadReviewRework(issue) && isCompletedIssue(issue)) bucket.firstPassPasses += 1;
|
|
1647
|
+
if (hadReviewRework(issue)) bucket.reworkIssues += 1;
|
|
1648
|
+
}
|
|
1649
|
+
return {
|
|
1650
|
+
solo: {
|
|
1651
|
+
...buckets.solo,
|
|
1652
|
+
gatePassRate: rate(buckets.solo.gatePasses, buckets.solo.reviewedIssues),
|
|
1653
|
+
firstPassPassRate: rate(buckets.solo.firstPassPasses, buckets.solo.completedReviewedIssues),
|
|
1654
|
+
reviewReworkRate: rate(buckets.solo.reworkIssues, buckets.solo.reviewedIssues)
|
|
1655
|
+
},
|
|
1656
|
+
standard: {
|
|
1657
|
+
...buckets.standard,
|
|
1658
|
+
gatePassRate: rate(buckets.standard.gatePasses, buckets.standard.reviewedIssues),
|
|
1659
|
+
firstPassPassRate: rate(buckets.standard.firstPassPasses, buckets.standard.completedReviewedIssues),
|
|
1660
|
+
reviewReworkRate: rate(buckets.standard.reworkIssues, buckets.standard.reviewedIssues)
|
|
1661
|
+
},
|
|
1662
|
+
contractual: {
|
|
1663
|
+
...buckets.contractual,
|
|
1664
|
+
gatePassRate: rate(buckets.contractual.gatePasses, buckets.contractual.reviewedIssues),
|
|
1665
|
+
firstPassPassRate: rate(buckets.contractual.firstPassPasses, buckets.contractual.completedReviewedIssues),
|
|
1666
|
+
reviewReworkRate: rate(buckets.contractual.reworkIssues, buckets.contractual.reviewedIssues)
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
function computeCheckpointPolicyStats(issues, profileName) {
|
|
1671
|
+
const buckets = {
|
|
1672
|
+
final_only: {
|
|
1673
|
+
reviewedIssues: 0,
|
|
1674
|
+
completedReviewedIssues: 0,
|
|
1675
|
+
gatePasses: 0,
|
|
1676
|
+
firstPassPasses: 0,
|
|
1677
|
+
reworkIssues: 0,
|
|
1678
|
+
checkpointFailures: 0,
|
|
1679
|
+
checkpointPasses: 0,
|
|
1680
|
+
checkpointRuns: 0
|
|
1681
|
+
},
|
|
1682
|
+
checkpointed: {
|
|
1683
|
+
reviewedIssues: 0,
|
|
1684
|
+
completedReviewedIssues: 0,
|
|
1685
|
+
gatePasses: 0,
|
|
1686
|
+
firstPassPasses: 0,
|
|
1687
|
+
reworkIssues: 0,
|
|
1688
|
+
checkpointFailures: 0,
|
|
1689
|
+
checkpointPasses: 0,
|
|
1690
|
+
checkpointRuns: 0
|
|
1691
|
+
}
|
|
1692
|
+
};
|
|
1693
|
+
for (const issue of issues) {
|
|
1694
|
+
if ((issue.plan?.harnessMode ?? "standard") !== "contractual") continue;
|
|
1695
|
+
const finalReviewRun = resolveLatestCompletedReviewRun(issue, "final");
|
|
1696
|
+
if (!finalReviewRun) continue;
|
|
1697
|
+
const effectiveProfile = finalReviewRun.reviewProfile ?? resolveEffectiveReviewProfile(issue);
|
|
1698
|
+
if (effectiveProfile.primary !== profileName) continue;
|
|
1699
|
+
const checkpointPolicy = issue.plan?.executionContract?.checkpointPolicy === "checkpointed" ? "checkpointed" : "final_only";
|
|
1700
|
+
const bucket = buckets[checkpointPolicy];
|
|
1701
|
+
bucket.reviewedIssues += 1;
|
|
1702
|
+
if (isCompletedIssue(issue)) bucket.completedReviewedIssues += 1;
|
|
1703
|
+
if (finalReviewRun.blockingVerdict === "PASS") bucket.gatePasses += 1;
|
|
1704
|
+
if ((issue.reviewAttempt ?? 0) <= 1 && !hadReviewRework(issue) && isCompletedIssue(issue)) bucket.firstPassPasses += 1;
|
|
1705
|
+
if (hadReviewRework(issue)) bucket.reworkIssues += 1;
|
|
1706
|
+
if (checkpointPolicy === "checkpointed") {
|
|
1707
|
+
const checkpointRuns = resolveLatestCompletedScopedReviewRuns(issue, "checkpoint");
|
|
1708
|
+
bucket.checkpointRuns += checkpointRuns.length;
|
|
1709
|
+
if (checkpointRuns.some((entry) => entry.blockingVerdict === "FAIL")) bucket.checkpointFailures += 1;
|
|
1710
|
+
if (checkpointRuns.some((entry) => entry.blockingVerdict === "PASS")) bucket.checkpointPasses += 1;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
return {
|
|
1714
|
+
final_only: {
|
|
1715
|
+
...buckets.final_only,
|
|
1716
|
+
gatePassRate: rate(buckets.final_only.gatePasses, buckets.final_only.reviewedIssues),
|
|
1717
|
+
firstPassPassRate: rate(buckets.final_only.firstPassPasses, buckets.final_only.completedReviewedIssues),
|
|
1718
|
+
reviewReworkRate: rate(buckets.final_only.reworkIssues, buckets.final_only.reviewedIssues),
|
|
1719
|
+
checkpointFailureRate: rate(buckets.final_only.checkpointFailures, buckets.final_only.reviewedIssues),
|
|
1720
|
+
checkpointPassRate: rate(buckets.final_only.checkpointPasses, buckets.final_only.reviewedIssues),
|
|
1721
|
+
avgCheckpointRunsPerIssue: rate(buckets.final_only.checkpointRuns, buckets.final_only.reviewedIssues)
|
|
1722
|
+
},
|
|
1723
|
+
checkpointed: {
|
|
1724
|
+
...buckets.checkpointed,
|
|
1725
|
+
gatePassRate: rate(buckets.checkpointed.gatePasses, buckets.checkpointed.reviewedIssues),
|
|
1726
|
+
firstPassPassRate: rate(buckets.checkpointed.firstPassPasses, buckets.checkpointed.completedReviewedIssues),
|
|
1727
|
+
reviewReworkRate: rate(buckets.checkpointed.reworkIssues, buckets.checkpointed.reviewedIssues),
|
|
1728
|
+
checkpointFailureRate: rate(buckets.checkpointed.checkpointFailures, buckets.checkpointed.reviewedIssues),
|
|
1729
|
+
checkpointPassRate: rate(buckets.checkpointed.checkpointPasses, buckets.checkpointed.reviewedIssues),
|
|
1730
|
+
avgCheckpointRunsPerIssue: rate(buckets.checkpointed.checkpointRuns, buckets.checkpointed.reviewedIssues)
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
function computeContractNegotiationStats(issues, profileName) {
|
|
1735
|
+
const bucket = {
|
|
1736
|
+
negotiatedIssues: 0,
|
|
1737
|
+
approvedIssues: 0,
|
|
1738
|
+
firstPassApprovals: 0,
|
|
1739
|
+
revisedIssues: 0,
|
|
1740
|
+
blockingConcernIssues: 0,
|
|
1741
|
+
totalRounds: 0
|
|
1742
|
+
};
|
|
1743
|
+
for (const issue of issues) {
|
|
1744
|
+
const planRuns = resolveLatestCompletedContractNegotiationRuns(issue);
|
|
1745
|
+
if (planRuns.length === 0) continue;
|
|
1746
|
+
const latestRun = planRuns[planRuns.length - 1];
|
|
1747
|
+
const effectiveProfile = latestRun.reviewProfile ?? resolveEffectiveReviewProfile(issue);
|
|
1748
|
+
if (effectiveProfile.primary !== profileName) continue;
|
|
1749
|
+
bucket.negotiatedIssues += 1;
|
|
1750
|
+
bucket.totalRounds += planRuns.length;
|
|
1751
|
+
if (latestRun.decisionStatus === "approved") bucket.approvedIssues += 1;
|
|
1752
|
+
if (planRuns.length === 1 && planRuns[0]?.decisionStatus === "approved") bucket.firstPassApprovals += 1;
|
|
1753
|
+
if (planRuns.some((entry) => entry.decisionStatus === "revise" || entry.appliedRefinement)) bucket.revisedIssues += 1;
|
|
1754
|
+
if (planRuns.some((entry) => (entry.blockingConcernsCount ?? 0) > 0)) bucket.blockingConcernIssues += 1;
|
|
1755
|
+
}
|
|
1756
|
+
return {
|
|
1757
|
+
...bucket,
|
|
1758
|
+
approvalRate: rate(bucket.approvedIssues, bucket.negotiatedIssues),
|
|
1759
|
+
firstPassApprovalRate: rate(bucket.firstPassApprovals, bucket.negotiatedIssues),
|
|
1760
|
+
revisionRate: rate(bucket.revisedIssues, bucket.negotiatedIssues),
|
|
1761
|
+
blockingConcernRate: rate(bucket.blockingConcernIssues, bucket.negotiatedIssues),
|
|
1762
|
+
avgRoundsPerIssue: rate(bucket.totalRounds, bucket.negotiatedIssues)
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
function recommendCheckpointPolicyForIssue(issues, issue, currentCheckpointPolicy, minSamples = 3) {
|
|
1766
|
+
if (issue.plan?.harnessMode !== "contractual") {
|
|
1767
|
+
if (currentCheckpointPolicy !== "final_only") {
|
|
1768
|
+
const profile2 = resolveEffectiveReviewProfile(issue);
|
|
1769
|
+
return {
|
|
1770
|
+
checkpointPolicy: "final_only",
|
|
1771
|
+
profile: profile2,
|
|
1772
|
+
basis: "heuristic",
|
|
1773
|
+
rationale: "Non-contractual plans must not request checkpoint review."
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
return null;
|
|
1777
|
+
}
|
|
1778
|
+
const profile = resolveEffectiveReviewProfile(issue);
|
|
1779
|
+
const complexity = issue.plan?.estimatedComplexity ?? "medium";
|
|
1780
|
+
const lowScope = complexity === "trivial" || complexity === "low";
|
|
1781
|
+
const highCheckpointRisk = HIGH_CHECKPOINT_PROFILES.has(profile.primary) && !lowScope;
|
|
1782
|
+
const stats = computeCheckpointPolicyStats(issues, profile.primary);
|
|
1783
|
+
const finalOnly = stats.final_only;
|
|
1784
|
+
const checkpointed = stats.checkpointed;
|
|
1785
|
+
const checkpointedSamplesReady = checkpointed.reviewedIssues >= minSamples;
|
|
1786
|
+
const finalOnlySamplesReady = finalOnly.reviewedIssues >= minSamples;
|
|
1787
|
+
if (currentCheckpointPolicy !== "checkpointed" && highCheckpointRisk) {
|
|
1788
|
+
if (checkpointedSamplesReady && (checkpointed.checkpointFailureRate ?? 0) >= 0.15) {
|
|
1789
|
+
return {
|
|
1790
|
+
checkpointPolicy: "checkpointed",
|
|
1791
|
+
profile,
|
|
1792
|
+
basis: "historical",
|
|
1793
|
+
rationale: `Enabled checkpoint review for ${profile.primary}: checkpointed runs caught blocking issues before final review in ${Math.round((checkpointed.checkpointFailureRate ?? 0) * 100)}% of ${checkpointed.reviewedIssues} comparable issue(s).`
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
if (!checkpointedSamplesReady) {
|
|
1797
|
+
return {
|
|
1798
|
+
checkpointPolicy: "checkpointed",
|
|
1799
|
+
profile,
|
|
1800
|
+
basis: "heuristic",
|
|
1801
|
+
rationale: `Enabled checkpoint review because ${profile.primary} changes are high-risk enough to benefit from an intermediate gate before final review.`
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
if (checkpointedSamplesReady && finalOnlySamplesReady) {
|
|
1806
|
+
const gateLift = (checkpointed.gatePassRate ?? 0) - (finalOnly.gatePassRate ?? 0);
|
|
1807
|
+
const firstPassLift = (checkpointed.firstPassPassRate ?? 0) - (finalOnly.firstPassPassRate ?? 0);
|
|
1808
|
+
const checkpointCatchRate = checkpointed.checkpointFailureRate ?? 0;
|
|
1809
|
+
if (currentCheckpointPolicy !== "checkpointed" && (checkpointCatchRate >= 0.18 || gateLift >= 0.08 || firstPassLift >= 0.1)) {
|
|
1810
|
+
return {
|
|
1811
|
+
checkpointPolicy: "checkpointed",
|
|
1812
|
+
profile,
|
|
1813
|
+
basis: "historical",
|
|
1814
|
+
rationale: `Enabled checkpoint review for ${profile.primary}: checkpointed runs show ${Math.round(checkpointCatchRate * 100)}% checkpoint catch rate, ${Math.round(gateLift * 100)}pp final gate lift, and ${Math.round(firstPassLift * 100)}pp first-pass lift over final-only contractual runs.`
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
if (currentCheckpointPolicy === "checkpointed" && !highCheckpointRisk && checkpointCatchRate <= 0.05 && (finalOnly.gatePassRate ?? 0) >= (checkpointed.gatePassRate ?? 0) - 0.05 && (finalOnly.firstPassPassRate ?? 0) >= (checkpointed.firstPassPassRate ?? 0) - 0.05) {
|
|
1818
|
+
return {
|
|
1819
|
+
checkpointPolicy: "final_only",
|
|
1820
|
+
profile,
|
|
1821
|
+
basis: "historical",
|
|
1822
|
+
rationale: `Disabled checkpoint review for ${profile.primary}: checkpointed runs almost never catch issues before final review (${Math.round(checkpointCatchRate * 100)}%), while final-only contractual runs stay within 5pp on final gate and first-pass outcomes.`
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
if (currentCheckpointPolicy === "checkpointed" && lowScope) {
|
|
1827
|
+
return {
|
|
1828
|
+
checkpointPolicy: "final_only",
|
|
1829
|
+
profile,
|
|
1830
|
+
basis: "heuristic",
|
|
1831
|
+
rationale: `Disabled checkpoint review because ${complexity} contractual work should keep the contract gate but avoid an intermediate review pass.`
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
return null;
|
|
1835
|
+
}
|
|
1836
|
+
function recommendHarnessModeForIssue(issues, issue, currentMode, minSamples = 3) {
|
|
1837
|
+
const profile = resolveEffectiveReviewProfile(issue);
|
|
1838
|
+
const complexity = issue.plan?.estimatedComplexity ?? "medium";
|
|
1839
|
+
const stats = computeHarnessModeStats(issues, profile.primary);
|
|
1840
|
+
const negotiation = computeContractNegotiationStats(issues, profile.primary);
|
|
1841
|
+
const highRisk = HIGH_RISK_PROFILES.has(profile.primary);
|
|
1842
|
+
const lowScope = complexity === "trivial" || complexity === "low";
|
|
1843
|
+
const negotiationSamplesReady = negotiation.negotiatedIssues >= minSamples;
|
|
1844
|
+
const highRiskNegotiationPressure = negotiationSamplesReady && ((negotiation.blockingConcernRate ?? 0) >= 0.15 || (negotiation.revisionRate ?? 0) >= 0.3);
|
|
1845
|
+
const generalNegotiationPressure = negotiationSamplesReady && !lowScope && ((negotiation.blockingConcernRate ?? 0) >= 0.25 || (negotiation.revisionRate ?? 0) >= 0.4 || (negotiation.avgRoundsPerIssue ?? 0) >= 1.6);
|
|
1846
|
+
const negotiationLowValue = negotiationSamplesReady && (negotiation.firstPassApprovalRate ?? 0) >= 0.9 && (negotiation.blockingConcernRate ?? 1) <= 0.08 && (negotiation.revisionRate ?? 1) <= 0.15;
|
|
1847
|
+
if (highRisk && currentMode !== "contractual") {
|
|
1848
|
+
if (highRiskNegotiationPressure) {
|
|
1849
|
+
return {
|
|
1850
|
+
mode: "contractual",
|
|
1851
|
+
profile,
|
|
1852
|
+
basis: "historical",
|
|
1853
|
+
rationale: `Switched to contractual for ${profile.primary}: contract negotiation found blocking concerns in ${Math.round((negotiation.blockingConcernRate ?? 0) * 100)}% of ${negotiation.negotiatedIssues} comparable issue(s) and forced revisions in ${Math.round((negotiation.revisionRate ?? 0) * 100)}%.`
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
const contractual2 = stats.contractual;
|
|
1857
|
+
if (contractual2.reviewedIssues >= minSamples) {
|
|
1858
|
+
return {
|
|
1859
|
+
mode: "contractual",
|
|
1860
|
+
profile,
|
|
1861
|
+
basis: "historical",
|
|
1862
|
+
rationale: `Switched to contractual for ${profile.primary}: historical gate pass ${Math.round((contractual2.gatePassRate ?? 0) * 100)}% across ${contractual2.reviewedIssues} reviewed issue(s).`
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
return {
|
|
1866
|
+
mode: "contractual",
|
|
1867
|
+
profile,
|
|
1868
|
+
basis: "heuristic",
|
|
1869
|
+
rationale: `Switched to contractual because ${profile.primary} is a high-risk profile and needs stronger contract negotiation plus skeptical review semantics.`
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
if (profile.primary === "general-quality" && lowScope) {
|
|
1873
|
+
const solo = stats.solo;
|
|
1874
|
+
if (solo.reviewedIssues >= minSamples && (solo.gatePassRate ?? 0) >= 0.95 && (solo.reviewReworkRate ?? 1) <= 0.1) {
|
|
1875
|
+
if (currentMode !== "solo") {
|
|
1876
|
+
return {
|
|
1877
|
+
mode: "solo",
|
|
1878
|
+
profile,
|
|
1879
|
+
basis: "historical",
|
|
1880
|
+
rationale: `Downgraded to solo for low-scope general work: solo gate pass ${Math.round((solo.gatePassRate ?? 0) * 100)}% with low rework over ${solo.reviewedIssues} reviewed issue(s).`
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
return null;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
if (currentMode !== "contractual" && generalNegotiationPressure) {
|
|
1887
|
+
return {
|
|
1888
|
+
mode: "contractual",
|
|
1889
|
+
profile,
|
|
1890
|
+
basis: "historical",
|
|
1891
|
+
rationale: `Switched to contractual for ${profile.primary}: contract negotiation found blocking concerns in ${Math.round((negotiation.blockingConcernRate ?? 0) * 100)}% of ${negotiation.negotiatedIssues} comparable issue(s), with revisions required in ${Math.round((negotiation.revisionRate ?? 0) * 100)}% and ${Math.round((negotiation.avgRoundsPerIssue ?? 0) * 10) / 10} rounds per issue on average.`
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
const standard = stats.standard;
|
|
1895
|
+
const contractual = stats.contractual;
|
|
1896
|
+
const contractualSamplesReady = contractual.reviewedIssues >= minSamples;
|
|
1897
|
+
const standardSamplesReady = standard.reviewedIssues >= minSamples;
|
|
1898
|
+
if (contractualSamplesReady && standardSamplesReady) {
|
|
1899
|
+
const contractualGateLift = (contractual.gatePassRate ?? 0) - (standard.gatePassRate ?? 0);
|
|
1900
|
+
const contractualFirstPassLift = (contractual.firstPassPassRate ?? 0) - (standard.firstPassPassRate ?? 0);
|
|
1901
|
+
if (currentMode !== "contractual" && (contractualGateLift >= 0.12 || contractualFirstPassLift >= 0.15)) {
|
|
1902
|
+
return {
|
|
1903
|
+
mode: "contractual",
|
|
1904
|
+
profile,
|
|
1905
|
+
basis: "historical",
|
|
1906
|
+
rationale: `Switched to contractual for ${profile.primary}: first-pass lift ${Math.round(contractualFirstPassLift * 100)}pp and gate lift ${Math.round(contractualGateLift * 100)}pp over standard.`
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
if (currentMode === "contractual" && !highRisk && !lowScope && negotiationLowValue && (standard.gatePassRate ?? 0) >= (contractual.gatePassRate ?? 0) - 0.05 && (standard.firstPassPassRate ?? 0) >= (contractual.firstPassPassRate ?? 0) - 0.05) {
|
|
1910
|
+
return {
|
|
1911
|
+
mode: "standard",
|
|
1912
|
+
profile,
|
|
1913
|
+
basis: "historical",
|
|
1914
|
+
rationale: `Downgraded to standard for ${profile.primary}: contract negotiation approved on first pass in ${Math.round((negotiation.firstPassApprovalRate ?? 0) * 100)}% of ${negotiation.negotiatedIssues} comparable issue(s), and standard review performance stays within 5pp of contractual.`
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
if (!highRisk && currentMode === "solo" && !lowScope) {
|
|
1919
|
+
return {
|
|
1920
|
+
mode: "standard",
|
|
1921
|
+
profile,
|
|
1922
|
+
basis: "heuristic",
|
|
1923
|
+
rationale: `Upgraded from solo to standard because ${complexity} complexity should keep an automated reviewer in the loop.`
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
return null;
|
|
1927
|
+
}
|
|
1928
|
+
function computeReviewRouteStats(issues, profileName) {
|
|
1929
|
+
const buckets = {};
|
|
1930
|
+
for (const issue of issues) {
|
|
1931
|
+
const reviewRun = resolveLatestCompletedReviewRun(issue, "final");
|
|
1932
|
+
if (!reviewRun) continue;
|
|
1933
|
+
const effectiveProfile = reviewRun.reviewProfile ?? resolveEffectiveReviewProfile(issue);
|
|
1934
|
+
if (effectiveProfile.primary !== profileName) continue;
|
|
1935
|
+
const routeKey = serializeReviewRouteSnapshot(reviewRun.routing);
|
|
1936
|
+
const bucket = buckets[routeKey] ||= {
|
|
1937
|
+
reviewedIssues: 0,
|
|
1938
|
+
completedReviewedIssues: 0,
|
|
1939
|
+
gatePasses: 0,
|
|
1940
|
+
blockingFailRuns: 0,
|
|
1941
|
+
advisoryFailRuns: 0
|
|
1942
|
+
};
|
|
1943
|
+
bucket.reviewedIssues += 1;
|
|
1944
|
+
if (isCompletedIssue(issue)) bucket.completedReviewedIssues += 1;
|
|
1945
|
+
if (reviewRun.blockingVerdict === "PASS") bucket.gatePasses += 1;
|
|
1946
|
+
if (reviewRun.blockingVerdict === "FAIL") bucket.blockingFailRuns += 1;
|
|
1947
|
+
if ((reviewRun.advisoryFailedCriteriaCount ?? 0) > 0) bucket.advisoryFailRuns += 1;
|
|
1948
|
+
}
|
|
1949
|
+
return Object.fromEntries(
|
|
1950
|
+
Object.entries(buckets).map(([routeKey, bucket]) => [
|
|
1951
|
+
routeKey,
|
|
1952
|
+
{
|
|
1953
|
+
...bucket,
|
|
1954
|
+
gatePassRate: rate(bucket.gatePasses, bucket.reviewedIssues),
|
|
1955
|
+
blockingFailRate: rate(bucket.blockingFailRuns, bucket.reviewedIssues)
|
|
1956
|
+
}
|
|
1957
|
+
])
|
|
1958
|
+
);
|
|
1959
|
+
}
|
|
1960
|
+
function recommendReviewRouteForIssue(issues, issue, candidates, minSamples = 3) {
|
|
1961
|
+
if (candidates.length === 0) return null;
|
|
1962
|
+
const profile = resolveEffectiveReviewProfile(issue);
|
|
1963
|
+
const routeStats = computeReviewRouteStats(issues, profile.primary);
|
|
1964
|
+
const scored = candidates.map((candidate) => {
|
|
1965
|
+
const routeKey = buildReviewRouteKey(candidate);
|
|
1966
|
+
const stats = routeStats[routeKey];
|
|
1967
|
+
const affinity = ROUTE_AFFINITY[profile.primary][candidate.provider] ?? 0;
|
|
1968
|
+
const historicalScore = stats ? (stats.gatePassRate ?? 0) * 4 - (stats.blockingFailRate ?? 0) * 3 + Math.min(stats.reviewedIssues, 6) * 0.15 : 0;
|
|
1969
|
+
return {
|
|
1970
|
+
candidate,
|
|
1971
|
+
routeKey,
|
|
1972
|
+
stats,
|
|
1973
|
+
score: affinity + historicalScore,
|
|
1974
|
+
affinity
|
|
1975
|
+
};
|
|
1976
|
+
}).sort((left, right) => {
|
|
1977
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
1978
|
+
return left.routeKey.localeCompare(right.routeKey);
|
|
1979
|
+
});
|
|
1980
|
+
const current = scored[0];
|
|
1981
|
+
if (!current) return null;
|
|
1982
|
+
if (current.stats && current.stats.reviewedIssues >= minSamples) {
|
|
1983
|
+
return {
|
|
1984
|
+
candidate: current.candidate,
|
|
1985
|
+
profile,
|
|
1986
|
+
basis: "historical",
|
|
1987
|
+
rationale: `Adaptive reviewer route for ${profile.primary}: ${current.routeKey} has ${Math.round((current.stats.gatePassRate ?? 0) * 100)}% gate pass over ${current.stats.reviewedIssues} reviewed issue(s).`
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
return {
|
|
1991
|
+
candidate: current.candidate,
|
|
1992
|
+
profile,
|
|
1993
|
+
basis: "heuristic",
|
|
1994
|
+
rationale: `Adaptive reviewer route for ${profile.primary}: preferred ${current.candidate.provider} based on profile affinity while historical samples are still sparse.`
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1186
1997
|
|
|
1187
1998
|
// src/agents/model-discovery.ts
|
|
1188
1999
|
import { execFileSync } from "child_process";
|
|
@@ -1191,17 +2002,6 @@ import { join as join4, dirname } from "path";
|
|
|
1191
2002
|
import { homedir } from "os";
|
|
1192
2003
|
var modelCache = /* @__PURE__ */ new Map();
|
|
1193
2004
|
var MODEL_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1194
|
-
function readClaudeConfig() {
|
|
1195
|
-
try {
|
|
1196
|
-
const settingsPath = join4(homedir(), ".claude", "settings.json");
|
|
1197
|
-
if (!existsSync5(settingsPath)) return {};
|
|
1198
|
-
const raw = readFileSync2(settingsPath, "utf8");
|
|
1199
|
-
const settings = JSON.parse(raw);
|
|
1200
|
-
return { model: typeof settings.model === "string" ? settings.model : void 0 };
|
|
1201
|
-
} catch {
|
|
1202
|
-
return {};
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
2005
|
function resolveGeminiModelsFile() {
|
|
1206
2006
|
try {
|
|
1207
2007
|
const binPath = execFileSync("which", ["gemini"], { encoding: "utf8", timeout: 3e3 }).trim();
|
|
@@ -1343,6 +2143,18 @@ function normalizeAgentProvider(value) {
|
|
|
1343
2143
|
if (!normalized) return "codex";
|
|
1344
2144
|
return normalized;
|
|
1345
2145
|
}
|
|
2146
|
+
function resolveProviderCapabilities(provider, overrides) {
|
|
2147
|
+
return getProviderCapabilities(provider, overrides ?? null);
|
|
2148
|
+
}
|
|
2149
|
+
function getProviderCapabilityWarnings(provider, overrides) {
|
|
2150
|
+
return describeProviderCapabilityWarnings(provider, resolveProviderCapabilities(provider, overrides));
|
|
2151
|
+
}
|
|
2152
|
+
function resolveAgentCommand(provider, explicitCommand, codexCommand, claudeCommand, reasoningEffort) {
|
|
2153
|
+
if (explicitCommand.trim()) return explicitCommand.trim();
|
|
2154
|
+
if (provider === "claude" && claudeCommand.trim()) return claudeCommand.trim();
|
|
2155
|
+
if (provider === "codex" && codexCommand.trim()) return codexCommand.trim();
|
|
2156
|
+
return getProviderDefaultCommand(provider, reasoningEffort);
|
|
2157
|
+
}
|
|
1346
2158
|
function resolveEffort(role, issueEffort, globalEffort) {
|
|
1347
2159
|
const roleKey = role;
|
|
1348
2160
|
if (issueEffort?.[roleKey]) return issueEffort[roleKey];
|
|
@@ -1353,7 +2165,8 @@ function resolveEffort(role, issueEffort, globalEffort) {
|
|
|
1353
2165
|
function getProviderDefaultCommand(provider, reasoningEffort, model) {
|
|
1354
2166
|
const adapter = ADAPTERS[provider];
|
|
1355
2167
|
if (!adapter) return "";
|
|
1356
|
-
const
|
|
2168
|
+
const capabilities = resolveProviderCapabilities(provider);
|
|
2169
|
+
const jsonSchema = usesNativeStructuredOutput(capabilities) ? CLAUDE_RESULT_SCHEMA : void 0;
|
|
1357
2170
|
return adapter.buildCommand({ model, effort: reasoningEffort, jsonSchema });
|
|
1358
2171
|
}
|
|
1359
2172
|
var cachedProviders = null;
|
|
@@ -1365,11 +2178,12 @@ function detectAvailableProviders() {
|
|
|
1365
2178
|
}
|
|
1366
2179
|
const providers = [];
|
|
1367
2180
|
for (const name of ["claude", "codex", "gemini"]) {
|
|
2181
|
+
const capabilities = resolveProviderCapabilities(name);
|
|
1368
2182
|
try {
|
|
1369
2183
|
const path = execFileSync2("which", [name], { encoding: "utf8", timeout: 5e3 }).trim();
|
|
1370
|
-
providers.push({ name, available: true, path });
|
|
2184
|
+
providers.push({ name, available: true, path, capabilities });
|
|
1371
2185
|
} catch {
|
|
1372
|
-
providers.push({ name, available: false, path: "" });
|
|
2186
|
+
providers.push({ name, available: false, path: "", capabilities });
|
|
1373
2187
|
}
|
|
1374
2188
|
}
|
|
1375
2189
|
cachedProviders = providers;
|
|
@@ -1402,23 +2216,133 @@ function readGeminiConfig() {
|
|
|
1402
2216
|
return {};
|
|
1403
2217
|
}
|
|
1404
2218
|
}
|
|
2219
|
+
function readClaudeConfig() {
|
|
2220
|
+
try {
|
|
2221
|
+
const settingsPath = join5(homedir2(), ".claude", "settings.json");
|
|
2222
|
+
if (!existsSync6(settingsPath)) return {};
|
|
2223
|
+
const raw = readFileSync3(settingsPath, "utf8");
|
|
2224
|
+
const settings = JSON.parse(raw);
|
|
2225
|
+
return {
|
|
2226
|
+
model: typeof settings.model === "string" ? settings.model : void 0
|
|
2227
|
+
};
|
|
2228
|
+
} catch {
|
|
2229
|
+
return {};
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
1405
2232
|
function resolveDefaultProvider(detected) {
|
|
1406
2233
|
const available = detected.filter((p) => p.available);
|
|
1407
2234
|
if (available.length === 0) return "";
|
|
1408
2235
|
if (available.some((p) => p.name === "codex")) return "codex";
|
|
1409
2236
|
return available[0].name;
|
|
1410
2237
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
]
|
|
2238
|
+
var EFFORT_ORDER = ["low", "medium", "high", "extra-high"];
|
|
2239
|
+
var REVIEW_PROFILE_MIN_EFFORT = {
|
|
2240
|
+
"general-quality": "medium",
|
|
2241
|
+
"ui-polish": "high",
|
|
2242
|
+
"workflow-fsm": "high",
|
|
2243
|
+
"integration-safety": "high",
|
|
2244
|
+
"api-contract": "high",
|
|
2245
|
+
"security-hardening": "extra-high"
|
|
2246
|
+
};
|
|
2247
|
+
var REVIEW_PROFILE_OVERLAYS = {
|
|
2248
|
+
"general-quality": [],
|
|
2249
|
+
"ui-polish": ["impeccable", "frontend-design"],
|
|
2250
|
+
"workflow-fsm": ["workflow-audit"],
|
|
2251
|
+
"integration-safety": ["integration-safety"],
|
|
2252
|
+
"api-contract": ["api-contract"],
|
|
2253
|
+
"security-hardening": ["security-hardening"]
|
|
2254
|
+
};
|
|
2255
|
+
function maxEffort(left, right) {
|
|
2256
|
+
if (!left) return right;
|
|
2257
|
+
if (!right) return left;
|
|
2258
|
+
return EFFORT_ORDER[Math.max(EFFORT_ORDER.indexOf(left), EFFORT_ORDER.indexOf(right))] ?? left;
|
|
2259
|
+
}
|
|
2260
|
+
function stageToRole(stage) {
|
|
2261
|
+
if (stage === "plan") return "planner";
|
|
2262
|
+
if (stage === "review") return "reviewer";
|
|
2263
|
+
return "executor";
|
|
2264
|
+
}
|
|
2265
|
+
function buildStageProvider(state, issue, stage, workflowConfig) {
|
|
2266
|
+
const role = stageToRole(stage);
|
|
2267
|
+
const stageConfig = workflowConfig?.[roleToStageKey(stageToRole(stage))];
|
|
2268
|
+
const effort = stageConfig?.effort || resolveEffort(role, issue.effort, state.config.defaultEffort);
|
|
2269
|
+
const providerName = stageConfig?.provider || state.config.agentProvider;
|
|
2270
|
+
const model = stageConfig?.model || void 0;
|
|
2271
|
+
const command = stageConfig ? getProviderDefaultCommand(providerName, effort, model) : stage === "execute" ? resolveAgentCommand(providerName, state.config.agentCommand, "", "", effort) : getProviderDefaultCommand(providerName, effort, model);
|
|
2272
|
+
return {
|
|
2273
|
+
provider: providerName,
|
|
2274
|
+
role,
|
|
2275
|
+
command,
|
|
2276
|
+
model,
|
|
2277
|
+
profile: "",
|
|
2278
|
+
profilePath: "",
|
|
2279
|
+
profileInstructions: "",
|
|
2280
|
+
reasoningEffort: effort,
|
|
2281
|
+
selectionReason: stageConfig ? `Using workflow ${stage} stage configuration.` : `Using default ${stage} stage provider configuration.`,
|
|
2282
|
+
overlays: [],
|
|
2283
|
+
capabilities: resolveProviderCapabilities(providerName)
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
function specializeReviewerProvider(baseProvider, issue) {
|
|
2287
|
+
const reviewProfile = issue.reviewProfile ?? deriveReviewProfile(issue);
|
|
2288
|
+
const minEffort = REVIEW_PROFILE_MIN_EFFORT[reviewProfile.primary];
|
|
2289
|
+
const reasoningEffort = maxEffort(baseProvider.reasoningEffort, minEffort);
|
|
2290
|
+
const overlays = [.../* @__PURE__ */ new Set([...baseProvider.overlays ?? [], ...REVIEW_PROFILE_OVERLAYS[reviewProfile.primary]])];
|
|
2291
|
+
const command = getProviderDefaultCommand(baseProvider.provider, reasoningEffort, baseProvider.model) || baseProvider.command;
|
|
2292
|
+
return {
|
|
2293
|
+
...baseProvider,
|
|
2294
|
+
command,
|
|
2295
|
+
reasoningEffort,
|
|
2296
|
+
overlays,
|
|
2297
|
+
selectionReason: `Reviewer specialized for ${reviewProfile.primary}; raised scrutiny with ${reasoningEffort ?? "default"} effort.`
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
function resolveSynchronousProviderModel(provider, workflowConfig) {
|
|
2301
|
+
const stages = workflowConfig ? [workflowConfig.review, workflowConfig.execute, workflowConfig.plan] : [];
|
|
2302
|
+
const fromWorkflow = stages.find((stage) => stage?.provider === provider)?.model;
|
|
2303
|
+
if (fromWorkflow) return fromWorkflow;
|
|
2304
|
+
if (provider === "codex") return readCodexConfig().model;
|
|
2305
|
+
if (provider === "gemini") return readGeminiConfig().model;
|
|
2306
|
+
if (provider === "claude") return readClaudeConfig().model;
|
|
2307
|
+
return void 0;
|
|
2308
|
+
}
|
|
2309
|
+
function buildAdaptiveReviewCandidates(baseProvider, workflowConfig) {
|
|
2310
|
+
const availableProviders = detectAvailableProviders().filter((provider) => provider.available).map((provider) => provider.name);
|
|
2311
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
2312
|
+
const addCandidate = (providerName, reason) => {
|
|
2313
|
+
const model = resolveSynchronousProviderModel(providerName, workflowConfig ?? null);
|
|
2314
|
+
const command = getProviderDefaultCommand(providerName, baseProvider.reasoningEffort, model) || baseProvider.command;
|
|
2315
|
+
const candidate = {
|
|
2316
|
+
...baseProvider,
|
|
2317
|
+
provider: providerName,
|
|
2318
|
+
model,
|
|
2319
|
+
command,
|
|
2320
|
+
selectionReason: reason,
|
|
2321
|
+
capabilities: resolveProviderCapabilities(providerName)
|
|
2322
|
+
};
|
|
2323
|
+
candidates.set(buildReviewRouteKey(candidate), candidate);
|
|
2324
|
+
};
|
|
2325
|
+
addCandidate(baseProvider.provider, baseProvider.selectionReason || "Configured review route.");
|
|
2326
|
+
for (const providerName of availableProviders) {
|
|
2327
|
+
if (providerName === baseProvider.provider) continue;
|
|
2328
|
+
addCandidate(providerName, `Adaptive routing candidate using available ${providerName} reviewer.`);
|
|
2329
|
+
}
|
|
2330
|
+
return [...candidates.values()];
|
|
2331
|
+
}
|
|
2332
|
+
function adaptReviewerProvider(state, issue, baseProvider, workflowConfig) {
|
|
2333
|
+
if (state.config.adaptiveReviewRouting === false) return baseProvider;
|
|
2334
|
+
const candidates = buildAdaptiveReviewCandidates(baseProvider, workflowConfig ?? null);
|
|
2335
|
+
const recommendation = recommendReviewRouteForIssue(
|
|
2336
|
+
state.issues,
|
|
2337
|
+
issue,
|
|
2338
|
+
candidates,
|
|
2339
|
+
state.config.adaptivePolicyMinSamples ?? 3
|
|
2340
|
+
);
|
|
2341
|
+
if (!recommendation) return baseProvider;
|
|
2342
|
+
return {
|
|
2343
|
+
...recommendation.candidate,
|
|
2344
|
+
selectionReason: `${recommendation.rationale} ${baseProvider.selectionReason ?? ""}`.trim()
|
|
2345
|
+
};
|
|
1422
2346
|
}
|
|
1423
2347
|
function roleToStageKey(role) {
|
|
1424
2348
|
switch (role) {
|
|
@@ -1430,1020 +2354,45 @@ function roleToStageKey(role) {
|
|
|
1430
2354
|
return "review";
|
|
1431
2355
|
}
|
|
1432
2356
|
}
|
|
1433
|
-
function
|
|
1434
|
-
|
|
1435
|
-
return providers.map((provider) => {
|
|
1436
|
-
const stageKey = roleToStageKey(provider.role);
|
|
1437
|
-
const stageConfig = workflowConfig[stageKey];
|
|
1438
|
-
if (!stageConfig) return provider;
|
|
1439
|
-
const newProvider = stageConfig.provider || provider.provider;
|
|
1440
|
-
const newModel = stageConfig.model || void 0;
|
|
1441
|
-
const newEffort = stageConfig.effort || provider.reasoningEffort;
|
|
1442
|
-
const command = getProviderDefaultCommand(newProvider, newEffort, newModel);
|
|
1443
|
-
return {
|
|
1444
|
-
...provider,
|
|
1445
|
-
provider: newProvider,
|
|
1446
|
-
model: newModel,
|
|
1447
|
-
command: command || provider.command,
|
|
1448
|
-
reasoningEffort: newEffort
|
|
1449
|
-
};
|
|
1450
|
-
});
|
|
2357
|
+
function getExecutionProviders(state, issue, workflowConfig) {
|
|
2358
|
+
return [buildStageProvider(state, issue, "execute", workflowConfig ?? null)];
|
|
1451
2359
|
}
|
|
1452
|
-
function
|
|
1453
|
-
const
|
|
1454
|
-
|
|
1455
|
-
const effort = resolveEffort(provider.role, issue.effort, state.config.defaultEffort);
|
|
1456
|
-
return {
|
|
1457
|
-
...provider,
|
|
1458
|
-
reasoningEffort: effort
|
|
1459
|
-
};
|
|
1460
|
-
});
|
|
1461
|
-
return applyWorkflowConfigToProviders(providers, workflowConfig ?? null);
|
|
2360
|
+
function getReviewProvider(state, issue, workflowConfig) {
|
|
2361
|
+
const specialized = specializeReviewerProvider(buildStageProvider(state, issue, "review", workflowConfig ?? null), issue);
|
|
2362
|
+
return adaptReviewerProvider(state, issue, specialized, workflowConfig ?? null);
|
|
1462
2363
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
maxConcurrentByState: {},
|
|
1469
|
-
commandTimeoutMs: 18e5,
|
|
1470
|
-
maxAttemptsDefault: 1,
|
|
1471
|
-
maxTurns: 1,
|
|
1472
|
-
retryDelayMs: 0,
|
|
1473
|
-
staleInProgressTimeoutMs: 0,
|
|
1474
|
-
logLinesTail: 12e3,
|
|
1475
|
-
maxPreviousOutputChars: 12e3,
|
|
1476
|
-
agentProvider: "codex",
|
|
1477
|
-
agentCommand: "",
|
|
1478
|
-
defaultEffort: { default: "medium" },
|
|
1479
|
-
runMode: "filesystem",
|
|
1480
|
-
autoReviewApproval: true,
|
|
1481
|
-
afterCreateHook: "",
|
|
1482
|
-
beforeRunHook: "",
|
|
1483
|
-
afterRunHook: "",
|
|
1484
|
-
beforeRemoveHook: ""
|
|
1485
|
-
};
|
|
1486
|
-
async function runCommandWithTimeout(command, workspacePath, issue, config, promptText, promptFile, extraEnv = {}, outputFile) {
|
|
1487
|
-
return new Promise((resolve2) => {
|
|
1488
|
-
const started = Date.now();
|
|
1489
|
-
const resultFile = extraEnv.FIFONY_RESULT_FILE;
|
|
1490
|
-
if (resultFile && extraEnv.FIFONY_PRESERVE_RESULT_FILE !== "1") {
|
|
1491
|
-
rmSync(resultFile, { force: true });
|
|
1492
|
-
}
|
|
1493
|
-
const allVars = {
|
|
1494
|
-
FIFONY_ISSUE_ID: issue.id,
|
|
1495
|
-
FIFONY_ISSUE_IDENTIFIER: issue.identifier,
|
|
1496
|
-
FIFONY_ISSUE_TITLE: issue.title,
|
|
1497
|
-
FIFONY_WORKSPACE_PATH: issue.worktreePath ?? workspacePath,
|
|
1498
|
-
FIFONY_PROMPT_FILE: promptFile
|
|
1499
|
-
};
|
|
1500
|
-
for (const [key, value] of Object.entries(extraEnv)) {
|
|
1501
|
-
if (value.length > 4e3) {
|
|
1502
|
-
const valFile = join6(workspacePath, `${key.toLowerCase()}.txt`);
|
|
1503
|
-
writeFileSync(valFile, value, "utf8");
|
|
1504
|
-
allVars[`${key}_FILE`] = valFile;
|
|
1505
|
-
} else {
|
|
1506
|
-
allVars[key] = value;
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
const envFilePath = join6(workspacePath, ".env.sh");
|
|
1510
|
-
const envFileLines = Object.entries(allVars).map(([k, v]) => `export ${k}='${String(v).replace(/'/g, "'\\''")}'`).join("\n");
|
|
1511
|
-
writeFileSync(envFilePath, envFileLines, "utf8");
|
|
1512
|
-
const wrappedCommand = `. "${envFilePath}" && ${command}`;
|
|
1513
|
-
const child = spawn(wrappedCommand, {
|
|
1514
|
-
shell: true,
|
|
1515
|
-
cwd: issue.worktreePath ?? workspacePath,
|
|
1516
|
-
detached: true,
|
|
1517
|
-
// Survive parent death
|
|
1518
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1519
|
-
});
|
|
1520
|
-
child.unref();
|
|
1521
|
-
if (child.stdin) {
|
|
1522
|
-
child.stdin.end();
|
|
1523
|
-
}
|
|
1524
|
-
const pidFile = join6(workspacePath, "agent.pid");
|
|
1525
|
-
const pid = child.pid;
|
|
1526
|
-
if (pid) {
|
|
1527
|
-
logger.debug({ issueId: issue.id, pid, command: command.slice(0, 120), cwd: workspacePath }, "[Agent] Process spawned");
|
|
1528
|
-
writeFileSync(pidFile, JSON.stringify({
|
|
1529
|
-
pid,
|
|
1530
|
-
issueId: issue.id,
|
|
1531
|
-
startedAt: new Date(started).toISOString(),
|
|
1532
|
-
command: command.slice(0, 200)
|
|
1533
|
-
}), "utf8");
|
|
1534
|
-
}
|
|
1535
|
-
let output = "";
|
|
1536
|
-
let timedOut = false;
|
|
1537
|
-
let outputBytes = 0;
|
|
1538
|
-
let outputHeader = "";
|
|
1539
|
-
const liveLogFile = join6(workspacePath, "live-output.log");
|
|
1540
|
-
writeFileSync(liveLogFile, "", "utf8");
|
|
1541
|
-
if (outputFile) {
|
|
1542
|
-
try {
|
|
1543
|
-
const header = `# fifony stdout capture
|
|
1544
|
-
# turn: ${extraEnv.FIFONY_TURN_INDEX ?? "?"}
|
|
1545
|
-
# provider: ${extraEnv.FIFONY_AGENT_PROVIDER ?? "?"}
|
|
1546
|
-
# role: ${extraEnv.FIFONY_AGENT_ROLE ?? "?"}
|
|
1547
|
-
# timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1548
|
-
---
|
|
1549
|
-
`;
|
|
1550
|
-
writeFileSync(outputFile, header, "utf8");
|
|
1551
|
-
} catch {
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
const onChunk = (chunk) => {
|
|
1555
|
-
const text = String(chunk);
|
|
1556
|
-
if (outputHeader.length < 2e3) outputHeader = (outputHeader + text).slice(0, 2e3);
|
|
1557
|
-
output = appendFileTail(output, text, config.logLinesTail);
|
|
1558
|
-
outputBytes += text.length;
|
|
1559
|
-
try {
|
|
1560
|
-
appendFileSync(liveLogFile, text);
|
|
1561
|
-
} catch {
|
|
1562
|
-
}
|
|
1563
|
-
if (outputFile) {
|
|
1564
|
-
try {
|
|
1565
|
-
appendFileSync(outputFile, text);
|
|
1566
|
-
} catch {
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
issue.commandOutputTail = output;
|
|
1570
|
-
};
|
|
1571
|
-
child.stdout?.on("data", onChunk);
|
|
1572
|
-
child.stderr?.on("data", onChunk);
|
|
1573
|
-
const AGENT_STALE_OUTPUT_MS = 18e5;
|
|
1574
|
-
const timer = setTimeout(() => {
|
|
1575
|
-
timedOut = true;
|
|
1576
|
-
if (pid) {
|
|
1577
|
-
try {
|
|
1578
|
-
process.kill(-pid, "SIGTERM");
|
|
1579
|
-
} catch {
|
|
1580
|
-
}
|
|
1581
|
-
} else {
|
|
1582
|
-
child.kill("SIGTERM");
|
|
1583
|
-
}
|
|
1584
|
-
}, config.commandTimeoutMs);
|
|
1585
|
-
let lastWatchdogBytes = 0;
|
|
1586
|
-
let lastOutputGrowthAt = Date.now();
|
|
1587
|
-
let watchdogKilled = false;
|
|
1588
|
-
const watchdog = setInterval(() => {
|
|
1589
|
-
if (pid) {
|
|
1590
|
-
try {
|
|
1591
|
-
process.kill(pid, 0);
|
|
1592
|
-
} catch {
|
|
1593
|
-
clearInterval(watchdog);
|
|
1594
|
-
clearTimeout(timer);
|
|
1595
|
-
watchdogKilled = true;
|
|
1596
|
-
try {
|
|
1597
|
-
rmSync(pidFile, { force: true });
|
|
1598
|
-
} catch {
|
|
1599
|
-
}
|
|
1600
|
-
resolve2({ success: false, code: null, output: appendFileTail(output, `
|
|
1601
|
-
Agent process died unexpectedly (PID ${pid}).`, config.logLinesTail) });
|
|
1602
|
-
return;
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
if (outputBytes > lastWatchdogBytes) {
|
|
1606
|
-
lastWatchdogBytes = outputBytes;
|
|
1607
|
-
lastOutputGrowthAt = Date.now();
|
|
1608
|
-
} else if (Date.now() - lastOutputGrowthAt > AGENT_STALE_OUTPUT_MS) {
|
|
1609
|
-
clearInterval(watchdog);
|
|
1610
|
-
clearTimeout(timer);
|
|
1611
|
-
timedOut = true;
|
|
1612
|
-
watchdogKilled = true;
|
|
1613
|
-
if (pid) {
|
|
1614
|
-
try {
|
|
1615
|
-
process.kill(-pid, "SIGTERM");
|
|
1616
|
-
} catch {
|
|
1617
|
-
}
|
|
1618
|
-
} else {
|
|
1619
|
-
child.kill("SIGTERM");
|
|
1620
|
-
}
|
|
1621
|
-
try {
|
|
1622
|
-
rmSync(pidFile, { force: true });
|
|
1623
|
-
} catch {
|
|
1624
|
-
}
|
|
1625
|
-
resolve2({ success: false, code: null, output: appendFileTail(output, `
|
|
1626
|
-
Agent process stuck \u2014 no output for ${Math.round(AGENT_STALE_OUTPUT_MS / 6e4)} minutes.`, config.logLinesTail) });
|
|
1627
|
-
}
|
|
1628
|
-
}, 3e4);
|
|
1629
|
-
const cleanup = () => {
|
|
1630
|
-
clearInterval(watchdog);
|
|
1631
|
-
try {
|
|
1632
|
-
rmSync(pidFile, { force: true });
|
|
1633
|
-
} catch {
|
|
1634
|
-
}
|
|
1635
|
-
};
|
|
1636
|
-
child.on("error", () => {
|
|
1637
|
-
clearTimeout(timer);
|
|
1638
|
-
cleanup();
|
|
1639
|
-
if (watchdogKilled) return;
|
|
1640
|
-
resolve2({ success: false, code: null, output: `Command execution failed for issue ${issue.id}.` });
|
|
1641
|
-
});
|
|
1642
|
-
child.on("close", (code) => {
|
|
1643
|
-
clearTimeout(timer);
|
|
1644
|
-
cleanup();
|
|
1645
|
-
if (watchdogKilled) return;
|
|
1646
|
-
const buildOutput = (suffix) => {
|
|
1647
|
-
const tail = appendFileTail(output, suffix, config.logLinesTail);
|
|
1648
|
-
return outputHeader.length > 0 && !tail.startsWith(outputHeader.slice(0, 80)) ? `${outputHeader}
|
|
1649
|
-
${tail}` : tail;
|
|
1650
|
-
};
|
|
1651
|
-
if (timedOut) {
|
|
1652
|
-
resolve2({ success: false, code: null, output: buildOutput(`
|
|
1653
|
-
Execution timeout after ${config.commandTimeoutMs}ms.`) });
|
|
1654
|
-
return;
|
|
1655
|
-
}
|
|
1656
|
-
const duration = Math.max(0, Date.now() - started);
|
|
1657
|
-
if (code === 0) {
|
|
1658
|
-
resolve2({ success: true, code, output: buildOutput(`
|
|
1659
|
-
Execution succeeded in ${duration}ms.`) });
|
|
1660
|
-
return;
|
|
1661
|
-
}
|
|
1662
|
-
resolve2({ success: false, code, output: buildOutput(`
|
|
1663
|
-
Command exit code ${code ?? "unknown"} after ${duration}ms.`) });
|
|
1664
|
-
});
|
|
1665
|
-
});
|
|
1666
|
-
}
|
|
1667
|
-
async function runHook(command, workspacePath, issue, hookName, extraEnv = {}) {
|
|
1668
|
-
if (!command.trim()) return;
|
|
1669
|
-
const result = await runCommandWithTimeout(command, workspacePath, issue, {
|
|
1670
|
-
...HOOK_RUNTIME_CONFIG,
|
|
1671
|
-
agentProvider: normalizeAgentProvider(env2.FIFONY_AGENT_PROVIDER ?? "codex"),
|
|
1672
|
-
agentCommand: command
|
|
1673
|
-
}, "", "", { FIFONY_HOOK_NAME: hookName, ...extraEnv });
|
|
1674
|
-
if (!result.success) {
|
|
1675
|
-
throw new Error(`${hookName} hook failed: ${result.output}`);
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
// src/agents/prompt-builder.ts
|
|
1680
|
-
function buildRetryContext(issue) {
|
|
1681
|
-
const summaries = issue.previousAttemptSummaries;
|
|
1682
|
-
if (!summaries || summaries.length === 0) return "";
|
|
1683
|
-
const lines = ["## Previous Attempts\n"];
|
|
1684
|
-
lines.push("The following previous attempts FAILED. Do NOT repeat the same approach. Try a fundamentally different strategy.\n");
|
|
1685
|
-
for (let i = 0; i < summaries.length; i++) {
|
|
1686
|
-
const s = summaries[i];
|
|
1687
|
-
const phaseLabel = s.phase === "review" ? "review" : s.phase === "crash" ? "crash" : s.phase === "plan" ? "plan" : "execution";
|
|
1688
|
-
lines.push(`### Attempt ${i + 1} \u2014 ${phaseLabel} failure (plan v${s.planVersion}, exec #${s.executeAttempt})`);
|
|
1689
|
-
if (s.phase === "review") {
|
|
1690
|
-
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.*");
|
|
1691
|
-
} else if (s.phase === "crash") {
|
|
1692
|
-
lines.push("*The agent process crashed or timed out. Simplify the approach \u2014 break the work into smaller steps.*");
|
|
1693
|
-
}
|
|
1694
|
-
if (s.insight) {
|
|
1695
|
-
lines.push(`**Failure type:** ${s.insight.errorType}`);
|
|
1696
|
-
lines.push(`**Root cause:** ${s.insight.rootCause}`);
|
|
1697
|
-
if (s.insight.failedCommand) lines.push(`**Failed command:** \`${s.insight.failedCommand}\``);
|
|
1698
|
-
if (s.insight.filesInvolved.length > 0) {
|
|
1699
|
-
lines.push(`**Files involved:** ${s.insight.filesInvolved.map((f) => `\`${f}\``).join(", ")}`);
|
|
1700
|
-
}
|
|
1701
|
-
lines.push(`**What to do differently:** ${s.insight.suggestion}`);
|
|
1702
|
-
} else {
|
|
1703
|
-
lines.push(`**Error:** ${s.error}`);
|
|
1704
|
-
}
|
|
1705
|
-
if (s.outputTail) {
|
|
1706
|
-
lines.push(`
|
|
1707
|
-
<details><summary>Output tail</summary>
|
|
1708
|
-
|
|
1709
|
-
\`\`\`
|
|
1710
|
-
${s.outputTail}
|
|
1711
|
-
\`\`\`
|
|
1712
|
-
</details>`);
|
|
1713
|
-
}
|
|
1714
|
-
if (s.outputFile) {
|
|
1715
|
-
lines.push(`*Full output saved in: outputs/${s.outputFile}*`);
|
|
1716
|
-
}
|
|
1717
|
-
lines.push("");
|
|
1718
|
-
}
|
|
1719
|
-
const full = lines.join("\n");
|
|
1720
|
-
return full.length > 8e3 ? full.slice(0, 8e3) + "\n[...truncated]" : full;
|
|
1721
|
-
}
|
|
1722
|
-
async function buildPrompt(issue, _workflowDefinition) {
|
|
1723
|
-
const rendered = await renderPrompt("workflow-default", { issue, attempt: issue.attempts || 0 });
|
|
1724
|
-
if (!issue.plan?.steps?.length) {
|
|
1725
|
-
return rendered;
|
|
1726
|
-
}
|
|
1727
|
-
const planSection = await renderPrompt("workflow-plan-section", {
|
|
1728
|
-
estimatedComplexity: issue.plan.estimatedComplexity,
|
|
1729
|
-
summary: issue.plan.summary,
|
|
1730
|
-
steps: issue.plan.steps.map((step) => ({
|
|
1731
|
-
step: step.step,
|
|
1732
|
-
action: step.action,
|
|
1733
|
-
files: step.files ?? [],
|
|
1734
|
-
details: step.details ?? ""
|
|
1735
|
-
}))
|
|
1736
|
-
});
|
|
1737
|
-
return `${rendered}
|
|
1738
|
-
|
|
1739
|
-
${planSection}`;
|
|
1740
|
-
}
|
|
1741
|
-
async function buildTurnPrompt(issue, basePrompt, previousOutput, turnIndex, maxTurns, nextPrompt) {
|
|
1742
|
-
if (turnIndex === 1) return basePrompt;
|
|
1743
|
-
return renderPrompt("agent-turn", {
|
|
1744
|
-
issueIdentifier: issue.identifier,
|
|
1745
|
-
turnIndex,
|
|
1746
|
-
maxTurns,
|
|
1747
|
-
basePrompt,
|
|
1748
|
-
continuation: nextPrompt.trim() || "Continue the work, inspect the workspace, and move the issue toward completion.",
|
|
1749
|
-
outputTail: previousOutput.trim() || "No previous output captured."
|
|
1750
|
-
});
|
|
1751
|
-
}
|
|
1752
|
-
async function buildProviderBasePrompt(provider, issue, basePrompt, workspacePath, skillContext, capabilitiesManifest) {
|
|
1753
|
-
return renderPrompt("agent-provider-base", {
|
|
1754
|
-
isPlanner: provider.role === "planner",
|
|
1755
|
-
isReviewer: provider.role === "reviewer",
|
|
1756
|
-
hasImpeccableOverlay: provider.overlays?.includes("impeccable") ?? false,
|
|
1757
|
-
hasFrontendDesignOverlay: provider.overlays?.includes("frontend-design") ?? false,
|
|
1758
|
-
profileInstructions: provider.profileInstructions || "",
|
|
1759
|
-
skillContext,
|
|
1760
|
-
capabilitiesManifest: capabilitiesManifest || "",
|
|
1761
|
-
capabilityCategory: "",
|
|
1762
|
-
selectionReason: provider.selectionReason ?? "",
|
|
1763
|
-
overlays: provider.overlays ?? [],
|
|
1764
|
-
targetPaths: issue.paths ?? [],
|
|
1765
|
-
workspacePath,
|
|
1766
|
-
basePrompt
|
|
1767
|
-
});
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
// src/domains/workspace.ts
|
|
1771
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1772
|
-
".git",
|
|
1773
|
-
".fifony",
|
|
1774
|
-
"node_modules",
|
|
1775
|
-
".venv",
|
|
1776
|
-
"data",
|
|
1777
|
-
"dist",
|
|
1778
|
-
"build",
|
|
1779
|
-
".turbo",
|
|
1780
|
-
".next",
|
|
1781
|
-
".nuxt",
|
|
1782
|
-
".tanstack",
|
|
1783
|
-
"coverage",
|
|
1784
|
-
"artifacts",
|
|
1785
|
-
"captures",
|
|
1786
|
-
"tmp",
|
|
1787
|
-
"temp"
|
|
1788
|
-
]);
|
|
1789
|
-
function shouldSkipPath(relativePath) {
|
|
1790
|
-
const parts = relativePath.split("/");
|
|
1791
|
-
if (parts.some((segment) => SKIP_DIRS.has(segment))) return true;
|
|
1792
|
-
const base = parts.at(-1) ?? "";
|
|
1793
|
-
if (base.startsWith("map_scan_") && extname2(base) === ".json") return true;
|
|
1794
|
-
if (extname2(base) === ".xlsx") return true;
|
|
1795
|
-
return false;
|
|
1796
|
-
}
|
|
1797
|
-
function bootstrapSource() {
|
|
1798
|
-
if (existsSync7(SOURCE_MARKER)) return;
|
|
1799
|
-
logger.info("Creating local source snapshot for Fifony (local-only runtime)...");
|
|
1800
|
-
const copyRecursive = (source, target, rel = "") => {
|
|
1801
|
-
mkdirSync(target, { recursive: true });
|
|
1802
|
-
const items = readdirSync(source, { withFileTypes: true });
|
|
1803
|
-
for (const item of items) {
|
|
1804
|
-
const nextRel = rel ? `${rel}/${item.name}` : item.name;
|
|
1805
|
-
if (shouldSkipPath(nextRel)) continue;
|
|
1806
|
-
const sourcePath = `${source}/${item.name}`;
|
|
1807
|
-
const targetPath = `${target}/${item.name}`;
|
|
1808
|
-
const itemStat = statSync(sourcePath);
|
|
1809
|
-
if (item.isDirectory()) {
|
|
1810
|
-
copyRecursive(sourcePath, targetPath, nextRel);
|
|
1811
|
-
continue;
|
|
1812
|
-
}
|
|
1813
|
-
if (item.isSymbolicLink() || itemStat.isSymbolicLink()) continue;
|
|
1814
|
-
if (itemStat.isFile() || itemStat.isFIFO()) {
|
|
1815
|
-
try {
|
|
1816
|
-
const file = readFileSync4(sourcePath);
|
|
1817
|
-
writeFileSync2(targetPath, file);
|
|
1818
|
-
} catch (error) {
|
|
1819
|
-
if (error.code === "ENOENT") {
|
|
1820
|
-
logger.debug(`Skipped missing source file: ${sourcePath}`);
|
|
1821
|
-
} else {
|
|
1822
|
-
throw error;
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
};
|
|
1828
|
-
mkdirSync(SOURCE_ROOT, { recursive: true });
|
|
1829
|
-
copyRecursive(TARGET_ROOT, SOURCE_ROOT);
|
|
1830
|
-
writeFileSync2(SOURCE_MARKER, `${now()}
|
|
1831
|
-
`, "utf8");
|
|
1832
|
-
}
|
|
1833
|
-
var sourceReadyPromise = null;
|
|
1834
|
-
var skipSourceFlag = false;
|
|
1835
|
-
function setSkipSource(skip) {
|
|
1836
|
-
skipSourceFlag = skip;
|
|
1837
|
-
}
|
|
1838
|
-
async function ensureSourceReady(onProgress) {
|
|
1839
|
-
if (skipSourceFlag) {
|
|
1840
|
-
onProgress?.("ready");
|
|
1841
|
-
return;
|
|
1842
|
-
}
|
|
1843
|
-
if (existsSync7(SOURCE_MARKER)) {
|
|
1844
|
-
onProgress?.("ready");
|
|
1845
|
-
return;
|
|
1846
|
-
}
|
|
1847
|
-
if (sourceReadyPromise) return sourceReadyPromise;
|
|
1848
|
-
sourceReadyPromise = (async () => {
|
|
1849
|
-
onProgress?.("copying");
|
|
1850
|
-
logger.info("Creating local source snapshot (async) for Fifony...");
|
|
1851
|
-
const copyRecursiveAsync = async (source, target, rel = "") => {
|
|
1852
|
-
await mkdir(target, { recursive: true });
|
|
1853
|
-
const items = await readdir(source, { withFileTypes: true });
|
|
1854
|
-
for (const item of items) {
|
|
1855
|
-
const nextRel = rel ? `${rel}/${item.name}` : item.name;
|
|
1856
|
-
if (shouldSkipPath(nextRel)) continue;
|
|
1857
|
-
const sourcePath = `${source}/${item.name}`;
|
|
1858
|
-
const targetPath = `${target}/${item.name}`;
|
|
1859
|
-
const itemStat = await stat(sourcePath);
|
|
1860
|
-
if (item.isDirectory()) {
|
|
1861
|
-
await copyRecursiveAsync(sourcePath, targetPath, nextRel);
|
|
1862
|
-
continue;
|
|
1863
|
-
}
|
|
1864
|
-
if (item.isSymbolicLink() || itemStat.isSymbolicLink()) continue;
|
|
1865
|
-
if (itemStat.isFile() || itemStat.isFIFO()) {
|
|
1866
|
-
try {
|
|
1867
|
-
await copyFile(sourcePath, targetPath);
|
|
1868
|
-
} catch (error) {
|
|
1869
|
-
if (error.code === "ENOENT") {
|
|
1870
|
-
logger.debug(`Skipped missing source file: ${sourcePath}`);
|
|
1871
|
-
} else {
|
|
1872
|
-
throw error;
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
};
|
|
1878
|
-
await mkdir(SOURCE_ROOT, { recursive: true });
|
|
1879
|
-
await copyRecursiveAsync(TARGET_ROOT, SOURCE_ROOT);
|
|
1880
|
-
await writeFile(SOURCE_MARKER, `${now()}
|
|
1881
|
-
`, "utf8");
|
|
1882
|
-
onProgress?.("ready");
|
|
1883
|
-
logger.info("Source snapshot ready (async).");
|
|
1884
|
-
})();
|
|
1885
|
-
return sourceReadyPromise;
|
|
1886
|
-
}
|
|
1887
|
-
function getGitRepoStatus(dir) {
|
|
1888
|
-
const isGit = (() => {
|
|
1889
|
-
try {
|
|
1890
|
-
execSync("git rev-parse --git-dir", { cwd: dir, stdio: "pipe" });
|
|
1891
|
-
return true;
|
|
1892
|
-
} catch {
|
|
1893
|
-
return false;
|
|
1894
|
-
}
|
|
1895
|
-
})();
|
|
1896
|
-
if (!isGit) {
|
|
1897
|
-
return { isGit: false, hasCommits: false, branch: null };
|
|
1898
|
-
}
|
|
1899
|
-
const branch = (() => {
|
|
1900
|
-
try {
|
|
1901
|
-
return execSync("git symbolic-ref --short HEAD", { cwd: dir, encoding: "utf8", stdio: "pipe" }).trim() || null;
|
|
1902
|
-
} catch {
|
|
1903
|
-
try {
|
|
1904
|
-
return execSync("git rev-parse --abbrev-ref HEAD", { cwd: dir, encoding: "utf8", stdio: "pipe" }).trim() || null;
|
|
1905
|
-
} catch {
|
|
1906
|
-
return null;
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
})();
|
|
1910
|
-
const hasCommits = (() => {
|
|
1911
|
-
try {
|
|
1912
|
-
execSync("git rev-parse --verify HEAD", { cwd: dir, stdio: "pipe" });
|
|
1913
|
-
return true;
|
|
1914
|
-
} catch {
|
|
1915
|
-
return false;
|
|
1916
|
-
}
|
|
1917
|
-
})();
|
|
1918
|
-
let isClean = true;
|
|
1919
|
-
let untrackedCount = 0;
|
|
1920
|
-
if (hasCommits) {
|
|
1921
|
-
try {
|
|
1922
|
-
const porcelain = execSync("git status --porcelain", { cwd: dir, encoding: "utf8", timeout: 5e3 }).trim();
|
|
1923
|
-
isClean = porcelain.length === 0;
|
|
1924
|
-
untrackedCount = porcelain.split("\n").filter((l) => l.startsWith("??")).length;
|
|
1925
|
-
} catch {
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
return { isGit: true, hasCommits, branch, isClean, untrackedCount };
|
|
1929
|
-
}
|
|
1930
|
-
function gitRequirementMessage(action) {
|
|
1931
|
-
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.`;
|
|
1932
|
-
}
|
|
1933
|
-
function ensureGitRepoReadyForWorktrees(dir, action = "run issue worktrees") {
|
|
1934
|
-
const status = getGitRepoStatus(dir);
|
|
1935
|
-
if (!status.isGit) {
|
|
1936
|
-
throw new Error(gitRequirementMessage(action));
|
|
1937
|
-
}
|
|
1938
|
-
if (!status.hasCommits) {
|
|
1939
|
-
throw new Error(`fifony requires at least one commit to ${action} because git worktree needs a base commit. Create an initial commit, then retry.`);
|
|
1940
|
-
}
|
|
1941
|
-
return status;
|
|
1942
|
-
}
|
|
1943
|
-
function initializeGitRepoForWorktrees(dir) {
|
|
1944
|
-
let status = getGitRepoStatus(dir);
|
|
1945
|
-
if (!status.isGit) {
|
|
1946
|
-
try {
|
|
1947
|
-
execSync("git init -b main", { cwd: dir, stdio: "pipe" });
|
|
1948
|
-
} catch {
|
|
1949
|
-
execSync("git init", { cwd: dir, stdio: "pipe" });
|
|
1950
|
-
}
|
|
1951
|
-
status = getGitRepoStatus(dir);
|
|
1952
|
-
}
|
|
1953
|
-
if (!status.hasCommits) {
|
|
1954
|
-
execSync(
|
|
1955
|
-
'git -c user.name="fifony" -c user.email="fifony@local.invalid" commit --allow-empty -m "Initial commit"',
|
|
1956
|
-
{ cwd: dir, stdio: "pipe" }
|
|
1957
|
-
);
|
|
1958
|
-
status = getGitRepoStatus(dir);
|
|
1959
|
-
}
|
|
1960
|
-
return status;
|
|
1961
|
-
}
|
|
1962
|
-
function assertIssueHasGitWorktree(issue, action) {
|
|
1963
|
-
if (!issue.branchName || !issue.baseBranch || !issue.worktreePath) {
|
|
1964
|
-
throw new Error(
|
|
1965
|
-
`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.`
|
|
1966
|
-
);
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
function detectDefaultBranch(dir) {
|
|
1970
|
-
try {
|
|
1971
|
-
const current = execSync("git rev-parse --abbrev-ref HEAD", { cwd: dir, encoding: "utf8" }).trim();
|
|
1972
|
-
if (current && current !== "HEAD") return current;
|
|
1973
|
-
const remote = execSync("git symbolic-ref refs/remotes/origin/HEAD", { cwd: dir, encoding: "utf8" }).trim();
|
|
1974
|
-
return remote.replace("refs/remotes/origin/", "");
|
|
1975
|
-
} catch {
|
|
1976
|
-
return "main";
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
1979
|
-
var CLI_CONFIG_DIRS = [".claude", ".codex", ".gemini"];
|
|
1980
|
-
var CLI_CONFIG_FILES = ["CLAUDE.md"];
|
|
1981
|
-
function copyCliConfigDirs(sourceRoot, worktreePath) {
|
|
1982
|
-
for (const dir of CLI_CONFIG_DIRS) {
|
|
1983
|
-
const src = join7(sourceRoot, dir);
|
|
1984
|
-
const dst = join7(worktreePath, dir);
|
|
1985
|
-
if (existsSync7(src) && statSync(src).isDirectory() && !existsSync7(dst)) {
|
|
1986
|
-
try {
|
|
1987
|
-
execSync(`cp -R "${src}" "${dst}"`, { stdio: "pipe", timeout: 1e4 });
|
|
1988
|
-
logger.debug({ dir, worktreePath }, "[Workspace] Copied CLI config dir to worktree");
|
|
1989
|
-
} catch (err) {
|
|
1990
|
-
logger.warn({ err: String(err), dir }, "[Workspace] Failed to copy CLI config dir");
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
}
|
|
1994
|
-
for (const file of CLI_CONFIG_FILES) {
|
|
1995
|
-
const src = join7(sourceRoot, file);
|
|
1996
|
-
const dst = join7(worktreePath, file);
|
|
1997
|
-
if (existsSync7(src) && !existsSync7(dst)) {
|
|
1998
|
-
try {
|
|
1999
|
-
execSync(`cp "${src}" "${dst}"`, { stdio: "pipe", timeout: 5e3 });
|
|
2000
|
-
logger.debug({ file, worktreePath }, "[Workspace] Copied CLI config file to worktree");
|
|
2001
|
-
} catch (err) {
|
|
2002
|
-
logger.warn({ err: String(err), file }, "[Workspace] Failed to copy CLI config file");
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
function isGitWorkingTree(dir) {
|
|
2008
|
-
try {
|
|
2009
|
-
execSync("git rev-parse --git-dir", { cwd: dir, stdio: "pipe", timeout: 5e3 });
|
|
2010
|
-
return true;
|
|
2011
|
-
} catch {
|
|
2012
|
-
return false;
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
function resolveTestWorkspacePath(issue) {
|
|
2016
|
-
const workspaceRoot = issue.workspacePath ?? join7(WORKSPACE_ROOT, idToSafePath(issue.id));
|
|
2017
|
-
return join7(workspaceRoot, "test-worktree");
|
|
2018
|
-
}
|
|
2019
|
-
function createTestWorkspace(issue) {
|
|
2020
|
-
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "create isolated test workspaces");
|
|
2021
|
-
assertIssueHasGitWorktree(issue, "create a test workspace");
|
|
2022
|
-
const workspaceRoot = issue.workspacePath ?? join7(WORKSPACE_ROOT, idToSafePath(issue.id));
|
|
2023
|
-
const testWorkspacePath = issue.testWorkspacePath ?? resolveTestWorkspacePath(issue);
|
|
2024
|
-
mkdirSync(workspaceRoot, { recursive: true });
|
|
2025
|
-
if (existsSync7(testWorkspacePath)) {
|
|
2026
|
-
if (isGitWorkingTree(testWorkspacePath)) {
|
|
2027
|
-
issue.testWorkspacePath = testWorkspacePath;
|
|
2028
|
-
issue.testApplied = true;
|
|
2029
|
-
return testWorkspacePath;
|
|
2030
|
-
}
|
|
2031
|
-
rmSync2(testWorkspacePath, { recursive: true, force: true });
|
|
2032
|
-
}
|
|
2033
|
-
try {
|
|
2034
|
-
execSync(`git worktree add --detach "${testWorkspacePath}" "${issue.branchName}"`, {
|
|
2035
|
-
cwd: TARGET_ROOT,
|
|
2036
|
-
stdio: "pipe",
|
|
2037
|
-
timeout: 3e4
|
|
2038
|
-
});
|
|
2039
|
-
} catch (err) {
|
|
2040
|
-
const msg = err.stderr || err.stdout || String(err);
|
|
2041
|
-
throw new Error(`Failed to create isolated test workspace: ${msg}`);
|
|
2042
|
-
}
|
|
2043
|
-
copyCliConfigDirs(TARGET_ROOT, testWorkspacePath);
|
|
2044
|
-
issue.testWorkspacePath = testWorkspacePath;
|
|
2045
|
-
issue.testApplied = true;
|
|
2046
|
-
return testWorkspacePath;
|
|
2047
|
-
}
|
|
2048
|
-
function removeTestWorkspace(issue) {
|
|
2049
|
-
const testWorkspacePath = issue.testWorkspacePath;
|
|
2050
|
-
issue.testApplied = false;
|
|
2051
|
-
issue.testWorkspacePath = void 0;
|
|
2052
|
-
if (!testWorkspacePath) return;
|
|
2053
|
-
try {
|
|
2054
|
-
execSync(`git worktree remove --force "${testWorkspacePath}"`, {
|
|
2055
|
-
cwd: TARGET_ROOT,
|
|
2056
|
-
stdio: "pipe",
|
|
2057
|
-
timeout: 3e4
|
|
2058
|
-
});
|
|
2059
|
-
logger.info({ issueId: issue.id, testWorkspacePath }, "[Workspace] Removed isolated test workspace");
|
|
2060
|
-
return;
|
|
2061
|
-
} catch (error) {
|
|
2062
|
-
logger.warn({ issueId: issue.id, testWorkspacePath, err: String(error) }, "[Workspace] Failed to remove isolated test workspace via git worktree");
|
|
2063
|
-
}
|
|
2064
|
-
try {
|
|
2065
|
-
rmSync2(testWorkspacePath, { recursive: true, force: true });
|
|
2066
|
-
} catch (error) {
|
|
2067
|
-
logger.warn({ issueId: issue.id, testWorkspacePath, err: String(error) }, "[Workspace] Failed to remove isolated test workspace directory");
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
async function createGitWorktree(issue, worktreePath, baseBranch) {
|
|
2071
|
-
let headCommitAtStart = "";
|
|
2072
|
-
const resolvedBaseBranch = baseBranch ?? detectDefaultBranch(TARGET_ROOT);
|
|
2073
|
-
try {
|
|
2074
|
-
headCommitAtStart = execSync("git rev-parse HEAD", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
|
|
2075
|
-
} catch {
|
|
2076
|
-
}
|
|
2077
|
-
const branchName = `fifony/${issue.id}`;
|
|
2078
|
-
execSync(`git worktree add "${worktreePath}" -B "${branchName}"`, {
|
|
2079
|
-
cwd: TARGET_ROOT,
|
|
2080
|
-
stdio: "pipe"
|
|
2081
|
-
});
|
|
2082
|
-
try {
|
|
2083
|
-
const gitFileContent = readFileSync4(join7(worktreePath, ".git"), "utf8").trim();
|
|
2084
|
-
const gitDirRel = gitFileContent.replace("gitdir: ", "").trim();
|
|
2085
|
-
const gitDirPath = resolve(worktreePath, gitDirRel);
|
|
2086
|
-
mkdirSync(join7(gitDirPath, "info"), { recursive: true });
|
|
2087
|
-
writeFileSync2(join7(gitDirPath, "info", "exclude"), "fifony-*\n.fifony-*\nfifony_*\n", "utf8");
|
|
2088
|
-
} catch (err) {
|
|
2089
|
-
logger.warn({ err: String(err) }, "[Agent] Failed to write worktree excludes");
|
|
2090
|
-
}
|
|
2091
|
-
issue.branchName = branchName;
|
|
2092
|
-
issue.baseBranch = resolvedBaseBranch;
|
|
2093
|
-
issue.headCommitAtStart = headCommitAtStart;
|
|
2094
|
-
issue.worktreePath = worktreePath;
|
|
2095
|
-
copyCliConfigDirs(TARGET_ROOT, worktreePath);
|
|
2096
|
-
logger.debug({ issueId: issue.id, branchName, baseBranch: resolvedBaseBranch, worktreePath }, "[Agent] Git worktree created");
|
|
2097
|
-
}
|
|
2098
|
-
async function prepareWorkspace(issue, state, defaultBranch) {
|
|
2099
|
-
const safeId = idToSafePath(issue.id);
|
|
2100
|
-
const workspaceRoot = join7(WORKSPACE_ROOT, safeId);
|
|
2101
|
-
const worktreePath = join7(workspaceRoot, "worktree");
|
|
2102
|
-
const createdNow = !existsSync7(worktreePath);
|
|
2103
|
-
if (createdNow) {
|
|
2104
|
-
mkdirSync(workspaceRoot, { recursive: true });
|
|
2105
|
-
logger.debug({ issueId: issue.id, identifier: issue.identifier, workspacePath: workspaceRoot }, "[Agent] Creating workspace");
|
|
2106
|
-
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "execute issues");
|
|
2107
|
-
if (state.config.afterCreateHook) {
|
|
2108
|
-
mkdirSync(worktreePath, { recursive: true });
|
|
2109
|
-
await runHook(state.config.afterCreateHook, worktreePath, issue, "after_create");
|
|
2110
|
-
} else {
|
|
2111
|
-
await createGitWorktree(issue, worktreePath, defaultBranch);
|
|
2112
|
-
}
|
|
2113
|
-
logger.debug({ issueId: issue.id, workspacePath: workspaceRoot, worktreePath }, "[Agent] Workspace created");
|
|
2114
|
-
} else {
|
|
2115
|
-
logger.debug({ issueId: issue.id, workspacePath: workspaceRoot }, "[Agent] Reusing existing workspace");
|
|
2116
|
-
}
|
|
2117
|
-
const metaPath = join7(workspaceRoot, "issue.json");
|
|
2118
|
-
const promptText = await buildPrompt(issue, null);
|
|
2119
|
-
const promptFile = join7(workspaceRoot, "prompt.md");
|
|
2120
|
-
writeFileSync2(metaPath, JSON.stringify({ ...issue, runtimeSource: SOURCE_ROOT, bootstrapAt: now() }, null, 2), "utf8");
|
|
2121
|
-
writeFileSync2(promptFile, `${promptText}
|
|
2122
|
-
`, "utf8");
|
|
2123
|
-
issue.workspacePath = workspaceRoot;
|
|
2124
|
-
issue.worktreePath = worktreePath;
|
|
2125
|
-
issue.workspacePreparedAt = now();
|
|
2126
|
-
return { workspacePath: workspaceRoot, promptText, promptFile };
|
|
2127
|
-
}
|
|
2128
|
-
async function cleanWorkspace(issueId, issue, state) {
|
|
2129
|
-
const safeId = idToSafePath(issueId);
|
|
2130
|
-
const workspacePath = issue?.workspacePath ?? join7(WORKSPACE_ROOT, safeId);
|
|
2131
|
-
if (!existsSync7(workspacePath)) return;
|
|
2132
|
-
if (state.config.beforeRemoveHook) {
|
|
2133
|
-
try {
|
|
2134
|
-
const dummyIssue = issue ?? { id: issueId, identifier: issueId };
|
|
2135
|
-
await runHook(state.config.beforeRemoveHook, workspacePath, dummyIssue, "before_remove");
|
|
2136
|
-
} catch (error) {
|
|
2137
|
-
logger.warn(`before_remove hook failed for ${issueId}: ${String(error)}`);
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
if (issue?.testWorkspacePath) {
|
|
2141
|
-
removeTestWorkspace(issue);
|
|
2142
|
-
}
|
|
2143
|
-
if (issue?.branchName && issue.worktreePath) {
|
|
2144
|
-
try {
|
|
2145
|
-
execSync(`git worktree remove --force "${issue.worktreePath}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
2146
|
-
logger.info(`Removed worktree for ${issueId}: ${issue.worktreePath}`);
|
|
2147
|
-
} catch (error) {
|
|
2148
|
-
logger.warn(`Failed to remove worktree for ${issueId}: ${String(error)}`);
|
|
2149
|
-
try {
|
|
2150
|
-
rmSync2(issue.worktreePath, { recursive: true, force: true });
|
|
2151
|
-
} catch {
|
|
2152
|
-
}
|
|
2153
|
-
}
|
|
2154
|
-
try {
|
|
2155
|
-
execSync(`git branch -D "${issue.branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
2156
|
-
} catch {
|
|
2157
|
-
}
|
|
2158
|
-
try {
|
|
2159
|
-
rmSync2(workspacePath, { recursive: true, force: true });
|
|
2160
|
-
} catch {
|
|
2161
|
-
}
|
|
2162
|
-
return;
|
|
2163
|
-
}
|
|
2164
|
-
try {
|
|
2165
|
-
rmSync2(workspacePath, { recursive: true, force: true });
|
|
2166
|
-
logger.info(`Cleaned workspace for ${issueId}: ${workspacePath}`);
|
|
2167
|
-
} catch (error) {
|
|
2168
|
-
logger.warn(`Failed to clean workspace for ${issueId}: ${String(error)}`);
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
function inferChangedWorkspacePaths(workspacePath, limit = 32, issue) {
|
|
2172
|
-
if (!issue?.baseBranch || !issue.branchName) return [];
|
|
2173
|
-
try {
|
|
2174
|
-
const output = execSync(
|
|
2175
|
-
`git diff --name-only "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
2176
|
-
{ cwd: TARGET_ROOT, encoding: "utf8", timeout: 1e4, stdio: "pipe" }
|
|
2177
|
-
);
|
|
2178
|
-
return output.trim().split("\n").filter(Boolean).slice(0, limit);
|
|
2179
|
-
} catch {
|
|
2180
|
-
return [];
|
|
2181
|
-
}
|
|
2182
|
-
}
|
|
2183
|
-
function computeDiffStats(issue) {
|
|
2184
|
-
if (!issue.baseBranch || !issue.branchName) return;
|
|
2185
|
-
try {
|
|
2186
|
-
let raw = "";
|
|
2187
|
-
try {
|
|
2188
|
-
raw = execSync(
|
|
2189
|
-
`git diff --stat "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
2190
|
-
{ cwd: TARGET_ROOT, encoding: "utf8", maxBuffer: 512e3, timeout: 1e4, stdio: "pipe" }
|
|
2191
|
-
);
|
|
2192
|
-
} catch (err) {
|
|
2193
|
-
raw = err.stdout || "";
|
|
2194
|
-
}
|
|
2195
|
-
if (raw) parseDiffStats(issue, raw);
|
|
2196
|
-
} catch {
|
|
2197
|
-
}
|
|
2198
|
-
}
|
|
2199
|
-
function parseDiffStats(issue, raw) {
|
|
2200
|
-
const lines = raw.trim().split("\n");
|
|
2201
|
-
const summary = lines[lines.length - 1] || "";
|
|
2202
|
-
const filesMatch = summary.match(/(\d+)\s+files?\s+changed/);
|
|
2203
|
-
const addMatch = summary.match(/(\d+)\s+insertions?\(\+\)/);
|
|
2204
|
-
const delMatch = summary.match(/(\d+)\s+deletions?\(-\)/);
|
|
2205
|
-
const internalRe = /fifony[-_]|\.fifony-|WORKFLOW\.local/;
|
|
2206
|
-
const fileLines = lines.slice(0, -1).filter((l) => {
|
|
2207
|
-
const name = l.trim().split("|")[0]?.trim().split("/").pop() || "";
|
|
2208
|
-
return !internalRe.test(name);
|
|
2209
|
-
});
|
|
2210
|
-
const regexFiles = filesMatch ? parseInt(filesMatch[1], 10) : 0;
|
|
2211
|
-
issue.filesChanged = fileLines.length > 0 ? fileLines.length : regexFiles;
|
|
2212
|
-
issue.linesAdded = addMatch ? parseInt(addMatch[1], 10) : 0;
|
|
2213
|
-
issue.linesRemoved = delMatch ? parseInt(delMatch[1], 10) : 0;
|
|
2214
|
-
}
|
|
2215
|
-
async function syncIssueDiffStatsToStore(issue) {
|
|
2216
|
-
if (!issue?.id) return;
|
|
2217
|
-
const { getIssueStateResource } = await import("./store-4HCGBN4L.js");
|
|
2218
|
-
const issueResource = getIssueStateResource();
|
|
2219
|
-
if (!issueResource) return;
|
|
2220
|
-
const toNumber = (value) => {
|
|
2221
|
-
const parsed = typeof value === "number" ? value : Number(value ?? 0);
|
|
2222
|
-
return Number.isFinite(parsed) ? parsed : 0;
|
|
2223
|
-
};
|
|
2224
|
-
const nextLinesAdded = toNumber(issue.linesAdded);
|
|
2225
|
-
const nextLinesRemoved = toNumber(issue.linesRemoved);
|
|
2226
|
-
const nextFilesChanged = toNumber(issue.filesChanged);
|
|
2227
|
-
if (nextLinesAdded === 0 && nextLinesRemoved === 0 && nextFilesChanged === 0 && !issue.branchName) {
|
|
2228
|
-
return;
|
|
2229
|
-
}
|
|
2230
|
-
const current = await issueResource.get?.(issue.id).catch(() => null);
|
|
2231
|
-
const previousLinesAdded = toNumber(current?.linesAdded);
|
|
2232
|
-
const previousLinesRemoved = toNumber(current?.linesRemoved);
|
|
2233
|
-
const previousFilesChanged = toNumber(current?.filesChanged);
|
|
2234
|
-
await issueResource.patch(issue.id, {
|
|
2235
|
-
linesAdded: nextLinesAdded,
|
|
2236
|
-
linesRemoved: nextLinesRemoved,
|
|
2237
|
-
filesChanged: nextFilesChanged,
|
|
2238
|
-
branchName: issue.branchName
|
|
2239
|
-
});
|
|
2240
|
-
const add = issueResource.add;
|
|
2241
|
-
const sub = issueResource.sub;
|
|
2242
|
-
if (typeof add !== "function" || typeof sub !== "function") {
|
|
2243
|
-
logger.debug({ issueId: issue.id }, "[DiffStats] resource.add/sub not available \u2014 EC plugin may not be installed");
|
|
2244
|
-
return;
|
|
2245
|
-
}
|
|
2246
|
-
const deltaAdded = nextLinesAdded - previousLinesAdded;
|
|
2247
|
-
const deltaRemoved = nextLinesRemoved - previousLinesRemoved;
|
|
2248
|
-
const deltaFiles = nextFilesChanged - previousFilesChanged;
|
|
2249
|
-
if (deltaAdded === 0 && deltaRemoved === 0 && deltaFiles === 0) {
|
|
2250
|
-
logger.debug({ issueId: issue.id, nextLinesAdded, previousLinesAdded }, "[DiffStats] No delta to send to EC (values already synced)");
|
|
2251
|
-
return;
|
|
2252
|
-
}
|
|
2253
|
-
logger.debug({ issueId: issue.id, deltaAdded, deltaRemoved, deltaFiles }, "[DiffStats] Sending deltas to EC");
|
|
2254
|
-
const applyDelta = async (field, delta) => {
|
|
2255
|
-
if (delta > 0) {
|
|
2256
|
-
await add.call(issueResource, issue.id, field, delta);
|
|
2257
|
-
} else if (delta < 0) {
|
|
2258
|
-
await sub.call(issueResource, issue.id, field, Math.abs(delta));
|
|
2259
|
-
}
|
|
2260
|
-
};
|
|
2261
|
-
await Promise.all([
|
|
2262
|
-
applyDelta("linesAdded", deltaAdded),
|
|
2263
|
-
applyDelta("linesRemoved", deltaRemoved),
|
|
2264
|
-
applyDelta("filesChanged", deltaFiles)
|
|
2265
|
-
]);
|
|
2266
|
-
}
|
|
2267
|
-
function ensureWorktreeCommitted(issue) {
|
|
2268
|
-
const worktreePath = issue.worktreePath;
|
|
2269
|
-
if (!worktreePath || !issue.branchName) return;
|
|
2270
|
-
execSync("git add -A", { cwd: worktreePath, stdio: "pipe" });
|
|
2271
|
-
const statusBeforeCommit = execSync("git status --porcelain", { cwd: worktreePath, encoding: "utf8" }).trim();
|
|
2272
|
-
if (!statusBeforeCommit) return;
|
|
2273
|
-
try {
|
|
2274
|
-
execSync(`git commit -m "fifony: agent changes for ${issue.identifier}"`, { cwd: worktreePath, stdio: "pipe" });
|
|
2275
|
-
} catch (error) {
|
|
2276
|
-
const remaining = execSync("git status --porcelain", { cwd: worktreePath, encoding: "utf8" }).trim();
|
|
2277
|
-
if (remaining) {
|
|
2278
|
-
throw new Error(`Failed to commit agent changes for ${issue.identifier}: ${String(error)}`);
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
const statusAfterCommit = execSync("git status --porcelain", { cwd: worktreePath, encoding: "utf8" }).trim();
|
|
2282
|
-
if (statusAfterCommit) {
|
|
2283
|
-
throw new Error(`Worktree for ${issue.identifier} still has uncommitted changes after commit.`);
|
|
2284
|
-
}
|
|
2285
|
-
}
|
|
2286
|
-
function mergeWorktree(issue, worktreePath, abortOnConflict = true) {
|
|
2287
|
-
const result = { copied: [], deleted: [], skipped: [], conflicts: [] };
|
|
2288
|
-
ensureWorktreeCommitted(issue);
|
|
2289
|
-
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
|
|
2290
|
-
if (currentBranch !== issue.baseBranch) {
|
|
2291
|
-
throw new Error(`Cannot merge ${issue.identifier}: current branch is ${currentBranch}, expected ${issue.baseBranch}.`);
|
|
2292
|
-
}
|
|
2293
|
-
const targetStatus = execSync("git status --porcelain", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
|
|
2294
|
-
if (targetStatus) {
|
|
2295
|
-
throw new Error(`Cannot merge ${issue.identifier}: target repository has uncommitted changes.`);
|
|
2296
|
-
}
|
|
2297
|
-
try {
|
|
2298
|
-
const diffOut = execSync(
|
|
2299
|
-
`git diff --name-status "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
2300
|
-
{ cwd: TARGET_ROOT, encoding: "utf8" }
|
|
2301
|
-
);
|
|
2302
|
-
for (const line of diffOut.trim().split("\n").filter(Boolean)) {
|
|
2303
|
-
const [statusChar, ...parts] = line.split(" ");
|
|
2304
|
-
const filePath = parts.join(" ");
|
|
2305
|
-
if (statusChar === "D") result.deleted.push(filePath);
|
|
2306
|
-
else result.copied.push(filePath);
|
|
2307
|
-
}
|
|
2308
|
-
} catch {
|
|
2309
|
-
}
|
|
2310
|
-
try {
|
|
2311
|
-
execSync(
|
|
2312
|
-
`git merge --no-ff "${issue.branchName}" -m "fifony: merge ${issue.identifier}"`,
|
|
2313
|
-
{ cwd: TARGET_ROOT, stdio: "pipe" }
|
|
2314
|
-
);
|
|
2315
|
-
} catch (err) {
|
|
2316
|
-
try {
|
|
2317
|
-
const conflictOut = execSync(
|
|
2318
|
-
"git diff --name-only --diff-filter=U",
|
|
2319
|
-
{ cwd: TARGET_ROOT, encoding: "utf8" }
|
|
2320
|
-
);
|
|
2321
|
-
result.conflicts.push(...conflictOut.trim().split("\n").filter(Boolean));
|
|
2322
|
-
} catch {
|
|
2323
|
-
}
|
|
2324
|
-
if (abortOnConflict) {
|
|
2325
|
-
try {
|
|
2326
|
-
execSync("git merge --abort", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
2327
|
-
} catch {
|
|
2328
|
-
}
|
|
2329
|
-
logger.warn({ issueId: issue.id, err: String(err) }, "[Agent] Git merge failed, aborted");
|
|
2330
|
-
} else {
|
|
2331
|
-
logger.info({ issueId: issue.id, conflicts: result.conflicts }, "[Agent] Git merge has conflicts \u2014 leaving markers for agent resolution");
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
return result;
|
|
2335
|
-
}
|
|
2336
|
-
function shouldSkipMergePath(relativePath) {
|
|
2337
|
-
const parts = relativePath.split("/");
|
|
2338
|
-
if (parts.some((s) => s === ".git" || s === "node_modules" || s === ".fifony" || s === "dist" || s === ".tanstack")) {
|
|
2339
|
-
return true;
|
|
2340
|
-
}
|
|
2341
|
-
const base = parts.at(-1) ?? "";
|
|
2342
|
-
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_");
|
|
2343
|
-
}
|
|
2344
|
-
function mergeWorkspace(issue, abortOnConflict = true) {
|
|
2345
|
-
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "merge issues");
|
|
2346
|
-
assertIssueHasGitWorktree(issue, "merge");
|
|
2347
|
-
return mergeWorktree(issue, issue.worktreePath, abortOnConflict);
|
|
2348
|
-
}
|
|
2349
|
-
function dryMerge(issue) {
|
|
2350
|
-
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "preview merges");
|
|
2351
|
-
assertIssueHasGitWorktree(issue, "preview merge");
|
|
2352
|
-
ensureWorktreeCommitted(issue);
|
|
2353
|
-
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
|
|
2354
|
-
if (currentBranch !== issue.baseBranch) {
|
|
2355
|
-
throw new Error(`Cannot preview merge: current branch is ${currentBranch}, expected ${issue.baseBranch}.`);
|
|
2356
|
-
}
|
|
2357
|
-
const targetStatus = execSync("git status --porcelain", { cwd: TARGET_ROOT, encoding: "utf8" }).trim();
|
|
2358
|
-
if (targetStatus) {
|
|
2359
|
-
throw new Error(`Cannot preview merge: target repository has uncommitted changes.`);
|
|
2360
|
-
}
|
|
2361
|
-
let conflictFiles = [];
|
|
2362
|
-
let willConflict = false;
|
|
2363
|
-
try {
|
|
2364
|
-
execSync(
|
|
2365
|
-
`git merge --no-commit --no-ff "${issue.branchName}"`,
|
|
2366
|
-
{ cwd: TARGET_ROOT, stdio: "pipe" }
|
|
2367
|
-
);
|
|
2368
|
-
} catch {
|
|
2369
|
-
willConflict = true;
|
|
2370
|
-
try {
|
|
2371
|
-
const conflictOut = execSync(
|
|
2372
|
-
"git diff --name-only --diff-filter=U",
|
|
2373
|
-
{ cwd: TARGET_ROOT, encoding: "utf8" }
|
|
2374
|
-
);
|
|
2375
|
-
conflictFiles = conflictOut.trim().split("\n").filter(Boolean);
|
|
2376
|
-
} catch {
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
try {
|
|
2380
|
-
execSync("git merge --abort", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
2381
|
-
} catch {
|
|
2382
|
-
try {
|
|
2383
|
-
execSync("git reset --merge ORIG_HEAD", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
2384
|
-
} catch {
|
|
2385
|
-
try {
|
|
2386
|
-
execSync("git reset --merge", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
2387
|
-
} catch (error) {
|
|
2388
|
-
logger.warn({ issueId: issue.id, err: String(error) }, "[Workspace] Failed to safely clean dry-merge state");
|
|
2389
|
-
}
|
|
2390
|
-
}
|
|
2391
|
-
}
|
|
2392
|
-
let changedFiles = 0;
|
|
2393
|
-
try {
|
|
2394
|
-
const diffOut = execSync(
|
|
2395
|
-
`git diff --name-only "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
2396
|
-
{ cwd: TARGET_ROOT, encoding: "utf8" }
|
|
2397
|
-
);
|
|
2398
|
-
changedFiles = diffOut.trim().split("\n").filter(Boolean).length;
|
|
2399
|
-
} catch {
|
|
2400
|
-
}
|
|
2401
|
-
return { willConflict, conflictFiles, canMerge: !willConflict, changedFiles };
|
|
2402
|
-
}
|
|
2403
|
-
function rebaseWorktree(issue) {
|
|
2404
|
-
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "rebase worktrees");
|
|
2405
|
-
assertIssueHasGitWorktree(issue, "rebase");
|
|
2406
|
-
ensureWorktreeCommitted(issue);
|
|
2407
|
-
try {
|
|
2408
|
-
execSync(
|
|
2409
|
-
`git rebase "${issue.baseBranch}"`,
|
|
2410
|
-
{ cwd: issue.worktreePath, stdio: "pipe" }
|
|
2411
|
-
);
|
|
2412
|
-
return { success: true, conflictFiles: [] };
|
|
2413
|
-
} catch {
|
|
2414
|
-
let conflictFiles = [];
|
|
2415
|
-
try {
|
|
2416
|
-
const conflictOut = execSync(
|
|
2417
|
-
"git diff --name-only --diff-filter=U",
|
|
2418
|
-
{ cwd: issue.worktreePath, encoding: "utf8" }
|
|
2419
|
-
);
|
|
2420
|
-
conflictFiles = conflictOut.trim().split("\n").filter(Boolean);
|
|
2421
|
-
} catch {
|
|
2422
|
-
}
|
|
2423
|
-
try {
|
|
2424
|
-
execSync("git rebase --abort", { cwd: issue.worktreePath, stdio: "pipe" });
|
|
2425
|
-
} catch {
|
|
2426
|
-
}
|
|
2427
|
-
return { success: false, conflictFiles };
|
|
2428
|
-
}
|
|
2429
|
-
}
|
|
2430
|
-
function hydrateIssuePathsFromWorkspace(issue) {
|
|
2431
|
-
const inferredPaths = inferChangedWorkspacePaths(issue.workspacePath ?? "", 32, issue);
|
|
2432
|
-
if (inferredPaths.length === 0) return [];
|
|
2433
|
-
issue.paths = [.../* @__PURE__ */ new Set([...issue.paths ?? [], ...inferredPaths])];
|
|
2434
|
-
return inferredPaths;
|
|
2435
|
-
}
|
|
2436
|
-
function writeVersionedArtifacts(workspacePath, prefix, planVersion, attempt, sources) {
|
|
2437
|
-
const { writeFileSync: _wfs, readFileSync: _rfs, existsSync: _es } = { writeFileSync: writeFileSync2, readFileSync: readFileSync4, existsSync: existsSync7 };
|
|
2438
|
-
for (const { srcFile, destSuffix } of sources) {
|
|
2439
|
-
const src = join7(workspacePath, srcFile);
|
|
2440
|
-
if (_es(src)) {
|
|
2441
|
-
_wfs(join7(workspacePath, `${prefix}.v${planVersion}a${attempt}.${destSuffix}`), _rfs(src, "utf8"), "utf8");
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2364
|
+
function getSessionProvidersForIssue(state, issue, workflowConfig) {
|
|
2365
|
+
return [
|
|
2366
|
+
...getExecutionProviders(state, issue, workflowConfig ?? null),
|
|
2367
|
+
getReviewProvider(state, issue, workflowConfig ?? null)
|
|
2368
|
+
];
|
|
2444
2369
|
}
|
|
2445
2370
|
|
|
2446
2371
|
export {
|
|
2372
|
+
markIssueDirty,
|
|
2373
|
+
markMilestoneDirty,
|
|
2374
|
+
markIssuePlanDirty,
|
|
2375
|
+
markEventDirty,
|
|
2376
|
+
hasDirtyState,
|
|
2377
|
+
getDirtyIssueIds,
|
|
2378
|
+
getDirtyMilestoneIds,
|
|
2379
|
+
getDirtyEventIds,
|
|
2380
|
+
snapshotAndClearDirtyIssueIds,
|
|
2381
|
+
snapshotAndClearDirtyMilestoneIds,
|
|
2382
|
+
snapshotAndClearDirtyIssuePlanIds,
|
|
2383
|
+
snapshotAndClearDirtyEventIds,
|
|
2384
|
+
markAllIssuesDirty,
|
|
2385
|
+
markAllMilestonesDirty,
|
|
2386
|
+
markAllIssuePlansDirty,
|
|
2387
|
+
markAllEventsDirty,
|
|
2388
|
+
deriveReviewProfile,
|
|
2389
|
+
serializeReviewRouteSnapshot,
|
|
2390
|
+
applyHarnessModeToPlan,
|
|
2391
|
+
applyCheckpointPolicyToPlan,
|
|
2392
|
+
recommendCheckpointPolicyForIssue,
|
|
2393
|
+
recommendHarnessModeForIssue,
|
|
2394
|
+
normalizeAcceptanceCriteria,
|
|
2395
|
+
deriveExecutionContract,
|
|
2447
2396
|
buildFullPlanPrompt,
|
|
2448
2397
|
buildExecutionPayload,
|
|
2449
2398
|
collectClaudeUsageFromCli,
|
|
@@ -2452,40 +2401,14 @@ export {
|
|
|
2452
2401
|
ADAPTERS,
|
|
2453
2402
|
discoverModels,
|
|
2454
2403
|
normalizeAgentProvider,
|
|
2404
|
+
resolveProviderCapabilities,
|
|
2405
|
+
getProviderCapabilityWarnings,
|
|
2455
2406
|
getProviderDefaultCommand,
|
|
2456
2407
|
detectAvailableProviders,
|
|
2457
2408
|
readCodexConfig,
|
|
2458
2409
|
resolveDefaultProvider,
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
buildRetryContext,
|
|
2463
|
-
buildPrompt,
|
|
2464
|
-
buildTurnPrompt,
|
|
2465
|
-
buildProviderBasePrompt,
|
|
2466
|
-
bootstrapSource,
|
|
2467
|
-
setSkipSource,
|
|
2468
|
-
ensureSourceReady,
|
|
2469
|
-
getGitRepoStatus,
|
|
2470
|
-
ensureGitRepoReadyForWorktrees,
|
|
2471
|
-
initializeGitRepoForWorktrees,
|
|
2472
|
-
assertIssueHasGitWorktree,
|
|
2473
|
-
detectDefaultBranch,
|
|
2474
|
-
createTestWorkspace,
|
|
2475
|
-
removeTestWorkspace,
|
|
2476
|
-
createGitWorktree,
|
|
2477
|
-
prepareWorkspace,
|
|
2478
|
-
cleanWorkspace,
|
|
2479
|
-
inferChangedWorkspacePaths,
|
|
2480
|
-
computeDiffStats,
|
|
2481
|
-
parseDiffStats,
|
|
2482
|
-
syncIssueDiffStatsToStore,
|
|
2483
|
-
ensureWorktreeCommitted,
|
|
2484
|
-
shouldSkipMergePath,
|
|
2485
|
-
mergeWorkspace,
|
|
2486
|
-
dryMerge,
|
|
2487
|
-
rebaseWorktree,
|
|
2488
|
-
hydrateIssuePathsFromWorkspace,
|
|
2489
|
-
writeVersionedArtifacts
|
|
2410
|
+
getExecutionProviders,
|
|
2411
|
+
getReviewProvider,
|
|
2412
|
+
getSessionProvidersForIssue
|
|
2490
2413
|
};
|
|
2491
|
-
//# sourceMappingURL=chunk-
|
|
2414
|
+
//# sourceMappingURL=chunk-FJNH3G2Z.js.map
|