fifony 0.1.43 → 0.1.47
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-M4VAMxCU.js → CommandPalette-CL8p78lG.js} +1 -1
- package/app/dist/assets/{KeyboardShortcutsHelp-DkvPUXQq.js → KeyboardShortcutsHelp-CqEFfGcE.js} +1 -1
- package/app/dist/assets/OnboardingWizard-BmI50ZUv.js +1 -0
- package/app/dist/assets/analytics.lazy-CXGjZabc.js +1 -0
- package/app/dist/assets/{api-CkVfYg_m.js → api-CEr_D4e5.js} +1 -1
- package/app/dist/assets/{createLucideIcon-Dfk_Hxud.js → createLucideIcon-luywpIq4.js} +1 -1
- package/app/dist/assets/index-CEaccpYh.js +96 -0
- package/app/dist/assets/index-CzzWGzux.css +1 -0
- package/app/dist/assets/vendor-uqBx3VSC.js +9 -0
- package/app/dist/index.html +12 -12
- package/app/dist/service-worker.js +15 -5
- package/dist/agent/pty-daemon.js +3 -2
- package/dist/agent/run-local.js +71 -52
- package/dist/{agent-RMQTTUEC.js → agent-DFSFG6DG.js} +18 -12
- package/dist/{analytics-broadcaster-O6YBP66L.js → analytics-broadcaster-O4AE3RUK.js} +21 -14
- package/dist/approve-plan.command-QGQZZXTQ.js +17 -0
- package/dist/{chunk-E2EWEYA4.js → chunk-2PRRKBG6.js} +20 -10
- package/dist/chunk-5AMWD66T.js +38 -0
- package/dist/{chunk-QQQLP3PL.js → chunk-7TXZYZR5.js} +9 -37
- package/dist/chunk-AAVROEQC.js +859 -0
- package/dist/{chunk-ESWHDHH6.js → chunk-AAZKYWOY.js} +4 -4
- package/dist/chunk-EBCSQFPR.js +682 -0
- package/dist/{chunk-BRSR26VK.js → chunk-FH7HUPZX.js} +2 -2
- package/dist/chunk-HOIOVUHI.js +35 -0
- package/dist/{chunk-AILXZ2TD.js → chunk-JRLWLZOD.js} +20 -13
- package/dist/{chunk-YRSH2CLW.js → chunk-K36BWMUV.js} +1741 -1216
- package/dist/chunk-N4KFNX2G.js +370 -0
- package/dist/chunk-PACI3T4I.js +125 -0
- package/dist/{chunk-FJNH3G2Z.js → chunk-PI7Y77R3.js} +38 -663
- package/dist/{chunk-DVU3CXWA.js → chunk-PXTIWKLQ.js} +2 -1
- package/dist/{chunk-SOBLO4YZ.js → chunk-QH6VCTET.js} +316 -127
- package/dist/{chunk-MVTGAKQK.js → chunk-QHISYRXJ.js} +2 -2
- package/dist/{chunk-42AMQAJG.js → chunk-VM5QAYP5.js} +2 -2
- package/dist/cli.js +17 -11
- package/dist/create-issue.command-VAKYRECC.js +24 -0
- package/dist/{fsm-issue-YGGF7SIL.js → fsm-issue-EHTSKMFN.js} +9 -8
- package/dist/fsm-service-7O4AJG2R.js +32 -0
- package/dist/{helpers-L7NYO5XS.js → helpers-ON2S7UEF.js} +2 -2
- package/dist/{issue-log-broadcaster-WZAHISYB.js → issue-log-broadcaster-FZGVEEIX.js} +20 -13
- package/dist/{issues-3QRR7KM6.js → issues-3YNNTB4U.js} +10 -7
- package/dist/{log-analyzer-K7MXQB4T.js → log-analyzer-EIX6R6PP.js} +82 -18
- package/dist/logger-IFLXTQPS.js +11 -0
- package/dist/mcp/server.js +2 -2
- package/dist/merge-workspace.command-T2NIGR4M.js +24 -0
- package/dist/{parallel-executor-6INE6NDO.js → parallel-executor-DWESCNX3.js} +20 -14
- package/dist/queue-workers-V57BYXAY.js +38 -0
- package/dist/replan-issue.command-2GQ3QXCR.js +17 -0
- package/dist/retry-issue.command-GJBUUYDJ.js +17 -0
- package/dist/scheduler-KYILMWLD.js +32 -0
- package/dist/{settings-ZAWDCFP2.js → settings-SOTIS6ZD.js} +32 -12
- package/dist/settings.resource-JMD3JQOS.js +30 -0
- package/dist/{store-M6NCKMZY.js → store-S3NAYZ3S.js} +18 -12
- package/dist/{web-push-AX5IIK3P.js → web-push-QCTLS7EJ.js} +3 -3
- package/dist/websocket-T2Y3BY4B.js +61 -0
- package/dist/{workspace-CJTWFWTJ.js → workspace-OS7GPMCN.js} +7 -6
- package/package.json +8 -5
- package/app/dist/assets/OnboardingWizard-B7V9hoCR.js +0 -1
- package/app/dist/assets/analytics.lazy-zVJdF880.js +0 -1
- package/app/dist/assets/index-BpiCi7Ew.css +0 -1
- package/app/dist/assets/index-D2INW0zc.js +0 -47
- package/app/dist/assets/vendor-BEoYbFV1.js +0 -9
- package/dist/queue-workers-XFZK3TT5.js +0 -32
- package/dist/replan-issue.command-4UCWYHGZ.js +0 -15
- package/dist/scheduler-ZP7GOZDW.js +0 -26
- package/dist/settings.resource-5CW456AZ.js +0 -24
|
@@ -1,26 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
requestReworkCommand,
|
|
3
|
+
retryIssueCommand
|
|
4
|
+
} from "./chunk-PACI3T4I.js";
|
|
5
|
+
import {
|
|
6
|
+
approvePlanCommand
|
|
7
|
+
} from "./chunk-HOIOVUHI.js";
|
|
1
8
|
import {
|
|
2
9
|
SETTING_ID_PUSH_SUBSCRIPTIONS,
|
|
3
10
|
addSubscription,
|
|
4
11
|
getSubscriptionCount,
|
|
5
12
|
getVapidPublicKey,
|
|
6
13
|
removeSubscription
|
|
7
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-FH7HUPZX.js";
|
|
15
|
+
import {
|
|
16
|
+
broadcastToWebSocketClients,
|
|
17
|
+
makeWebSocketConfig,
|
|
18
|
+
sendToMeshRoom
|
|
19
|
+
} from "./chunk-N4KFNX2G.js";
|
|
20
|
+
import {
|
|
21
|
+
getAllServiceStatuses,
|
|
22
|
+
getServiceGraph,
|
|
23
|
+
getServiceStatus,
|
|
24
|
+
getTrafficBuffer,
|
|
25
|
+
getTrafficProxyPort,
|
|
26
|
+
getTrafficProxyStats,
|
|
27
|
+
isTrafficProxyRunning,
|
|
28
|
+
normalizeServiceEnvironment,
|
|
29
|
+
readServiceLogTail,
|
|
30
|
+
reconcileServiceStates,
|
|
31
|
+
sendServiceEvent,
|
|
32
|
+
serviceLogPath,
|
|
33
|
+
serviceStateMachineConfig,
|
|
34
|
+
setServiceResourceStateApi,
|
|
35
|
+
setServicesAccessor,
|
|
36
|
+
startTrafficProxy,
|
|
37
|
+
stopTrafficProxy
|
|
38
|
+
} from "./chunk-AAVROEQC.js";
|
|
39
|
+
import {
|
|
40
|
+
replanIssueCommand
|
|
41
|
+
} from "./chunk-5AMWD66T.js";
|
|
8
42
|
import {
|
|
9
43
|
deleteIssueStateMachineResourceState,
|
|
10
44
|
getIssueStateMachineTransitions,
|
|
11
45
|
getIssueStateMachineVisualization,
|
|
12
46
|
getIssueTransitionHistoryForIssue,
|
|
13
|
-
replanIssueCommand,
|
|
14
47
|
syncIssueStateFromFsm,
|
|
15
48
|
syncIssueStateInMemory,
|
|
16
49
|
transitionIssueCommand
|
|
17
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-7TXZYZR5.js";
|
|
18
51
|
import {
|
|
19
52
|
assertPlanReadyForExecution,
|
|
20
53
|
executeTransition,
|
|
21
54
|
extractFailureInsights,
|
|
22
55
|
getMetrics,
|
|
23
|
-
getPlanExecutionBlocker,
|
|
24
56
|
issueStateMachineConfig,
|
|
25
57
|
needsContractNegotiationWork,
|
|
26
58
|
requiresContractNegotiation,
|
|
@@ -29,7 +61,7 @@ import {
|
|
|
29
61
|
setIssueResourceStateApi,
|
|
30
62
|
setIssueStateMachinePlugin,
|
|
31
63
|
setPersistNowFn
|
|
32
|
-
} from "./chunk-
|
|
64
|
+
} from "./chunk-JRLWLZOD.js";
|
|
33
65
|
import {
|
|
34
66
|
CONTAINER_PLANNING,
|
|
35
67
|
assertIssueHasGitWorktree,
|
|
@@ -59,7 +91,7 @@ import {
|
|
|
59
91
|
runCommandWithTimeout,
|
|
60
92
|
runHook,
|
|
61
93
|
writeToDaemon
|
|
62
|
-
} from "./chunk-
|
|
94
|
+
} from "./chunk-QH6VCTET.js";
|
|
63
95
|
import {
|
|
64
96
|
SETTING_ID_PROJECT_NAME,
|
|
65
97
|
addEvent,
|
|
@@ -79,24 +111,18 @@ import {
|
|
|
79
111
|
resolveProjectMetadata,
|
|
80
112
|
scanProjectFiles,
|
|
81
113
|
setIssueTransitionExecutor,
|
|
114
|
+
shouldUseFastMode,
|
|
82
115
|
syncReferenceRepositories,
|
|
83
116
|
transitionIssue
|
|
84
|
-
} from "./chunk-
|
|
85
|
-
import {
|
|
86
|
-
computeMetrics,
|
|
87
|
-
computeQualityGateMetrics
|
|
88
|
-
} from "./chunk-MVTGAKQK.js";
|
|
117
|
+
} from "./chunk-2PRRKBG6.js";
|
|
89
118
|
import {
|
|
90
119
|
ADAPTERS,
|
|
91
|
-
applyCheckpointPolicyToPlan,
|
|
92
|
-
applyHarnessModeToPlan,
|
|
93
120
|
buildExecutionPayload,
|
|
94
121
|
buildFullPlanPrompt,
|
|
95
122
|
collectClaudeUsageFromCli,
|
|
96
123
|
collectCodexUsageFromCli,
|
|
97
124
|
collectGeminiUsageFromCli,
|
|
98
125
|
deriveExecutionContract,
|
|
99
|
-
deriveReviewProfile,
|
|
100
126
|
detectAvailableProviders,
|
|
101
127
|
discoverModels,
|
|
102
128
|
getDirtyEventIds,
|
|
@@ -117,20 +143,26 @@ import {
|
|
|
117
143
|
normalizeAcceptanceCriteria,
|
|
118
144
|
normalizeAgentProvider,
|
|
119
145
|
readCodexConfig,
|
|
120
|
-
recommendCheckpointPolicyForIssue,
|
|
121
|
-
recommendHarnessModeForIssue,
|
|
122
146
|
resolveProviderCapabilities,
|
|
123
147
|
snapshotAndClearDirtyEventIds,
|
|
124
148
|
snapshotAndClearDirtyIssueIds,
|
|
125
149
|
snapshotAndClearDirtyIssuePlanIds,
|
|
126
150
|
snapshotAndClearDirtyMilestoneIds
|
|
127
|
-
} from "./chunk-
|
|
151
|
+
} from "./chunk-PI7Y77R3.js";
|
|
128
152
|
import {
|
|
129
|
-
|
|
130
|
-
|
|
153
|
+
computeMetrics,
|
|
154
|
+
computeQualityGateMetrics
|
|
155
|
+
} from "./chunk-QHISYRXJ.js";
|
|
156
|
+
import {
|
|
157
|
+
applyCheckpointPolicyToPlan,
|
|
158
|
+
applyHarnessModeToPlan,
|
|
159
|
+
deriveReviewProfile,
|
|
160
|
+
recommendCheckpointPolicyForIssue,
|
|
161
|
+
recommendHarnessModeForIssue
|
|
162
|
+
} from "./chunk-EBCSQFPR.js";
|
|
131
163
|
import {
|
|
132
164
|
renderPrompt
|
|
133
|
-
} from "./chunk-
|
|
165
|
+
} from "./chunk-AAZKYWOY.js";
|
|
134
166
|
import {
|
|
135
167
|
ATTACHMENTS_ROOT,
|
|
136
168
|
BLUEPRINT_ARTIFACTS_DIRNAME,
|
|
@@ -194,7 +226,10 @@ import {
|
|
|
194
226
|
toNumberValue,
|
|
195
227
|
toStringArray,
|
|
196
228
|
toStringValue
|
|
197
|
-
} from "./chunk-
|
|
229
|
+
} from "./chunk-VM5QAYP5.js";
|
|
230
|
+
import {
|
|
231
|
+
logger
|
|
232
|
+
} from "./chunk-PXTIWKLQ.js";
|
|
198
233
|
import {
|
|
199
234
|
isAgentStillRunning,
|
|
200
235
|
isDaemonAlive,
|
|
@@ -202,48 +237,6 @@ import {
|
|
|
202
237
|
readAgentPid
|
|
203
238
|
} from "./chunk-3NE23NYW.js";
|
|
204
239
|
|
|
205
|
-
// src/domains/service-env.ts
|
|
206
|
-
var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
207
|
-
function isValidServiceEnvKey(key) {
|
|
208
|
-
return ENV_KEY_PATTERN.test(key);
|
|
209
|
-
}
|
|
210
|
-
function normalizeServiceEnvironment(value) {
|
|
211
|
-
if (value === void 0 || value === null) {
|
|
212
|
-
return { env: {}, errors: [] };
|
|
213
|
-
}
|
|
214
|
-
if (typeof value !== "object" || Array.isArray(value)) {
|
|
215
|
-
return { env: {}, errors: ["Environment must be an object map of KEY -> value."] };
|
|
216
|
-
}
|
|
217
|
-
const env3 = {};
|
|
218
|
-
const errors = [];
|
|
219
|
-
for (const [rawKey, rawValue] of Object.entries(value)) {
|
|
220
|
-
const key = rawKey.trim();
|
|
221
|
-
if (!key) continue;
|
|
222
|
-
if (!isValidServiceEnvKey(key)) {
|
|
223
|
-
errors.push(`Invalid environment variable name: ${rawKey}`);
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
env3[key] = rawValue === void 0 || rawValue === null ? "" : String(rawValue);
|
|
227
|
-
}
|
|
228
|
-
return { env: env3, errors };
|
|
229
|
-
}
|
|
230
|
-
function mergeServiceEnvironment(globalEnv, serviceEnv) {
|
|
231
|
-
return {
|
|
232
|
-
...globalEnv ?? {},
|
|
233
|
-
...serviceEnv ?? {}
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
function shellQuoteEnvValue(value) {
|
|
237
|
-
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
238
|
-
}
|
|
239
|
-
function buildServiceCommand(command, globalEnv, serviceEnv) {
|
|
240
|
-
const baseCommand = command.trim();
|
|
241
|
-
if (!baseCommand) return "";
|
|
242
|
-
const env3 = mergeServiceEnvironment(globalEnv, serviceEnv);
|
|
243
|
-
const assignments = Object.entries(env3).map(([key, value]) => `${key}=${shellQuoteEnvValue(value)}`);
|
|
244
|
-
return assignments.length > 0 ? `${assignments.join(" ")} ${baseCommand}` : baseCommand;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
240
|
// src/persistence/plugins/api-runtime-context.ts
|
|
248
241
|
var context = null;
|
|
249
242
|
function setApiRuntimeContext(state) {
|
|
@@ -261,8 +254,8 @@ function getApiRuntimeContextOrThrow() {
|
|
|
261
254
|
|
|
262
255
|
// src/persistence/plugins/api-server.ts
|
|
263
256
|
import {
|
|
264
|
-
existsSync as
|
|
265
|
-
readFileSync as
|
|
257
|
+
existsSync as existsSync19,
|
|
258
|
+
readFileSync as readFileSync17
|
|
266
259
|
} from "fs";
|
|
267
260
|
|
|
268
261
|
// src/concerns/app-shell-routes.ts
|
|
@@ -281,10 +274,11 @@ var APP_SHELL_ROUTES = [
|
|
|
281
274
|
"/settings/notifications",
|
|
282
275
|
"/settings/execution",
|
|
283
276
|
"/settings/quality",
|
|
284
|
-
"/settings/
|
|
277
|
+
"/settings/assets",
|
|
285
278
|
"/settings/services",
|
|
286
279
|
"/settings/appearance",
|
|
287
|
-
"/settings/providers"
|
|
280
|
+
"/settings/providers",
|
|
281
|
+
"/chat"
|
|
288
282
|
];
|
|
289
283
|
|
|
290
284
|
// src/persistence/resources/runtime-state.resource.ts
|
|
@@ -349,6 +343,61 @@ function addTokenUsage(issue, usage, role) {
|
|
|
349
343
|
if (!issue.usage) issue.usage = { tokens: {} };
|
|
350
344
|
issue.usage.tokens[model] = (issue.usage.tokens[model] || 0) + usage.totalTokens;
|
|
351
345
|
}
|
|
346
|
+
function extractJsonEnvelopeResult(text) {
|
|
347
|
+
try {
|
|
348
|
+
const env4 = JSON.parse(text.trim());
|
|
349
|
+
if (env4 && typeof env4 === "object" && typeof env4.result === "string") return env4.result;
|
|
350
|
+
} catch {
|
|
351
|
+
}
|
|
352
|
+
const start = text.indexOf("{");
|
|
353
|
+
if (start < 0) return null;
|
|
354
|
+
const end = text.lastIndexOf("}");
|
|
355
|
+
if (end <= start) return null;
|
|
356
|
+
try {
|
|
357
|
+
const env4 = JSON.parse(text.slice(start, end + 1));
|
|
358
|
+
if (env4 && typeof env4 === "object" && typeof env4.result === "string") return env4.result;
|
|
359
|
+
} catch {
|
|
360
|
+
}
|
|
361
|
+
const m = text.match(/"result"\s*:\s*"([\s\S]+)/);
|
|
362
|
+
if (m) {
|
|
363
|
+
let raw = "";
|
|
364
|
+
let i = 0;
|
|
365
|
+
const src = m[1];
|
|
366
|
+
while (i < src.length) {
|
|
367
|
+
if (src[i] === "\\" && i + 1 < src.length) {
|
|
368
|
+
const next = src[i + 1];
|
|
369
|
+
if (next === "n") {
|
|
370
|
+
raw += "\n";
|
|
371
|
+
i += 2;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (next === "t") {
|
|
375
|
+
raw += " ";
|
|
376
|
+
i += 2;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (next === '"') {
|
|
380
|
+
raw += '"';
|
|
381
|
+
i += 2;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (next === "\\") {
|
|
385
|
+
raw += "\\";
|
|
386
|
+
i += 2;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
raw += next;
|
|
390
|
+
i += 2;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (src[i] === '"') break;
|
|
394
|
+
raw += src[i];
|
|
395
|
+
i += 1;
|
|
396
|
+
}
|
|
397
|
+
if (raw.length > 100) return raw;
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
352
401
|
function extractOutputMarker(output, name) {
|
|
353
402
|
const match = output.match(new RegExp(`^${name}=(.+)$`, "im"));
|
|
354
403
|
return match?.[1]?.trim() ?? "";
|
|
@@ -770,7 +819,7 @@ import {
|
|
|
770
819
|
mkdirSync as mkdirSync8,
|
|
771
820
|
writeFileSync as writeFileSync11
|
|
772
821
|
} from "fs";
|
|
773
|
-
import { join as
|
|
822
|
+
import { join as join16 } from "path";
|
|
774
823
|
|
|
775
824
|
// src/agents/adapters/index.ts
|
|
776
825
|
import { writeFileSync } from "fs";
|
|
@@ -831,6 +880,9 @@ async function compileReview(issue, reviewer, workspacePath, diffSummary, config
|
|
|
831
880
|
const reviewProfile = deriveReviewProfile(issue);
|
|
832
881
|
const scopeConfig = buildReviewScopeConfig(scope);
|
|
833
882
|
const hasFrontendChanges2 = !!playwrightMcpConfigPath;
|
|
883
|
+
const complexity = issue.plan?.estimatedComplexity;
|
|
884
|
+
const harnessMode = issue.plan?.harnessMode ?? "standard";
|
|
885
|
+
const lightReview = (complexity === "trivial" || complexity === "low") && (harnessMode === "solo" || harnessMode === "standard");
|
|
834
886
|
const prompt = await renderPrompt("compile-review", {
|
|
835
887
|
issueIdentifier: issue.identifier,
|
|
836
888
|
title: issue.title,
|
|
@@ -851,7 +903,8 @@ async function compileReview(issue, reviewer, workspacePath, diffSummary, config
|
|
|
851
903
|
diffSummary,
|
|
852
904
|
hasFrontendChanges: hasFrontendChanges2,
|
|
853
905
|
images: issue.images?.length ? issue.images : void 0,
|
|
854
|
-
preReviewValidation: issue.preReviewValidation ?? null
|
|
906
|
+
preReviewValidation: issue.preReviewValidation ?? null,
|
|
907
|
+
lightReview
|
|
855
908
|
});
|
|
856
909
|
const adapter = ADAPTERS[reviewer.provider];
|
|
857
910
|
let command = adapter ? adapter.buildReviewCommand(reviewer, config) : reviewer.command;
|
|
@@ -1332,13 +1385,13 @@ function buildCapabilitiesManifest(skills, agents, commands) {
|
|
|
1332
1385
|
|
|
1333
1386
|
// src/persistence/plugins/fsm-agent.ts
|
|
1334
1387
|
import {
|
|
1335
|
-
existsSync as
|
|
1388
|
+
existsSync as existsSync7,
|
|
1336
1389
|
mkdirSync as mkdirSync7,
|
|
1337
|
-
readFileSync as
|
|
1390
|
+
readFileSync as readFileSync6,
|
|
1338
1391
|
rmSync as rmSync3,
|
|
1339
1392
|
writeFileSync as writeFileSync9
|
|
1340
1393
|
} from "fs";
|
|
1341
|
-
import { join as
|
|
1394
|
+
import { join as join14 } from "path";
|
|
1342
1395
|
import { execSync } from "child_process";
|
|
1343
1396
|
|
|
1344
1397
|
// src/agents/planning/planning-session.ts
|
|
@@ -1667,11 +1720,11 @@ async function getLocalFeatureExtractor(model) {
|
|
|
1667
1720
|
"[Embeddings] Migrated local embedding cache into the shared Fifony cache"
|
|
1668
1721
|
);
|
|
1669
1722
|
}
|
|
1670
|
-
const { pipeline, env:
|
|
1723
|
+
const { pipeline, env: env4 } = await import("@huggingface/transformers");
|
|
1671
1724
|
mkdirSync2(EMBEDDING_LOCAL_CACHE_DIR, { recursive: true });
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1725
|
+
env4.cacheDir = EMBEDDING_LOCAL_CACHE_DIR;
|
|
1726
|
+
env4.allowLocalModels = true;
|
|
1727
|
+
env4.allowRemoteModels = true;
|
|
1675
1728
|
return await pipeline("feature-extraction", model);
|
|
1676
1729
|
})();
|
|
1677
1730
|
localExtractorPromises.set(model, promise);
|
|
@@ -2929,6 +2982,78 @@ async function resolvePlanStageConfig(config) {
|
|
|
2929
2982
|
const model = provider === configuredProvider ? configuredModel : void 0;
|
|
2930
2983
|
return { provider, model, effort: configuredEffort };
|
|
2931
2984
|
}
|
|
2985
|
+
async function resolveEnhanceStageConfig(config) {
|
|
2986
|
+
const providers = detectAvailableProviders();
|
|
2987
|
+
const available = providers.filter((p) => p.available).map((p) => p.name);
|
|
2988
|
+
let configuredProvider;
|
|
2989
|
+
let configuredModel;
|
|
2990
|
+
let configuredEffort;
|
|
2991
|
+
try {
|
|
2992
|
+
const settings = await loadRuntimeSettings();
|
|
2993
|
+
const workflowConfig = getWorkflowConfig(settings);
|
|
2994
|
+
if (workflowConfig?.enhance) {
|
|
2995
|
+
configuredProvider = workflowConfig.enhance.provider;
|
|
2996
|
+
configuredModel = workflowConfig.enhance.model;
|
|
2997
|
+
configuredEffort = workflowConfig.enhance.effort;
|
|
2998
|
+
}
|
|
2999
|
+
} catch {
|
|
3000
|
+
}
|
|
3001
|
+
if (!configuredProvider) {
|
|
3002
|
+
return resolvePlanStageConfig(config);
|
|
3003
|
+
}
|
|
3004
|
+
const provider = configuredProvider && available.includes(configuredProvider) ? configuredProvider : config.agentProvider && available.includes(config.agentProvider) ? config.agentProvider : available[0];
|
|
3005
|
+
if (!provider) throw new Error("No AI provider available for enhance.");
|
|
3006
|
+
const model = provider === configuredProvider ? configuredModel : void 0;
|
|
3007
|
+
return { provider, model, effort: configuredEffort };
|
|
3008
|
+
}
|
|
3009
|
+
async function resolveServicesStageConfig(config) {
|
|
3010
|
+
const providers = detectAvailableProviders();
|
|
3011
|
+
const available = providers.filter((p) => p.available).map((p) => p.name);
|
|
3012
|
+
let configuredProvider;
|
|
3013
|
+
let configuredModel;
|
|
3014
|
+
let configuredEffort;
|
|
3015
|
+
try {
|
|
3016
|
+
const settings = await loadRuntimeSettings();
|
|
3017
|
+
const workflowConfig = getWorkflowConfig(settings);
|
|
3018
|
+
if (workflowConfig?.services) {
|
|
3019
|
+
configuredProvider = workflowConfig.services.provider;
|
|
3020
|
+
configuredModel = workflowConfig.services.model;
|
|
3021
|
+
configuredEffort = workflowConfig.services.effort;
|
|
3022
|
+
}
|
|
3023
|
+
} catch {
|
|
3024
|
+
}
|
|
3025
|
+
if (!configuredProvider) {
|
|
3026
|
+
return resolvePlanStageConfig(config);
|
|
3027
|
+
}
|
|
3028
|
+
const provider = configuredProvider && available.includes(configuredProvider) ? configuredProvider : config.agentProvider && available.includes(config.agentProvider) ? config.agentProvider : available[0];
|
|
3029
|
+
if (!provider) throw new Error("No AI provider available for services.");
|
|
3030
|
+
const model = provider === configuredProvider ? configuredModel : void 0;
|
|
3031
|
+
return { provider, model, effort: configuredEffort };
|
|
3032
|
+
}
|
|
3033
|
+
async function resolveChatStageConfig(config) {
|
|
3034
|
+
const providers = detectAvailableProviders();
|
|
3035
|
+
const available = providers.filter((p) => p.available).map((p) => p.name);
|
|
3036
|
+
let configuredProvider;
|
|
3037
|
+
let configuredModel;
|
|
3038
|
+
let configuredEffort;
|
|
3039
|
+
try {
|
|
3040
|
+
const settings = await loadRuntimeSettings();
|
|
3041
|
+
const workflowConfig = getWorkflowConfig(settings);
|
|
3042
|
+
if (workflowConfig?.chat) {
|
|
3043
|
+
configuredProvider = workflowConfig.chat.provider;
|
|
3044
|
+
configuredModel = workflowConfig.chat.model;
|
|
3045
|
+
configuredEffort = workflowConfig.chat.effort;
|
|
3046
|
+
}
|
|
3047
|
+
} catch {
|
|
3048
|
+
}
|
|
3049
|
+
if (!configuredProvider) {
|
|
3050
|
+
return resolvePlanStageConfig(config);
|
|
3051
|
+
}
|
|
3052
|
+
const provider = configuredProvider && available.includes(configuredProvider) ? configuredProvider : config.agentProvider && available.includes(config.agentProvider) ? config.agentProvider : available[0];
|
|
3053
|
+
if (!provider) throw new Error("No AI provider available for chat.");
|
|
3054
|
+
const model = provider === configuredProvider ? configuredModel : void 0;
|
|
3055
|
+
return { provider, model, effort: configuredEffort };
|
|
3056
|
+
}
|
|
2932
3057
|
var PLAN_TIMEOUT_MS = 18e5;
|
|
2933
3058
|
var PLAN_STALE_OUTPUT_MS = 18e5;
|
|
2934
3059
|
async function runPlanningProcess(options) {
|
|
@@ -3399,13 +3524,8 @@ function completeContractNegotiationRun(issue, round, reviewResult, decision, co
|
|
|
3399
3524
|
}
|
|
3400
3525
|
function extractContractDecision(text) {
|
|
3401
3526
|
const candidates = [text];
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
if (envelope && typeof envelope === "object") {
|
|
3405
|
-
if (typeof envelope.result === "string") candidates.push(envelope.result);
|
|
3406
|
-
}
|
|
3407
|
-
} catch {
|
|
3408
|
-
}
|
|
3527
|
+
const envelopeResult = extractJsonEnvelopeResult(text);
|
|
3528
|
+
if (envelopeResult) candidates.push(envelopeResult);
|
|
3409
3529
|
for (const candidate of candidates) {
|
|
3410
3530
|
const match = candidate.match(/```json contract_decision\n([\s\S]+?)```/);
|
|
3411
3531
|
if (!match) continue;
|
|
@@ -3544,6 +3664,15 @@ async function runContractNegotiation(state, issue, workflowConfig, workspacePat
|
|
|
3544
3664
|
markIssueDirty(issue.id);
|
|
3545
3665
|
return { status: "skipped", approved: true, rounds: 0 };
|
|
3546
3666
|
}
|
|
3667
|
+
const complexity = issue.plan?.estimatedComplexity;
|
|
3668
|
+
if (complexity === "trivial" || complexity === "low") {
|
|
3669
|
+
issue.contractNegotiationStatus = "skipped";
|
|
3670
|
+
issue.contractNegotiationAttempt = 0;
|
|
3671
|
+
issue.planningError = void 0;
|
|
3672
|
+
markIssueDirty(issue.id);
|
|
3673
|
+
addEvent(state, issue.id, "info", `Contract negotiation skipped for ${issue.identifier}: ${complexity} complexity does not warrant negotiation.`);
|
|
3674
|
+
return { status: "skipped", approved: true, rounds: 0 };
|
|
3675
|
+
}
|
|
3547
3676
|
issue.contractNegotiationStatus = "running";
|
|
3548
3677
|
issue.planningError = void 0;
|
|
3549
3678
|
markIssueDirty(issue.id);
|
|
@@ -3826,7 +3955,110 @@ function refinePlanInBackground(state, issue, feedback) {
|
|
|
3826
3955
|
}
|
|
3827
3956
|
|
|
3828
3957
|
// src/domains/validation.ts
|
|
3829
|
-
import { execFile } from "child_process";
|
|
3958
|
+
import { execFile, execFileSync as execFileSync2 } from "child_process";
|
|
3959
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync3 } from "fs";
|
|
3960
|
+
import { join as join12 } from "path";
|
|
3961
|
+
var cachedMonorepoInfo = null;
|
|
3962
|
+
function detectMonorepo(root) {
|
|
3963
|
+
if (cachedMonorepoInfo) return cachedMonorepoInfo;
|
|
3964
|
+
const pm = existsSync6(join12(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync6(join12(root, "yarn.lock")) ? "yarn" : "npm";
|
|
3965
|
+
const packages = /* @__PURE__ */ new Map();
|
|
3966
|
+
const parentDirs = [];
|
|
3967
|
+
const pnpmWs = join12(root, "pnpm-workspace.yaml");
|
|
3968
|
+
if (existsSync6(pnpmWs)) {
|
|
3969
|
+
try {
|
|
3970
|
+
const content = readFileSync5(pnpmWs, "utf8");
|
|
3971
|
+
for (const match of content.matchAll(/^\s+-\s+["']?([^"'\n]+)["']?/gm)) {
|
|
3972
|
+
const glob = match[1].trim().replace(/\/\*.*$/, "");
|
|
3973
|
+
if (glob) parentDirs.push(glob);
|
|
3974
|
+
}
|
|
3975
|
+
} catch {
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
if (parentDirs.length === 0) {
|
|
3979
|
+
try {
|
|
3980
|
+
const rootPkg = JSON.parse(readFileSync5(join12(root, "package.json"), "utf8"));
|
|
3981
|
+
if (Array.isArray(rootPkg.workspaces)) {
|
|
3982
|
+
for (const glob of rootPkg.workspaces) {
|
|
3983
|
+
const parent = String(glob).replace(/\/\*.*$/, "");
|
|
3984
|
+
if (parent) parentDirs.push(parent);
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
} catch {
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
if (parentDirs.length === 0) {
|
|
3991
|
+
cachedMonorepoInfo = { isMonorepo: false, pm, packages };
|
|
3992
|
+
return cachedMonorepoInfo;
|
|
3993
|
+
}
|
|
3994
|
+
for (const parent of parentDirs) {
|
|
3995
|
+
const absParent = join12(root, parent);
|
|
3996
|
+
if (!existsSync6(absParent)) continue;
|
|
3997
|
+
try {
|
|
3998
|
+
for (const child of readdirSync3(absParent, { withFileTypes: true })) {
|
|
3999
|
+
if (!child.isDirectory()) continue;
|
|
4000
|
+
const pkgJsonPath = join12(absParent, child.name, "package.json");
|
|
4001
|
+
if (!existsSync6(pkgJsonPath)) continue;
|
|
4002
|
+
try {
|
|
4003
|
+
const pkg = JSON.parse(readFileSync5(pkgJsonPath, "utf8"));
|
|
4004
|
+
const name = typeof pkg.name === "string" ? pkg.name : child.name;
|
|
4005
|
+
packages.set(`${parent}/${child.name}`, name);
|
|
4006
|
+
} catch {
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
} catch {
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
cachedMonorepoInfo = { isMonorepo: true, pm, packages };
|
|
4013
|
+
return cachedMonorepoInfo;
|
|
4014
|
+
}
|
|
4015
|
+
function getChangedFiles(issue) {
|
|
4016
|
+
const cwd = issue.worktreePath;
|
|
4017
|
+
if (!cwd || !issue.baseBranch) return [];
|
|
4018
|
+
try {
|
|
4019
|
+
const out = execFileSync2("git", ["diff", "--name-only", `${issue.baseBranch}...HEAD`], {
|
|
4020
|
+
cwd,
|
|
4021
|
+
encoding: "utf8",
|
|
4022
|
+
timeout: 1e4
|
|
4023
|
+
});
|
|
4024
|
+
return out.trim().split("\n").filter(Boolean);
|
|
4025
|
+
} catch {
|
|
4026
|
+
return [];
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
function affectedPackages(changedFiles, packages) {
|
|
4030
|
+
const affected = /* @__PURE__ */ new Set();
|
|
4031
|
+
for (const file of changedFiles) {
|
|
4032
|
+
for (const [dir, name] of packages) {
|
|
4033
|
+
if (file.startsWith(dir + "/")) {
|
|
4034
|
+
affected.add(name);
|
|
4035
|
+
break;
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
return [...affected];
|
|
4040
|
+
}
|
|
4041
|
+
function buildScopedTestCommand(issue, baseCommand) {
|
|
4042
|
+
const root = TARGET_ROOT;
|
|
4043
|
+
const mono = detectMonorepo(root);
|
|
4044
|
+
if (!mono.isMonorepo || mono.packages.size === 0) return null;
|
|
4045
|
+
const changedFiles = getChangedFiles(issue);
|
|
4046
|
+
if (changedFiles.length === 0) return null;
|
|
4047
|
+
const affected = affectedPackages(changedFiles, mono.packages);
|
|
4048
|
+
if (affected.length === 0) {
|
|
4049
|
+
logger.info({ issueId: issue.id, changedFiles: changedFiles.length }, "[Validation] Changed files don't belong to any workspace package \u2014 skipping gate");
|
|
4050
|
+
return "true";
|
|
4051
|
+
}
|
|
4052
|
+
if (mono.pm === "pnpm") {
|
|
4053
|
+
const filters = affected.map((pkg) => `--filter "${pkg}"`).join(" ");
|
|
4054
|
+
return `pnpm ${filters} test`;
|
|
4055
|
+
}
|
|
4056
|
+
if (mono.pm === "yarn") {
|
|
4057
|
+
const includes = affected.map((pkg) => `--include "${pkg}"`).join(" ");
|
|
4058
|
+
return `yarn workspaces foreach ${includes} run test`;
|
|
4059
|
+
}
|
|
4060
|
+
return affected.map((pkg) => `npm -w "${pkg}" test`).join(" && ");
|
|
4061
|
+
}
|
|
3830
4062
|
async function runValidationGate(issue, config) {
|
|
3831
4063
|
if (!config.testCommand) return null;
|
|
3832
4064
|
const cwd = issue.worktreePath ?? issue.workspacePath;
|
|
@@ -3834,8 +4066,9 @@ async function runValidationGate(issue, config) {
|
|
|
3834
4066
|
logger.warn({ issueId: issue.id }, "[Validation] No workspace path \u2014 skipping gate");
|
|
3835
4067
|
return null;
|
|
3836
4068
|
}
|
|
3837
|
-
const
|
|
3838
|
-
|
|
4069
|
+
const scopedCommand = buildScopedTestCommand(issue, config.testCommand);
|
|
4070
|
+
const command = scopedCommand ?? config.testCommand;
|
|
4071
|
+
logger.info({ issueId: issue.id, command, scoped: !!scopedCommand, cwd }, "[Validation] Running validation gate");
|
|
3839
4072
|
return new Promise((resolve5) => {
|
|
3840
4073
|
execFile("sh", ["-c", command], {
|
|
3841
4074
|
cwd,
|
|
@@ -3845,7 +4078,7 @@ async function runValidationGate(issue, config) {
|
|
|
3845
4078
|
}, (err, stdout, stderr) => {
|
|
3846
4079
|
const combined = (stdout || "") + (stderr || "");
|
|
3847
4080
|
if (!err) {
|
|
3848
|
-
logger.info({ issueId: issue.id }, "[Validation] Gate passed");
|
|
4081
|
+
logger.info({ issueId: issue.id, command }, "[Validation] Gate passed");
|
|
3849
4082
|
resolve5({
|
|
3850
4083
|
passed: true,
|
|
3851
4084
|
output: combined.slice(-2048),
|
|
@@ -3854,7 +4087,7 @@ async function runValidationGate(issue, config) {
|
|
|
3854
4087
|
});
|
|
3855
4088
|
return;
|
|
3856
4089
|
}
|
|
3857
|
-
logger.warn({ issueId: issue.id, exitCode: err.code }, "[Validation] Gate failed");
|
|
4090
|
+
logger.warn({ issueId: issue.id, exitCode: err.code, command }, "[Validation] Gate failed");
|
|
3858
4091
|
resolve5({
|
|
3859
4092
|
passed: false,
|
|
3860
4093
|
output: combined.slice(-2048) || String(err).slice(0, 2048),
|
|
@@ -3985,6 +4218,7 @@ var waiters = [];
|
|
|
3985
4218
|
var staleInterval = null;
|
|
3986
4219
|
var persistInterval = null;
|
|
3987
4220
|
var analyticsInterval = null;
|
|
4221
|
+
var blockedRetryInterval = null;
|
|
3988
4222
|
async function initQueueWorkers(state) {
|
|
3989
4223
|
runtimeState = state;
|
|
3990
4224
|
active = true;
|
|
@@ -3996,13 +4230,13 @@ async function initQueueWorkers(state) {
|
|
|
3996
4230
|
}, 3e4);
|
|
3997
4231
|
persistInterval = setInterval(() => {
|
|
3998
4232
|
if (!active || !runtimeState) return;
|
|
3999
|
-
import("./store-
|
|
4233
|
+
import("./store-S3NAYZ3S.js").then(
|
|
4000
4234
|
({ persistState: persistState2 }) => persistState2(runtimeState).catch(() => {
|
|
4001
4235
|
})
|
|
4002
4236
|
).catch(() => {
|
|
4003
4237
|
});
|
|
4004
4238
|
}, 5e3);
|
|
4005
|
-
import("./analytics-broadcaster-
|
|
4239
|
+
import("./analytics-broadcaster-O4AE3RUK.js").then(({ initAnalyticsBroadcaster, pushAllAnalytics }) => {
|
|
4006
4240
|
initAnalyticsBroadcaster(state);
|
|
4007
4241
|
analyticsInterval = setInterval(() => {
|
|
4008
4242
|
if (!active || !runtimeState) return;
|
|
@@ -4011,6 +4245,12 @@ async function initQueueWorkers(state) {
|
|
|
4011
4245
|
);
|
|
4012
4246
|
}, 3e4);
|
|
4013
4247
|
}).catch((err) => logger.error({ err }, "[Queue] Failed to init analytics broadcaster"));
|
|
4248
|
+
blockedRetryInterval = setInterval(() => {
|
|
4249
|
+
if (!active || !runtimeState) return;
|
|
4250
|
+
autoRetryBlockedIssues(runtimeState).catch(
|
|
4251
|
+
(err) => logger.error({ err }, "[Queue] Blocked retry check failed")
|
|
4252
|
+
);
|
|
4253
|
+
}, 1e4);
|
|
4014
4254
|
logger.info("[Queue] Unified work queue ready");
|
|
4015
4255
|
}
|
|
4016
4256
|
async function stopQueueWorkers() {
|
|
@@ -4027,6 +4267,10 @@ async function stopQueueWorkers() {
|
|
|
4027
4267
|
clearInterval(analyticsInterval);
|
|
4028
4268
|
analyticsInterval = null;
|
|
4029
4269
|
}
|
|
4270
|
+
if (blockedRetryInterval) {
|
|
4271
|
+
clearInterval(blockedRetryInterval);
|
|
4272
|
+
blockedRetryInterval = null;
|
|
4273
|
+
}
|
|
4030
4274
|
runtimeState = null;
|
|
4031
4275
|
queue.length = 0;
|
|
4032
4276
|
waiters.length = 0;
|
|
@@ -4092,9 +4336,27 @@ async function dispatchReview(issue) {
|
|
|
4092
4336
|
}
|
|
4093
4337
|
async function checkStaleIssues() {
|
|
4094
4338
|
if (!runtimeState) return;
|
|
4095
|
-
const { ensureNotStale: ensureNotStale2 } = await import("./scheduler-
|
|
4339
|
+
const { ensureNotStale: ensureNotStale2 } = await import("./scheduler-KYILMWLD.js");
|
|
4096
4340
|
await ensureNotStale2(runtimeState, runtimeState.config.staleInProgressTimeoutMs);
|
|
4097
4341
|
}
|
|
4342
|
+
async function autoRetryBlockedIssues(state) {
|
|
4343
|
+
const now3 = Date.now();
|
|
4344
|
+
for (const issue of state.issues) {
|
|
4345
|
+
if (issue.state !== "Blocked") continue;
|
|
4346
|
+
if (!issue.nextRetryAt) continue;
|
|
4347
|
+
if (issue.attempts >= issue.maxAttempts) continue;
|
|
4348
|
+
const retryAt = new Date(issue.nextRetryAt).getTime();
|
|
4349
|
+
if (isNaN(retryAt) || retryAt > now3) continue;
|
|
4350
|
+
try {
|
|
4351
|
+
const { transitionIssue: transitionIssue2 } = await import("./issues-3YNNTB4U.js");
|
|
4352
|
+
issue.nextRetryAt = void 0;
|
|
4353
|
+
await transitionIssue2(issue, "UNBLOCK", { note: `Auto-retry: nextRetryAt reached.` });
|
|
4354
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier }, "[Queue] Auto-retried blocked issue");
|
|
4355
|
+
} catch (err) {
|
|
4356
|
+
logger.warn({ err: String(err), issueId: issue.id }, "[Queue] Failed to auto-retry blocked issue");
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4098
4360
|
var draining = false;
|
|
4099
4361
|
async function drain() {
|
|
4100
4362
|
if (draining) return;
|
|
@@ -4179,8 +4441,8 @@ async function recoverState() {
|
|
|
4179
4441
|
}
|
|
4180
4442
|
async function recoverOrphans() {
|
|
4181
4443
|
if (!runtimeState) return;
|
|
4182
|
-
const { isAgentStillRunning: isAgentStillRunning2, cleanStalePidFile: cleanStalePidFile2, isDaemonAlive: isDaemonAlive2, isDaemonSocketReady: isDaemonSocketReady2 } = await import("./agent-
|
|
4183
|
-
const { addEvent: addEvent2 } = await import("./issues-
|
|
4444
|
+
const { isAgentStillRunning: isAgentStillRunning2, cleanStalePidFile: cleanStalePidFile2, isDaemonAlive: isDaemonAlive2, isDaemonSocketReady: isDaemonSocketReady2 } = await import("./agent-DFSFG6DG.js");
|
|
4445
|
+
const { addEvent: addEvent2 } = await import("./issues-3YNNTB4U.js");
|
|
4184
4446
|
const candidates = runtimeState.issues.filter((i) => i.state === "Running" || i.state === "Queued");
|
|
4185
4447
|
logger.debug({ count: candidates.length }, "[Queue] Checking for orphaned agent processes");
|
|
4186
4448
|
for (const issue of candidates) {
|
|
@@ -4241,7 +4503,7 @@ function cleanTerminalWorkspaces() {
|
|
|
4241
4503
|
logger.info({ count: terminals.length }, "[Queue] Scheduling terminal workspace cleanup in background");
|
|
4242
4504
|
const state = runtimeState;
|
|
4243
4505
|
setImmediate(async () => {
|
|
4244
|
-
const { cleanWorkspace: cleanWorkspace2 } = await import("./agent-
|
|
4506
|
+
const { cleanWorkspace: cleanWorkspace2 } = await import("./agent-DFSFG6DG.js");
|
|
4245
4507
|
for (const issue of terminals) {
|
|
4246
4508
|
try {
|
|
4247
4509
|
await cleanWorkspace2(issue.id, issue, state);
|
|
@@ -4285,7 +4547,8 @@ function createContainer(state) {
|
|
|
4285
4547
|
});
|
|
4286
4548
|
setEnqueueFn((issue, job) => enqueue(issue, job));
|
|
4287
4549
|
setPersistNowFn(() => {
|
|
4288
|
-
persistState(state).catch(() => {
|
|
4550
|
+
persistState(state).catch((err) => {
|
|
4551
|
+
console.error("[PersistNow] Failed to persist state after FSM transition:", err);
|
|
4289
4552
|
});
|
|
4290
4553
|
});
|
|
4291
4554
|
setIssueTransitionExecutor((issue, event, context2) => executeTransition(issue, event, context2));
|
|
@@ -4296,34 +4559,6 @@ function getContainer() {
|
|
|
4296
4559
|
return _container;
|
|
4297
4560
|
}
|
|
4298
4561
|
|
|
4299
|
-
// src/commands/request-rework.command.ts
|
|
4300
|
-
async function requestReworkCommand(input, deps) {
|
|
4301
|
-
const { issue, reviewerFeedback, note, eventKind } = input;
|
|
4302
|
-
if (issue.state !== "Reviewing" && issue.state !== "PendingDecision") {
|
|
4303
|
-
throw new Error(
|
|
4304
|
-
`requestReworkCommand requires Reviewing or PendingDecision state, got ${issue.state}.`
|
|
4305
|
-
);
|
|
4306
|
-
}
|
|
4307
|
-
const archivalFeedback = reviewerFeedback.trim() || note?.trim() || issue.lastError || "Manual rework request.";
|
|
4308
|
-
issue.lastError = archivalFeedback;
|
|
4309
|
-
issue.lastFailedPhase = "review";
|
|
4310
|
-
if (issue.state === "Reviewing") {
|
|
4311
|
-
await transitionIssueCommand(
|
|
4312
|
-
{ issue, target: "PendingDecision", note: `Reviewer completed for ${issue.identifier}.` },
|
|
4313
|
-
deps
|
|
4314
|
-
);
|
|
4315
|
-
}
|
|
4316
|
-
await transitionIssueCommand(
|
|
4317
|
-
{ issue, target: "Queued", note: archivalFeedback },
|
|
4318
|
-
deps
|
|
4319
|
-
);
|
|
4320
|
-
deps.eventStore.addEvent(
|
|
4321
|
-
issue.id,
|
|
4322
|
-
eventKind ?? "runner",
|
|
4323
|
-
note ?? `Issue ${issue.identifier} sent back for rework by reviewer.`
|
|
4324
|
-
);
|
|
4325
|
-
}
|
|
4326
|
-
|
|
4327
4562
|
// src/commands/agent-issue-outcomes.command.ts
|
|
4328
4563
|
async function sendIssueToManualDecisionCommand(issue, note, deps) {
|
|
4329
4564
|
await transitionIssueCommand({ issue, target: "PendingDecision", note }, deps);
|
|
@@ -4337,13 +4572,10 @@ async function startIssueReviewCommand(issue, note, deps) {
|
|
|
4337
4572
|
async function blockIssueForRetryCommand(issue, note, deps) {
|
|
4338
4573
|
await transitionIssueCommand({ issue, target: "Blocked", note }, deps);
|
|
4339
4574
|
}
|
|
4340
|
-
async function cancelIssueFromAgentCommand(issue, note, deps) {
|
|
4341
|
-
await transitionIssueCommand({ issue, target: "Cancelled", note }, deps);
|
|
4342
|
-
}
|
|
4343
4575
|
|
|
4344
4576
|
// src/agents/blueprints.ts
|
|
4345
4577
|
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
4346
|
-
import { join as
|
|
4578
|
+
import { join as join13 } from "path";
|
|
4347
4579
|
import { randomUUID } from "crypto";
|
|
4348
4580
|
var EXECUTION_NODE_IDS = {
|
|
4349
4581
|
ingestContext: "ingest_context",
|
|
@@ -4462,17 +4694,17 @@ function buildHarnessBlueprint(plan, config) {
|
|
|
4462
4694
|
};
|
|
4463
4695
|
}
|
|
4464
4696
|
function resolveBlueprintArtifactsRoot(workspacePath) {
|
|
4465
|
-
return
|
|
4697
|
+
return join13(workspacePath, BLUEPRINT_ARTIFACTS_DIRNAME);
|
|
4466
4698
|
}
|
|
4467
4699
|
function ensureNodeDir(workspacePath, runId, nodeId) {
|
|
4468
|
-
const dir =
|
|
4700
|
+
const dir = join13(resolveBlueprintArtifactsRoot(workspacePath), runId, nodeId);
|
|
4469
4701
|
mkdirSync6(dir, { recursive: true });
|
|
4470
4702
|
return dir;
|
|
4471
4703
|
}
|
|
4472
4704
|
function writeBlueprintArtifact(workspacePath, runId, nodeId, kind, content, extension = "md") {
|
|
4473
4705
|
const dir = ensureNodeDir(workspacePath, runId, nodeId);
|
|
4474
4706
|
const fileName = `${kind}.${extension}`;
|
|
4475
|
-
const absolutePath =
|
|
4707
|
+
const absolutePath = join13(dir, fileName);
|
|
4476
4708
|
writeFileSync8(absolutePath, content, "utf8");
|
|
4477
4709
|
return {
|
|
4478
4710
|
id: `${nodeId}:${kind}`,
|
|
@@ -4577,16 +4809,16 @@ var BLUEPRINT_EXECUTION_NODE_IDS = EXECUTION_NODE_IDS;
|
|
|
4577
4809
|
// src/persistence/plugins/fsm-agent.ts
|
|
4578
4810
|
var AGENT_WATCHER_INTERVAL_MS = 5e3;
|
|
4579
4811
|
function jobStatePath(fifonyDir, issueId) {
|
|
4580
|
-
return
|
|
4812
|
+
return join14(fifonyDir, `agent-${idToSafePath(issueId)}.job.json`);
|
|
4581
4813
|
}
|
|
4582
4814
|
function agentLogPath(workspacePath) {
|
|
4583
|
-
return
|
|
4815
|
+
return join14(workspacePath, "live-output.log");
|
|
4584
4816
|
}
|
|
4585
4817
|
function readJobState(fifonyDir, issueId) {
|
|
4586
4818
|
const path = jobStatePath(fifonyDir, issueId);
|
|
4587
|
-
if (!
|
|
4819
|
+
if (!existsSync7(path)) return null;
|
|
4588
4820
|
try {
|
|
4589
|
-
return JSON.parse(
|
|
4821
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
4590
4822
|
} catch {
|
|
4591
4823
|
return null;
|
|
4592
4824
|
}
|
|
@@ -4630,7 +4862,7 @@ function getAgentStatus(fifonyDir, issueId, identifier) {
|
|
|
4630
4862
|
let pid = null;
|
|
4631
4863
|
let actualState = job.state;
|
|
4632
4864
|
if (job.state === "running" || job.state === "preparing") {
|
|
4633
|
-
if (job.workspacePath &&
|
|
4865
|
+
if (job.workspacePath && existsSync7(job.workspacePath)) {
|
|
4634
4866
|
const pidInfo = readAgentPid(job.workspacePath);
|
|
4635
4867
|
if (pidInfo && isProcessAlive(pidInfo.pid)) {
|
|
4636
4868
|
pid = pidInfo.pid;
|
|
@@ -4734,7 +4966,7 @@ function buildReplanFailureContext(issue) {
|
|
|
4734
4966
|
async function runPlanPhase(state, issue, fifonyDir = STATE_ROOT, onTransition) {
|
|
4735
4967
|
const _op = deriveAgentOperation(issue);
|
|
4736
4968
|
const safeId = idToSafePath(issue.id);
|
|
4737
|
-
const workspaceDir =
|
|
4969
|
+
const workspaceDir = join14(WORKSPACE_ROOT, safeId);
|
|
4738
4970
|
const _job = {
|
|
4739
4971
|
issueId: issue.id,
|
|
4740
4972
|
identifier: issue.identifier,
|
|
@@ -4757,7 +4989,7 @@ async function runPlanPhase(state, issue, fifonyDir = STATE_ROOT, onTransition)
|
|
|
4757
4989
|
issue.planningError = void 0;
|
|
4758
4990
|
issue.updatedAt = now();
|
|
4759
4991
|
markIssueDirty(issue.id);
|
|
4760
|
-
import("./store-
|
|
4992
|
+
import("./store-S3NAYZ3S.js").then(({ persistState: persistState2 }) => persistState2(state).catch(() => {
|
|
4761
4993
|
})).catch(() => {
|
|
4762
4994
|
});
|
|
4763
4995
|
mkdirSync7(workspaceDir, { recursive: true });
|
|
@@ -4785,12 +5017,16 @@ async function runPlanPhase(state, issue, fifonyDir = STATE_ROOT, onTransition)
|
|
|
4785
5017
|
if (failureContext) {
|
|
4786
5018
|
addEvent(state, issue.id, "info", `Injecting replan failure context into plan prompt (v${issue.planVersion ?? 1}).`);
|
|
4787
5019
|
}
|
|
5020
|
+
const fast = shouldUseFastMode(issue);
|
|
5021
|
+
if (fast) {
|
|
5022
|
+
addEvent(state, issue.id, "info", `Fast mode activated for ${issue.identifier} (type: ${issue.issueType ?? "unset"}, desc length: ${(issue.description ?? "").length}).`);
|
|
5023
|
+
}
|
|
4788
5024
|
const { plan, usage, prompt } = await generatePlan(
|
|
4789
5025
|
issue.title,
|
|
4790
5026
|
issue.description,
|
|
4791
5027
|
state.config,
|
|
4792
5028
|
null,
|
|
4793
|
-
{ persistSession: false, failureContext }
|
|
5029
|
+
{ fast, persistSession: false, failureContext }
|
|
4794
5030
|
);
|
|
4795
5031
|
const plannedIssue = {
|
|
4796
5032
|
...issue,
|
|
@@ -4856,7 +5092,7 @@ async function runPlanPhase(state, issue, fifonyDir = STATE_ROOT, onTransition)
|
|
|
4856
5092
|
issue.plan = plan;
|
|
4857
5093
|
issue.planVersion = Math.max(issue.planVersion ?? 0, 1);
|
|
4858
5094
|
try {
|
|
4859
|
-
const { savePlanForIssue: savePlanForIssue2 } = await import("./store-
|
|
5095
|
+
const { savePlanForIssue: savePlanForIssue2 } = await import("./store-S3NAYZ3S.js");
|
|
4860
5096
|
await savePlanForIssue2(issue.id, plan, issue.planVersion);
|
|
4861
5097
|
logger.debug({ issueId: issue.id, planVersion: issue.planVersion }, "[AgentFSM] Plan saved to issue_plans resource");
|
|
4862
5098
|
} catch (err) {
|
|
@@ -4874,8 +5110,8 @@ async function runPlanPhase(state, issue, fifonyDir = STATE_ROOT, onTransition)
|
|
|
4874
5110
|
}
|
|
4875
5111
|
const pv = issue.planVersion;
|
|
4876
5112
|
try {
|
|
4877
|
-
writeFileSync9(
|
|
4878
|
-
writeFileSync9(
|
|
5113
|
+
writeFileSync9(join14(workspaceDir, `plan.v${pv}.json`), JSON.stringify(plan, null, 2), "utf8");
|
|
5114
|
+
writeFileSync9(join14(workspaceDir, `plan.v${pv}.prompt.md`), prompt, "utf8");
|
|
4879
5115
|
} catch (artifactErr) {
|
|
4880
5116
|
logger.warn({ err: String(artifactErr) }, "[AgentFSM] Failed to write versioned plan artifacts");
|
|
4881
5117
|
}
|
|
@@ -4895,8 +5131,21 @@ async function runPlanPhase(state, issue, fifonyDir = STATE_ROOT, onTransition)
|
|
|
4895
5131
|
logger.warn({ issueId: issue.id, identifier: issue.identifier }, "[AgentFSM] Plan job completed for stale issue reference \u2014 skipping PLANNED transition");
|
|
4896
5132
|
} else {
|
|
4897
5133
|
try {
|
|
4898
|
-
const { transitionIssue: transitionIssue2 } = await import("./issues-
|
|
5134
|
+
const { transitionIssue: transitionIssue2 } = await import("./issues-3YNNTB4U.js");
|
|
4899
5135
|
await transitionIssue2(issue, "PLANNED", { issue });
|
|
5136
|
+
if (state.config.autoApproveTrivialPlans !== false) {
|
|
5137
|
+
const complexity = issue.plan?.estimatedComplexity;
|
|
5138
|
+
if (complexity === "trivial" || complexity === "low") {
|
|
5139
|
+
try {
|
|
5140
|
+
await transitionIssueCommand(
|
|
5141
|
+
{ issue, target: "Queued", note: `Auto-approved ${complexity} plan for ${issue.identifier}.` }
|
|
5142
|
+
);
|
|
5143
|
+
addEvent(state, issue.id, "info", `Auto-approved ${complexity} plan for ${issue.identifier}.`);
|
|
5144
|
+
} catch (autoErr) {
|
|
5145
|
+
logger.warn({ err: autoErr, issueId: issue.id }, "[AgentFSM] Auto-approve failed, staying in PendingApproval");
|
|
5146
|
+
}
|
|
5147
|
+
}
|
|
5148
|
+
}
|
|
4900
5149
|
} catch (transErr) {
|
|
4901
5150
|
logger.warn({ err: transErr, issueId: issue.id }, "[AgentFSM] PLANNED transition failed after plan generation");
|
|
4902
5151
|
}
|
|
@@ -4915,13 +5164,19 @@ async function runPlanPhase(state, issue, fifonyDir = STATE_ROOT, onTransition)
|
|
|
4915
5164
|
}
|
|
4916
5165
|
}
|
|
4917
5166
|
function extractGradingReport(text) {
|
|
4918
|
-
const
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
5167
|
+
const candidates = [text];
|
|
5168
|
+
const envelopeResult = extractJsonEnvelopeResult(text);
|
|
5169
|
+
if (envelopeResult) candidates.push(envelopeResult);
|
|
5170
|
+
for (const candidate of candidates) {
|
|
5171
|
+
const match = candidate.match(/```json grading_report\n([\s\S]+?)```/);
|
|
5172
|
+
if (!match) continue;
|
|
5173
|
+
try {
|
|
5174
|
+
return JSON.parse(match[1]);
|
|
5175
|
+
} catch {
|
|
5176
|
+
continue;
|
|
5177
|
+
}
|
|
4924
5178
|
}
|
|
5179
|
+
return null;
|
|
4925
5180
|
}
|
|
4926
5181
|
function buildGradingFailureSummary(report, failureScope = "all") {
|
|
4927
5182
|
const failed = report.criteria.filter((c) => c.result === "FAIL" && (failureScope === "all" || c.blocking));
|
|
@@ -4931,10 +5186,21 @@ function buildGradingFailureSummary(report, failureScope = "all") {
|
|
|
4931
5186
|
function resolveHarnessMode(issue) {
|
|
4932
5187
|
return issue.plan?.harnessMode ?? "standard";
|
|
4933
5188
|
}
|
|
5189
|
+
var COMPLEXITY_TURN_FACTOR = {
|
|
5190
|
+
trivial: 0.3,
|
|
5191
|
+
// solo 10 → 3, standard 20 → 6
|
|
5192
|
+
low: 0.5,
|
|
5193
|
+
// solo 10 → 5, standard 20 → 10
|
|
5194
|
+
medium: 1,
|
|
5195
|
+
high: 1
|
|
5196
|
+
};
|
|
4934
5197
|
function resolveMaxTurns(issue, config) {
|
|
4935
5198
|
if (config.maxTurns) return config.maxTurns;
|
|
4936
5199
|
const mode = resolveHarnessMode(issue);
|
|
4937
|
-
|
|
5200
|
+
const base = DEFAULT_MAX_TURNS_BY_MODE[mode] ?? DEFAULT_MAX_TURNS;
|
|
5201
|
+
const complexity = issue.plan?.estimatedComplexity;
|
|
5202
|
+
const factor = (complexity && COMPLEXITY_TURN_FACTOR[complexity]) ?? 1;
|
|
5203
|
+
return Math.max(3, Math.round(base * factor));
|
|
4938
5204
|
}
|
|
4939
5205
|
function requiresCheckpointReview(issue) {
|
|
4940
5206
|
return resolveHarnessMode(issue) === "contractual" && issue.plan?.executionContract?.checkpointPolicy === "checkpointed" && !issue.checkpointPassedAt;
|
|
@@ -5003,16 +5269,21 @@ async function finalizeReviewSuccess(state, issue, container, completionNote) {
|
|
|
5003
5269
|
issue.mergedReason = autoReviewApproval ? completionNote : `${completionNote} Waiting for manual approval.`;
|
|
5004
5270
|
await sendIssueToManualDecisionCommand(issue, completionNote, container);
|
|
5005
5271
|
if (!autoReviewApproval) return;
|
|
5006
|
-
const
|
|
5007
|
-
if (
|
|
5008
|
-
issue.
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5272
|
+
const preReviewAlreadyPassed = issue.preReviewValidation?.passed === true;
|
|
5273
|
+
if (preReviewAlreadyPassed) {
|
|
5274
|
+
addEvent(state, issue.id, "info", `Post-approval validation skipped for ${issue.identifier}: pre-review gate already passed.`);
|
|
5275
|
+
} else {
|
|
5276
|
+
const validation = await runValidationGate(issue, state.config);
|
|
5277
|
+
if (validation) {
|
|
5278
|
+
issue.validationResult = validation;
|
|
5279
|
+
markIssueDirty(issue.id);
|
|
5280
|
+
if (!validation.passed) {
|
|
5281
|
+
addEvent(state, issue.id, "error", `Validation gate failed for ${issue.identifier}: ${validation.command}`);
|
|
5282
|
+
logger.warn({ issueId: issue.id, command: validation.command }, "[AgentFSM] Validation gate failed after successful review path");
|
|
5283
|
+
return;
|
|
5284
|
+
}
|
|
5285
|
+
addEvent(state, issue.id, "info", `Validation gate passed for ${issue.identifier}.`);
|
|
5014
5286
|
}
|
|
5015
|
-
addEvent(state, issue.id, "info", `Validation gate passed for ${issue.identifier}.`);
|
|
5016
5287
|
}
|
|
5017
5288
|
await approveIssueAfterReviewCommand(issue, completionNote, container);
|
|
5018
5289
|
}
|
|
@@ -5028,8 +5299,8 @@ function ensurePlaywrightMcpConfig(stateRoot) {
|
|
|
5028
5299
|
} catch {
|
|
5029
5300
|
return null;
|
|
5030
5301
|
}
|
|
5031
|
-
const configPath =
|
|
5032
|
-
if (!
|
|
5302
|
+
const configPath = join14(stateRoot, "playwright-mcp.json");
|
|
5303
|
+
if (!existsSync7(configPath)) {
|
|
5033
5304
|
try {
|
|
5034
5305
|
writeFileSync9(configPath, JSON.stringify({
|
|
5035
5306
|
mcpServers: {
|
|
@@ -5054,7 +5325,7 @@ function resolveReviewArtifactPrefix(issue, scope) {
|
|
|
5054
5325
|
return `${scope}.v${planVersion}a${attempt}`;
|
|
5055
5326
|
}
|
|
5056
5327
|
function resolveReviewPromptPath(workspacePath, scope) {
|
|
5057
|
-
return
|
|
5328
|
+
return join14(workspacePath, scope === "checkpoint" ? "checkpoint-review-prompt.md" : "review-prompt.md");
|
|
5058
5329
|
}
|
|
5059
5330
|
function resolveReviewRunId(issue, scope) {
|
|
5060
5331
|
return `review.${resolveReviewArtifactPrefix(issue, scope)}`;
|
|
@@ -5248,13 +5519,17 @@ async function runScopedReviewEvaluation(state, issue, workspacePath, reviewer,
|
|
|
5248
5519
|
issue.commandExitCode = reviewResult.code;
|
|
5249
5520
|
issue.commandOutputTail = reviewResult.output;
|
|
5250
5521
|
const rawGradingReport = extractGradingReport(reviewResult.output);
|
|
5251
|
-
const
|
|
5522
|
+
const isReviewerCrash = !rawGradingReport && !reviewResult.success && resolveHarnessMode(issue) === "contractual";
|
|
5523
|
+
const gradingReport = rawGradingReport ? applyHarnessReviewPolicy(issue, rawGradingReport, scope) : resolveHarnessMode(issue) === "contractual" && !isReviewerCrash ? applyHarnessReviewPolicy(issue, {
|
|
5252
5524
|
scope,
|
|
5253
5525
|
overallVerdict: "FAIL",
|
|
5254
5526
|
blockingVerdict: "FAIL",
|
|
5255
5527
|
reviewAttempt: resolveReviewAttemptNumber(issue, scope),
|
|
5256
5528
|
criteria: []
|
|
5257
5529
|
}, scope) : null;
|
|
5530
|
+
if (isReviewerCrash) {
|
|
5531
|
+
addEvent(state, issue.id, "error", `Reviewer crashed or produced no grading report for ${issue.identifier}. Treating as infrastructure failure, not code quality FAIL.`);
|
|
5532
|
+
}
|
|
5258
5533
|
if (gradingReport) {
|
|
5259
5534
|
gradingReport.scope = scope;
|
|
5260
5535
|
gradingReport.reviewAttempt = resolveReviewAttemptNumber(issue, scope);
|
|
@@ -5271,12 +5546,12 @@ async function runScopedReviewEvaluation(state, issue, workspacePath, reviewer,
|
|
|
5271
5546
|
try {
|
|
5272
5547
|
const artifactPrefix = resolveReviewArtifactPrefix(issue, scope);
|
|
5273
5548
|
const reviewPromptSrc = resolveReviewPromptPath(workspacePath, scope);
|
|
5274
|
-
const reviewAuditSrc =
|
|
5275
|
-
if (
|
|
5276
|
-
writeFileSync9(
|
|
5549
|
+
const reviewAuditSrc = join14(workspacePath, "execution-audit.json");
|
|
5550
|
+
if (existsSync7(reviewPromptSrc)) {
|
|
5551
|
+
writeFileSync9(join14(workspacePath, `${artifactPrefix}.prompt.md`), readFileSync6(reviewPromptSrc, "utf8"), "utf8");
|
|
5277
5552
|
}
|
|
5278
|
-
if (
|
|
5279
|
-
writeFileSync9(
|
|
5553
|
+
if (existsSync7(reviewAuditSrc)) {
|
|
5554
|
+
writeFileSync9(join14(workspacePath, `${artifactPrefix}.audit.json`), readFileSync6(reviewAuditSrc, "utf8"), "utf8");
|
|
5280
5555
|
}
|
|
5281
5556
|
} catch (vErr) {
|
|
5282
5557
|
logger.warn({ err: String(vErr), scope }, "[AgentFSM] Failed to write versioned review artifacts");
|
|
@@ -5413,7 +5688,7 @@ async function runCheckpointReviewOnce(state, issue, workspacePath, reviewer) {
|
|
|
5413
5688
|
"runner",
|
|
5414
5689
|
`Auto-replan triggered for ${issue.identifier}: checkpoint is failing on the same blocking criteria repeatedly.`
|
|
5415
5690
|
);
|
|
5416
|
-
const { replanIssueCommand: replanIssueCommand2 } = await import("./replan-issue.command-
|
|
5691
|
+
const { replanIssueCommand: replanIssueCommand2 } = await import("./replan-issue.command-2GQ3QXCR.js");
|
|
5417
5692
|
await replanIssueCommand2({ issue }, container);
|
|
5418
5693
|
return "replanned";
|
|
5419
5694
|
}
|
|
@@ -5467,6 +5742,17 @@ async function runReviewOnce(state, issue, workspacePath, reviewer) {
|
|
|
5467
5742
|
);
|
|
5468
5743
|
return;
|
|
5469
5744
|
}
|
|
5745
|
+
const complexity = issue.plan?.estimatedComplexity;
|
|
5746
|
+
if (harnessMode === "standard" && complexity === "trivial") {
|
|
5747
|
+
addEvent(state, issue.id, "info", `Trivial complexity for ${issue.identifier} \u2014 skipping review, validation gate is sufficient.`);
|
|
5748
|
+
await finalizeReviewSuccess(
|
|
5749
|
+
state,
|
|
5750
|
+
issue,
|
|
5751
|
+
container,
|
|
5752
|
+
`Trivial issue ${issue.identifier} auto-approved; validation gate passed.`
|
|
5753
|
+
);
|
|
5754
|
+
return;
|
|
5755
|
+
}
|
|
5470
5756
|
if (!reviewer) {
|
|
5471
5757
|
if (harnessMode === "contractual") {
|
|
5472
5758
|
issue.mergedReason = "Contractual harness requires review evidence; manual review required.";
|
|
@@ -5535,7 +5821,7 @@ async function runReviewOnce(state, issue, workspacePath, reviewer) {
|
|
|
5535
5821
|
"runner",
|
|
5536
5822
|
`Auto-replan triggered for ${issue.identifier}: final review is failing on the same blocking criteria repeatedly.`
|
|
5537
5823
|
);
|
|
5538
|
-
const { replanIssueCommand: replanIssueCommand2 } = await import("./replan-issue.command-
|
|
5824
|
+
const { replanIssueCommand: replanIssueCommand2 } = await import("./replan-issue.command-2GQ3QXCR.js");
|
|
5539
5825
|
await replanIssueCommand2({ issue }, container);
|
|
5540
5826
|
return;
|
|
5541
5827
|
}
|
|
@@ -5587,8 +5873,7 @@ async function runReviewOnce(state, issue, workspacePath, reviewer) {
|
|
|
5587
5873
|
issue.lastFailedPhase = "review";
|
|
5588
5874
|
issue.attempts += 1;
|
|
5589
5875
|
if (issue.attempts >= issue.maxAttempts) {
|
|
5590
|
-
issue
|
|
5591
|
-
await cancelIssueFromAgentCommand(issue, `Review failed, max attempts reached for ${issue.identifier}.`, container);
|
|
5876
|
+
await blockIssueForRetryCommand(issue, `Review failed \u2014 max attempts reached (${issue.attempts}/${issue.maxAttempts}) for ${issue.identifier}. Manual intervention required.`, container);
|
|
5592
5877
|
} else {
|
|
5593
5878
|
issue.nextRetryAt = getNextRetryAt(issue, state.config.retryDelayMs);
|
|
5594
5879
|
await blockIssueForRetryCommand(issue, `Review failed for ${issue.identifier}. Retry at ${issue.nextRetryAt}.`, container);
|
|
@@ -5626,13 +5911,13 @@ async function runExecuteOnce(state, issue, workspacePath, promptText, promptFil
|
|
|
5626
5911
|
try {
|
|
5627
5912
|
const epv = issue.planVersion ?? 1;
|
|
5628
5913
|
const eea = issue.executeAttempt ?? 1;
|
|
5629
|
-
const vExecPromptSrc =
|
|
5630
|
-
const vExecAuditSrc =
|
|
5631
|
-
if (
|
|
5632
|
-
writeFileSync9(
|
|
5914
|
+
const vExecPromptSrc = join14(workspacePath, "prompt.md");
|
|
5915
|
+
const vExecAuditSrc = join14(workspacePath, "execution-audit.json");
|
|
5916
|
+
if (existsSync7(vExecPromptSrc)) {
|
|
5917
|
+
writeFileSync9(join14(workspacePath, `execute.v${epv}a${eea}.prompt.md`), readFileSync6(vExecPromptSrc, "utf8"), "utf8");
|
|
5633
5918
|
}
|
|
5634
|
-
if (
|
|
5635
|
-
writeFileSync9(
|
|
5919
|
+
if (existsSync7(vExecAuditSrc)) {
|
|
5920
|
+
writeFileSync9(join14(workspacePath, `execute.v${epv}a${eea}.audit.json`), readFileSync6(vExecAuditSrc, "utf8"), "utf8");
|
|
5636
5921
|
}
|
|
5637
5922
|
} catch (vErr) {
|
|
5638
5923
|
logger.warn({ err: String(vErr) }, "[AgentFSM] Failed to write versioned execute artifacts");
|
|
@@ -5657,8 +5942,7 @@ ${preReviewValidation.output}`;
|
|
|
5657
5942
|
issue.lastFailedPhase = "execute";
|
|
5658
5943
|
issue.attempts += 1;
|
|
5659
5944
|
if (issue.attempts >= issue.maxAttempts) {
|
|
5660
|
-
issue
|
|
5661
|
-
await cancelIssueFromAgentCommand(issue, `Max attempts reached (${issue.attempts}/${issue.maxAttempts}): pre-review gate failed.`, container);
|
|
5945
|
+
await blockIssueForRetryCommand(issue, `Pre-review validation gate failed \u2014 max attempts reached (${issue.attempts}/${issue.maxAttempts}). Manual intervention required.`, container);
|
|
5662
5946
|
} else {
|
|
5663
5947
|
issue.nextRetryAt = getNextRetryAt(issue, state.config.retryDelayMs);
|
|
5664
5948
|
await blockIssueForRetryCommand(issue, `Pre-review validation gate failed on attempt ${issue.attempts}/${issue.maxAttempts}. Retry scheduled at ${issue.nextRetryAt}.`, container);
|
|
@@ -5698,15 +5982,14 @@ ${preReviewValidation.output}`;
|
|
|
5698
5982
|
"runner",
|
|
5699
5983
|
`Auto-replan: "${currentInsight.errorType}" repeated ${stallThreshold}\xD7 \u2014 replanning to break the loop.`
|
|
5700
5984
|
);
|
|
5701
|
-
const { replanIssueCommand: replanIssueCommand2 } = await import("./replan-issue.command-
|
|
5985
|
+
const { replanIssueCommand: replanIssueCommand2 } = await import("./replan-issue.command-2GQ3QXCR.js");
|
|
5702
5986
|
await replanIssueCommand2({ issue }, container);
|
|
5703
5987
|
return;
|
|
5704
5988
|
}
|
|
5705
5989
|
}
|
|
5706
5990
|
if (issue.attempts >= issue.maxAttempts) {
|
|
5707
5991
|
issue.commandExitCode = runResult.code;
|
|
5708
|
-
issue
|
|
5709
|
-
await cancelIssueFromAgentCommand(issue, `Max attempts reached (${issue.attempts}/${issue.maxAttempts}).`, container);
|
|
5992
|
+
await blockIssueForRetryCommand(issue, `Execution failed \u2014 max attempts reached (${issue.attempts}/${issue.maxAttempts}). Manual intervention required.`, container);
|
|
5710
5993
|
} else {
|
|
5711
5994
|
issue.nextRetryAt = getNextRetryAt(issue, state.config.retryDelayMs);
|
|
5712
5995
|
await blockIssueForRetryCommand(issue, `${runResult.blocked ? "Agent requested manual intervention" : "Failure"} on attempt ${issue.attempts}/${issue.maxAttempts}; retry scheduled at ${issue.nextRetryAt}.`, container);
|
|
@@ -5717,7 +6000,7 @@ async function runReviewPhase(state, issue, running2, fifonyDir = STATE_ROOT, on
|
|
|
5717
6000
|
const startTs = Date.now();
|
|
5718
6001
|
logger.info({ issueId: issue.id, identifier: issue.identifier, state: issue.state, attempt: issue.attempts + 1 }, "[AgentFSM] Review phase starting");
|
|
5719
6002
|
const _op = deriveAgentOperation(issue);
|
|
5720
|
-
const _workspacePath = issue.workspacePath ??
|
|
6003
|
+
const _workspacePath = issue.workspacePath ?? join14(WORKSPACE_ROOT, idToSafePath(issue.id));
|
|
5721
6004
|
writeJobState(fifonyDir, {
|
|
5722
6005
|
issueId: issue.id,
|
|
5723
6006
|
identifier: issue.identifier,
|
|
@@ -5752,7 +6035,7 @@ async function runReviewPhase(state, issue, running2, fifonyDir = STATE_ROOT, on
|
|
|
5752
6035
|
const { workspacePath } = await prepareWorkspace(issue, state, state.config.defaultBranch);
|
|
5753
6036
|
container.issueRepository.markDirty(issue.id);
|
|
5754
6037
|
container.eventStore.addEvent(issue.id, "info", `Workspace ready at ${workspacePath}.`);
|
|
5755
|
-
const { startIssueLogBroadcasting: _startReviewLog } = await import("./issue-log-broadcaster-
|
|
6038
|
+
const { startIssueLogBroadcasting: _startReviewLog } = await import("./issue-log-broadcaster-FZGVEEIX.js");
|
|
5756
6039
|
_startReviewLog(issue.id, workspacePath);
|
|
5757
6040
|
const reviewer = getReviewProvider(state, issue, workflowConfig);
|
|
5758
6041
|
if (state.config.enablePlaywrightReview && hasFrontendChanges(issue, "")) {
|
|
@@ -5777,8 +6060,7 @@ async function runReviewPhase(state, issue, running2, fifonyDir = STATE_ROOT, on
|
|
|
5777
6060
|
issue.lastError = String(error);
|
|
5778
6061
|
issue.lastFailedPhase = "review";
|
|
5779
6062
|
if (issue.attempts >= issue.maxAttempts) {
|
|
5780
|
-
issue
|
|
5781
|
-
await cancelIssueFromAgentCommand(issue, `Issue failed unexpectedly: ${issue.lastError}`, container);
|
|
6063
|
+
await blockIssueForRetryCommand(issue, `Unexpected failure \u2014 max attempts reached (${issue.attempts}/${issue.maxAttempts}): ${issue.lastError?.slice(0, 120) ?? "unknown error"}. Manual intervention required.`, container);
|
|
5782
6064
|
} else {
|
|
5783
6065
|
issue.nextRetryAt = getNextRetryAt(issue, state.config.retryDelayMs);
|
|
5784
6066
|
await blockIssueForRetryCommand(issue, `Unexpected failure. Retry scheduled at ${issue.nextRetryAt}.`, container);
|
|
@@ -5794,15 +6076,15 @@ async function runReviewPhase(state, issue, running2, fifonyDir = STATE_ROOT, on
|
|
|
5794
6076
|
await container.persistencePort.persistState(state);
|
|
5795
6077
|
cleanAgentJobState(fifonyDir, issue.id);
|
|
5796
6078
|
onTransition?.({ issueId: issue.id, identifier: issue.identifier, operation: _op, from: "running", to: "done", pid: null, reason: "review phase complete", at: now() });
|
|
5797
|
-
import("./issue-log-broadcaster-
|
|
6079
|
+
import("./issue-log-broadcaster-FZGVEEIX.js").then(({ stopIssueLogBroadcasting }) => stopIssueLogBroadcasting(issue.id)).catch(() => {
|
|
5798
6080
|
});
|
|
5799
6081
|
}
|
|
5800
6082
|
}
|
|
5801
|
-
async function runExecutePhase(state, issue, running2,
|
|
6083
|
+
async function runExecutePhase(state, issue, running2, active2, getCurrentIssue2, fifonyDir = STATE_ROOT, onTransition) {
|
|
5802
6084
|
const startTs = Date.now();
|
|
5803
6085
|
logger.info({ issueId: issue.id, identifier: issue.identifier, state: issue.state, attempt: issue.attempts + 1 }, "[AgentFSM] Execute phase starting");
|
|
5804
6086
|
const _op = deriveAgentOperation(issue);
|
|
5805
|
-
const _initWorkspacePath = issue.workspacePath ??
|
|
6087
|
+
const _initWorkspacePath = issue.workspacePath ?? join14(WORKSPACE_ROOT, idToSafePath(issue.id));
|
|
5806
6088
|
const _jobInit = {
|
|
5807
6089
|
issueId: issue.id,
|
|
5808
6090
|
identifier: issue.identifier,
|
|
@@ -5841,7 +6123,7 @@ async function runExecutePhase(state, issue, running2, active3, getCurrentIssue2
|
|
|
5841
6123
|
}
|
|
5842
6124
|
let current = issue;
|
|
5843
6125
|
try {
|
|
5844
|
-
while (
|
|
6126
|
+
while (active2() && current) {
|
|
5845
6127
|
if (current.state !== "Queued" && current.state !== "Running") break;
|
|
5846
6128
|
const workspaceDerivedPaths = hydrateIssuePathsFromWorkspace(current);
|
|
5847
6129
|
void workspaceDerivedPaths;
|
|
@@ -5850,7 +6132,7 @@ async function runExecutePhase(state, issue, running2, active3, getCurrentIssue2
|
|
|
5850
6132
|
const _turnNum = (readJobState(fifonyDir, issue.id)?.turn ?? 0) + 1;
|
|
5851
6133
|
writeJobState(fifonyDir, { ..._jobInit, workspacePath, logFile: agentLogPath(workspacePath), state: "running", turn: _turnNum, updatedAt: now() });
|
|
5852
6134
|
try {
|
|
5853
|
-
const { getIssueStateResource: getIssueStateResource2 } = await import("./store-
|
|
6135
|
+
const { getIssueStateResource: getIssueStateResource2 } = await import("./store-S3NAYZ3S.js");
|
|
5854
6136
|
const res = getIssueStateResource2();
|
|
5855
6137
|
if (res) {
|
|
5856
6138
|
await res.patch(current.id, {
|
|
@@ -5863,7 +6145,7 @@ async function runExecutePhase(state, issue, running2, active3, getCurrentIssue2
|
|
|
5863
6145
|
} catch {
|
|
5864
6146
|
}
|
|
5865
6147
|
container.eventStore.addEvent(current.id, "info", `Workspace ready at ${workspacePath}.`);
|
|
5866
|
-
const { startIssueLogBroadcasting } = await import("./issue-log-broadcaster-
|
|
6148
|
+
const { startIssueLogBroadcasting } = await import("./issue-log-broadcaster-FZGVEEIX.js");
|
|
5867
6149
|
startIssueLogBroadcasting(current.id, workspacePath);
|
|
5868
6150
|
const executeProviders = getExecutionProviders(state, current, workflowConfig);
|
|
5869
6151
|
const reviewer = getReviewProvider(state, current, workflowConfig);
|
|
@@ -5876,8 +6158,7 @@ async function runExecutePhase(state, issue, running2, active3, getCurrentIssue2
|
|
|
5876
6158
|
target.lastError = String(error);
|
|
5877
6159
|
target.lastFailedPhase = target.lastFailedPhase ?? "execute";
|
|
5878
6160
|
if (target.attempts >= target.maxAttempts) {
|
|
5879
|
-
target
|
|
5880
|
-
await cancelIssueFromAgentCommand(target, `Issue failed unexpectedly: ${target.lastError}`, container);
|
|
6161
|
+
await blockIssueForRetryCommand(target, `Unexpected failure \u2014 max attempts reached (${target.attempts}/${target.maxAttempts}): ${target.lastError?.slice(0, 120) ?? "unknown error"}. Manual intervention required.`, container);
|
|
5881
6162
|
} else {
|
|
5882
6163
|
target.nextRetryAt = getNextRetryAt(target, state.config.retryDelayMs);
|
|
5883
6164
|
await blockIssueForRetryCommand(target, `Unexpected failure. Retry scheduled at ${target.nextRetryAt}.`, container);
|
|
@@ -5894,7 +6175,7 @@ async function runExecutePhase(state, issue, running2, active3, getCurrentIssue2
|
|
|
5894
6175
|
await container.persistencePort.persistState(state);
|
|
5895
6176
|
cleanAgentJobState(fifonyDir, issue.id);
|
|
5896
6177
|
onTransition?.({ issueId: issue.id, identifier: issue.identifier, operation: _op, from: "running", to: "done", pid: null, reason: "execute phase complete", at: now() });
|
|
5897
|
-
import("./issue-log-broadcaster-
|
|
6178
|
+
import("./issue-log-broadcaster-FZGVEEIX.js").then(({ stopIssueLogBroadcasting }) => stopIssueLogBroadcasting(issue.id)).catch(() => {
|
|
5898
6179
|
});
|
|
5899
6180
|
}
|
|
5900
6181
|
}
|
|
@@ -5903,7 +6184,7 @@ function tickOneAgent(issue, fifonyDir) {
|
|
|
5903
6184
|
if (!job) return null;
|
|
5904
6185
|
if (job.state === "done" || job.state === "failed" || job.state === "idle" || job.state === "crashed") return null;
|
|
5905
6186
|
if (job.state === "running" || job.state === "preparing") {
|
|
5906
|
-
if (!job.workspacePath || !
|
|
6187
|
+
if (!job.workspacePath || !existsSync7(job.workspacePath)) return null;
|
|
5907
6188
|
const pidInfo = readAgentPid(job.workspacePath);
|
|
5908
6189
|
const alive = pidInfo ? isProcessAlive(pidInfo.pid) : false;
|
|
5909
6190
|
if (!alive && pidInfo) {
|
|
@@ -5953,7 +6234,7 @@ function reconcileAgentStates(issues, fifonyDir) {
|
|
|
5953
6234
|
if (!job) continue;
|
|
5954
6235
|
if (job.state === "done" || job.state === "failed" || job.state === "idle" || job.state === "crashed") continue;
|
|
5955
6236
|
if (job.state === "running" || job.state === "preparing") {
|
|
5956
|
-
const pidInfo = job.workspacePath &&
|
|
6237
|
+
const pidInfo = job.workspacePath && existsSync7(job.workspacePath) ? readAgentPid(job.workspacePath) : null;
|
|
5957
6238
|
const alive = pidInfo ? isProcessAlive(pidInfo.pid) : false;
|
|
5958
6239
|
if (!alive) {
|
|
5959
6240
|
const crashCount = (job.crashCount ?? 0) + 1;
|
|
@@ -5977,10 +6258,10 @@ function reconcileAgentStates(issues, fifonyDir) {
|
|
|
5977
6258
|
|
|
5978
6259
|
// src/agents/handoff-writer.ts
|
|
5979
6260
|
import { writeFileSync as writeFileSync10 } from "fs";
|
|
5980
|
-
import { join as
|
|
6261
|
+
import { join as join15 } from "path";
|
|
5981
6262
|
import { execSync as execSync2 } from "child_process";
|
|
5982
6263
|
function writeHandoffArtifact(workspacePath, issue, lastOutput, nextPrompt) {
|
|
5983
|
-
const handoffPath =
|
|
6264
|
+
const handoffPath = join15(workspacePath, "handoff.md");
|
|
5984
6265
|
let diffStat = "";
|
|
5985
6266
|
try {
|
|
5986
6267
|
diffStat = execSync2("git diff --stat HEAD", { cwd: workspacePath, timeout: 5e3 }).toString().trim();
|
|
@@ -6052,7 +6333,7 @@ async function runAgentSession(state, issue, provider, cycle, workspacePath, bas
|
|
|
6052
6333
|
let nextPrompt = session.nextPrompt;
|
|
6053
6334
|
let lastCode = session.lastCode;
|
|
6054
6335
|
let lastOutput = session.lastOutput;
|
|
6055
|
-
const resultFile =
|
|
6336
|
+
const resultFile = join16(workspacePath, `result-${provider.role}-${provider.provider}.json`);
|
|
6056
6337
|
if (session.status === "done" && session.turns.length > 0) {
|
|
6057
6338
|
logger.debug({ issueId: issue.id, identifier: issue.identifier, provider: provider.provider, role: provider.role }, "[Agent] Session already completed, returning cached result");
|
|
6058
6339
|
return { success: true, blocked: false, continueRequested: false, code: session.lastCode, output: session.lastOutput, turns: session.turns.length };
|
|
@@ -6102,7 +6383,7 @@ ${previousOutput.slice(-maxOutputChars)}` : previousOutput;
|
|
|
6102
6383
|
|
|
6103
6384
|
${basePromptText}` : basePromptText;
|
|
6104
6385
|
const turnPrompt = await buildTurnPrompt(issue, effectiveBasePrompt, compactedOutput, turnIndex, maxTurns, nextPrompt);
|
|
6105
|
-
const turnPromptFile = turnIndex === 1 ? basePromptFile :
|
|
6386
|
+
const turnPromptFile = turnIndex === 1 ? basePromptFile : join16(workspacePath, `turn-${String(turnIndex).padStart(2, "0")}.md`);
|
|
6106
6387
|
if (turnIndex > 1) writeFileSync11(turnPromptFile, `${turnPrompt}
|
|
6107
6388
|
`, "utf8");
|
|
6108
6389
|
session.status = "running";
|
|
@@ -6110,7 +6391,7 @@ ${basePromptText}` : basePromptText;
|
|
|
6110
6391
|
session.lastPromptFile = turnPromptFile;
|
|
6111
6392
|
session.maxTurns = maxTurns;
|
|
6112
6393
|
await persistAgentSessionState(sessionKey, issue, provider, cycle, session);
|
|
6113
|
-
const outputsDir =
|
|
6394
|
+
const outputsDir = join16(workspacePath, "outputs");
|
|
6114
6395
|
mkdirSync8(outputsDir, { recursive: true });
|
|
6115
6396
|
const outputFileName = resolveOutputFileName(
|
|
6116
6397
|
provider.role,
|
|
@@ -6118,7 +6399,7 @@ ${basePromptText}` : basePromptText;
|
|
|
6118
6399
|
provider.role === "planner" ? 0 : issue.executeAttempt ?? 1,
|
|
6119
6400
|
turnIndex
|
|
6120
6401
|
);
|
|
6121
|
-
const outputFilePath =
|
|
6402
|
+
const outputFilePath = join16(outputsDir, outputFileName);
|
|
6122
6403
|
logger.info({ issueId: issue.id, identifier: issue.identifier, turn: turnIndex, maxTurns, provider: provider.provider, role: provider.role, cycle, command: provider.command.slice(0, 120) }, "[Agent] Spawning agent command");
|
|
6123
6404
|
const turnStartedAt = now();
|
|
6124
6405
|
const turnEnv = {
|
|
@@ -6289,7 +6570,7 @@ async function runAgentPipeline(state, issue, workspacePath, basePromptText, bas
|
|
|
6289
6570
|
const commands = discoverCommands(workspacePath);
|
|
6290
6571
|
const capabilitiesManifest = buildCapabilitiesManifest(skills, agents, commands);
|
|
6291
6572
|
if (skillContext) {
|
|
6292
|
-
writeFileSync11(
|
|
6573
|
+
writeFileSync11(join16(workspacePath, "skills.md"), skillContext, "utf8");
|
|
6293
6574
|
}
|
|
6294
6575
|
const compiled = await compileExecution(issue, activeProvider, state.config, workspacePath, skillContext, capabilitiesManifest);
|
|
6295
6576
|
const blueprint = issue.plan ? buildHarnessBlueprint(issue.plan, state.config) : null;
|
|
@@ -6314,7 +6595,7 @@ async function runAgentPipeline(state, issue, workspacePath, basePromptText, bas
|
|
|
6314
6595
|
`Plan compiled for ${compiled.meta.adapter}: effort=${compiled.meta.reasoningEffort}, skills=[${compiled.meta.skillsActivated.join(",")}], subagents=[${compiled.meta.subagentsRequested.join(",")}].`
|
|
6315
6596
|
);
|
|
6316
6597
|
if (Object.keys(compiled.env).length > 0) {
|
|
6317
|
-
const envFile =
|
|
6598
|
+
const envFile = join16(workspacePath, ".compiled-env.sh");
|
|
6318
6599
|
const envLines = Object.entries(compiled.env).map(([k, v]) => `export ${k}=${JSON.stringify(v)}`).join("\n");
|
|
6319
6600
|
writeFileSync11(envFile, envLines, "utf8");
|
|
6320
6601
|
}
|
|
@@ -6353,9 +6634,9 @@ ${retryCtx}`;
|
|
|
6353
6634
|
}
|
|
6354
6635
|
}
|
|
6355
6636
|
if (issue.lastHandoffFile && (issue.contextResetCount ?? 0) > 0) {
|
|
6356
|
-
const { readFileSync:
|
|
6357
|
-
if (
|
|
6358
|
-
const handoffContent =
|
|
6637
|
+
const { readFileSync: readFileSync18, existsSync: existsSync20 } = await import("fs");
|
|
6638
|
+
if (existsSync20(issue.lastHandoffFile)) {
|
|
6639
|
+
const handoffContent = readFileSync18(issue.lastHandoffFile, "utf8");
|
|
6359
6640
|
providerPrompt = `## Context Reset \u2014 Prior Session Summary
|
|
6360
6641
|
|
|
6361
6642
|
${handoffContent}
|
|
@@ -6442,7 +6723,7 @@ ${providerPrompt}`;
|
|
|
6442
6723
|
}
|
|
6443
6724
|
let parallelExecuted = false;
|
|
6444
6725
|
if (issue.plan?.executionContract?.parallelSubTasks && issue.plan.executionContract.parallelSubTasks.length >= 2) {
|
|
6445
|
-
const { spawnParallelSubTasks } = await import("./parallel-executor-
|
|
6726
|
+
const { spawnParallelSubTasks } = await import("./parallel-executor-DWESCNX3.js");
|
|
6446
6727
|
const parallelSuccess = await spawnParallelSubTasks(state, issue, effectiveProvider, pipeline.cycle, providerPrompt, basePromptFile);
|
|
6447
6728
|
parallelExecuted = true;
|
|
6448
6729
|
if (!parallelSuccess) {
|
|
@@ -6632,19 +6913,19 @@ function issueHasResumableSession(issue) {
|
|
|
6632
6913
|
}
|
|
6633
6914
|
|
|
6634
6915
|
// src/commands/create-issue.command.ts
|
|
6635
|
-
import { existsSync as
|
|
6636
|
-
import { basename as basename2, join as
|
|
6916
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync9, renameSync } from "fs";
|
|
6917
|
+
import { basename as basename2, join as join17 } from "path";
|
|
6637
6918
|
async function createIssueCommand(input, deps) {
|
|
6638
6919
|
const { payload, state } = input;
|
|
6639
6920
|
const issue = createIssueFromPayload(payload, state.issues, state.config.defaultBranch);
|
|
6640
6921
|
const tempImages = Array.isArray(payload.images) ? payload.images : [];
|
|
6641
6922
|
if (tempImages.length) {
|
|
6642
|
-
const issueAttachDir =
|
|
6923
|
+
const issueAttachDir = join17(ATTACHMENTS_ROOT, issue.id);
|
|
6643
6924
|
mkdirSync9(issueAttachDir, { recursive: true });
|
|
6644
6925
|
const finalPaths = [];
|
|
6645
6926
|
for (const tempPath of tempImages) {
|
|
6646
|
-
if (typeof tempPath === "string" &&
|
|
6647
|
-
const dest =
|
|
6927
|
+
if (typeof tempPath === "string" && existsSync8(tempPath)) {
|
|
6928
|
+
const dest = join17(issueAttachDir, basename2(tempPath));
|
|
6648
6929
|
try {
|
|
6649
6930
|
renameSync(tempPath, dest);
|
|
6650
6931
|
finalPaths.push(dest);
|
|
@@ -6747,13 +7028,13 @@ async function deleteIssueCommand(input) {
|
|
|
6747
7028
|
}
|
|
6748
7029
|
|
|
6749
7030
|
// src/commands/merge-workspace.command.ts
|
|
6750
|
-
import { existsSync as
|
|
7031
|
+
import { existsSync as existsSync9 } from "fs";
|
|
6751
7032
|
import { execSync as execSync4 } from "child_process";
|
|
6752
7033
|
|
|
6753
7034
|
// src/domains/merge-conflict-resolver.ts
|
|
6754
7035
|
import { execSync as execSync3 } from "child_process";
|
|
6755
7036
|
import { mkdtempSync as mkdtempSync3, writeFileSync as writeFileSync12, rmSync as rmSync4 } from "fs";
|
|
6756
|
-
import { join as
|
|
7037
|
+
import { join as join18 } from "path";
|
|
6757
7038
|
import { tmpdir as tmpdir3 } from "os";
|
|
6758
7039
|
async function resolveConflictsWithAgent(options) {
|
|
6759
7040
|
const { issue, conflictFiles, provider, model, targetRoot } = options;
|
|
@@ -6774,8 +7055,8 @@ async function resolveConflictsWithAgent(options) {
|
|
|
6774
7055
|
featureBranch: issue.branchName || "unknown",
|
|
6775
7056
|
conflictFiles
|
|
6776
7057
|
});
|
|
6777
|
-
const tempDir = mkdtempSync3(
|
|
6778
|
-
const promptFile =
|
|
7058
|
+
const tempDir = mkdtempSync3(join18(tmpdir3(), "fifony-conflict-"));
|
|
7059
|
+
const promptFile = join18(tempDir, "fifony-conflict-prompt.md");
|
|
6779
7060
|
writeFileSync12(promptFile, `${prompt}
|
|
6780
7061
|
`, "utf8");
|
|
6781
7062
|
let output = "";
|
|
@@ -6854,7 +7135,7 @@ async function mergeWorkspaceCommand(input, deps) {
|
|
|
6854
7135
|
);
|
|
6855
7136
|
}
|
|
6856
7137
|
const wp = issue.worktreePath ?? issue.workspacePath;
|
|
6857
|
-
if (!wp || !
|
|
7138
|
+
if (!wp || !existsSync9(wp)) {
|
|
6858
7139
|
throw new Error(`No mergeable workspace found for ${issue.identifier}. This issue likely ran before git was initialized for the project. Re-run the issue after git setup.`);
|
|
6859
7140
|
}
|
|
6860
7141
|
if (issue.branchName && issue.baseBranch) {
|
|
@@ -6894,13 +7175,32 @@ async function mergeWorkspaceCommand(input, deps) {
|
|
|
6894
7175
|
}
|
|
6895
7176
|
}
|
|
6896
7177
|
let result;
|
|
6897
|
-
const
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
7178
|
+
const autoCommit = state.config.autoCommitBeforeMerge ?? true;
|
|
7179
|
+
const autoResolve = state.config.autoResolveConflicts ?? false;
|
|
7180
|
+
try {
|
|
7181
|
+
result = mergeWorkspace(
|
|
7182
|
+
issue,
|
|
7183
|
+
/* abortOnConflict */
|
|
7184
|
+
!autoResolve,
|
|
7185
|
+
autoCommit
|
|
7186
|
+
);
|
|
7187
|
+
} catch (mergeErr) {
|
|
7188
|
+
const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
|
|
7189
|
+
if (msg.includes("uncommitted changes") && !autoCommit) {
|
|
7190
|
+
issue.lastError = msg;
|
|
7191
|
+
issue.lastFailedPhase = "merge";
|
|
7192
|
+
deps.issueRepository.markDirty(issue.id);
|
|
7193
|
+
deps.eventStore.addEvent(issue.id, "error", `Merge blocked: ${msg}`);
|
|
7194
|
+
await transitionIssueCommand(
|
|
7195
|
+
{ issue, target: "Blocked", note: "Merge blocked by uncommitted changes in target repo. Enable 'Auto-commit before merge' or commit manually." },
|
|
7196
|
+
deps
|
|
7197
|
+
);
|
|
7198
|
+
await deps.persistencePort.persistState(state);
|
|
7199
|
+
return { copied: [], deleted: [], skipped: [], conflicts: [] };
|
|
7200
|
+
}
|
|
7201
|
+
throw mergeErr;
|
|
7202
|
+
}
|
|
7203
|
+
if (result.conflicts.length > 0 && autoResolve) {
|
|
6904
7204
|
deps.eventStore.addEvent(issue.id, "info", `Merge conflicts in ${result.conflicts.length} file(s): ${result.conflicts.join(", ")}. Attempting agent-based resolution...`);
|
|
6905
7205
|
logger.info({ issueId: issue.id, conflicts: result.conflicts }, "[Merge] Conflicts detected \u2014 spawning agent to resolve");
|
|
6906
7206
|
try {
|
|
@@ -6990,7 +7290,17 @@ async function mergeWorkspaceCommand(input, deps) {
|
|
|
6990
7290
|
...issue.mergeResult?.conflictResolution ? { conflictResolution: issue.mergeResult.conflictResolution } : {}
|
|
6991
7291
|
};
|
|
6992
7292
|
if (result.conflicts.length > 0) {
|
|
6993
|
-
|
|
7293
|
+
const conflictMsg = `Merge has ${result.conflicts.length} conflict(s): ${result.conflicts.join(", ")}`;
|
|
7294
|
+
deps.eventStore.addEvent(issue.id, "error", conflictMsg);
|
|
7295
|
+
if (!autoResolve) {
|
|
7296
|
+
issue.lastError = conflictMsg;
|
|
7297
|
+
issue.lastFailedPhase = "merge";
|
|
7298
|
+
deps.issueRepository.markDirty(issue.id);
|
|
7299
|
+
await transitionIssueCommand(
|
|
7300
|
+
{ issue, target: "Blocked", note: "Merge blocked by conflicts. Enable 'Auto-resolve conflicts' or resolve manually." },
|
|
7301
|
+
deps
|
|
7302
|
+
);
|
|
7303
|
+
}
|
|
6994
7304
|
await deps.persistencePort.persistState(state);
|
|
6995
7305
|
return result;
|
|
6996
7306
|
}
|
|
@@ -7012,10 +7322,10 @@ async function mergeWorkspaceCommand(input, deps) {
|
|
|
7012
7322
|
}
|
|
7013
7323
|
|
|
7014
7324
|
// src/commands/push-workspace.command.ts
|
|
7015
|
-
import { execFileSync as
|
|
7325
|
+
import { execFileSync as execFileSync3, execSync as execSync5 } from "child_process";
|
|
7016
7326
|
function isGhAvailable() {
|
|
7017
7327
|
try {
|
|
7018
|
-
|
|
7328
|
+
execFileSync3("gh", ["--version"], { stdio: "pipe", timeout: 5e3 });
|
|
7019
7329
|
return true;
|
|
7020
7330
|
} catch {
|
|
7021
7331
|
return false;
|
|
@@ -7032,7 +7342,7 @@ function getCompareUrl(branchName, baseBranch) {
|
|
|
7032
7342
|
}
|
|
7033
7343
|
function findExistingPr(branchName) {
|
|
7034
7344
|
try {
|
|
7035
|
-
const result =
|
|
7345
|
+
const result = execFileSync3(
|
|
7036
7346
|
"gh",
|
|
7037
7347
|
["pr", "view", branchName, "--json", "url,state", "--jq", 'select(.state == "OPEN") | .url'],
|
|
7038
7348
|
{ cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe", timeout: 15e3 }
|
|
@@ -7043,7 +7353,7 @@ function findExistingPr(branchName) {
|
|
|
7043
7353
|
}
|
|
7044
7354
|
}
|
|
7045
7355
|
function createPr(branchName, baseBranch, title, body) {
|
|
7046
|
-
return
|
|
7356
|
+
return execFileSync3(
|
|
7047
7357
|
"gh",
|
|
7048
7358
|
["pr", "create", "--head", branchName, "--base", baseBranch, "--title", title, "--body", body],
|
|
7049
7359
|
{ cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe", timeout: 3e4 }
|
|
@@ -7124,118 +7434,20 @@ ${diffStat || "No diff stats available"}
|
|
|
7124
7434
|
return { prUrl, ghAvailable };
|
|
7125
7435
|
}
|
|
7126
7436
|
|
|
7127
|
-
// src/commands/
|
|
7128
|
-
async function
|
|
7129
|
-
const { issue
|
|
7130
|
-
if (issue.state
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
);
|
|
7437
|
+
// src/commands/execute-issue.command.ts
|
|
7438
|
+
async function executeIssueCommand(input, deps) {
|
|
7439
|
+
const { issue } = input;
|
|
7440
|
+
if (issue.state === "Queued" || issue.state === "Running") return;
|
|
7441
|
+
if (issue.state !== "PendingApproval") {
|
|
7442
|
+
throw new Error(`Cannot execute issue in state ${issue.state}. Must be in PendingApproval.`);
|
|
7134
7443
|
}
|
|
7444
|
+
assertPlanReadyForExecution(issue, "execute this issue");
|
|
7445
|
+
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "execute issues");
|
|
7135
7446
|
await transitionIssueCommand(
|
|
7136
|
-
{ issue, target: "Queued", note:
|
|
7447
|
+
{ issue, target: "Queued", note: `Execution requested for ${issue.identifier}.` },
|
|
7137
7448
|
deps
|
|
7138
7449
|
);
|
|
7139
|
-
deps.eventStore.addEvent(
|
|
7140
|
-
issue.id,
|
|
7141
|
-
"manual",
|
|
7142
|
-
`Execution retry requested for ${issue.identifier} \u2014 re-queued from Blocked.`
|
|
7143
|
-
);
|
|
7144
|
-
}
|
|
7145
|
-
|
|
7146
|
-
// src/commands/retry-issue.command.ts
|
|
7147
|
-
async function retryIssueCommand(input, deps) {
|
|
7148
|
-
const { issue, feedback } = input;
|
|
7149
|
-
const note = feedback ? `Rework requested for ${issue.identifier}: ${feedback.slice(0, 200)}` : `Manual retry for ${issue.identifier}.`;
|
|
7150
|
-
if (TERMINAL_STATES.has(issue.state)) {
|
|
7151
|
-
await transitionIssueCommand({ issue, target: "Planning", note }, deps);
|
|
7152
|
-
if (issue.plan?.steps?.length && getPlanExecutionBlocker(issue) === null) {
|
|
7153
|
-
await transitionIssueCommand({ issue, target: "PendingApproval", note: "Existing plan found." }, deps);
|
|
7154
|
-
await transitionIssueCommand({ issue, target: "Queued", note: "Auto-queued after plan approval." }, deps);
|
|
7155
|
-
}
|
|
7156
|
-
deps.eventStore.addEvent(issue.id, "manual", `Manual retry requested for ${issue.id}.`);
|
|
7157
|
-
return;
|
|
7158
|
-
}
|
|
7159
|
-
if (issue.state === "Approved") {
|
|
7160
|
-
await transitionIssueCommand({ issue, target: "Planning", note }, deps);
|
|
7161
|
-
if (issue.plan?.steps?.length && getPlanExecutionBlocker(issue) === null) {
|
|
7162
|
-
await transitionIssueCommand({ issue, target: "PendingApproval", note: "Existing plan found." }, deps);
|
|
7163
|
-
await transitionIssueCommand({ issue, target: "Queued", note: "Auto-queued for rework." }, deps);
|
|
7164
|
-
}
|
|
7165
|
-
deps.eventStore.addEvent(issue.id, "manual", `Manual retry requested for ${issue.id}.`);
|
|
7166
|
-
return;
|
|
7167
|
-
}
|
|
7168
|
-
if (issue.state === "Blocked" && issue.lastFailedPhase === "review") {
|
|
7169
|
-
if (issue.checkpointStatus === "failed") {
|
|
7170
|
-
await retryExecutionCommand({ issue, note }, deps);
|
|
7171
|
-
return;
|
|
7172
|
-
}
|
|
7173
|
-
issue.lastError = void 0;
|
|
7174
|
-
issue.lastFailedPhase = void 0;
|
|
7175
|
-
await transitionIssueCommand({ issue, target: "Reviewing", note }, deps);
|
|
7176
|
-
deps.eventStore.addEvent(issue.id, "manual", `Manual retry requested for ${issue.id}.`);
|
|
7177
|
-
return;
|
|
7178
|
-
}
|
|
7179
|
-
if (issue.state === "Blocked") {
|
|
7180
|
-
await retryExecutionCommand({ issue, note }, deps);
|
|
7181
|
-
return;
|
|
7182
|
-
}
|
|
7183
|
-
if (issue.state === "Reviewing" || issue.state === "PendingDecision") {
|
|
7184
|
-
await requestReworkCommand(
|
|
7185
|
-
{
|
|
7186
|
-
issue,
|
|
7187
|
-
reviewerFeedback: feedback || issue.lastError || "Manual rework request.",
|
|
7188
|
-
note: `Manual rework requested for ${issue.identifier}.`,
|
|
7189
|
-
eventKind: "manual"
|
|
7190
|
-
},
|
|
7191
|
-
deps
|
|
7192
|
-
);
|
|
7193
|
-
return;
|
|
7194
|
-
}
|
|
7195
|
-
if (issue.state === "PendingApproval") {
|
|
7196
|
-
assertPlanReadyForExecution(issue, "retry this issue");
|
|
7197
|
-
await transitionIssueCommand({ issue, target: "Queued", note }, deps);
|
|
7198
|
-
deps.eventStore.addEvent(issue.id, "manual", `Manual retry requested for ${issue.id}.`);
|
|
7199
|
-
return;
|
|
7200
|
-
}
|
|
7201
|
-
issue.lastError = void 0;
|
|
7202
|
-
issue.nextRetryAt = void 0;
|
|
7203
|
-
issue.updatedAt = now();
|
|
7204
|
-
deps.eventStore.addEvent(issue.id, "manual", `Manual retry requested for ${issue.id}.`);
|
|
7205
|
-
}
|
|
7206
|
-
|
|
7207
|
-
// src/commands/approve-plan.command.ts
|
|
7208
|
-
async function approvePlanCommand(input, deps) {
|
|
7209
|
-
const { issue } = input;
|
|
7210
|
-
if (issue.state !== "Planning") {
|
|
7211
|
-
throw new Error(`Cannot approve issue in state ${issue.state}. Must be in Planning.`);
|
|
7212
|
-
}
|
|
7213
|
-
assertPlanReadyForExecution(issue, "approve this plan");
|
|
7214
|
-
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "execute approved plans");
|
|
7215
|
-
await transitionIssueCommand(
|
|
7216
|
-
{ issue, target: "PendingApproval", note: `Plan approved for ${issue.identifier}. Ready for execution.` },
|
|
7217
|
-
deps
|
|
7218
|
-
);
|
|
7219
|
-
await transitionIssueCommand(
|
|
7220
|
-
{ issue, target: "Queued", note: `${issue.identifier} queued for execution after plan approval.` },
|
|
7221
|
-
deps
|
|
7222
|
-
);
|
|
7223
|
-
}
|
|
7224
|
-
|
|
7225
|
-
// src/commands/execute-issue.command.ts
|
|
7226
|
-
async function executeIssueCommand(input, deps) {
|
|
7227
|
-
const { issue } = input;
|
|
7228
|
-
if (issue.state === "Queued" || issue.state === "Running") return;
|
|
7229
|
-
if (issue.state !== "PendingApproval") {
|
|
7230
|
-
throw new Error(`Cannot execute issue in state ${issue.state}. Must be in PendingApproval.`);
|
|
7231
|
-
}
|
|
7232
|
-
assertPlanReadyForExecution(issue, "execute this issue");
|
|
7233
|
-
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "execute issues");
|
|
7234
|
-
await transitionIssueCommand(
|
|
7235
|
-
{ issue, target: "Queued", note: `Execution requested for ${issue.identifier}.` },
|
|
7236
|
-
deps
|
|
7237
|
-
);
|
|
7238
|
-
deps.eventStore.addEvent(issue.id, "state", `Execute requested \u2014 ${issue.identifier} moved to Queued.`);
|
|
7450
|
+
deps.eventStore.addEvent(issue.id, "state", `Execute requested \u2014 ${issue.identifier} moved to Queued.`);
|
|
7239
7451
|
}
|
|
7240
7452
|
|
|
7241
7453
|
// src/persistence/resources/issue-milestone.api.ts
|
|
@@ -7284,12 +7496,12 @@ async function assignIssueMilestoneForState(state, c, deps) {
|
|
|
7284
7496
|
}
|
|
7285
7497
|
|
|
7286
7498
|
// src/persistence/resources/issues.resource.ts
|
|
7287
|
-
import { closeSync, existsSync as
|
|
7499
|
+
import { closeSync, existsSync as existsSync10, mkdirSync as mkdirSync10, openSync, readFileSync as readFileSync7, readSync, readdirSync as readdirSync4, statSync as statSync2, writeFileSync as writeFileSync13 } from "fs";
|
|
7288
7500
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
7289
|
-
import { basename as basename3, extname as extname2, join as
|
|
7501
|
+
import { basename as basename3, extname as extname2, join as join19 } from "path";
|
|
7290
7502
|
import { execSync as execSync6 } from "child_process";
|
|
7291
7503
|
async function loadIssueApiDeps() {
|
|
7292
|
-
const { persistState: persistState2 } = await import("./store-
|
|
7504
|
+
const { persistState: persistState2 } = await import("./store-S3NAYZ3S.js");
|
|
7293
7505
|
return { persistState: persistState2 };
|
|
7294
7506
|
}
|
|
7295
7507
|
function respond(c, result, createdStatus) {
|
|
@@ -7345,7 +7557,7 @@ async function getIssueContext(c) {
|
|
|
7345
7557
|
}
|
|
7346
7558
|
async function patchIssueState(c) {
|
|
7347
7559
|
const context2 = getApiRuntimeContextOrThrow();
|
|
7348
|
-
const { persistState: persistState2 } = await import("./store-
|
|
7560
|
+
const { persistState: persistState2 } = await import("./store-S3NAYZ3S.js");
|
|
7349
7561
|
const issueId = parseIssue(c);
|
|
7350
7562
|
if (!issueId) {
|
|
7351
7563
|
return { status: 400, body: { ok: false, error: "Issue id is required." } };
|
|
@@ -7376,7 +7588,7 @@ async function patchIssueState(c) {
|
|
|
7376
7588
|
}
|
|
7377
7589
|
async function retryIssue(c) {
|
|
7378
7590
|
const context2 = getApiRuntimeContextOrThrow();
|
|
7379
|
-
const { persistState: persistState2 } = await import("./store-
|
|
7591
|
+
const { persistState: persistState2 } = await import("./store-S3NAYZ3S.js");
|
|
7380
7592
|
const issueId = parseIssue(c);
|
|
7381
7593
|
if (!issueId) {
|
|
7382
7594
|
return { status: 400, body: { ok: false, error: "Issue id is required." } };
|
|
@@ -7399,7 +7611,7 @@ async function retryIssue(c) {
|
|
|
7399
7611
|
}
|
|
7400
7612
|
async function createIssue(c) {
|
|
7401
7613
|
const context2 = getApiRuntimeContextOrThrow();
|
|
7402
|
-
const { persistState: persistState2 } = await import("./store-
|
|
7614
|
+
const { persistState: persistState2 } = await import("./store-S3NAYZ3S.js");
|
|
7403
7615
|
try {
|
|
7404
7616
|
const payload = await c.req.json();
|
|
7405
7617
|
const container = getContainer();
|
|
@@ -7415,7 +7627,7 @@ async function createIssue(c) {
|
|
|
7415
7627
|
}
|
|
7416
7628
|
async function cancelIssue(c) {
|
|
7417
7629
|
const context2 = getApiRuntimeContextOrThrow();
|
|
7418
|
-
const { persistState: persistState2 } = await import("./store-
|
|
7630
|
+
const { persistState: persistState2 } = await import("./store-S3NAYZ3S.js");
|
|
7419
7631
|
const issueId = parseIssue(c);
|
|
7420
7632
|
if (!issueId) {
|
|
7421
7633
|
return { status: 400, body: { ok: false, error: "Issue id is required." } };
|
|
@@ -7434,7 +7646,7 @@ async function cancelIssue(c) {
|
|
|
7434
7646
|
}
|
|
7435
7647
|
async function deleteIssue(c) {
|
|
7436
7648
|
const context2 = getApiRuntimeContextOrThrow();
|
|
7437
|
-
const { persistState: persistState2 } = await import("./store-
|
|
7649
|
+
const { persistState: persistState2 } = await import("./store-S3NAYZ3S.js");
|
|
7438
7650
|
const issueId = parseIssue(c);
|
|
7439
7651
|
if (!issueId) {
|
|
7440
7652
|
return { status: 400, body: { ok: false, error: "Issue id is required." } };
|
|
@@ -7456,7 +7668,7 @@ async function deleteIssue(c) {
|
|
|
7456
7668
|
}
|
|
7457
7669
|
async function approveAndMerge(c) {
|
|
7458
7670
|
const context2 = getApiRuntimeContextOrThrow();
|
|
7459
|
-
const { persistState: persistState2 } = await import("./store-
|
|
7671
|
+
const { persistState: persistState2 } = await import("./store-S3NAYZ3S.js");
|
|
7460
7672
|
const issueId = parseIssue(c);
|
|
7461
7673
|
if (!issueId) {
|
|
7462
7674
|
return { status: 400, body: { ok: false, error: "Issue id is required." } };
|
|
@@ -7509,7 +7721,7 @@ async function approveIssue(c) {
|
|
|
7509
7721
|
try {
|
|
7510
7722
|
const container = getContainer();
|
|
7511
7723
|
await approvePlanCommand({ issue }, container);
|
|
7512
|
-
const { persistState: persistState2 } = await import("./store-
|
|
7724
|
+
const { persistState: persistState2 } = await import("./store-S3NAYZ3S.js");
|
|
7513
7725
|
await persistState2(context2.state);
|
|
7514
7726
|
return { body: { ok: true, issue } };
|
|
7515
7727
|
} catch (error) {
|
|
@@ -7526,7 +7738,7 @@ async function executeIssue(c) {
|
|
|
7526
7738
|
try {
|
|
7527
7739
|
const container = getContainer();
|
|
7528
7740
|
await executeIssueCommand({ issue }, container);
|
|
7529
|
-
const { persistState: persistState2 } = await import("./store-
|
|
7741
|
+
const { persistState: persistState2 } = await import("./store-S3NAYZ3S.js");
|
|
7530
7742
|
await persistState2(context2.state);
|
|
7531
7743
|
return { body: { ok: true, issue } };
|
|
7532
7744
|
} catch (error) {
|
|
@@ -7543,7 +7755,7 @@ async function replanIssue(c) {
|
|
|
7543
7755
|
try {
|
|
7544
7756
|
const container = getContainer();
|
|
7545
7757
|
await replanIssueCommand({ issue }, container);
|
|
7546
|
-
const { persistState: persistState2 } = await import("./store-
|
|
7758
|
+
const { persistState: persistState2 } = await import("./store-S3NAYZ3S.js");
|
|
7547
7759
|
await persistState2(context2.state);
|
|
7548
7760
|
return { body: { ok: true, issue } };
|
|
7549
7761
|
} catch (error) {
|
|
@@ -7577,8 +7789,9 @@ async function mergeIssuePreview(c) {
|
|
|
7577
7789
|
const issue = findIssue(context2.state, issueId);
|
|
7578
7790
|
if (!issue) return { status: 404, body: { ok: false, error: "Issue not found." } };
|
|
7579
7791
|
try {
|
|
7580
|
-
const { dryMerge } = await import("./workspace-
|
|
7581
|
-
const
|
|
7792
|
+
const { dryMerge } = await import("./workspace-OS7GPMCN.js");
|
|
7793
|
+
const autoCommit = context2.state.config.autoCommitBeforeMerge ?? true;
|
|
7794
|
+
const result = dryMerge(issue, autoCommit);
|
|
7582
7795
|
return { body: { ok: true, ...result } };
|
|
7583
7796
|
} catch (error) {
|
|
7584
7797
|
logger.error(`Failed to preview merge for ${issueId}: ${String(error)}`);
|
|
@@ -7592,12 +7805,12 @@ async function rebaseIssue(c) {
|
|
|
7592
7805
|
const issue = findIssue(context2.state, issueId);
|
|
7593
7806
|
if (!issue) return { status: 404, body: { ok: false, error: "Issue not found." } };
|
|
7594
7807
|
try {
|
|
7595
|
-
const { rebaseWorktree: rebaseWorktree2 } = await import("./workspace-
|
|
7808
|
+
const { rebaseWorktree: rebaseWorktree2 } = await import("./workspace-OS7GPMCN.js");
|
|
7596
7809
|
const result = rebaseWorktree2(issue);
|
|
7597
7810
|
if (result.success) {
|
|
7598
7811
|
addEvent(context2.state, issue.id, "info", `Branch ${issue.branchName} rebased onto ${issue.baseBranch}.`);
|
|
7599
7812
|
}
|
|
7600
|
-
const { persistState: persistState2 } = await import("./store-
|
|
7813
|
+
const { persistState: persistState2 } = await import("./store-S3NAYZ3S.js");
|
|
7601
7814
|
await persistState2(context2.state);
|
|
7602
7815
|
return { body: { ok: true, ...result } };
|
|
7603
7816
|
} catch (error) {
|
|
@@ -7617,7 +7830,7 @@ async function createIssueTestWorkspace(c) {
|
|
|
7617
7830
|
const testWorkspacePath = createTestWorkspace(issue);
|
|
7618
7831
|
markIssueDirty(issue.id);
|
|
7619
7832
|
addEvent(context2.state, issue.id, "manual", `Isolated test workspace created at ${testWorkspacePath}.`);
|
|
7620
|
-
const { persistState: persistState2 } = await import("./store-
|
|
7833
|
+
const { persistState: persistState2 } = await import("./store-S3NAYZ3S.js");
|
|
7621
7834
|
await persistState2(context2.state);
|
|
7622
7835
|
return { body: { ok: true, issue } };
|
|
7623
7836
|
}
|
|
@@ -7630,7 +7843,7 @@ async function revertIssueTestWorkspace(c) {
|
|
|
7630
7843
|
removeTestWorkspace(issue);
|
|
7631
7844
|
markIssueDirty(issue.id);
|
|
7632
7845
|
addEvent(context2.state, issue.id, "manual", "Isolated test workspace removed.");
|
|
7633
|
-
const { persistState: persistState2 } = await import("./store-
|
|
7846
|
+
const { persistState: persistState2 } = await import("./store-S3NAYZ3S.js");
|
|
7634
7847
|
await persistState2(context2.state);
|
|
7635
7848
|
return { body: { ok: true, issue } };
|
|
7636
7849
|
}
|
|
@@ -7658,7 +7871,7 @@ async function rollbackIssue(c) {
|
|
|
7658
7871
|
container
|
|
7659
7872
|
);
|
|
7660
7873
|
addEvent(context2.state, issue.id, "manual", `${issue.identifier} rolled back. Worktree and branch removed.`);
|
|
7661
|
-
const { persistState: persistState2 } = await import("./store-
|
|
7874
|
+
const { persistState: persistState2 } = await import("./store-S3NAYZ3S.js");
|
|
7662
7875
|
await persistState2(context2.state);
|
|
7663
7876
|
return { body: { ok: true, issue } };
|
|
7664
7877
|
}
|
|
@@ -7697,21 +7910,21 @@ async function uploadIssueImages(c) {
|
|
|
7697
7910
|
if (!Array.isArray(payload.files) || payload.files.length === 0) {
|
|
7698
7911
|
return { status: 400, body: { ok: false, error: "No files provided." } };
|
|
7699
7912
|
}
|
|
7700
|
-
const issueAttachDir =
|
|
7913
|
+
const issueAttachDir = join19(ATTACHMENTS_ROOT, issue.id);
|
|
7701
7914
|
mkdirSync10(issueAttachDir, { recursive: true });
|
|
7702
7915
|
const newPaths = [];
|
|
7703
7916
|
for (const file of payload.files) {
|
|
7704
7917
|
if (typeof file.data !== "string" || !file.name) continue;
|
|
7705
7918
|
const safeExt = extname2(file.name).replace(/[^a-z0-9.]/gi, "").slice(0, 10) || ".bin";
|
|
7706
7919
|
const safeName = `${randomUUID2()}${safeExt}`;
|
|
7707
|
-
const dest =
|
|
7920
|
+
const dest = join19(issueAttachDir, safeName);
|
|
7708
7921
|
writeFileSync13(dest, Buffer.from(file.data, "base64"));
|
|
7709
7922
|
newPaths.push(dest);
|
|
7710
7923
|
}
|
|
7711
7924
|
issue.images = [...issue.images ?? [], ...newPaths];
|
|
7712
7925
|
issue.updatedAt = now();
|
|
7713
7926
|
markIssueDirty(issue.id);
|
|
7714
|
-
const { persistState: persistState2 } = await import("./store-
|
|
7927
|
+
const { persistState: persistState2 } = await import("./store-S3NAYZ3S.js");
|
|
7715
7928
|
await persistState2(context2.state);
|
|
7716
7929
|
return { body: { ok: true, paths: newPaths, issue } };
|
|
7717
7930
|
} catch (error) {
|
|
@@ -7725,8 +7938,8 @@ async function getIssueImage(c) {
|
|
|
7725
7938
|
const filename = c?.req?.param?.("filename") ?? "";
|
|
7726
7939
|
if (!filename) return { status: 400, body: { ok: false, error: "Filename is required." } };
|
|
7727
7940
|
const safeName = basename3(filename);
|
|
7728
|
-
const filePath =
|
|
7729
|
-
if (!
|
|
7941
|
+
const filePath = join19(ATTACHMENTS_ROOT, issueId, safeName);
|
|
7942
|
+
if (!existsSync10(filePath)) return { status: 404, body: { ok: false, error: "Image not found." } };
|
|
7730
7943
|
const ext = extname2(safeName).toLowerCase();
|
|
7731
7944
|
const mimeMap = {
|
|
7732
7945
|
".png": "image/png",
|
|
@@ -7737,7 +7950,7 @@ async function getIssueImage(c) {
|
|
|
7737
7950
|
".svg": "image/svg+xml"
|
|
7738
7951
|
};
|
|
7739
7952
|
const mime = mimeMap[ext] ?? "application/octet-stream";
|
|
7740
|
-
const data =
|
|
7953
|
+
const data = readFileSync7(filePath);
|
|
7741
7954
|
return { body: new Response(data, { headers: { "Content-Type": mime, "Cache-Control": "private, max-age=86400" } }) };
|
|
7742
7955
|
}
|
|
7743
7956
|
async function getIssueHistory(c) {
|
|
@@ -7770,7 +7983,7 @@ async function writeToIssueAgent(c) {
|
|
|
7770
7983
|
return { status: 400, body: { ok: false, error: "Body must include { text: string } \u2014 the text to send to the agent's PTY stdin." } };
|
|
7771
7984
|
}
|
|
7772
7985
|
const payload = text.endsWith("\r") || text.endsWith("\n") ? text : `${text}\r`;
|
|
7773
|
-
const { writeToDaemon: writeToDaemon2, isDaemonSocketReady: isDaemonSocketReady2 } = await import("./agent-
|
|
7986
|
+
const { writeToDaemon: writeToDaemon2, isDaemonSocketReady: isDaemonSocketReady2 } = await import("./agent-DFSFG6DG.js");
|
|
7774
7987
|
if (!isDaemonSocketReady2(issue.workspacePath)) {
|
|
7775
7988
|
return { status: 409, body: { ok: false, error: "Agent is running but not via PTY daemon \u2014 write not supported for this session." } };
|
|
7776
7989
|
}
|
|
@@ -7807,7 +8020,7 @@ async function streamIssueAgentRuntime(c) {
|
|
|
7807
8020
|
start(ctrl) {
|
|
7808
8021
|
let lastSize = 0;
|
|
7809
8022
|
const logFile = getLogFile();
|
|
7810
|
-
if (logFile &&
|
|
8023
|
+
if (logFile && existsSync10(logFile)) {
|
|
7811
8024
|
try {
|
|
7812
8025
|
const stat = statSync2(logFile);
|
|
7813
8026
|
lastSize = stat.size;
|
|
@@ -7825,7 +8038,7 @@ async function streamIssueAgentRuntime(c) {
|
|
|
7825
8038
|
}
|
|
7826
8039
|
chunkIntervalId = setInterval(() => {
|
|
7827
8040
|
const lf = getLogFile();
|
|
7828
|
-
if (!lf || !
|
|
8041
|
+
if (!lf || !existsSync10(lf)) return;
|
|
7829
8042
|
try {
|
|
7830
8043
|
const stat = statSync2(lf);
|
|
7831
8044
|
if (stat.size < lastSize) {
|
|
@@ -7910,7 +8123,7 @@ async function getIssueLive(c) {
|
|
|
7910
8123
|
const liveLog = wp ? `${wp}/live-output.log` : null;
|
|
7911
8124
|
let logTail = "";
|
|
7912
8125
|
let logSize = 0;
|
|
7913
|
-
if (liveLog &&
|
|
8126
|
+
if (liveLog && existsSync10(liveLog)) {
|
|
7914
8127
|
try {
|
|
7915
8128
|
const stat = statSync2(liveLog);
|
|
7916
8129
|
logSize = stat.size;
|
|
@@ -7924,7 +8137,7 @@ async function getIssueLive(c) {
|
|
|
7924
8137
|
}
|
|
7925
8138
|
}
|
|
7926
8139
|
const agentStatus = isAgentStillRunning(issue);
|
|
7927
|
-
const daemonSocketReady = wp ?
|
|
8140
|
+
const daemonSocketReady = wp ? existsSync10(join19(wp, "agent.sock")) : false;
|
|
7928
8141
|
return {
|
|
7929
8142
|
body: {
|
|
7930
8143
|
ok: true,
|
|
@@ -7964,7 +8177,7 @@ async function streamIssueLive(c) {
|
|
|
7964
8177
|
const wp = issue.workspacePath;
|
|
7965
8178
|
const liveLog = wp ? `${wp}/live-output.log` : null;
|
|
7966
8179
|
let lastSize = 0;
|
|
7967
|
-
if (liveLog &&
|
|
8180
|
+
if (liveLog && existsSync10(liveLog)) {
|
|
7968
8181
|
try {
|
|
7969
8182
|
const stat = statSync2(liveLog);
|
|
7970
8183
|
lastSize = stat.size;
|
|
@@ -7992,7 +8205,7 @@ async function streamIssueLive(c) {
|
|
|
7992
8205
|
return;
|
|
7993
8206
|
}
|
|
7994
8207
|
const logPath = currentIssue.workspacePath ? `${currentIssue.workspacePath}/live-output.log` : null;
|
|
7995
|
-
if (logPath &&
|
|
8208
|
+
if (logPath && existsSync10(logPath)) {
|
|
7996
8209
|
try {
|
|
7997
8210
|
const stat = statSync2(logPath);
|
|
7998
8211
|
if (stat.size > lastSize) {
|
|
@@ -8040,7 +8253,7 @@ async function getIssueDiff(c) {
|
|
|
8040
8253
|
if (!issue) return { status: 404, body: { ok: false, error: "Issue not found." } };
|
|
8041
8254
|
try {
|
|
8042
8255
|
const wp = issue.workspacePath;
|
|
8043
|
-
if (!wp || !
|
|
8256
|
+
if (!wp || !existsSync10(wp)) {
|
|
8044
8257
|
return { body: { ok: true, files: [], diff: "", message: "No workspace found." } };
|
|
8045
8258
|
}
|
|
8046
8259
|
let raw = "";
|
|
@@ -8055,7 +8268,7 @@ async function getIssueDiff(c) {
|
|
|
8055
8268
|
raw = typeof failedDiff.stdout === "string" ? failedDiff.stdout : "";
|
|
8056
8269
|
}
|
|
8057
8270
|
} else {
|
|
8058
|
-
if (!
|
|
8271
|
+
if (!existsSync10(SOURCE_ROOT)) {
|
|
8059
8272
|
return { body: { ok: true, files: [], diff: "", message: "Source root not found." } };
|
|
8060
8273
|
}
|
|
8061
8274
|
try {
|
|
@@ -8113,11 +8326,11 @@ async function listIssueOutputs(c) {
|
|
|
8113
8326
|
if (!issue) return { status: 404, body: { ok: false, error: "Issue not found." } };
|
|
8114
8327
|
const wp = issue.workspacePath;
|
|
8115
8328
|
if (!wp) return { body: { ok: true, files: [] } };
|
|
8116
|
-
const outputsDir =
|
|
8117
|
-
if (!
|
|
8118
|
-
const entries =
|
|
8329
|
+
const outputsDir = join19(wp, "outputs");
|
|
8330
|
+
if (!existsSync10(outputsDir)) return { body: { ok: true, files: [] } };
|
|
8331
|
+
const entries = readdirSync4(outputsDir).filter((file) => file.endsWith(".stdout.log")).map((file) => {
|
|
8119
8332
|
try {
|
|
8120
|
-
const stats = statSync2(
|
|
8333
|
+
const stats = statSync2(join19(outputsDir, file));
|
|
8121
8334
|
return { name: file, size: stats.size };
|
|
8122
8335
|
} catch {
|
|
8123
8336
|
return { name: file, size: 0 };
|
|
@@ -8139,9 +8352,9 @@ async function getIssueOutput(c) {
|
|
|
8139
8352
|
}
|
|
8140
8353
|
const wp = issue.workspacePath;
|
|
8141
8354
|
if (!wp) return { status: 404, body: { ok: false, error: "No workspace found." } };
|
|
8142
|
-
const filePath =
|
|
8143
|
-
if (!
|
|
8144
|
-
const content =
|
|
8355
|
+
const filePath = join19(wp, "outputs", safeName);
|
|
8356
|
+
if (!existsSync10(filePath)) return { status: 404, body: { ok: false, error: "Output file not found." } };
|
|
8357
|
+
const content = readFileSync7(filePath, "utf8");
|
|
8145
8358
|
return { body: new Response(content, { headers: { "Content-Type": "text/plain; charset=utf-8" } }) };
|
|
8146
8359
|
}
|
|
8147
8360
|
async function assignIssueMilestone(c, deps) {
|
|
@@ -8363,7 +8576,7 @@ var issues_resource_default = {
|
|
|
8363
8576
|
|
|
8364
8577
|
// src/persistence/resources/milestones.resource.ts
|
|
8365
8578
|
async function loadMilestoneApiDeps() {
|
|
8366
|
-
const { getMilestoneStateResource: getMilestoneStateResource2, persistState: persistState2 } = await import("./store-
|
|
8579
|
+
const { getMilestoneStateResource: getMilestoneStateResource2, persistState: persistState2 } = await import("./store-S3NAYZ3S.js");
|
|
8367
8580
|
return {
|
|
8368
8581
|
persistState: persistState2,
|
|
8369
8582
|
deleteMilestoneRecord: async (id) => {
|
|
@@ -8556,7 +8769,7 @@ async function getSetting(c) {
|
|
|
8556
8769
|
}
|
|
8557
8770
|
async function updateSetting(c) {
|
|
8558
8771
|
const context2 = getApiRuntimeContextOrThrow();
|
|
8559
|
-
const { persistState: persistState2 } = await import("./store-
|
|
8772
|
+
const { persistState: persistState2 } = await import("./store-S3NAYZ3S.js");
|
|
8560
8773
|
const settingId = c.req.param("id") || "";
|
|
8561
8774
|
if (!settingId) {
|
|
8562
8775
|
return respond2(c, { ok: false, error: "Setting id is required" }, 400);
|
|
@@ -8684,7 +8897,7 @@ async function loadServiceApiDeps() {
|
|
|
8684
8897
|
replacePersistedService: replacePersistedService2,
|
|
8685
8898
|
deletePersistedService: deletePersistedService2,
|
|
8686
8899
|
replaceAllServices: replaceAllServices2
|
|
8687
|
-
} = await import("./store-
|
|
8900
|
+
} = await import("./store-S3NAYZ3S.js");
|
|
8688
8901
|
return {
|
|
8689
8902
|
replacePersistedService: replacePersistedService2,
|
|
8690
8903
|
deletePersistedService: deletePersistedService2,
|
|
@@ -8814,7 +9027,7 @@ var services_resource_default = {
|
|
|
8814
9027
|
};
|
|
8815
9028
|
|
|
8816
9029
|
// src/persistence/resources/variables.resource.ts
|
|
8817
|
-
var
|
|
9030
|
+
var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8818
9031
|
function getRuntimeState2() {
|
|
8819
9032
|
return getApiRuntimeContextOrThrow().state;
|
|
8820
9033
|
}
|
|
@@ -8827,7 +9040,7 @@ function normalizeVariableEntry(value) {
|
|
|
8827
9040
|
const scope = typeof raw.scope === "string" ? raw.scope.trim() : "";
|
|
8828
9041
|
const val = typeof raw.value === "string" ? raw.value : String(raw.value ?? "");
|
|
8829
9042
|
if (!key) return { error: "Variable key is required." };
|
|
8830
|
-
if (!
|
|
9043
|
+
if (!ENV_KEY_PATTERN.test(key)) return { error: `Invalid variable name "${key}". Must match [A-Za-z_][A-Za-z0-9_]*.` };
|
|
8831
9044
|
if (!scope) return { error: "Variable scope is required." };
|
|
8832
9045
|
const id = `${scope}:${key}`;
|
|
8833
9046
|
const entry = { id, key, value: val, scope, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
@@ -8928,640 +9141,6 @@ var NATIVE_RESOURCE_CONFIGS = [
|
|
|
8928
9141
|
];
|
|
8929
9142
|
var NATIVE_RESOURCE_NAMES = NATIVE_RESOURCE_CONFIGS.map((resource) => resource.name);
|
|
8930
9143
|
|
|
8931
|
-
// src/persistence/plugins/service-log-broadcaster.ts
|
|
8932
|
-
import {
|
|
8933
|
-
closeSync as closeSync3,
|
|
8934
|
-
existsSync as existsSync11,
|
|
8935
|
-
openSync as openSync3,
|
|
8936
|
-
readSync as readSync3,
|
|
8937
|
-
statSync as statSync4,
|
|
8938
|
-
watch
|
|
8939
|
-
} from "fs";
|
|
8940
|
-
|
|
8941
|
-
// src/persistence/plugins/fsm-service.ts
|
|
8942
|
-
import {
|
|
8943
|
-
closeSync as closeSync2,
|
|
8944
|
-
existsSync as existsSync10,
|
|
8945
|
-
openSync as openSync2,
|
|
8946
|
-
readFileSync as readFileSync7,
|
|
8947
|
-
readSync as readSync2,
|
|
8948
|
-
rmSync as rmSync5,
|
|
8949
|
-
statSync as statSync3,
|
|
8950
|
-
writeFileSync as writeFileSync14
|
|
8951
|
-
} from "fs";
|
|
8952
|
-
import { join as join19, resolve as resolve3 } from "path";
|
|
8953
|
-
import { spawn as spawn2 } from "child_process";
|
|
8954
|
-
var STARTING_GRACE_MS = 3e3;
|
|
8955
|
-
var STOPPING_KILL_MS = 5e3;
|
|
8956
|
-
var SERVICE_WATCHER_INTERVAL_MS = 5e3;
|
|
8957
|
-
function pidPath(fifonyDir, id) {
|
|
8958
|
-
return join19(fifonyDir, `service-${id}.pid`);
|
|
8959
|
-
}
|
|
8960
|
-
function serviceLogPath(fifonyDir, id) {
|
|
8961
|
-
return join19(fifonyDir, `service-${id}.log`);
|
|
8962
|
-
}
|
|
8963
|
-
function readPidInfo(fifonyDir, id) {
|
|
8964
|
-
const path = pidPath(fifonyDir, id);
|
|
8965
|
-
if (!existsSync10(path)) return null;
|
|
8966
|
-
try {
|
|
8967
|
-
const data = JSON.parse(readFileSync7(path, "utf8"));
|
|
8968
|
-
if (!data?.pid || typeof data.pid !== "number") return null;
|
|
8969
|
-
if (!data.state) {
|
|
8970
|
-
data.state = isProcessAlive(data.pid) ? "running" : "crashed";
|
|
8971
|
-
data.crashCount ??= 0;
|
|
8972
|
-
}
|
|
8973
|
-
return data;
|
|
8974
|
-
} catch {
|
|
8975
|
-
return null;
|
|
8976
|
-
}
|
|
8977
|
-
}
|
|
8978
|
-
function writePidInfo(fifonyDir, id, info) {
|
|
8979
|
-
writeFileSync14(pidPath(fifonyDir, id), JSON.stringify(info));
|
|
8980
|
-
}
|
|
8981
|
-
function removePidInfo(fifonyDir, id) {
|
|
8982
|
-
try {
|
|
8983
|
-
rmSync5(pidPath(fifonyDir, id), { force: true });
|
|
8984
|
-
} catch {
|
|
8985
|
-
}
|
|
8986
|
-
}
|
|
8987
|
-
function spawnProcess(entry, targetRoot, fifonyDir, globalEnv) {
|
|
8988
|
-
const cwd = entry.cwd ? resolve3(targetRoot, entry.cwd) : targetRoot;
|
|
8989
|
-
const log = serviceLogPath(fifonyDir, entry.id);
|
|
8990
|
-
const command = buildServiceCommand(entry.command, globalEnv, entry.env);
|
|
8991
|
-
try {
|
|
8992
|
-
writeFileSync14(log, "");
|
|
8993
|
-
} catch {
|
|
8994
|
-
}
|
|
8995
|
-
const logFd = openSync2(log, "a");
|
|
8996
|
-
const child = spawn2(command, [], {
|
|
8997
|
-
shell: true,
|
|
8998
|
-
cwd,
|
|
8999
|
-
detached: true,
|
|
9000
|
-
stdio: ["ignore", logFd, logFd]
|
|
9001
|
-
});
|
|
9002
|
-
try {
|
|
9003
|
-
closeSync2(logFd);
|
|
9004
|
-
} catch {
|
|
9005
|
-
}
|
|
9006
|
-
child.unref();
|
|
9007
|
-
if (child.pid === void 0) {
|
|
9008
|
-
throw new Error(`Failed to spawn service process: ${command}`);
|
|
9009
|
-
}
|
|
9010
|
-
return { pid: child.pid, command };
|
|
9011
|
-
}
|
|
9012
|
-
function getServiceStatus(entry, fifonyDir) {
|
|
9013
|
-
const info = readPidInfo(fifonyDir, entry.id);
|
|
9014
|
-
const alive = info !== null && isProcessAlive(info.pid);
|
|
9015
|
-
let state;
|
|
9016
|
-
if (!info) {
|
|
9017
|
-
state = "stopped";
|
|
9018
|
-
} else if (info.state === "stopping") {
|
|
9019
|
-
state = alive ? "stopping" : "stopped";
|
|
9020
|
-
} else if (info.state === "starting" || info.state === "running") {
|
|
9021
|
-
state = alive ? info.state : "crashed";
|
|
9022
|
-
} else {
|
|
9023
|
-
state = info.state;
|
|
9024
|
-
}
|
|
9025
|
-
const logFile = serviceLogPath(fifonyDir, entry.id);
|
|
9026
|
-
let logSize = 0;
|
|
9027
|
-
if (existsSync10(logFile)) {
|
|
9028
|
-
try {
|
|
9029
|
-
logSize = statSync3(logFile).size;
|
|
9030
|
-
} catch {
|
|
9031
|
-
}
|
|
9032
|
-
}
|
|
9033
|
-
const startedAt = info?.startedAt ?? null;
|
|
9034
|
-
const running2 = state === "starting" || state === "running";
|
|
9035
|
-
const uptime = startedAt && running2 ? Date.now() - Date.parse(startedAt) : 0;
|
|
9036
|
-
return {
|
|
9037
|
-
id: entry.id,
|
|
9038
|
-
name: entry.name,
|
|
9039
|
-
command: entry.command,
|
|
9040
|
-
cwd: entry.cwd,
|
|
9041
|
-
env: entry.env,
|
|
9042
|
-
autoStart: entry.autoStart,
|
|
9043
|
-
autoRestart: entry.autoRestart,
|
|
9044
|
-
maxCrashes: entry.maxCrashes,
|
|
9045
|
-
port: entry.port,
|
|
9046
|
-
state,
|
|
9047
|
-
running: running2,
|
|
9048
|
-
pid: alive ? info?.pid ?? null : null,
|
|
9049
|
-
startedAt,
|
|
9050
|
-
uptime: Number.isFinite(uptime) ? uptime : 0,
|
|
9051
|
-
logSize,
|
|
9052
|
-
crashCount: info?.crashCount ?? 0,
|
|
9053
|
-
nextRetryAt: info?.nextRetryAt
|
|
9054
|
-
};
|
|
9055
|
-
}
|
|
9056
|
-
function getAllServiceStatuses(entries, fifonyDir) {
|
|
9057
|
-
return entries.map((e) => getServiceStatus(e, fifonyDir));
|
|
9058
|
-
}
|
|
9059
|
-
function cmdStart(entry, targetRoot, fifonyDir, globalEnv) {
|
|
9060
|
-
const existing = readPidInfo(fifonyDir, entry.id);
|
|
9061
|
-
const fromState = existing?.state ?? "none";
|
|
9062
|
-
if (existing && isProcessAlive(existing.pid)) {
|
|
9063
|
-
try {
|
|
9064
|
-
process.kill(-existing.pid, "SIGKILL");
|
|
9065
|
-
} catch {
|
|
9066
|
-
}
|
|
9067
|
-
try {
|
|
9068
|
-
process.kill(existing.pid, "SIGKILL");
|
|
9069
|
-
} catch {
|
|
9070
|
-
}
|
|
9071
|
-
}
|
|
9072
|
-
const spawned = spawnProcess(entry, targetRoot, fifonyDir, globalEnv);
|
|
9073
|
-
writePidInfo(fifonyDir, entry.id, {
|
|
9074
|
-
pid: spawned.pid,
|
|
9075
|
-
command: spawned.command,
|
|
9076
|
-
startedAt: now(),
|
|
9077
|
-
state: "starting",
|
|
9078
|
-
crashCount: 0
|
|
9079
|
-
// manual start always resets crash count
|
|
9080
|
-
});
|
|
9081
|
-
logger.info({ id: entry.id, pid: spawned.pid, from: fromState }, "[Service] FSM: \u2192 starting (manual start)");
|
|
9082
|
-
return { id: entry.id, from: fromState, to: "starting", pid: spawned.pid, reason: "manual start" };
|
|
9083
|
-
}
|
|
9084
|
-
function cmdStop(id, fifonyDir) {
|
|
9085
|
-
const existing = readPidInfo(fifonyDir, id);
|
|
9086
|
-
if (!existing || existing.state === "stopped") return null;
|
|
9087
|
-
const fromState = existing.state;
|
|
9088
|
-
if (isProcessAlive(existing.pid)) {
|
|
9089
|
-
try {
|
|
9090
|
-
process.kill(-existing.pid, "SIGTERM");
|
|
9091
|
-
} catch {
|
|
9092
|
-
}
|
|
9093
|
-
}
|
|
9094
|
-
writePidInfo(fifonyDir, id, {
|
|
9095
|
-
...existing,
|
|
9096
|
-
state: "stopping",
|
|
9097
|
-
stoppingAt: now()
|
|
9098
|
-
});
|
|
9099
|
-
logger.info({ id, pid: existing.pid, from: fromState }, "[Service] FSM: \u2192 stopping (manual stop)");
|
|
9100
|
-
return { id, from: fromState, to: "stopping", pid: existing.pid, reason: "manual stop" };
|
|
9101
|
-
}
|
|
9102
|
-
function autoRestartBackoffMs(crashCount) {
|
|
9103
|
-
return Math.min(Math.pow(2, crashCount) * 1e3, 6e4);
|
|
9104
|
-
}
|
|
9105
|
-
function tickOne(entry, globalEnv, fifonyDir, targetRoot) {
|
|
9106
|
-
const info = readPidInfo(fifonyDir, entry.id);
|
|
9107
|
-
if (!info) return null;
|
|
9108
|
-
const alive = isProcessAlive(info.pid);
|
|
9109
|
-
const nowMs = Date.now();
|
|
9110
|
-
switch (info.state) {
|
|
9111
|
-
case "starting": {
|
|
9112
|
-
if (!alive) {
|
|
9113
|
-
const crashCount = (info.crashCount ?? 0) + 1;
|
|
9114
|
-
const maxCrashes = entry.maxCrashes ?? 5;
|
|
9115
|
-
const autoRestart = entry.autoRestart ?? false;
|
|
9116
|
-
const nextRetryAt = autoRestart && crashCount < maxCrashes ? new Date(nowMs + autoRestartBackoffMs(crashCount)).toISOString() : void 0;
|
|
9117
|
-
writePidInfo(fifonyDir, entry.id, {
|
|
9118
|
-
...info,
|
|
9119
|
-
state: "crashed",
|
|
9120
|
-
crashCount,
|
|
9121
|
-
lastCrashAt: now(),
|
|
9122
|
-
nextRetryAt
|
|
9123
|
-
});
|
|
9124
|
-
logger.warn({ id: entry.id, crashCount, nextRetryAt }, "[Service] FSM: starting \u2192 crashed");
|
|
9125
|
-
return {
|
|
9126
|
-
id: entry.id,
|
|
9127
|
-
from: "starting",
|
|
9128
|
-
to: "crashed",
|
|
9129
|
-
pid: null,
|
|
9130
|
-
reason: `died during startup (crash #${crashCount})`
|
|
9131
|
-
};
|
|
9132
|
-
}
|
|
9133
|
-
const ageMs = nowMs - Date.parse(info.startedAt);
|
|
9134
|
-
if (ageMs >= STARTING_GRACE_MS) {
|
|
9135
|
-
writePidInfo(fifonyDir, entry.id, { ...info, state: "running" });
|
|
9136
|
-
logger.info({ id: entry.id, pid: info.pid }, "[Service] FSM: starting \u2192 running");
|
|
9137
|
-
return {
|
|
9138
|
-
id: entry.id,
|
|
9139
|
-
from: "starting",
|
|
9140
|
-
to: "running",
|
|
9141
|
-
pid: info.pid,
|
|
9142
|
-
reason: "startup grace period elapsed"
|
|
9143
|
-
};
|
|
9144
|
-
}
|
|
9145
|
-
return null;
|
|
9146
|
-
}
|
|
9147
|
-
case "running": {
|
|
9148
|
-
if (!alive) {
|
|
9149
|
-
const crashCount = (info.crashCount ?? 0) + 1;
|
|
9150
|
-
const maxCrashes = entry.maxCrashes ?? 5;
|
|
9151
|
-
const autoRestart = entry.autoRestart ?? false;
|
|
9152
|
-
const nextRetryAt = autoRestart && crashCount < maxCrashes ? new Date(nowMs + autoRestartBackoffMs(crashCount)).toISOString() : void 0;
|
|
9153
|
-
writePidInfo(fifonyDir, entry.id, {
|
|
9154
|
-
...info,
|
|
9155
|
-
state: "crashed",
|
|
9156
|
-
crashCount,
|
|
9157
|
-
lastCrashAt: now(),
|
|
9158
|
-
nextRetryAt
|
|
9159
|
-
});
|
|
9160
|
-
logger.warn({ id: entry.id, crashCount, nextRetryAt }, "[Service] FSM: running \u2192 crashed");
|
|
9161
|
-
return {
|
|
9162
|
-
id: entry.id,
|
|
9163
|
-
from: "running",
|
|
9164
|
-
to: "crashed",
|
|
9165
|
-
pid: null,
|
|
9166
|
-
reason: `process died unexpectedly (crash #${crashCount})`
|
|
9167
|
-
};
|
|
9168
|
-
}
|
|
9169
|
-
return null;
|
|
9170
|
-
}
|
|
9171
|
-
case "stopping": {
|
|
9172
|
-
if (!alive) {
|
|
9173
|
-
removePidInfo(fifonyDir, entry.id);
|
|
9174
|
-
logger.info({ id: entry.id }, "[Service] FSM: stopping \u2192 stopped (process exited)");
|
|
9175
|
-
return {
|
|
9176
|
-
id: entry.id,
|
|
9177
|
-
from: "stopping",
|
|
9178
|
-
to: "stopped",
|
|
9179
|
-
pid: null,
|
|
9180
|
-
reason: "process exited gracefully"
|
|
9181
|
-
};
|
|
9182
|
-
}
|
|
9183
|
-
const stoppingAgeMs = info.stoppingAt ? nowMs - Date.parse(info.stoppingAt) : STOPPING_KILL_MS + 1;
|
|
9184
|
-
if (stoppingAgeMs >= STOPPING_KILL_MS) {
|
|
9185
|
-
try {
|
|
9186
|
-
process.kill(-info.pid, "SIGKILL");
|
|
9187
|
-
} catch {
|
|
9188
|
-
}
|
|
9189
|
-
try {
|
|
9190
|
-
process.kill(info.pid, "SIGKILL");
|
|
9191
|
-
} catch {
|
|
9192
|
-
}
|
|
9193
|
-
removePidInfo(fifonyDir, entry.id);
|
|
9194
|
-
logger.info({ id: entry.id, pid: info.pid }, "[Service] FSM: stopping \u2192 stopped (SIGKILL)");
|
|
9195
|
-
return {
|
|
9196
|
-
id: entry.id,
|
|
9197
|
-
from: "stopping",
|
|
9198
|
-
to: "stopped",
|
|
9199
|
-
pid: null,
|
|
9200
|
-
reason: "SIGKILL after stop timeout"
|
|
9201
|
-
};
|
|
9202
|
-
}
|
|
9203
|
-
return null;
|
|
9204
|
-
}
|
|
9205
|
-
case "crashed": {
|
|
9206
|
-
const maxCrashes = entry.maxCrashes ?? 5;
|
|
9207
|
-
if (!(entry.autoRestart ?? false) || (info.crashCount ?? 0) >= maxCrashes) return null;
|
|
9208
|
-
const nextRetryMs = info.nextRetryAt ? Date.parse(info.nextRetryAt) : 0;
|
|
9209
|
-
if (nowMs < nextRetryMs) return null;
|
|
9210
|
-
const spawned = spawnProcess(entry, targetRoot, fifonyDir, globalEnv);
|
|
9211
|
-
writePidInfo(fifonyDir, entry.id, {
|
|
9212
|
-
pid: spawned.pid,
|
|
9213
|
-
command: spawned.command,
|
|
9214
|
-
startedAt: now(),
|
|
9215
|
-
state: "starting",
|
|
9216
|
-
crashCount: info.crashCount
|
|
9217
|
-
// preserve crash count on auto-restart
|
|
9218
|
-
});
|
|
9219
|
-
logger.info(
|
|
9220
|
-
{ id: entry.id, pid: spawned.pid, crashCount: info.crashCount },
|
|
9221
|
-
"[Service] FSM: crashed \u2192 starting (auto-restart)"
|
|
9222
|
-
);
|
|
9223
|
-
return {
|
|
9224
|
-
id: entry.id,
|
|
9225
|
-
from: "crashed",
|
|
9226
|
-
to: "starting",
|
|
9227
|
-
pid: spawned.pid,
|
|
9228
|
-
reason: `auto-restart after backoff (crash #${info.crashCount})`
|
|
9229
|
-
};
|
|
9230
|
-
}
|
|
9231
|
-
case "stopped":
|
|
9232
|
-
return null;
|
|
9233
|
-
default:
|
|
9234
|
-
return null;
|
|
9235
|
-
}
|
|
9236
|
-
}
|
|
9237
|
-
function tickServiceWatcher(entries, globalEnv, fifonyDir, targetRoot) {
|
|
9238
|
-
const transitions = [];
|
|
9239
|
-
for (const entry of entries) {
|
|
9240
|
-
try {
|
|
9241
|
-
const t = tickOne(entry, globalEnv, fifonyDir, targetRoot);
|
|
9242
|
-
if (t) transitions.push(t);
|
|
9243
|
-
} catch (err) {
|
|
9244
|
-
logger.warn({ err, id: entry.id }, "[Service] Watcher tick error");
|
|
9245
|
-
}
|
|
9246
|
-
}
|
|
9247
|
-
return transitions;
|
|
9248
|
-
}
|
|
9249
|
-
function initServiceWatcher(getEntries, getGlobalEnv, fifonyDir, targetRoot, onTransition) {
|
|
9250
|
-
const intervalId = setInterval(() => {
|
|
9251
|
-
const entries = getEntries();
|
|
9252
|
-
if (entries.length === 0) return;
|
|
9253
|
-
const transitions = tickServiceWatcher(entries, getGlobalEnv(), fifonyDir, targetRoot);
|
|
9254
|
-
for (const t of transitions) onTransition(t);
|
|
9255
|
-
}, SERVICE_WATCHER_INTERVAL_MS);
|
|
9256
|
-
return { stop: () => clearInterval(intervalId) };
|
|
9257
|
-
}
|
|
9258
|
-
function readServiceLogTail(id, fifonyDir, bytes = 8192) {
|
|
9259
|
-
const log = serviceLogPath(fifonyDir, id);
|
|
9260
|
-
if (!existsSync10(log)) return "";
|
|
9261
|
-
try {
|
|
9262
|
-
const size = statSync3(log).size;
|
|
9263
|
-
const readSize = Math.min(size, bytes);
|
|
9264
|
-
const fd = openSync2(log, "r");
|
|
9265
|
-
const buf = Buffer.alloc(readSize);
|
|
9266
|
-
readSync2(fd, buf, 0, readSize, Math.max(0, size - readSize));
|
|
9267
|
-
closeSync2(fd);
|
|
9268
|
-
return buf.toString("utf8");
|
|
9269
|
-
} catch {
|
|
9270
|
-
return "";
|
|
9271
|
-
}
|
|
9272
|
-
}
|
|
9273
|
-
function reconcileServiceStates(entries, fifonyDir) {
|
|
9274
|
-
for (const entry of entries) {
|
|
9275
|
-
const info = readPidInfo(fifonyDir, entry.id);
|
|
9276
|
-
if (!info) continue;
|
|
9277
|
-
if (info.state === "stopped") continue;
|
|
9278
|
-
if (!isProcessAlive(info.pid)) {
|
|
9279
|
-
const crashCount = (info.crashCount ?? 0) + 1;
|
|
9280
|
-
writePidInfo(fifonyDir, entry.id, {
|
|
9281
|
-
...info,
|
|
9282
|
-
state: "crashed",
|
|
9283
|
-
crashCount,
|
|
9284
|
-
lastCrashAt: now()
|
|
9285
|
-
});
|
|
9286
|
-
logger.info({ id: entry.id, crashCount }, "[Service] Boot: process dead \u2192 crashed");
|
|
9287
|
-
}
|
|
9288
|
-
}
|
|
9289
|
-
}
|
|
9290
|
-
|
|
9291
|
-
// src/persistence/plugins/service-log-broadcaster.ts
|
|
9292
|
-
var MAX_CHUNK_BYTES = 16384;
|
|
9293
|
-
var active2 = /* @__PURE__ */ new Map();
|
|
9294
|
-
function readNewBytes(logPath, position) {
|
|
9295
|
-
try {
|
|
9296
|
-
const size = statSync4(logPath).size;
|
|
9297
|
-
if (size <= position) return null;
|
|
9298
|
-
const toRead = Math.min(size - position, MAX_CHUNK_BYTES);
|
|
9299
|
-
const buf = Buffer.alloc(toRead);
|
|
9300
|
-
const fd = openSync3(logPath, "r");
|
|
9301
|
-
const n = readSync3(fd, buf, 0, toRead, position);
|
|
9302
|
-
closeSync3(fd);
|
|
9303
|
-
if (n <= 0) return null;
|
|
9304
|
-
return { chunk: buf.slice(0, n).toString("utf8"), newPosition: position + n };
|
|
9305
|
-
} catch {
|
|
9306
|
-
return null;
|
|
9307
|
-
}
|
|
9308
|
-
}
|
|
9309
|
-
function startServiceLogBroadcasting(id, fifonyDir) {
|
|
9310
|
-
stopServiceLogBroadcasting(id);
|
|
9311
|
-
const logPath = serviceLogPath(fifonyDir, id);
|
|
9312
|
-
if (!existsSync11(logPath)) return;
|
|
9313
|
-
const entry = { watcher: null, position: 0 };
|
|
9314
|
-
const flush = () => {
|
|
9315
|
-
if (serviceLogRoomSize(id) === 0) return;
|
|
9316
|
-
try {
|
|
9317
|
-
const size = statSync4(logPath).size;
|
|
9318
|
-
if (size < entry.position) entry.position = 0;
|
|
9319
|
-
} catch {
|
|
9320
|
-
return;
|
|
9321
|
-
}
|
|
9322
|
-
const result = readNewBytes(logPath, entry.position);
|
|
9323
|
-
if (!result) return;
|
|
9324
|
-
entry.position = result.newPosition;
|
|
9325
|
-
sendToServiceLogRoom(id, JSON.stringify({ type: "service:log", id, chunk: result.chunk }));
|
|
9326
|
-
};
|
|
9327
|
-
try {
|
|
9328
|
-
const watcher = watch(logPath, { persistent: false }, flush);
|
|
9329
|
-
watcher.on("error", () => active2.delete(id));
|
|
9330
|
-
entry.watcher = watcher;
|
|
9331
|
-
active2.set(id, entry);
|
|
9332
|
-
logger.debug({ id }, "[ServiceLogBroadcaster] Started");
|
|
9333
|
-
flush();
|
|
9334
|
-
} catch (err) {
|
|
9335
|
-
logger.debug({ err, id }, "[ServiceLogBroadcaster] Could not watch log file");
|
|
9336
|
-
}
|
|
9337
|
-
}
|
|
9338
|
-
function stopServiceLogBroadcasting(id) {
|
|
9339
|
-
const entry = active2.get(id);
|
|
9340
|
-
if (!entry) return;
|
|
9341
|
-
try {
|
|
9342
|
-
entry.watcher.close();
|
|
9343
|
-
} catch {
|
|
9344
|
-
}
|
|
9345
|
-
active2.delete(id);
|
|
9346
|
-
}
|
|
9347
|
-
|
|
9348
|
-
// src/routes/websocket.ts
|
|
9349
|
-
var wsClients = /* @__PURE__ */ new Map();
|
|
9350
|
-
var broadcastSeq = 0;
|
|
9351
|
-
var lastBroadcastIssueSnapshot = /* @__PURE__ */ new Map();
|
|
9352
|
-
var serviceLogRooms = /* @__PURE__ */ new Map();
|
|
9353
|
-
function subscribeServiceLogRoom(socketId, serviceId) {
|
|
9354
|
-
if (!serviceLogRooms.has(serviceId)) serviceLogRooms.set(serviceId, /* @__PURE__ */ new Set());
|
|
9355
|
-
serviceLogRooms.get(serviceId).add(socketId);
|
|
9356
|
-
}
|
|
9357
|
-
function unsubscribeServiceLogRoom(socketId, serviceId) {
|
|
9358
|
-
serviceLogRooms.get(serviceId)?.delete(socketId);
|
|
9359
|
-
}
|
|
9360
|
-
var analyticsRooms = /* @__PURE__ */ new Map();
|
|
9361
|
-
var analyticsOnSubscribeFn = null;
|
|
9362
|
-
function setAnalyticsOnSubscribeFn(fn) {
|
|
9363
|
-
analyticsOnSubscribeFn = fn;
|
|
9364
|
-
}
|
|
9365
|
-
function subscribeAnalyticsRoom(socketId, topic) {
|
|
9366
|
-
if (!analyticsRooms.has(topic)) analyticsRooms.set(topic, /* @__PURE__ */ new Set());
|
|
9367
|
-
analyticsRooms.get(topic).add(socketId);
|
|
9368
|
-
analyticsOnSubscribeFn?.(socketId, topic);
|
|
9369
|
-
}
|
|
9370
|
-
function unsubscribeAnalyticsRoom(socketId, topic) {
|
|
9371
|
-
analyticsRooms.get(topic)?.delete(socketId);
|
|
9372
|
-
}
|
|
9373
|
-
function analyticsRoomHasSubscribers(topic) {
|
|
9374
|
-
return (analyticsRooms.get(topic)?.size ?? 0) > 0;
|
|
9375
|
-
}
|
|
9376
|
-
function sendToAnalyticsRoom(topic, data) {
|
|
9377
|
-
const room = analyticsRooms.get(topic);
|
|
9378
|
-
if (!room || room.size === 0) return;
|
|
9379
|
-
const msg = JSON.stringify({ type: "analytics:update", topic, data });
|
|
9380
|
-
for (const socketId of [...room]) {
|
|
9381
|
-
const send = wsClients.get(socketId);
|
|
9382
|
-
if (!send) {
|
|
9383
|
-
room.delete(socketId);
|
|
9384
|
-
continue;
|
|
9385
|
-
}
|
|
9386
|
-
try {
|
|
9387
|
-
send(msg);
|
|
9388
|
-
} catch {
|
|
9389
|
-
wsClients.delete(socketId);
|
|
9390
|
-
room.delete(socketId);
|
|
9391
|
-
}
|
|
9392
|
-
}
|
|
9393
|
-
}
|
|
9394
|
-
var issueLogRooms = /* @__PURE__ */ new Map();
|
|
9395
|
-
function subscribeIssueLogRoom(socketId, issueId) {
|
|
9396
|
-
if (!issueLogRooms.has(issueId)) issueLogRooms.set(issueId, /* @__PURE__ */ new Set());
|
|
9397
|
-
issueLogRooms.get(issueId).add(socketId);
|
|
9398
|
-
}
|
|
9399
|
-
function unsubscribeIssueLogRoom(socketId, issueId) {
|
|
9400
|
-
issueLogRooms.get(issueId)?.delete(socketId);
|
|
9401
|
-
}
|
|
9402
|
-
function issueLogRoomSize(issueId) {
|
|
9403
|
-
return issueLogRooms.get(issueId)?.size ?? 0;
|
|
9404
|
-
}
|
|
9405
|
-
function sendToIssueLogRoom(issueId, data) {
|
|
9406
|
-
const room = issueLogRooms.get(issueId);
|
|
9407
|
-
if (!room || room.size === 0) return;
|
|
9408
|
-
for (const socketId of [...room]) {
|
|
9409
|
-
const send = wsClients.get(socketId);
|
|
9410
|
-
if (!send) {
|
|
9411
|
-
room.delete(socketId);
|
|
9412
|
-
continue;
|
|
9413
|
-
}
|
|
9414
|
-
try {
|
|
9415
|
-
send(data);
|
|
9416
|
-
} catch {
|
|
9417
|
-
wsClients.delete(socketId);
|
|
9418
|
-
room.delete(socketId);
|
|
9419
|
-
}
|
|
9420
|
-
}
|
|
9421
|
-
}
|
|
9422
|
-
function unsubscribeFromAllRooms(socketId) {
|
|
9423
|
-
for (const room of serviceLogRooms.values()) room.delete(socketId);
|
|
9424
|
-
for (const room of analyticsRooms.values()) room.delete(socketId);
|
|
9425
|
-
for (const room of issueLogRooms.values()) room.delete(socketId);
|
|
9426
|
-
}
|
|
9427
|
-
function serviceLogRoomSize(serviceId) {
|
|
9428
|
-
return serviceLogRooms.get(serviceId)?.size ?? 0;
|
|
9429
|
-
}
|
|
9430
|
-
function sendToServiceLogRoom(serviceId, data) {
|
|
9431
|
-
const room = serviceLogRooms.get(serviceId);
|
|
9432
|
-
if (!room || room.size === 0) return;
|
|
9433
|
-
for (const socketId of [...room]) {
|
|
9434
|
-
const send = wsClients.get(socketId);
|
|
9435
|
-
if (!send) {
|
|
9436
|
-
room.delete(socketId);
|
|
9437
|
-
continue;
|
|
9438
|
-
}
|
|
9439
|
-
try {
|
|
9440
|
-
send(data);
|
|
9441
|
-
} catch {
|
|
9442
|
-
wsClients.delete(socketId);
|
|
9443
|
-
room.delete(socketId);
|
|
9444
|
-
}
|
|
9445
|
-
}
|
|
9446
|
-
}
|
|
9447
|
-
function sendToAllClients(data) {
|
|
9448
|
-
for (const [socketId, send] of [...wsClients]) {
|
|
9449
|
-
try {
|
|
9450
|
-
send(data);
|
|
9451
|
-
} catch (error) {
|
|
9452
|
-
logger.debug(`WebSocket send failed for ${socketId}, removing (remaining: ${wsClients.size - 1}): ${String(error)}`);
|
|
9453
|
-
wsClients.delete(socketId);
|
|
9454
|
-
}
|
|
9455
|
-
}
|
|
9456
|
-
}
|
|
9457
|
-
function broadcastToWebSocketClients(message) {
|
|
9458
|
-
if (wsClients.size === 0) return;
|
|
9459
|
-
broadcastSeq++;
|
|
9460
|
-
logger.debug({ seq: broadcastSeq, type: message.type, clientCount: wsClients.size }, "[WebSocket] Broadcasting state update");
|
|
9461
|
-
const issues = message.issues;
|
|
9462
|
-
if (issues && lastBroadcastIssueSnapshot.size > 0) {
|
|
9463
|
-
const currentIds = /* @__PURE__ */ new Set();
|
|
9464
|
-
const changedIssues = [];
|
|
9465
|
-
for (const issue of issues) {
|
|
9466
|
-
const id = issue.id;
|
|
9467
|
-
currentIds.add(id);
|
|
9468
|
-
const serialized = JSON.stringify(issue);
|
|
9469
|
-
if (lastBroadcastIssueSnapshot.get(id) !== serialized) {
|
|
9470
|
-
changedIssues.push(issue);
|
|
9471
|
-
}
|
|
9472
|
-
}
|
|
9473
|
-
const removedIds = [];
|
|
9474
|
-
for (const prevId of lastBroadcastIssueSnapshot.keys()) {
|
|
9475
|
-
if (!currentIds.has(prevId)) {
|
|
9476
|
-
removedIds.push(prevId);
|
|
9477
|
-
}
|
|
9478
|
-
}
|
|
9479
|
-
lastBroadcastIssueSnapshot = new Map(
|
|
9480
|
-
issues.map((issue) => [issue.id, JSON.stringify(issue)])
|
|
9481
|
-
);
|
|
9482
|
-
if (changedIssues.length < issues.length / 2 || changedIssues.length <= 3) {
|
|
9483
|
-
const delta = {
|
|
9484
|
-
type: "state:delta",
|
|
9485
|
-
seq: broadcastSeq,
|
|
9486
|
-
metrics: message.metrics,
|
|
9487
|
-
milestones: message.milestones,
|
|
9488
|
-
updatedAt: message.updatedAt,
|
|
9489
|
-
issuesDelta: changedIssues,
|
|
9490
|
-
issuesRemoved: removedIds,
|
|
9491
|
-
events: message.events
|
|
9492
|
-
};
|
|
9493
|
-
sendToAllClients(JSON.stringify(delta));
|
|
9494
|
-
return;
|
|
9495
|
-
}
|
|
9496
|
-
}
|
|
9497
|
-
if (issues) {
|
|
9498
|
-
lastBroadcastIssueSnapshot = new Map(
|
|
9499
|
-
issues.map((issue) => [issue.id, JSON.stringify(issue)])
|
|
9500
|
-
);
|
|
9501
|
-
}
|
|
9502
|
-
sendToAllClients(JSON.stringify({
|
|
9503
|
-
...message,
|
|
9504
|
-
seq: broadcastSeq
|
|
9505
|
-
}));
|
|
9506
|
-
}
|
|
9507
|
-
function makeWebSocketConfig(state) {
|
|
9508
|
-
return {
|
|
9509
|
-
enabled: true,
|
|
9510
|
-
path: "/ws",
|
|
9511
|
-
maxPayloadBytes: 512e3,
|
|
9512
|
-
onConnection: (socketId, send) => {
|
|
9513
|
-
wsClients.set(socketId, send);
|
|
9514
|
-
logger.debug(`WebSocket client connected: ${socketId} (total: ${wsClients.size})`);
|
|
9515
|
-
try {
|
|
9516
|
-
send(JSON.stringify({
|
|
9517
|
-
type: "connected",
|
|
9518
|
-
seq: broadcastSeq,
|
|
9519
|
-
timestamp: now(),
|
|
9520
|
-
metrics: computeMetrics(state.issues),
|
|
9521
|
-
milestones: state.milestones,
|
|
9522
|
-
issues: state.issues,
|
|
9523
|
-
events: state.events.slice(0, 50)
|
|
9524
|
-
}));
|
|
9525
|
-
} catch (error) {
|
|
9526
|
-
logger.debug(`WebSocket initial send failed for ${socketId}: ${String(error)}`);
|
|
9527
|
-
}
|
|
9528
|
-
},
|
|
9529
|
-
onMessage: (socketId, message, send) => {
|
|
9530
|
-
try {
|
|
9531
|
-
const msg = JSON.parse(typeof message === "string" ? message : message.toString("utf8"));
|
|
9532
|
-
if (msg.type === "ping") {
|
|
9533
|
-
send(JSON.stringify({ type: "pong", timestamp: now() }));
|
|
9534
|
-
} else if (msg.type === "service:log:subscribe" && typeof msg.id === "string") {
|
|
9535
|
-
subscribeServiceLogRoom(socketId, msg.id);
|
|
9536
|
-
startServiceLogBroadcasting(msg.id, STATE_ROOT);
|
|
9537
|
-
logger.debug({ socketId, serviceId: msg.id }, "[WebSocket] Subscribed to service log room");
|
|
9538
|
-
} else if (msg.type === "service:log:unsubscribe" && typeof msg.id === "string") {
|
|
9539
|
-
unsubscribeServiceLogRoom(socketId, msg.id);
|
|
9540
|
-
logger.debug({ socketId, serviceId: msg.id }, "[WebSocket] Unsubscribed from service log room");
|
|
9541
|
-
} else if (msg.type === "analytics:subscribe" && typeof msg.topic === "string") {
|
|
9542
|
-
subscribeAnalyticsRoom(socketId, msg.topic);
|
|
9543
|
-
logger.debug({ socketId, topic: msg.topic }, "[WebSocket] Subscribed to analytics room");
|
|
9544
|
-
} else if (msg.type === "analytics:unsubscribe" && typeof msg.topic === "string") {
|
|
9545
|
-
unsubscribeAnalyticsRoom(socketId, msg.topic);
|
|
9546
|
-
logger.debug({ socketId, topic: msg.topic }, "[WebSocket] Unsubscribed from analytics room");
|
|
9547
|
-
} else if (msg.type === "issue:log:subscribe" && typeof msg.id === "string") {
|
|
9548
|
-
subscribeIssueLogRoom(socketId, msg.id);
|
|
9549
|
-
logger.debug({ socketId, issueId: msg.id }, "[WebSocket] Subscribed to issue log room");
|
|
9550
|
-
} else if (msg.type === "issue:log:unsubscribe" && typeof msg.id === "string") {
|
|
9551
|
-
unsubscribeIssueLogRoom(socketId, msg.id);
|
|
9552
|
-
logger.debug({ socketId, issueId: msg.id }, "[WebSocket] Unsubscribed from issue log room");
|
|
9553
|
-
}
|
|
9554
|
-
} catch {
|
|
9555
|
-
}
|
|
9556
|
-
},
|
|
9557
|
-
onClose: (socketId) => {
|
|
9558
|
-
wsClients.delete(socketId);
|
|
9559
|
-
unsubscribeFromAllRooms(socketId);
|
|
9560
|
-
logger.debug(`WebSocket client disconnected: ${socketId} (total: ${wsClients.size})`);
|
|
9561
|
-
}
|
|
9562
|
-
};
|
|
9563
|
-
}
|
|
9564
|
-
|
|
9565
9144
|
// src/persistence/plugins/scheduler.ts
|
|
9566
9145
|
var shuttingDown = false;
|
|
9567
9146
|
function isShuttingDown() {
|
|
@@ -9718,7 +9297,7 @@ function hasTerminalQueue(state) {
|
|
|
9718
9297
|
// src/agents/providers-usage.ts
|
|
9719
9298
|
import { execFile as execFile2 } from "child_process";
|
|
9720
9299
|
import { promisify } from "util";
|
|
9721
|
-
import { existsSync as
|
|
9300
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8, readdirSync as readdirSync5, realpathSync } from "fs";
|
|
9722
9301
|
import { join as join20, dirname as dirname2 } from "path";
|
|
9723
9302
|
import { homedir as homedir2 } from "os";
|
|
9724
9303
|
import { env } from "process";
|
|
@@ -9757,7 +9336,7 @@ function resolveCodexHomeCandidates() {
|
|
|
9757
9336
|
}
|
|
9758
9337
|
function resolveCodexDir() {
|
|
9759
9338
|
for (const candidate of resolveCodexHomeCandidates()) {
|
|
9760
|
-
if (
|
|
9339
|
+
if (existsSync11(candidate)) {
|
|
9761
9340
|
return candidate;
|
|
9762
9341
|
}
|
|
9763
9342
|
}
|
|
@@ -9765,16 +9344,16 @@ function resolveCodexDir() {
|
|
|
9765
9344
|
}
|
|
9766
9345
|
function findLatestCodexDb(codexDir) {
|
|
9767
9346
|
const explicit = join20(codexDir, "state_5.sqlite");
|
|
9768
|
-
if (
|
|
9769
|
-
const candidates =
|
|
9347
|
+
if (existsSync11(explicit)) return explicit;
|
|
9348
|
+
const candidates = readdirSync5(codexDir).filter((name) => name.startsWith("state_") && name.endsWith(".sqlite")).sort().reverse();
|
|
9770
9349
|
if (candidates.length === 0) return null;
|
|
9771
9350
|
return join20(codexDir, candidates[0]);
|
|
9772
9351
|
}
|
|
9773
9352
|
function computeNextMonday() {
|
|
9774
|
-
const
|
|
9775
|
-
const utcDay =
|
|
9353
|
+
const now3 = /* @__PURE__ */ new Date();
|
|
9354
|
+
const utcDay = now3.getUTCDay();
|
|
9776
9355
|
const daysUntilMonday = utcDay === 0 ? 1 : utcDay === 1 ? 7 : 8 - utcDay;
|
|
9777
|
-
const next = new Date(
|
|
9356
|
+
const next = new Date(now3);
|
|
9778
9357
|
next.setUTCDate(next.getUTCDate() + daysUntilMonday);
|
|
9779
9358
|
next.setUTCHours(0, 0, 0, 0);
|
|
9780
9359
|
return next;
|
|
@@ -9830,7 +9409,7 @@ function resolveClaudePlanKey(displayName) {
|
|
|
9830
9409
|
async function collectClaudeUsage() {
|
|
9831
9410
|
const home = homedir2();
|
|
9832
9411
|
const claudeDir = join20(home, ".claude");
|
|
9833
|
-
if (!
|
|
9412
|
+
if (!existsSync11(claudeDir)) return null;
|
|
9834
9413
|
const available = await whichExists("claude");
|
|
9835
9414
|
const projectsDir = join20(claudeDir, "projects");
|
|
9836
9415
|
let totalInputTokens = 0;
|
|
@@ -9851,15 +9430,15 @@ async function collectClaudeUsage() {
|
|
|
9851
9430
|
const weekMs = weekStart.getTime();
|
|
9852
9431
|
const last5hStart = computeLastHoursStart(5);
|
|
9853
9432
|
const last5hMs = last5hStart.getTime();
|
|
9854
|
-
if (
|
|
9433
|
+
if (existsSync11(projectsDir)) {
|
|
9855
9434
|
try {
|
|
9856
|
-
const projectDirs =
|
|
9435
|
+
const projectDirs = readdirSync5(projectsDir, { withFileTypes: true });
|
|
9857
9436
|
for (const dir of projectDirs) {
|
|
9858
9437
|
if (!dir.isDirectory()) continue;
|
|
9859
9438
|
const projectPath = join20(projectsDir, dir.name);
|
|
9860
9439
|
let sessionFiles;
|
|
9861
9440
|
try {
|
|
9862
|
-
sessionFiles =
|
|
9441
|
+
sessionFiles = readdirSync5(projectPath).filter((f) => f.endsWith(".jsonl"));
|
|
9863
9442
|
} catch {
|
|
9864
9443
|
continue;
|
|
9865
9444
|
}
|
|
@@ -9933,7 +9512,7 @@ async function collectClaudeUsage() {
|
|
|
9933
9512
|
let resetInfo = "Weekly reset (every Monday 00:00 UTC)";
|
|
9934
9513
|
let currentModel = "";
|
|
9935
9514
|
const settingsPath = join20(claudeDir, "settings.json");
|
|
9936
|
-
if (
|
|
9515
|
+
if (existsSync11(settingsPath)) {
|
|
9937
9516
|
try {
|
|
9938
9517
|
const settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
|
|
9939
9518
|
if (settings.plan === "max" || settings.plan === "max5x") {
|
|
@@ -10057,7 +9636,7 @@ function aggregateCodexSessionUsageFromJsonl(lines) {
|
|
|
10057
9636
|
}
|
|
10058
9637
|
function collectCodexSessionUsagesFromJsonl(codexDir) {
|
|
10059
9638
|
const sessionsDir = join20(codexDir, "sessions");
|
|
10060
|
-
if (!
|
|
9639
|
+
if (!existsSync11(sessionsDir)) return [];
|
|
10061
9640
|
const stack = [sessionsDir];
|
|
10062
9641
|
const usageByFile = [];
|
|
10063
9642
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -10067,7 +9646,7 @@ function collectCodexSessionUsagesFromJsonl(codexDir) {
|
|
|
10067
9646
|
seen.add(current);
|
|
10068
9647
|
let entries = [];
|
|
10069
9648
|
try {
|
|
10070
|
-
entries =
|
|
9649
|
+
entries = readdirSync5(current, { withFileTypes: true });
|
|
10071
9650
|
} catch {
|
|
10072
9651
|
continue;
|
|
10073
9652
|
}
|
|
@@ -10097,7 +9676,7 @@ async function collectCodexUsage() {
|
|
|
10097
9676
|
const models = [];
|
|
10098
9677
|
const modelsCachePath = join20(codexDir, "models_cache.json");
|
|
10099
9678
|
let currentModel = "";
|
|
10100
|
-
if (
|
|
9679
|
+
if (existsSync11(modelsCachePath)) {
|
|
10101
9680
|
try {
|
|
10102
9681
|
const cache = JSON.parse(readFileSync8(modelsCachePath, "utf8"));
|
|
10103
9682
|
for (const m of cache.models || []) {
|
|
@@ -10111,7 +9690,7 @@ async function collectCodexUsage() {
|
|
|
10111
9690
|
}
|
|
10112
9691
|
}
|
|
10113
9692
|
const configPath = join20(codexDir, "config.toml");
|
|
10114
|
-
if (
|
|
9693
|
+
if (existsSync11(configPath)) {
|
|
10115
9694
|
try {
|
|
10116
9695
|
const configContent = readFileSync8(configPath, "utf8");
|
|
10117
9696
|
const modelMatch = configContent.match(/^model\s*=\s*"([^"]+)"/m);
|
|
@@ -10374,28 +9953,28 @@ function aggregateGeminiSessionUsageFromJson(content) {
|
|
|
10374
9953
|
}
|
|
10375
9954
|
function collectGeminiSessionUsages() {
|
|
10376
9955
|
const geminiTmp = join20(homedir2(), ".gemini", "tmp");
|
|
10377
|
-
if (!
|
|
9956
|
+
if (!existsSync11(geminiTmp)) return [];
|
|
10378
9957
|
const usages = [];
|
|
10379
9958
|
let entries = [];
|
|
10380
9959
|
try {
|
|
10381
|
-
entries =
|
|
9960
|
+
entries = readdirSync5(geminiTmp, { withFileTypes: true });
|
|
10382
9961
|
} catch {
|
|
10383
9962
|
return usages;
|
|
10384
9963
|
}
|
|
10385
9964
|
for (const profile of entries) {
|
|
10386
9965
|
if (!profile.isDirectory()) continue;
|
|
10387
9966
|
const chatsDir = join20(geminiTmp, profile.name, "chats");
|
|
10388
|
-
if (!
|
|
9967
|
+
if (!existsSync11(chatsDir)) continue;
|
|
10389
9968
|
let sessions = [];
|
|
10390
9969
|
try {
|
|
10391
|
-
sessions =
|
|
9970
|
+
sessions = readdirSync5(chatsDir).filter((name) => name.startsWith("session-") && (name.endsWith(".json") || name.endsWith(".jsonl")));
|
|
10392
9971
|
} catch {
|
|
10393
9972
|
continue;
|
|
10394
9973
|
}
|
|
10395
9974
|
for (const sessionFile of sessions) {
|
|
10396
|
-
const
|
|
9975
|
+
const sessionPath2 = join20(chatsDir, sessionFile);
|
|
10397
9976
|
try {
|
|
10398
|
-
const usage = aggregateGeminiSessionUsageFromJson(readFileSync8(
|
|
9977
|
+
const usage = aggregateGeminiSessionUsageFromJson(readFileSync8(sessionPath2, "utf8"));
|
|
10399
9978
|
if (usage.totalTokens > 0) usages.push(usage);
|
|
10400
9979
|
} catch {
|
|
10401
9980
|
}
|
|
@@ -10415,7 +9994,7 @@ async function collectGeminiUsage() {
|
|
|
10415
9994
|
}
|
|
10416
9995
|
let account = null;
|
|
10417
9996
|
const accountsPath = join20(homedir2(), ".gemini", "google_accounts.json");
|
|
10418
|
-
if (
|
|
9997
|
+
if (existsSync11(accountsPath)) {
|
|
10419
9998
|
try {
|
|
10420
9999
|
const data = JSON.parse(readFileSync8(accountsPath, "utf8"));
|
|
10421
10000
|
if (typeof data.active === "string" && data.active.includes("@")) {
|
|
@@ -10433,7 +10012,7 @@ async function collectGeminiUsage() {
|
|
|
10433
10012
|
const { stdout: binPath } = await execFileAsync("which", ["gemini"], { encoding: "utf8", timeout: 3e3 });
|
|
10434
10013
|
const realBin = realpathSync(binPath.trim());
|
|
10435
10014
|
const modelsPath = join20(dirname2(dirname2(realBin)), "node_modules", "@google", "gemini-cli-core", "dist", "src", "config", "models.js");
|
|
10436
|
-
if (
|
|
10015
|
+
if (existsSync11(modelsPath)) {
|
|
10437
10016
|
const content = readFileSync8(modelsPath, "utf8");
|
|
10438
10017
|
const regex = /export const ([A-Z0-9_]+)\s*=\s*'(gemini-[^']+)';/g;
|
|
10439
10018
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -10453,7 +10032,7 @@ async function collectGeminiUsage() {
|
|
|
10453
10032
|
}
|
|
10454
10033
|
let currentModel = "";
|
|
10455
10034
|
const settingsPath = join20(homedir2(), ".gemini", "settings.json");
|
|
10456
|
-
if (
|
|
10035
|
+
if (existsSync11(settingsPath)) {
|
|
10457
10036
|
try {
|
|
10458
10037
|
const settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
|
|
10459
10038
|
if (typeof settings.model === "string" && settings.model.trim()) {
|
|
@@ -10594,26 +10173,29 @@ function listServiceStatuses(entries, fifonyDir) {
|
|
|
10594
10173
|
function getManagedServiceLogPath(id, fifonyDir) {
|
|
10595
10174
|
return serviceLogPath(fifonyDir, id);
|
|
10596
10175
|
}
|
|
10597
|
-
function startManagedService(
|
|
10598
|
-
|
|
10176
|
+
async function startManagedService(id) {
|
|
10177
|
+
await sendServiceEvent(id, "START");
|
|
10599
10178
|
}
|
|
10600
|
-
function stopManagedService(id
|
|
10601
|
-
|
|
10179
|
+
async function stopManagedService(id) {
|
|
10180
|
+
await sendServiceEvent(id, "STOP");
|
|
10602
10181
|
}
|
|
10603
|
-
function startAutoConfiguredServices(entries
|
|
10604
|
-
const
|
|
10182
|
+
async function startAutoConfiguredServices(entries) {
|
|
10183
|
+
const started = [];
|
|
10605
10184
|
for (const entry of entries) {
|
|
10606
10185
|
if (!entry.autoStart) continue;
|
|
10607
|
-
|
|
10186
|
+
try {
|
|
10187
|
+
await sendServiceEvent(entry.id, "START");
|
|
10188
|
+
started.push(entry.id);
|
|
10189
|
+
} catch (err) {
|
|
10190
|
+
const { logger: logger2 } = await import("./logger-IFLXTQPS.js");
|
|
10191
|
+
logger2.warn({ err, id: entry.id }, "[Service] Auto-start failed");
|
|
10192
|
+
}
|
|
10608
10193
|
}
|
|
10609
|
-
return
|
|
10194
|
+
return started;
|
|
10610
10195
|
}
|
|
10611
10196
|
function reconcileManagedServiceStates(entries, fifonyDir) {
|
|
10612
10197
|
reconcileServiceStates(entries, fifonyDir);
|
|
10613
10198
|
}
|
|
10614
|
-
function initManagedServiceWatcher(getEntries, getGlobalEnv, fifonyDir, targetRoot, onTransition) {
|
|
10615
|
-
return initServiceWatcher(getEntries, getGlobalEnv, fifonyDir, targetRoot, onTransition);
|
|
10616
|
-
}
|
|
10617
10199
|
|
|
10618
10200
|
// src/domains/runtime-diagnostics.ts
|
|
10619
10201
|
function findConfiguredCapabilities(configuredProvider, providers) {
|
|
@@ -10899,8 +10481,8 @@ function registerStateRoutes(app, state) {
|
|
|
10899
10481
|
|
|
10900
10482
|
// src/agents/planning/issue-enhancer.ts
|
|
10901
10483
|
import { env as env2 } from "process";
|
|
10902
|
-
import { existsSync as
|
|
10903
|
-
import { spawn as
|
|
10484
|
+
import { existsSync as existsSync12, mkdtempSync as mkdtempSync4, readFileSync as readFileSync9, rmSync as rmSync5, writeFileSync as writeFileSync14 } from "fs";
|
|
10485
|
+
import { spawn as spawn2 } from "child_process";
|
|
10904
10486
|
import { tmpdir as tmpdir4 } from "os";
|
|
10905
10487
|
import { join as join21 } from "path";
|
|
10906
10488
|
async function buildPrompt2(field, title, description, issueType, images) {
|
|
@@ -10985,7 +10567,7 @@ function parseCandidate(raw, expectedField) {
|
|
|
10985
10567
|
return "";
|
|
10986
10568
|
}
|
|
10987
10569
|
function readProviderOutput(resultFile, fallback) {
|
|
10988
|
-
if (
|
|
10570
|
+
if (existsSync12(resultFile)) {
|
|
10989
10571
|
try {
|
|
10990
10572
|
return readFileSync9(resultFile, "utf8").trim();
|
|
10991
10573
|
} catch {
|
|
@@ -10998,9 +10580,9 @@ async function runProviderCommand(command, provider, prompt, title, description,
|
|
|
10998
10580
|
const promptFile = join21(tempDir, "fifony-enhance-prompt.md");
|
|
10999
10581
|
const issuePayloadFile = join21(tempDir, "fifony-issue.json");
|
|
11000
10582
|
const resultFile = join21(tempDir, "fifony-result.txt");
|
|
11001
|
-
|
|
10583
|
+
writeFileSync14(promptFile, `${prompt}
|
|
11002
10584
|
`, "utf8");
|
|
11003
|
-
|
|
10585
|
+
writeFileSync14(issuePayloadFile, JSON.stringify({ title, description, field }, null, 2), "utf8");
|
|
11004
10586
|
const effectiveCommand = command;
|
|
11005
10587
|
const spawnEnv = {
|
|
11006
10588
|
...env2,
|
|
@@ -11018,7 +10600,7 @@ async function runProviderCommand(command, provider, prompt, title, description,
|
|
|
11018
10600
|
const startedAt = Date.now();
|
|
11019
10601
|
let output = "";
|
|
11020
10602
|
let timeout = false;
|
|
11021
|
-
const child =
|
|
10603
|
+
const child = spawn2(effectiveCommand, {
|
|
11022
10604
|
shell: true,
|
|
11023
10605
|
cwd: TARGET_ROOT,
|
|
11024
10606
|
env: spawnEnv
|
|
@@ -11036,18 +10618,18 @@ async function runProviderCommand(command, provider, prompt, title, description,
|
|
|
11036
10618
|
}, Math.max(timeoutMs, 1e3));
|
|
11037
10619
|
child.on("error", () => {
|
|
11038
10620
|
clearTimeout(timer);
|
|
11039
|
-
|
|
10621
|
+
rmSync5(tempDir, { recursive: true, force: true });
|
|
11040
10622
|
reject(new Error("Could not execute AI command."));
|
|
11041
10623
|
});
|
|
11042
10624
|
child.on("close", (code) => {
|
|
11043
10625
|
clearTimeout(timer);
|
|
11044
10626
|
if (timeout) {
|
|
11045
|
-
|
|
10627
|
+
rmSync5(tempDir, { recursive: true, force: true });
|
|
11046
10628
|
reject(new Error(`Enhance command timeout after ${Date.now() - startedAt}ms.`));
|
|
11047
10629
|
return;
|
|
11048
10630
|
}
|
|
11049
10631
|
const commandOutput = readProviderOutput(resultFile, output);
|
|
11050
|
-
|
|
10632
|
+
rmSync5(tempDir, { recursive: true, force: true });
|
|
11051
10633
|
if (code !== 0) {
|
|
11052
10634
|
if (commandOutput.trim()) {
|
|
11053
10635
|
logger.warn({ exitCode: code, provider }, `[Enhance] Provider exited ${code} but produced output \u2014 attempting to use it`);
|
|
@@ -11067,7 +10649,7 @@ async function enhanceIssueField(payload, config, _workflowDefinition) {
|
|
|
11067
10649
|
const description = typeof payload.description === "string" ? payload.description.trim() : "";
|
|
11068
10650
|
const issueType = typeof payload.issueType === "string" ? payload.issueType.trim() : void 0;
|
|
11069
10651
|
const images = Array.isArray(payload.images) ? payload.images.filter((p) => typeof p === "string") : void 0;
|
|
11070
|
-
const { provider: selectedProvider, model: selectedModel } = await
|
|
10652
|
+
const { provider: selectedProvider, model: selectedModel } = await resolveEnhanceStageConfig(config);
|
|
11071
10653
|
const providers = detectAvailableProviders();
|
|
11072
10654
|
const isAvailable = providers.some((p) => p.name === selectedProvider && p.available);
|
|
11073
10655
|
if (!isAvailable) {
|
|
@@ -11115,6 +10697,175 @@ async function enhanceIssueField(payload, config, _workflowDefinition) {
|
|
|
11115
10697
|
return { field, value, provider: selectedProvider };
|
|
11116
10698
|
}
|
|
11117
10699
|
|
|
10700
|
+
// src/agents/planning/issue-chat.ts
|
|
10701
|
+
import { env as env3 } from "process";
|
|
10702
|
+
import { existsSync as existsSync13, mkdtempSync as mkdtempSync5, readFileSync as readFileSync10, rmSync as rmSync6, writeFileSync as writeFileSync15 } from "fs";
|
|
10703
|
+
import { spawn as spawn3 } from "child_process";
|
|
10704
|
+
import { tmpdir as tmpdir5 } from "os";
|
|
10705
|
+
import { join as join22 } from "path";
|
|
10706
|
+
function readProviderOutput2(resultFile, fallback) {
|
|
10707
|
+
if (existsSync13(resultFile)) {
|
|
10708
|
+
try {
|
|
10709
|
+
return readFileSync10(resultFile, "utf8").trim();
|
|
10710
|
+
} catch {
|
|
10711
|
+
}
|
|
10712
|
+
}
|
|
10713
|
+
return fallback;
|
|
10714
|
+
}
|
|
10715
|
+
async function runOneShot(command, provider, prompt, timeoutMs) {
|
|
10716
|
+
const tempDir = mkdtempSync5(join22(tmpdir5(), "fifony-chat-"));
|
|
10717
|
+
const promptFile = join22(tempDir, "prompt.md");
|
|
10718
|
+
const resultFile = join22(tempDir, "result.txt");
|
|
10719
|
+
writeFileSync15(promptFile, `${prompt}
|
|
10720
|
+
`, "utf8");
|
|
10721
|
+
const spawnEnv = {
|
|
10722
|
+
...env3,
|
|
10723
|
+
FIFONY_PROMPT_FILE: promptFile,
|
|
10724
|
+
FIFONY_PROMPT: prompt,
|
|
10725
|
+
FIFONY_AGENT_PROVIDER: provider,
|
|
10726
|
+
FIFONY_RESULT_FILE: resultFile
|
|
10727
|
+
};
|
|
10728
|
+
return await new Promise((resolve5, reject) => {
|
|
10729
|
+
const startedAt = Date.now();
|
|
10730
|
+
let output = "";
|
|
10731
|
+
let timedOut = false;
|
|
10732
|
+
const child = spawn3(command, { shell: true, cwd: TARGET_ROOT, env: spawnEnv });
|
|
10733
|
+
if (child.stdin) child.stdin.end();
|
|
10734
|
+
child.stdout?.on("data", (chunk) => {
|
|
10735
|
+
output = appendFileTail(output, String(chunk), 12e3);
|
|
10736
|
+
});
|
|
10737
|
+
child.stderr?.on("data", (chunk) => {
|
|
10738
|
+
output = appendFileTail(output, String(chunk), 12e3);
|
|
10739
|
+
});
|
|
10740
|
+
const timer = setTimeout(() => {
|
|
10741
|
+
timedOut = true;
|
|
10742
|
+
child.kill("SIGTERM");
|
|
10743
|
+
}, Math.max(timeoutMs, 1e3));
|
|
10744
|
+
child.on("error", () => {
|
|
10745
|
+
clearTimeout(timer);
|
|
10746
|
+
rmSync6(tempDir, { recursive: true, force: true });
|
|
10747
|
+
reject(new Error("Could not execute AI command."));
|
|
10748
|
+
});
|
|
10749
|
+
child.on("close", (code) => {
|
|
10750
|
+
clearTimeout(timer);
|
|
10751
|
+
const commandOutput = readProviderOutput2(resultFile, output);
|
|
10752
|
+
rmSync6(tempDir, { recursive: true, force: true });
|
|
10753
|
+
if (timedOut) {
|
|
10754
|
+
reject(new Error(`Chat command timed out after ${Date.now() - startedAt}ms.`));
|
|
10755
|
+
return;
|
|
10756
|
+
}
|
|
10757
|
+
if (code !== 0 && !commandOutput.trim()) {
|
|
10758
|
+
reject(new Error(`Chat command failed (exit ${code ?? "unknown"}) with no output.`));
|
|
10759
|
+
return;
|
|
10760
|
+
}
|
|
10761
|
+
if (code !== 0) {
|
|
10762
|
+
logger.warn({ exitCode: code, provider }, "[Chat] Provider exited non-zero but produced output \u2014 attempting to use it");
|
|
10763
|
+
}
|
|
10764
|
+
resolve5(commandOutput);
|
|
10765
|
+
});
|
|
10766
|
+
});
|
|
10767
|
+
}
|
|
10768
|
+
function buildChatPrompt(payload) {
|
|
10769
|
+
const { title, description, plan, message, history } = payload;
|
|
10770
|
+
const planSection = plan ? `Plan summary: ${plan.summary ?? "(none)"}
|
|
10771
|
+
Steps: ${plan.steps?.map((s) => s.action || s.title || "").filter(Boolean).join(", ") ?? "(none)"}` : "No plan yet.";
|
|
10772
|
+
const historySection = history?.length ? history.map((m) => `${m.role}: ${m.content}`).join("\n") : "";
|
|
10773
|
+
return `You are a helpful assistant discussing issue "${title}".
|
|
10774
|
+
|
|
10775
|
+
## Issue Context
|
|
10776
|
+
Title: ${title}
|
|
10777
|
+
Description: ${description || "(none provided)"}
|
|
10778
|
+
${planSection}
|
|
10779
|
+
|
|
10780
|
+
${historySection ? `## Conversation
|
|
10781
|
+
${historySection}
|
|
10782
|
+
` : ""}user: ${message}
|
|
10783
|
+
|
|
10784
|
+
Respond concisely and helpfully. Focus on the issue context.`;
|
|
10785
|
+
}
|
|
10786
|
+
function stripProviderChrome(raw, provider) {
|
|
10787
|
+
let text = raw;
|
|
10788
|
+
if (provider === "codex") {
|
|
10789
|
+
const codexMarker = text.lastIndexOf("\ncodex\n");
|
|
10790
|
+
if (codexMarker >= 0) {
|
|
10791
|
+
text = text.slice(codexMarker + "\ncodex\n".length);
|
|
10792
|
+
} else {
|
|
10793
|
+
const altMarker = text.lastIndexOf("codex\n");
|
|
10794
|
+
if (altMarker >= 0) {
|
|
10795
|
+
text = text.slice(altMarker + "codex\n".length);
|
|
10796
|
+
}
|
|
10797
|
+
}
|
|
10798
|
+
text = text.replace(/tokens used[\s\d,]+/gi, "");
|
|
10799
|
+
text = text.replace(/^Reading prompt from stdin.*$/gm, "");
|
|
10800
|
+
text = text.replace(/^OpenAI Codex v[\d.]+.*$/gm, "");
|
|
10801
|
+
text = text.replace(/^-+$/gm, "");
|
|
10802
|
+
text = text.replace(/^workdir:.*$/gm, "");
|
|
10803
|
+
text = text.replace(/^model:.*$/gm, "");
|
|
10804
|
+
text = text.replace(/^provider:.*$/gm, "");
|
|
10805
|
+
text = text.replace(/^approval:.*$/gm, "");
|
|
10806
|
+
text = text.replace(/^sandbox:.*$/gm, "");
|
|
10807
|
+
text = text.replace(/^reasoning effort:.*$/gm, "");
|
|
10808
|
+
text = text.replace(/^reasoning summaries:.*$/gm, "");
|
|
10809
|
+
text = text.replace(/^session id:.*$/gm, "");
|
|
10810
|
+
text = text.replace(/^user\n/gm, "");
|
|
10811
|
+
const trimmed = text.trim();
|
|
10812
|
+
const half = Math.floor(trimmed.length / 2);
|
|
10813
|
+
if (half > 50) {
|
|
10814
|
+
const first = trimmed.slice(0, half).trim();
|
|
10815
|
+
const second = trimmed.slice(half).trim();
|
|
10816
|
+
if (first === second) {
|
|
10817
|
+
text = first;
|
|
10818
|
+
}
|
|
10819
|
+
}
|
|
10820
|
+
}
|
|
10821
|
+
if (provider === "claude") {
|
|
10822
|
+
try {
|
|
10823
|
+
const parsed = JSON.parse(text.trim());
|
|
10824
|
+
if (parsed && typeof parsed === "object" && typeof parsed.result === "string") {
|
|
10825
|
+
return parsed.result;
|
|
10826
|
+
}
|
|
10827
|
+
} catch {
|
|
10828
|
+
}
|
|
10829
|
+
}
|
|
10830
|
+
if (provider === "gemini") {
|
|
10831
|
+
const lines = text.split("\n");
|
|
10832
|
+
const contentStart = lines.findIndex(
|
|
10833
|
+
(l) => l.trim() && !l.startsWith("Model:") && !l.startsWith("Thinking") && !l.startsWith("\u2500")
|
|
10834
|
+
);
|
|
10835
|
+
if (contentStart > 0) {
|
|
10836
|
+
text = lines.slice(contentStart).join("\n");
|
|
10837
|
+
}
|
|
10838
|
+
}
|
|
10839
|
+
text = text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
10840
|
+
return text.trim();
|
|
10841
|
+
}
|
|
10842
|
+
async function chatWithIssue(payload, config) {
|
|
10843
|
+
const { provider: selectedProvider, model: selectedModel } = await resolveChatStageConfig(config);
|
|
10844
|
+
const providers = detectAvailableProviders();
|
|
10845
|
+
const isAvailable = providers.some((p) => p.name === selectedProvider && p.available);
|
|
10846
|
+
if (!isAvailable) {
|
|
10847
|
+
const known = providers.map((e) => `${e.name}:${e.available ? "ok" : "missing"}`).join(", ");
|
|
10848
|
+
throw new Error(`Chat provider "${selectedProvider}" is not available. Detected: ${known}`);
|
|
10849
|
+
}
|
|
10850
|
+
const adapter = ADAPTERS[selectedProvider];
|
|
10851
|
+
if (!adapter) throw new Error(`No adapter for provider "${selectedProvider}".`);
|
|
10852
|
+
const caps = resolveProviderCapabilities(selectedProvider);
|
|
10853
|
+
const command = adapter.buildCommand({
|
|
10854
|
+
model: selectedModel,
|
|
10855
|
+
readOnly: caps.readOnlyExecution !== "none"
|
|
10856
|
+
});
|
|
10857
|
+
const prompt = buildChatPrompt(payload);
|
|
10858
|
+
const timeoutMs = config.commandTimeoutMs ?? 6e4;
|
|
10859
|
+
logger.debug({ provider: selectedProvider, issueId: payload.issueId }, "[Chat] Running chat command");
|
|
10860
|
+
const raw = await runOneShot(command, selectedProvider, prompt, timeoutMs);
|
|
10861
|
+
const response = stripProviderChrome(raw, selectedProvider).trim();
|
|
10862
|
+
if (!response) {
|
|
10863
|
+
throw new Error("AI provider returned an empty response.");
|
|
10864
|
+
}
|
|
10865
|
+
logger.info({ provider: selectedProvider, model: selectedModel, issueId: payload.issueId, responseLength: response.length }, "[Chat] Chat response received");
|
|
10866
|
+
return { response, provider: selectedProvider };
|
|
10867
|
+
}
|
|
10868
|
+
|
|
11118
10869
|
// src/routes/plan.ts
|
|
11119
10870
|
function registerPlanRoutes(app, state) {
|
|
11120
10871
|
app.get("/api/planning/session", async (c) => {
|
|
@@ -11223,12 +10974,44 @@ function registerPlanRoutes(app, state) {
|
|
|
11223
10974
|
);
|
|
11224
10975
|
}
|
|
11225
10976
|
});
|
|
10977
|
+
app.post("/api/issues/:id/chat", async (c) => {
|
|
10978
|
+
const issueId = c.req.param("id");
|
|
10979
|
+
const issue = state.issues.find((i) => i.id === issueId || i.identifier === issueId);
|
|
10980
|
+
if (!issue) {
|
|
10981
|
+
return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
10982
|
+
}
|
|
10983
|
+
try {
|
|
10984
|
+
const body = await c.req.json();
|
|
10985
|
+
const message = typeof body.message === "string" ? body.message.trim() : "";
|
|
10986
|
+
if (!message) {
|
|
10987
|
+
return c.json({ ok: false, error: "Message is required." }, 400);
|
|
10988
|
+
}
|
|
10989
|
+
const history = Array.isArray(body.history) ? body.history.filter(
|
|
10990
|
+
(m) => typeof m === "object" && m !== null && (m.role === "user" || m.role === "assistant") && typeof m.content === "string"
|
|
10991
|
+
) : void 0;
|
|
10992
|
+
const result = await chatWithIssue(
|
|
10993
|
+
{
|
|
10994
|
+
issueId: issue.id,
|
|
10995
|
+
title: issue.title,
|
|
10996
|
+
description: issue.description ?? "",
|
|
10997
|
+
plan: issue.plan ?? null,
|
|
10998
|
+
message,
|
|
10999
|
+
history
|
|
11000
|
+
},
|
|
11001
|
+
state.config
|
|
11002
|
+
);
|
|
11003
|
+
return c.json({ ok: true, response: result.response, provider: result.provider });
|
|
11004
|
+
} catch (error) {
|
|
11005
|
+
logger.error({ err: error, issueId }, `Issue chat failed: ${String(error)}`);
|
|
11006
|
+
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
11007
|
+
}
|
|
11008
|
+
});
|
|
11226
11009
|
}
|
|
11227
11010
|
|
|
11228
11011
|
// src/routes/settings.ts
|
|
11229
11012
|
function registerSettingsRoutes(app, state) {
|
|
11230
11013
|
app.post("/api/settings/:id", async (c) => {
|
|
11231
|
-
const { updateSetting: updateSetting2 } = await import("./settings.resource-
|
|
11014
|
+
const { updateSetting: updateSetting2 } = await import("./settings.resource-JMD3JQOS.js");
|
|
11232
11015
|
return updateSetting2(c);
|
|
11233
11016
|
});
|
|
11234
11017
|
app.post("/api/config/concurrency", async (c) => {
|
|
@@ -11285,11 +11068,19 @@ function registerSettingsRoutes(app, state) {
|
|
|
11285
11068
|
const payload = await c.req.json();
|
|
11286
11069
|
const workflow = payload.workflow;
|
|
11287
11070
|
if (!workflow?.plan?.provider || !workflow?.execute?.provider || !workflow?.review?.provider) {
|
|
11288
|
-
return c.json({ ok: false, error: "Invalid workflow config.
|
|
11071
|
+
return c.json({ ok: false, error: "Invalid workflow config. plan, execute, and review stages are required with a provider." }, 400);
|
|
11289
11072
|
}
|
|
11290
|
-
|
|
11291
|
-
|
|
11292
|
-
|
|
11073
|
+
const config = {
|
|
11074
|
+
plan: workflow.plan,
|
|
11075
|
+
execute: workflow.execute,
|
|
11076
|
+
review: workflow.review
|
|
11077
|
+
};
|
|
11078
|
+
if (workflow.enhance?.provider) config.enhance = workflow.enhance;
|
|
11079
|
+
if (workflow.chat?.provider) config.chat = workflow.chat;
|
|
11080
|
+
if (workflow.services?.provider) config.services = workflow.services;
|
|
11081
|
+
await persistWorkflowConfig(config);
|
|
11082
|
+
addEvent(state, void 0, "manual", `Workflow config updated: plan=${config.plan.provider}/${config.plan.model}, execute=${config.execute.provider}/${config.execute.model}, review=${config.review.provider}/${config.review.model}.`);
|
|
11083
|
+
return c.json({ ok: true, workflow: config });
|
|
11293
11084
|
} catch (error) {
|
|
11294
11085
|
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
11295
11086
|
}
|
|
@@ -11417,8 +11208,8 @@ function registerScanningRoutes(app, _state) {
|
|
|
11417
11208
|
}
|
|
11418
11209
|
|
|
11419
11210
|
// src/agents/claude-md-manager.ts
|
|
11420
|
-
import { existsSync as existsSync14, readFileSync as
|
|
11421
|
-
import { join as
|
|
11211
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11, writeFileSync as writeFileSync16 } from "fs";
|
|
11212
|
+
import { join as join23 } from "path";
|
|
11422
11213
|
var BLOCK_START = "<!-- FIFONY:START \u2014 managed by fifony, do not edit manually -->";
|
|
11423
11214
|
var BLOCK_END = "<!-- FIFONY:END -->";
|
|
11424
11215
|
var BLOCK_PATTERN = /<!-- FIFONY:START[^>]*-->[\s\S]*?<!-- FIFONY:END -->/;
|
|
@@ -11449,11 +11240,11 @@ function buildManagedBlock(skills, agents, commands) {
|
|
|
11449
11240
|
}
|
|
11450
11241
|
function updateClaudeMdManagedBlock(targetRoot, skills, agents, commands) {
|
|
11451
11242
|
if (skills.length === 0 && agents.length === 0 && commands.length === 0) return;
|
|
11452
|
-
const claudeMdPath =
|
|
11243
|
+
const claudeMdPath = join23(targetRoot, "CLAUDE.md");
|
|
11453
11244
|
const newBlock = buildManagedBlock(skills, agents, commands);
|
|
11454
11245
|
let existing = "";
|
|
11455
11246
|
if (existsSync14(claudeMdPath)) {
|
|
11456
|
-
existing =
|
|
11247
|
+
existing = readFileSync11(claudeMdPath, "utf8");
|
|
11457
11248
|
}
|
|
11458
11249
|
let updated;
|
|
11459
11250
|
if (BLOCK_PATTERN.test(existing)) {
|
|
@@ -11602,14 +11393,14 @@ import {
|
|
|
11602
11393
|
appendFileSync,
|
|
11603
11394
|
existsSync as existsSync15,
|
|
11604
11395
|
mkdirSync as mkdirSync11,
|
|
11605
|
-
readFileSync as
|
|
11396
|
+
readFileSync as readFileSync12,
|
|
11606
11397
|
writeFileSync as writeFileSync17
|
|
11607
11398
|
} from "fs";
|
|
11608
11399
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
11609
|
-
import { extname as extname3, join as
|
|
11400
|
+
import { extname as extname3, join as join24 } from "path";
|
|
11610
11401
|
function registerMiscRoutes(app, state) {
|
|
11611
11402
|
app.get("/api/queue/stats", async (c) => {
|
|
11612
|
-
const { getQueueStats: getQueueStats2 } = await import("./queue-workers-
|
|
11403
|
+
const { getQueueStats: getQueueStats2 } = await import("./queue-workers-V57BYXAY.js");
|
|
11613
11404
|
return c.json(await getQueueStats2());
|
|
11614
11405
|
});
|
|
11615
11406
|
app.get("/api/live/:id/stream", async (c) => {
|
|
@@ -11691,11 +11482,11 @@ function registerMiscRoutes(app, state) {
|
|
|
11691
11482
|
});
|
|
11692
11483
|
app.get("/api/gitignore/status", async (c) => {
|
|
11693
11484
|
try {
|
|
11694
|
-
const gitignorePath =
|
|
11485
|
+
const gitignorePath = join24(TARGET_ROOT, ".gitignore");
|
|
11695
11486
|
if (!existsSync15(gitignorePath)) {
|
|
11696
11487
|
return c.json({ exists: false, hasFifony: false });
|
|
11697
11488
|
}
|
|
11698
|
-
const content =
|
|
11489
|
+
const content = readFileSync12(gitignorePath, "utf-8");
|
|
11699
11490
|
const lines = content.split("\n").map((l) => l.trim());
|
|
11700
11491
|
const hasFifony = lines.some((l) => l === ".fifony" || l === ".fifony/" || l === "/.fifony" || l === "/.fifony/");
|
|
11701
11492
|
return c.json({ exists: true, hasFifony });
|
|
@@ -11706,12 +11497,12 @@ function registerMiscRoutes(app, state) {
|
|
|
11706
11497
|
});
|
|
11707
11498
|
app.post("/api/gitignore/add", async (c) => {
|
|
11708
11499
|
try {
|
|
11709
|
-
const gitignorePath =
|
|
11500
|
+
const gitignorePath = join24(TARGET_ROOT, ".gitignore");
|
|
11710
11501
|
if (!existsSync15(gitignorePath)) {
|
|
11711
11502
|
writeFileSync17(gitignorePath, "# Fifony state directory\n.fifony/\n", "utf-8");
|
|
11712
11503
|
return c.json({ ok: true, created: true });
|
|
11713
11504
|
}
|
|
11714
|
-
const content =
|
|
11505
|
+
const content = readFileSync12(gitignorePath, "utf-8");
|
|
11715
11506
|
const lines = content.split("\n").map((l) => l.trim());
|
|
11716
11507
|
const hasFifony = lines.some((l) => l === ".fifony" || l === ".fifony/" || l === "/.fifony" || l === "/.fifony/");
|
|
11717
11508
|
if (hasFifony) {
|
|
@@ -11735,14 +11526,14 @@ function registerMiscRoutes(app, state) {
|
|
|
11735
11526
|
return c.json({ ok: false, error: "No files provided." }, 400);
|
|
11736
11527
|
}
|
|
11737
11528
|
const uploadId = randomUUID3();
|
|
11738
|
-
const uploadDir =
|
|
11529
|
+
const uploadDir = join24(ATTACHMENTS_ROOT, "temp", uploadId);
|
|
11739
11530
|
mkdirSync11(uploadDir, { recursive: true });
|
|
11740
11531
|
const paths = [];
|
|
11741
11532
|
for (const file of payload.files) {
|
|
11742
11533
|
if (typeof file.data !== "string" || !file.name) continue;
|
|
11743
11534
|
const safeExt = extname3(file.name).replace(/[^a-z0-9.]/gi, "").slice(0, 10) || ".bin";
|
|
11744
11535
|
const safeName = `${randomUUID3()}${safeExt}`;
|
|
11745
|
-
const dest =
|
|
11536
|
+
const dest = join24(uploadDir, safeName);
|
|
11746
11537
|
writeFileSync17(dest, Buffer.from(file.data, "base64"));
|
|
11747
11538
|
paths.push(dest);
|
|
11748
11539
|
}
|
|
@@ -11756,26 +11547,26 @@ function registerMiscRoutes(app, state) {
|
|
|
11756
11547
|
|
|
11757
11548
|
// src/routes/services.ts
|
|
11758
11549
|
import {
|
|
11759
|
-
closeSync as
|
|
11550
|
+
closeSync as closeSync2,
|
|
11760
11551
|
existsSync as existsSync16,
|
|
11761
|
-
openSync as
|
|
11762
|
-
readFileSync as
|
|
11763
|
-
readSync as
|
|
11764
|
-
readdirSync as
|
|
11765
|
-
statSync as
|
|
11552
|
+
openSync as openSync2,
|
|
11553
|
+
readFileSync as readFileSync13,
|
|
11554
|
+
readSync as readSync2,
|
|
11555
|
+
readdirSync as readdirSync6,
|
|
11556
|
+
statSync as statSync3
|
|
11766
11557
|
} from "fs";
|
|
11767
|
-
import { join as
|
|
11558
|
+
import { join as join25 } from "path";
|
|
11768
11559
|
function detectServices(targetRoot) {
|
|
11769
11560
|
const suggestions = [];
|
|
11770
|
-
if (existsSync16(
|
|
11561
|
+
if (existsSync16(join25(targetRoot, "turbo.json"))) {
|
|
11771
11562
|
suggestions.push({ label: "All (turbo dev)", command: "pnpm turbo dev", isRoot: true });
|
|
11772
11563
|
}
|
|
11773
|
-
const pnpmWorkspaceFile =
|
|
11774
|
-
const rootPkgFile =
|
|
11564
|
+
const pnpmWorkspaceFile = join25(targetRoot, "pnpm-workspace.yaml");
|
|
11565
|
+
const rootPkgFile = join25(targetRoot, "package.json");
|
|
11775
11566
|
const workspaceParentDirs = [];
|
|
11776
11567
|
if (existsSync16(pnpmWorkspaceFile)) {
|
|
11777
11568
|
try {
|
|
11778
|
-
const content =
|
|
11569
|
+
const content = readFileSync13(pnpmWorkspaceFile, "utf8");
|
|
11779
11570
|
for (const match of content.matchAll(/^\s+-\s+["']?([^"'\n]+)["']?/gm)) {
|
|
11780
11571
|
const glob = match[1].trim();
|
|
11781
11572
|
const parent = glob.replace(/\/\*.*$/, "");
|
|
@@ -11787,7 +11578,7 @@ function detectServices(targetRoot) {
|
|
|
11787
11578
|
}
|
|
11788
11579
|
} else if (existsSync16(rootPkgFile)) {
|
|
11789
11580
|
try {
|
|
11790
|
-
const pkg = JSON.parse(
|
|
11581
|
+
const pkg = JSON.parse(readFileSync13(rootPkgFile, "utf8"));
|
|
11791
11582
|
if (Array.isArray(pkg.workspaces)) {
|
|
11792
11583
|
for (const glob of pkg.workspaces) {
|
|
11793
11584
|
const parent = String(glob).replace(/\/\*.*$/, "");
|
|
@@ -11800,16 +11591,16 @@ function detectServices(targetRoot) {
|
|
|
11800
11591
|
}
|
|
11801
11592
|
}
|
|
11802
11593
|
for (const parent of workspaceParentDirs) {
|
|
11803
|
-
const parentAbs =
|
|
11594
|
+
const parentAbs = join25(targetRoot, parent);
|
|
11804
11595
|
if (!existsSync16(parentAbs)) continue;
|
|
11805
11596
|
try {
|
|
11806
|
-
const children =
|
|
11597
|
+
const children = readdirSync6(parentAbs, { withFileTypes: true });
|
|
11807
11598
|
for (const child of children) {
|
|
11808
11599
|
if (!child.isDirectory()) continue;
|
|
11809
|
-
const childPkg =
|
|
11600
|
+
const childPkg = join25(parentAbs, child.name, "package.json");
|
|
11810
11601
|
if (!existsSync16(childPkg)) continue;
|
|
11811
11602
|
try {
|
|
11812
|
-
const pkg = JSON.parse(
|
|
11603
|
+
const pkg = JSON.parse(readFileSync13(childPkg, "utf8"));
|
|
11813
11604
|
const pkgName = typeof pkg.name === "string" ? pkg.name : child.name;
|
|
11814
11605
|
const scripts = pkg.scripts ?? {};
|
|
11815
11606
|
const preferred = ["dev", "start", "serve"];
|
|
@@ -11830,10 +11621,10 @@ function detectServices(targetRoot) {
|
|
|
11830
11621
|
} catch {
|
|
11831
11622
|
}
|
|
11832
11623
|
}
|
|
11833
|
-
const makefile =
|
|
11624
|
+
const makefile = join25(targetRoot, "Makefile");
|
|
11834
11625
|
if (existsSync16(makefile)) {
|
|
11835
11626
|
try {
|
|
11836
|
-
const content =
|
|
11627
|
+
const content = readFileSync13(makefile, "utf8");
|
|
11837
11628
|
for (const target of ["dev", "start", "serve"]) {
|
|
11838
11629
|
if (new RegExp(`^${target}:`, "m").test(content)) {
|
|
11839
11630
|
suggestions.push({ label: `make ${target}`, command: `make ${target}`, isRoot: true });
|
|
@@ -11845,7 +11636,7 @@ function detectServices(targetRoot) {
|
|
|
11845
11636
|
}
|
|
11846
11637
|
if (workspaceParentDirs.length === 0 && existsSync16(rootPkgFile)) {
|
|
11847
11638
|
try {
|
|
11848
|
-
const pkg = JSON.parse(
|
|
11639
|
+
const pkg = JSON.parse(readFileSync13(rootPkgFile, "utf8"));
|
|
11849
11640
|
const scripts = pkg.scripts ?? {};
|
|
11850
11641
|
for (const script of ["dev", "start", "serve"]) {
|
|
11851
11642
|
if (scripts[script]) {
|
|
@@ -11856,20 +11647,11 @@ function detectServices(targetRoot) {
|
|
|
11856
11647
|
} catch {
|
|
11857
11648
|
}
|
|
11858
11649
|
}
|
|
11859
|
-
if (existsSync16(
|
|
11650
|
+
if (existsSync16(join25(targetRoot, "docker-compose.yml")) || existsSync16(join25(targetRoot, "docker-compose.yaml"))) {
|
|
11860
11651
|
suggestions.push({ label: "docker compose up", command: "docker compose up", isRoot: true });
|
|
11861
11652
|
}
|
|
11862
11653
|
return suggestions;
|
|
11863
11654
|
}
|
|
11864
|
-
function broadcastTransition(t) {
|
|
11865
|
-
broadcastToWebSocketClients({
|
|
11866
|
-
type: "service",
|
|
11867
|
-
id: t.id,
|
|
11868
|
-
state: t.to,
|
|
11869
|
-
running: t.to === "starting" || t.to === "running",
|
|
11870
|
-
pid: t.pid ?? null
|
|
11871
|
-
});
|
|
11872
|
-
}
|
|
11873
11655
|
function registerServiceRoutes(app, state) {
|
|
11874
11656
|
app.get("/api/services", (c) => {
|
|
11875
11657
|
const entries = state.config.services ?? [];
|
|
@@ -11891,35 +11673,71 @@ function registerServiceRoutes(app, state) {
|
|
|
11891
11673
|
if (!entry) return c.json({ ok: false, error: "Service not found." }, 404);
|
|
11892
11674
|
return c.json({ ok: true, ...getServiceRuntimeStatus(entry, STATE_ROOT) });
|
|
11893
11675
|
});
|
|
11894
|
-
app.post("/api/services/:id/start", (c) => {
|
|
11676
|
+
app.post("/api/services/:id/start", async (c) => {
|
|
11895
11677
|
const id = c.req.param("id");
|
|
11896
11678
|
const entry = (state.config.services ?? []).find((e) => e.id === id);
|
|
11897
11679
|
if (!entry) return c.json({ ok: false, error: "Service not found." }, 404);
|
|
11898
11680
|
try {
|
|
11899
|
-
|
|
11900
|
-
|
|
11901
|
-
);
|
|
11902
|
-
const serviceVars = Object.fromEntries(
|
|
11903
|
-
(state.variables ?? []).filter((v) => v.scope === entry.id).map((v) => [v.key, v.value])
|
|
11904
|
-
);
|
|
11905
|
-
const mergedEnv = { ...entry.env, ...globalVars, ...serviceVars };
|
|
11906
|
-
const t = startManagedService(entry, TARGET_ROOT, STATE_ROOT, mergedEnv);
|
|
11907
|
-
broadcastTransition(t);
|
|
11908
|
-
startServiceLogBroadcasting(entry.id, STATE_ROOT);
|
|
11909
|
-
return c.json({ ok: true, pid: t.pid, state: t.to });
|
|
11681
|
+
await startManagedService(id);
|
|
11682
|
+
const status = getServiceRuntimeStatus(entry, STATE_ROOT);
|
|
11683
|
+
return c.json({ ok: true, pid: status.pid, state: status.state });
|
|
11910
11684
|
} catch (err) {
|
|
11911
11685
|
logger.error({ err }, `[Service] Failed to start ${id}`);
|
|
11912
11686
|
return c.json({ ok: false, error: String(err) }, 500);
|
|
11913
11687
|
}
|
|
11914
11688
|
});
|
|
11915
|
-
app.post("/api/services/:id/
|
|
11689
|
+
app.post("/api/services/:id/restart", async (c) => {
|
|
11690
|
+
const id = c.req.param("id");
|
|
11691
|
+
const entry = (state.config.services ?? []).find((e) => e.id === id);
|
|
11692
|
+
if (!entry) return c.json({ ok: false, error: "Service not found." }, 404);
|
|
11693
|
+
try {
|
|
11694
|
+
try {
|
|
11695
|
+
await stopManagedService(id);
|
|
11696
|
+
} catch {
|
|
11697
|
+
}
|
|
11698
|
+
await startManagedService(id);
|
|
11699
|
+
const status = getServiceRuntimeStatus(entry, STATE_ROOT);
|
|
11700
|
+
return c.json({ ok: true, pid: status.pid, state: status.state });
|
|
11701
|
+
} catch (err) {
|
|
11702
|
+
logger.error({ err }, `[Service] Failed to restart ${id}`);
|
|
11703
|
+
return c.json({ ok: false, error: String(err) }, 500);
|
|
11704
|
+
}
|
|
11705
|
+
});
|
|
11706
|
+
app.post("/api/services/:id/stop", async (c) => {
|
|
11916
11707
|
const id = c.req.param("id");
|
|
11917
11708
|
const entry = (state.config.services ?? []).find((e) => e.id === id);
|
|
11918
11709
|
if (!entry) return c.json({ ok: false, error: "Service not found." }, 404);
|
|
11919
|
-
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
11710
|
+
try {
|
|
11711
|
+
await stopManagedService(id);
|
|
11712
|
+
} catch {
|
|
11713
|
+
}
|
|
11714
|
+
const status = getServiceRuntimeStatus(entry, STATE_ROOT);
|
|
11715
|
+
return c.json({ ok: true, state: status.state });
|
|
11716
|
+
});
|
|
11717
|
+
app.get("/api/services/:id/health", async (c) => {
|
|
11718
|
+
const id = c.req.param("id");
|
|
11719
|
+
const entry = (state.config.services ?? []).find((e) => e.id === id);
|
|
11720
|
+
if (!entry) return c.json({ ok: false, error: "Service not found." }, 404);
|
|
11721
|
+
const status = getServiceRuntimeStatus(entry, STATE_ROOT);
|
|
11722
|
+
if (!status.running) {
|
|
11723
|
+
return c.json({ ok: false, error: "Service is not running." });
|
|
11724
|
+
}
|
|
11725
|
+
const port = entry.port;
|
|
11726
|
+
if (!port) {
|
|
11727
|
+
return c.json({ ok: false, error: "No port configured." });
|
|
11728
|
+
}
|
|
11729
|
+
const url = entry.healthcheck?.endpoint ?? `http://localhost:${port}`;
|
|
11730
|
+
const start = Date.now();
|
|
11731
|
+
try {
|
|
11732
|
+
const res = await fetch(url, {
|
|
11733
|
+
signal: AbortSignal.timeout(3e3)
|
|
11734
|
+
});
|
|
11735
|
+
const latencyMs = Date.now() - start;
|
|
11736
|
+
return c.json({ ok: true, healthy: res.ok, latencyMs, status: res.status });
|
|
11737
|
+
} catch (err) {
|
|
11738
|
+
const latencyMs = Date.now() - start;
|
|
11739
|
+
return c.json({ ok: true, healthy: false, latencyMs, error: String(err) });
|
|
11740
|
+
}
|
|
11923
11741
|
});
|
|
11924
11742
|
app.get("/api/services/:id/log", (c) => {
|
|
11925
11743
|
const id = c.req.param("id");
|
|
@@ -11929,7 +11747,7 @@ function registerServiceRoutes(app, state) {
|
|
|
11929
11747
|
let logSize = 0;
|
|
11930
11748
|
if (existsSync16(logFile)) {
|
|
11931
11749
|
try {
|
|
11932
|
-
logSize =
|
|
11750
|
+
logSize = statSync3(logFile).size;
|
|
11933
11751
|
} catch {
|
|
11934
11752
|
}
|
|
11935
11753
|
}
|
|
@@ -11938,10 +11756,10 @@ function registerServiceRoutes(app, state) {
|
|
|
11938
11756
|
if (after !== null && !isNaN(after) && after >= 0 && logSize > after) {
|
|
11939
11757
|
const readSize = logSize - after;
|
|
11940
11758
|
try {
|
|
11941
|
-
const fd =
|
|
11759
|
+
const fd = openSync2(logFile, "r");
|
|
11942
11760
|
const buf = Buffer.alloc(readSize);
|
|
11943
|
-
|
|
11944
|
-
|
|
11761
|
+
readSync2(fd, buf, 0, readSize, after);
|
|
11762
|
+
closeSync2(fd);
|
|
11945
11763
|
return c.json({ ok: true, text: buf.toString("utf8"), logSize, truncated: false });
|
|
11946
11764
|
} catch {
|
|
11947
11765
|
return c.json({ ok: true, text: "", logSize, truncated: false });
|
|
@@ -11969,13 +11787,13 @@ function registerServiceRoutes(app, state) {
|
|
|
11969
11787
|
let lastSize = 0;
|
|
11970
11788
|
if (existsSync16(logFile)) {
|
|
11971
11789
|
try {
|
|
11972
|
-
const stat =
|
|
11790
|
+
const stat = statSync3(logFile);
|
|
11973
11791
|
lastSize = stat.size;
|
|
11974
11792
|
const readSize = Math.min(lastSize, 16384);
|
|
11975
|
-
const fd =
|
|
11793
|
+
const fd = openSync2(logFile, "r");
|
|
11976
11794
|
const buf = Buffer.alloc(readSize);
|
|
11977
|
-
|
|
11978
|
-
|
|
11795
|
+
readSync2(fd, buf, 0, readSize, Math.max(0, lastSize - readSize));
|
|
11796
|
+
closeSync2(fd);
|
|
11979
11797
|
ctrl.enqueue(sseMsg({ type: "init", text: buf.toString("utf8"), size: lastSize }));
|
|
11980
11798
|
} catch {
|
|
11981
11799
|
}
|
|
@@ -11985,26 +11803,26 @@ function registerServiceRoutes(app, state) {
|
|
|
11985
11803
|
chunkIntervalId = setInterval(() => {
|
|
11986
11804
|
if (!existsSync16(logFile)) return;
|
|
11987
11805
|
try {
|
|
11988
|
-
const stat =
|
|
11806
|
+
const stat = statSync3(logFile);
|
|
11989
11807
|
if (stat.size < lastSize) {
|
|
11990
11808
|
lastSize = 0;
|
|
11991
11809
|
const readSize = Math.min(stat.size, 16384);
|
|
11992
11810
|
let text = "";
|
|
11993
11811
|
if (readSize > 0) {
|
|
11994
|
-
const fd =
|
|
11812
|
+
const fd = openSync2(logFile, "r");
|
|
11995
11813
|
const buf = Buffer.alloc(readSize);
|
|
11996
|
-
|
|
11997
|
-
|
|
11814
|
+
readSync2(fd, buf, 0, readSize, 0);
|
|
11815
|
+
closeSync2(fd);
|
|
11998
11816
|
text = buf.toString("utf8");
|
|
11999
11817
|
lastSize = stat.size;
|
|
12000
11818
|
}
|
|
12001
11819
|
ctrl.enqueue(sseMsg({ type: "init", text, size: lastSize }));
|
|
12002
11820
|
} else if (stat.size > lastSize) {
|
|
12003
11821
|
const readSize = stat.size - lastSize;
|
|
12004
|
-
const fd =
|
|
11822
|
+
const fd = openSync2(logFile, "r");
|
|
12005
11823
|
const buf = Buffer.alloc(readSize);
|
|
12006
|
-
|
|
12007
|
-
|
|
11824
|
+
readSync2(fd, buf, 0, readSize, lastSize);
|
|
11825
|
+
closeSync2(fd);
|
|
12008
11826
|
lastSize = stat.size;
|
|
12009
11827
|
ctrl.enqueue(sseMsg({ type: "chunk", text: buf.toString("utf8"), size: lastSize }));
|
|
12010
11828
|
}
|
|
@@ -12045,7 +11863,7 @@ function registerServiceRoutes(app, state) {
|
|
|
12045
11863
|
app.post("/api/services/config", async (c) => {
|
|
12046
11864
|
return replaceServiceConfigs(c, {
|
|
12047
11865
|
replaceAllServices: async (entries) => {
|
|
12048
|
-
const { replaceAllServices: replaceAllServices2 } = await import("./store-
|
|
11866
|
+
const { replaceAllServices: replaceAllServices2 } = await import("./store-S3NAYZ3S.js");
|
|
12049
11867
|
await replaceAllServices2(entries);
|
|
12050
11868
|
},
|
|
12051
11869
|
replacePersistedService: async () => {
|
|
@@ -12057,13 +11875,12 @@ function registerServiceRoutes(app, state) {
|
|
|
12057
11875
|
app.delete("/api/services/:id", async (c) => {
|
|
12058
11876
|
const id = c.req.param("id");
|
|
12059
11877
|
try {
|
|
12060
|
-
|
|
12061
|
-
if (t) broadcastTransition(t);
|
|
11878
|
+
await stopManagedService(id);
|
|
12062
11879
|
} catch {
|
|
12063
11880
|
}
|
|
12064
11881
|
return deleteServiceConfig(c, {
|
|
12065
11882
|
deletePersistedService: async (serviceId) => {
|
|
12066
|
-
const { deletePersistedService: deletePersistedService2 } = await import("./store-
|
|
11883
|
+
const { deletePersistedService: deletePersistedService2 } = await import("./store-S3NAYZ3S.js");
|
|
12067
11884
|
await deletePersistedService2(serviceId);
|
|
12068
11885
|
},
|
|
12069
11886
|
replacePersistedService: async () => {
|
|
@@ -12075,7 +11892,7 @@ function registerServiceRoutes(app, state) {
|
|
|
12075
11892
|
app.put("/api/services/:id", async (c) => {
|
|
12076
11893
|
return upsertServiceConfig(c, {
|
|
12077
11894
|
replacePersistedService: async (entry) => {
|
|
12078
|
-
const { replacePersistedService: replacePersistedService2 } = await import("./store-
|
|
11895
|
+
const { replacePersistedService: replacePersistedService2 } = await import("./store-S3NAYZ3S.js");
|
|
12079
11896
|
await replacePersistedService2(entry);
|
|
12080
11897
|
},
|
|
12081
11898
|
deletePersistedService: async () => {
|
|
@@ -12093,13 +11910,13 @@ function registerServiceRoutes(app, state) {
|
|
|
12093
11910
|
if (!logTail.trim()) {
|
|
12094
11911
|
return c.json({ ok: false, error: "Service log is empty \u2014 start the service first." }, 400);
|
|
12095
11912
|
}
|
|
12096
|
-
const { analyzeLogForHealthcheck } = await import("./log-analyzer-
|
|
11913
|
+
const { analyzeLogForHealthcheck } = await import("./log-analyzer-EIX6R6PP.js");
|
|
12097
11914
|
const healthcheck = await analyzeLogForHealthcheck(logTail, entry.name, state.config);
|
|
12098
11915
|
if (!healthcheck) {
|
|
12099
11916
|
return c.json({ ok: true, found: false, healthcheck: null });
|
|
12100
11917
|
}
|
|
12101
11918
|
entry.healthcheck = healthcheck;
|
|
12102
|
-
const { replacePersistedService: replacePersistedService2 } = await import("./store-
|
|
11919
|
+
const { replacePersistedService: replacePersistedService2 } = await import("./store-S3NAYZ3S.js");
|
|
12103
11920
|
await replacePersistedService2(entry);
|
|
12104
11921
|
return c.json({ ok: true, found: true, healthcheck, saved: true });
|
|
12105
11922
|
} catch (err) {
|
|
@@ -12116,7 +11933,7 @@ function registerServiceRoutes(app, state) {
|
|
|
12116
11933
|
if (!logTail.trim()) {
|
|
12117
11934
|
return c.json({ ok: false, error: "Service log is empty \u2014 start the service first." }, 400);
|
|
12118
11935
|
}
|
|
12119
|
-
const { analyzeLogForFix } = await import("./log-analyzer-
|
|
11936
|
+
const { analyzeLogForFix } = await import("./log-analyzer-EIX6R6PP.js");
|
|
12120
11937
|
const suggestion = await analyzeLogForFix(logTail, entry.name, state.config);
|
|
12121
11938
|
if (!suggestion) {
|
|
12122
11939
|
return c.json({ ok: false, error: "Could not parse AI response." }, 422);
|
|
@@ -12130,6 +11947,142 @@ function registerServiceRoutes(app, state) {
|
|
|
12130
11947
|
return c.json({ ok: false, error: String(err) }, 500);
|
|
12131
11948
|
}
|
|
12132
11949
|
});
|
|
11950
|
+
app.post("/api/services/:id/insights", async (c) => {
|
|
11951
|
+
const id = c.req.param("id");
|
|
11952
|
+
const entry = (state.config.services ?? []).find((e) => e.id === id);
|
|
11953
|
+
if (!entry) return c.json({ ok: false, error: "Service not found." }, 404);
|
|
11954
|
+
try {
|
|
11955
|
+
const logTail = readServiceLogTail(id, STATE_ROOT, 16384);
|
|
11956
|
+
if (!logTail.trim()) {
|
|
11957
|
+
return c.json({ ok: false, error: "Service log is empty \u2014 start the service first." }, 400);
|
|
11958
|
+
}
|
|
11959
|
+
const { analyzeLogForInsights } = await import("./log-analyzer-EIX6R6PP.js");
|
|
11960
|
+
const insights = await analyzeLogForInsights(logTail, entry.name, state.config);
|
|
11961
|
+
if (!insights) {
|
|
11962
|
+
return c.json({ ok: false, error: "Could not parse AI response." }, 422);
|
|
11963
|
+
}
|
|
11964
|
+
return c.json({ ok: true, insights });
|
|
11965
|
+
} catch (err) {
|
|
11966
|
+
logger.error({ err, id }, "[Service] insights analysis failed");
|
|
11967
|
+
return c.json({ ok: false, error: String(err) }, 500);
|
|
11968
|
+
}
|
|
11969
|
+
});
|
|
11970
|
+
}
|
|
11971
|
+
|
|
11972
|
+
// src/routes/traffic.ts
|
|
11973
|
+
var MESH_VAR_KEYS = ["HTTP_PROXY", "http_proxy", "NO_PROXY", "no_proxy"];
|
|
11974
|
+
function registerTrafficRoutes(collector, state) {
|
|
11975
|
+
collector.get("/api/mesh", (c) => {
|
|
11976
|
+
const graph = getServiceGraph();
|
|
11977
|
+
if (!graph) return c.json({ ok: false, error: "Mesh proxy not running" }, 503);
|
|
11978
|
+
const entries = state.config.services ?? [];
|
|
11979
|
+
const services = listServiceStatuses(entries, STATE_ROOT);
|
|
11980
|
+
return c.json({ ok: true, graph: graph.getGraph(services) });
|
|
11981
|
+
});
|
|
11982
|
+
collector.get("/api/mesh/traffic", (c) => {
|
|
11983
|
+
const buf = getTrafficBuffer();
|
|
11984
|
+
if (!buf) return c.json({ ok: false, error: "Mesh proxy not running" }, 503);
|
|
11985
|
+
const limit = Number(c.req.query("limit") ?? 100);
|
|
11986
|
+
return c.json({ ok: true, entries: buf.getRecent(limit) });
|
|
11987
|
+
});
|
|
11988
|
+
collector.get("/api/mesh/stats", (c) => {
|
|
11989
|
+
const stats = getTrafficProxyStats();
|
|
11990
|
+
if (!stats) return c.json({ ok: false, error: "Mesh proxy not running" }, 503);
|
|
11991
|
+
return c.json({ ok: true, stats });
|
|
11992
|
+
});
|
|
11993
|
+
collector.post("/api/mesh/clear", (c) => {
|
|
11994
|
+
getTrafficBuffer()?.clear();
|
|
11995
|
+
getServiceGraph()?.reset();
|
|
11996
|
+
return c.json({ ok: true });
|
|
11997
|
+
});
|
|
11998
|
+
collector.get("/api/mesh/status", (c) => {
|
|
11999
|
+
return c.json({
|
|
12000
|
+
ok: true,
|
|
12001
|
+
enabled: state.config.meshEnabled ?? false,
|
|
12002
|
+
running: isTrafficProxyRunning(),
|
|
12003
|
+
port: getTrafficProxyPort()
|
|
12004
|
+
});
|
|
12005
|
+
});
|
|
12006
|
+
collector.post("/api/mesh/toggle", async (c) => {
|
|
12007
|
+
const body = await c.req.json();
|
|
12008
|
+
const enabled = body.enabled === true;
|
|
12009
|
+
const { persistSetting: persistSetting2 } = await import("./settings-SOTIS6ZD.js");
|
|
12010
|
+
await persistSetting2("runtime.meshEnabled", enabled, { scope: "runtime", source: "user" });
|
|
12011
|
+
state.config.meshEnabled = enabled;
|
|
12012
|
+
if (enabled && !isTrafficProxyRunning()) {
|
|
12013
|
+
try {
|
|
12014
|
+
setServicesAccessor(() => listServiceStatuses(state.config.services ?? [], STATE_ROOT));
|
|
12015
|
+
const port = await startTrafficProxy({
|
|
12016
|
+
port: state.config.meshProxyPort ?? 0,
|
|
12017
|
+
bufferSize: state.config.meshBufferSize ?? 1e3,
|
|
12018
|
+
onEntry: (entry) => sendToMeshRoom({ type: "mesh:entry", entry })
|
|
12019
|
+
});
|
|
12020
|
+
const dashPort = Number(state.config.dashboardPort ?? 4e3);
|
|
12021
|
+
const servicePorts = (state.config.services ?? []).filter((s) => s.port).map((s) => `localhost:${s.port}`);
|
|
12022
|
+
const noProxyList = [`localhost:${dashPort}`, ...servicePorts].join(",");
|
|
12023
|
+
const proxyUrl = `http://localhost:${port}`;
|
|
12024
|
+
const vars = state.variables ?? [];
|
|
12025
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
12026
|
+
const globalVars = {
|
|
12027
|
+
HTTP_PROXY: proxyUrl,
|
|
12028
|
+
http_proxy: proxyUrl,
|
|
12029
|
+
NO_PROXY: noProxyList,
|
|
12030
|
+
no_proxy: noProxyList
|
|
12031
|
+
};
|
|
12032
|
+
for (const [key, value] of Object.entries(globalVars)) {
|
|
12033
|
+
const id = `global:${key}`;
|
|
12034
|
+
const idx = vars.findIndex((v) => v.id === id);
|
|
12035
|
+
const entry = { id, key, value, scope: "global", updatedAt: ts };
|
|
12036
|
+
if (idx >= 0) vars[idx] = entry;
|
|
12037
|
+
else vars.push(entry);
|
|
12038
|
+
}
|
|
12039
|
+
state.variables = vars;
|
|
12040
|
+
logger.info({ port }, "[Mesh] Proxy started + global env vars injected");
|
|
12041
|
+
const runningServices = (state.config.services ?? []).filter((s) => {
|
|
12042
|
+
const status = getServiceRuntimeStatus(s, STATE_ROOT);
|
|
12043
|
+
return status.running;
|
|
12044
|
+
});
|
|
12045
|
+
for (const svc of runningServices) {
|
|
12046
|
+
try {
|
|
12047
|
+
const { sendServiceEvent: sendServiceEvent2 } = await import("./fsm-service-7O4AJG2R.js");
|
|
12048
|
+
await sendServiceEvent2(svc.id, "STOP");
|
|
12049
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
12050
|
+
await sendServiceEvent2(svc.id, "START");
|
|
12051
|
+
logger.info({ id: svc.id }, "[Mesh] Restarted service to apply proxy env");
|
|
12052
|
+
} catch (err) {
|
|
12053
|
+
logger.warn({ err, id: svc.id }, "[Mesh] Failed to restart service for proxy");
|
|
12054
|
+
}
|
|
12055
|
+
}
|
|
12056
|
+
return c.json({ ok: true, running: true, port, restarted: runningServices.map((s) => s.id) });
|
|
12057
|
+
} catch (err) {
|
|
12058
|
+
logger.error({ err }, "[Mesh] Failed to start proxy");
|
|
12059
|
+
return c.json({ ok: false, error: String(err) }, 500);
|
|
12060
|
+
}
|
|
12061
|
+
}
|
|
12062
|
+
if (!enabled && isTrafficProxyRunning()) {
|
|
12063
|
+
await stopTrafficProxy();
|
|
12064
|
+
state.variables = (state.variables ?? []).filter(
|
|
12065
|
+
(v) => v.scope !== "global" || !MESH_VAR_KEYS.includes(v.key)
|
|
12066
|
+
);
|
|
12067
|
+
logger.info("[Mesh] Proxy stopped + global env vars removed");
|
|
12068
|
+
const runningServices = (state.config.services ?? []).filter((s) => {
|
|
12069
|
+
const status = getServiceRuntimeStatus(s, STATE_ROOT);
|
|
12070
|
+
return status.running;
|
|
12071
|
+
});
|
|
12072
|
+
for (const svc of runningServices) {
|
|
12073
|
+
try {
|
|
12074
|
+
const { sendServiceEvent: sendServiceEvent2 } = await import("./fsm-service-7O4AJG2R.js");
|
|
12075
|
+
await sendServiceEvent2(svc.id, "STOP");
|
|
12076
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
12077
|
+
await sendServiceEvent2(svc.id, "START");
|
|
12078
|
+
logger.info({ id: svc.id }, "[Mesh] Restarted service to remove proxy env");
|
|
12079
|
+
} catch (err) {
|
|
12080
|
+
logger.warn({ err, id: svc.id }, "[Mesh] Failed to restart service after proxy disable");
|
|
12081
|
+
}
|
|
12082
|
+
}
|
|
12083
|
+
}
|
|
12084
|
+
return c.json({ ok: true, running: isTrafficProxyRunning(), port: getTrafficProxyPort() });
|
|
12085
|
+
});
|
|
12133
12086
|
}
|
|
12134
12087
|
|
|
12135
12088
|
// src/routes/variables.ts
|
|
@@ -12141,7 +12094,7 @@ function registerVariableRoutes(app, state) {
|
|
|
12141
12094
|
app.put("/api/variables/:id", async (c) => {
|
|
12142
12095
|
const result = await upsertVariable(c, {
|
|
12143
12096
|
upsertPersistedVariable: async (entry) => {
|
|
12144
|
-
const { upsertPersistedVariable: upsertPersistedVariable2 } = await import("./store-
|
|
12097
|
+
const { upsertPersistedVariable: upsertPersistedVariable2 } = await import("./store-S3NAYZ3S.js");
|
|
12145
12098
|
await upsertPersistedVariable2(entry);
|
|
12146
12099
|
}
|
|
12147
12100
|
});
|
|
@@ -12154,7 +12107,7 @@ function registerVariableRoutes(app, state) {
|
|
|
12154
12107
|
const id = c.req.param("id");
|
|
12155
12108
|
const result = await deleteVariable(c, {
|
|
12156
12109
|
deletePersistedVariable: async (varId) => {
|
|
12157
|
-
const { deletePersistedVariable: deletePersistedVariable2 } = await import("./store-
|
|
12110
|
+
const { deletePersistedVariable: deletePersistedVariable2 } = await import("./store-S3NAYZ3S.js");
|
|
12158
12111
|
await deletePersistedVariable2(varId);
|
|
12159
12112
|
}
|
|
12160
12113
|
});
|
|
@@ -12170,29 +12123,29 @@ import {
|
|
|
12170
12123
|
cpSync as cpSync2,
|
|
12171
12124
|
existsSync as existsSync17,
|
|
12172
12125
|
mkdirSync as mkdirSync12,
|
|
12173
|
-
readFileSync as
|
|
12174
|
-
readdirSync as
|
|
12126
|
+
readFileSync as readFileSync14,
|
|
12127
|
+
readdirSync as readdirSync7,
|
|
12175
12128
|
renameSync as renameSync2,
|
|
12176
12129
|
rmSync as rmSync7,
|
|
12177
|
-
statSync as
|
|
12130
|
+
statSync as statSync4,
|
|
12178
12131
|
writeFileSync as writeFileSync18
|
|
12179
12132
|
} from "fs";
|
|
12180
12133
|
import { execSync as execSync8 } from "child_process";
|
|
12181
|
-
import { join as
|
|
12134
|
+
import { join as join26, resolve as resolve3 } from "path";
|
|
12182
12135
|
var DEV_PROFILE_DEFAULT_PORT = 4100;
|
|
12183
12136
|
var CLI_CONFIG_DIRS = [".claude", ".codex", ".gemini"];
|
|
12184
12137
|
var CLI_CONFIG_FILES = ["CLAUDE.md"];
|
|
12185
12138
|
function resolvePaths(stateRoot, profileName = "dev") {
|
|
12186
|
-
const profileRoot =
|
|
12139
|
+
const profileRoot = join26(stateRoot, "profiles", profileName);
|
|
12187
12140
|
return {
|
|
12188
12141
|
profileName,
|
|
12189
12142
|
profileRoot,
|
|
12190
|
-
workspaceRoot:
|
|
12191
|
-
persistenceRoot:
|
|
12192
|
-
trashRoot:
|
|
12193
|
-
runbooksRoot:
|
|
12194
|
-
bootstrapRoot:
|
|
12195
|
-
metadataFile:
|
|
12143
|
+
workspaceRoot: join26(profileRoot, "workspace"),
|
|
12144
|
+
persistenceRoot: join26(profileRoot, ".fifony"),
|
|
12145
|
+
trashRoot: join26(stateRoot, "trash", "dev-profiles"),
|
|
12146
|
+
runbooksRoot: join26(profileRoot, "runbooks"),
|
|
12147
|
+
bootstrapRoot: join26(profileRoot, "bootstrap"),
|
|
12148
|
+
metadataFile: join26(profileRoot, "profile.json")
|
|
12196
12149
|
};
|
|
12197
12150
|
}
|
|
12198
12151
|
function branchNameFor(profileName) {
|
|
@@ -12200,7 +12153,7 @@ function branchNameFor(profileName) {
|
|
|
12200
12153
|
}
|
|
12201
12154
|
function readProfileMetadata(paths) {
|
|
12202
12155
|
try {
|
|
12203
|
-
return existsSync17(paths.metadataFile) ? JSON.parse(
|
|
12156
|
+
return existsSync17(paths.metadataFile) ? JSON.parse(readFileSync14(paths.metadataFile, "utf8")) : {};
|
|
12204
12157
|
} catch {
|
|
12205
12158
|
return {};
|
|
12206
12159
|
}
|
|
@@ -12210,26 +12163,26 @@ function ensureDir(path) {
|
|
|
12210
12163
|
}
|
|
12211
12164
|
function copyCliConfigArtifacts(sourceRoot, workspaceRoot) {
|
|
12212
12165
|
for (const dir of CLI_CONFIG_DIRS) {
|
|
12213
|
-
const source =
|
|
12214
|
-
const target =
|
|
12215
|
-
if (!existsSync17(source) || existsSync17(target) || !
|
|
12166
|
+
const source = join26(sourceRoot, dir);
|
|
12167
|
+
const target = join26(workspaceRoot, dir);
|
|
12168
|
+
if (!existsSync17(source) || existsSync17(target) || !statSync4(source).isDirectory()) continue;
|
|
12216
12169
|
cpSync2(source, target, { recursive: true });
|
|
12217
12170
|
}
|
|
12218
12171
|
for (const file of CLI_CONFIG_FILES) {
|
|
12219
|
-
const source =
|
|
12220
|
-
const target =
|
|
12172
|
+
const source = join26(sourceRoot, file);
|
|
12173
|
+
const target = join26(workspaceRoot, file);
|
|
12221
12174
|
if (!existsSync17(source) || existsSync17(target)) continue;
|
|
12222
|
-
writeFileSync18(target,
|
|
12175
|
+
writeFileSync18(target, readFileSync14(source));
|
|
12223
12176
|
}
|
|
12224
12177
|
}
|
|
12225
12178
|
function ensureWorktreeLocalExcludes(workspaceRoot) {
|
|
12226
12179
|
try {
|
|
12227
|
-
const gitFile =
|
|
12180
|
+
const gitFile = readFileSync14(join26(workspaceRoot, ".git"), "utf8").trim();
|
|
12228
12181
|
const gitDirPath = gitFile.replace(/^gitdir:\s*/, "").trim();
|
|
12229
|
-
const resolvedGitDir =
|
|
12230
|
-
const excludePath =
|
|
12231
|
-
ensureDir(
|
|
12232
|
-
const current = existsSync17(excludePath) ?
|
|
12182
|
+
const resolvedGitDir = resolve3(workspaceRoot, gitDirPath);
|
|
12183
|
+
const excludePath = join26(resolvedGitDir, "info", "exclude");
|
|
12184
|
+
ensureDir(join26(resolvedGitDir, "info"));
|
|
12185
|
+
const current = existsSync17(excludePath) ? readFileSync14(excludePath, "utf8") : "";
|
|
12233
12186
|
const entries = [
|
|
12234
12187
|
"WORKFLOW.local.md",
|
|
12235
12188
|
"FIFONY.md",
|
|
@@ -12250,7 +12203,7 @@ function seedProfileFiles(targetRoot, paths, branchName) {
|
|
|
12250
12203
|
ensureDir(paths.profileRoot);
|
|
12251
12204
|
ensureDir(paths.runbooksRoot);
|
|
12252
12205
|
ensureDir(paths.bootstrapRoot);
|
|
12253
|
-
ensureDir(
|
|
12206
|
+
ensureDir(join26(paths.workspaceRoot, ".fifony-dev"));
|
|
12254
12207
|
const workflowLocal = [
|
|
12255
12208
|
"# Dev Workflow",
|
|
12256
12209
|
"",
|
|
@@ -12310,15 +12263,15 @@ function seedProfileFiles(targetRoot, paths, branchName) {
|
|
|
12310
12263
|
"- Use analytics to inspect memory/context coverage and layer hit mix.",
|
|
12311
12264
|
""
|
|
12312
12265
|
].join("\n");
|
|
12313
|
-
writeFileSync18(
|
|
12266
|
+
writeFileSync18(join26(paths.workspaceRoot, "WORKFLOW.local.md"), `${workflowLocal}
|
|
12314
12267
|
`, "utf8");
|
|
12315
|
-
writeFileSync18(
|
|
12268
|
+
writeFileSync18(join26(paths.workspaceRoot, "FIFONY.md"), `${fifonyMd}
|
|
12316
12269
|
`, "utf8");
|
|
12317
|
-
writeFileSync18(
|
|
12270
|
+
writeFileSync18(join26(paths.runbooksRoot, "doctor.md"), `${doctorRunbook}
|
|
12318
12271
|
`, "utf8");
|
|
12319
|
-
writeFileSync18(
|
|
12272
|
+
writeFileSync18(join26(paths.runbooksRoot, "services.md"), `${servicesRunbook}
|
|
12320
12273
|
`, "utf8");
|
|
12321
|
-
writeFileSync18(
|
|
12274
|
+
writeFileSync18(join26(paths.runbooksRoot, "context.md"), `${contextRunbook}
|
|
12322
12275
|
`, "utf8");
|
|
12323
12276
|
writeFileSync18(paths.metadataFile, JSON.stringify({
|
|
12324
12277
|
profileName: paths.profileName,
|
|
@@ -12344,7 +12297,7 @@ function getDevProfileStatus(targetRoot, stateRoot, profileName = "dev") {
|
|
|
12344
12297
|
const paths = resolvePaths(stateRoot, profileName);
|
|
12345
12298
|
const metadata = readProfileMetadata(paths);
|
|
12346
12299
|
const branchName = branchNameFor(profileName);
|
|
12347
|
-
const trashEntries = existsSync17(paths.trashRoot) ?
|
|
12300
|
+
const trashEntries = existsSync17(paths.trashRoot) ? readdirSync7(paths.trashRoot).filter((entry) => entry.startsWith(`${profileName}-`)).sort().reverse() : [];
|
|
12348
12301
|
return {
|
|
12349
12302
|
profileName,
|
|
12350
12303
|
profileRoot: paths.profileRoot,
|
|
@@ -12355,12 +12308,12 @@ function getDevProfileStatus(targetRoot, stateRoot, profileName = "dev") {
|
|
|
12355
12308
|
dashboardPort: DEV_PROFILE_DEFAULT_PORT,
|
|
12356
12309
|
workspaceExists: existsSync17(paths.workspaceRoot),
|
|
12357
12310
|
persistenceExists: existsSync17(paths.persistenceRoot),
|
|
12358
|
-
bootstrapped: existsSync17(
|
|
12311
|
+
bootstrapped: existsSync17(join26(paths.workspaceRoot, "WORKFLOW.local.md")) && existsSync17(join26(paths.workspaceRoot, "FIFONY.md")),
|
|
12359
12312
|
worktreeAttached: isWorktreeAttached(targetRoot, paths.workspaceRoot),
|
|
12360
12313
|
bootstrapFiles: {
|
|
12361
|
-
workflowLocal: existsSync17(
|
|
12362
|
-
fifony: existsSync17(
|
|
12363
|
-
runbooks: existsSync17(paths.runbooksRoot) ?
|
|
12314
|
+
workflowLocal: existsSync17(join26(paths.workspaceRoot, "WORKFLOW.local.md")),
|
|
12315
|
+
fifony: existsSync17(join26(paths.workspaceRoot, "FIFONY.md")),
|
|
12316
|
+
runbooks: existsSync17(paths.runbooksRoot) ? readdirSync7(paths.runbooksRoot).sort() : []
|
|
12364
12317
|
},
|
|
12365
12318
|
trashEntries,
|
|
12366
12319
|
launchCommand: `fifony dev run --workspace "${targetRoot}" --port ${DEV_PROFILE_DEFAULT_PORT}`,
|
|
@@ -12414,7 +12367,7 @@ function resetDevProfile(targetRoot, stateRoot, profileName = "dev") {
|
|
|
12414
12367
|
};
|
|
12415
12368
|
}
|
|
12416
12369
|
ensureDir(paths.trashRoot);
|
|
12417
|
-
const trashPath =
|
|
12370
|
+
const trashPath = join26(paths.trashRoot, `${profileName}-${Date.now()}`);
|
|
12418
12371
|
const metadata = readProfileMetadata(paths);
|
|
12419
12372
|
writeFileSync18(paths.metadataFile, JSON.stringify({
|
|
12420
12373
|
...metadata,
|
|
@@ -12468,6 +12421,430 @@ function registerDevProfileRoutes(app) {
|
|
|
12468
12421
|
});
|
|
12469
12422
|
}
|
|
12470
12423
|
|
|
12424
|
+
// src/agents/chat/chat-session.ts
|
|
12425
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync13, readFileSync as readFileSync15, writeFileSync as writeFileSync19, rmSync as rmSync8, readdirSync as readdirSync8 } from "fs";
|
|
12426
|
+
import { join as join27 } from "path";
|
|
12427
|
+
var CHAT_DIR = join27(STATE_ROOT, "chat-sessions");
|
|
12428
|
+
function ensureChatDir() {
|
|
12429
|
+
mkdirSync13(CHAT_DIR, { recursive: true });
|
|
12430
|
+
}
|
|
12431
|
+
function sessionPath(issueId) {
|
|
12432
|
+
return join27(CHAT_DIR, `issue-${issueId}.json`);
|
|
12433
|
+
}
|
|
12434
|
+
async function loadChatSession(issueId) {
|
|
12435
|
+
const path = sessionPath(issueId);
|
|
12436
|
+
if (!existsSync18(path)) return null;
|
|
12437
|
+
try {
|
|
12438
|
+
return JSON.parse(readFileSync15(path, "utf8"));
|
|
12439
|
+
} catch {
|
|
12440
|
+
return null;
|
|
12441
|
+
}
|
|
12442
|
+
}
|
|
12443
|
+
async function persistChatSession(session) {
|
|
12444
|
+
ensureChatDir();
|
|
12445
|
+
session.updatedAt = now();
|
|
12446
|
+
writeFileSync19(sessionPath(session.issueId), JSON.stringify(session, null, 2), "utf8");
|
|
12447
|
+
}
|
|
12448
|
+
async function createIssueChat(issueId, cli, existingTurns) {
|
|
12449
|
+
const ts = now();
|
|
12450
|
+
const session = {
|
|
12451
|
+
issueId,
|
|
12452
|
+
cli,
|
|
12453
|
+
turns: existingTurns ?? [],
|
|
12454
|
+
createdAt: ts,
|
|
12455
|
+
updatedAt: ts
|
|
12456
|
+
};
|
|
12457
|
+
await persistChatSession(session);
|
|
12458
|
+
logger.debug({ issueId, provider: cli.provider }, "[Chat] Issue chat created");
|
|
12459
|
+
return session;
|
|
12460
|
+
}
|
|
12461
|
+
async function deleteChatSession(issueId) {
|
|
12462
|
+
try {
|
|
12463
|
+
rmSync8(sessionPath(issueId), { force: true });
|
|
12464
|
+
return true;
|
|
12465
|
+
} catch {
|
|
12466
|
+
return false;
|
|
12467
|
+
}
|
|
12468
|
+
}
|
|
12469
|
+
async function listChatSessions() {
|
|
12470
|
+
ensureChatDir();
|
|
12471
|
+
try {
|
|
12472
|
+
const files = readdirSync8(CHAT_DIR).filter((f) => f.startsWith("issue-") && f.endsWith(".json"));
|
|
12473
|
+
const sessions = [];
|
|
12474
|
+
for (const file of files) {
|
|
12475
|
+
try {
|
|
12476
|
+
const raw = readFileSync15(join27(CHAT_DIR, file), "utf8");
|
|
12477
|
+
sessions.push(JSON.parse(raw));
|
|
12478
|
+
} catch {
|
|
12479
|
+
}
|
|
12480
|
+
}
|
|
12481
|
+
return sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
12482
|
+
} catch (err) {
|
|
12483
|
+
logger.warn({ err }, "[Chat] Failed to list sessions");
|
|
12484
|
+
return [];
|
|
12485
|
+
}
|
|
12486
|
+
}
|
|
12487
|
+
function appendTurn(session, turn) {
|
|
12488
|
+
session.turns.push({ ...turn, timestamp: now() });
|
|
12489
|
+
}
|
|
12490
|
+
function sessionFileToMeta(session) {
|
|
12491
|
+
return {
|
|
12492
|
+
id: session.issueId,
|
|
12493
|
+
name: `Issue ${session.issueId}`,
|
|
12494
|
+
status: "active",
|
|
12495
|
+
provider: session.cli.provider,
|
|
12496
|
+
turns: session.turns,
|
|
12497
|
+
createdAt: session.createdAt,
|
|
12498
|
+
updatedAt: session.updatedAt
|
|
12499
|
+
};
|
|
12500
|
+
}
|
|
12501
|
+
|
|
12502
|
+
// src/agents/chat/chat-prompt.ts
|
|
12503
|
+
function buildGlobalChatPrompt(state) {
|
|
12504
|
+
const projectName = state.projectName || state.detectedProjectName || "unnamed project";
|
|
12505
|
+
const issueRows = state.issues.slice(0, 20).map(
|
|
12506
|
+
(i) => `| ${i.identifier} | ${i.title.slice(0, 50)} | ${i.state} |`
|
|
12507
|
+
);
|
|
12508
|
+
const issuesSection = issueRows.length ? `## Current Issues
|
|
12509
|
+
| ID | Title | State |
|
|
12510
|
+
|---|---|---|
|
|
12511
|
+
${issueRows.join("\n")}` : "## Current Issues\nNo issues.";
|
|
12512
|
+
const services = state.config.services ?? [];
|
|
12513
|
+
const serviceRows = services.slice(0, 10).map(
|
|
12514
|
+
(s) => `| ${s.name} | ${s.port ?? "-"} |`
|
|
12515
|
+
);
|
|
12516
|
+
const servicesSection = serviceRows.length ? `## Services
|
|
12517
|
+
| Name | Port |
|
|
12518
|
+
|---|---|
|
|
12519
|
+
${serviceRows.join("\n")}` : "## Services\nNo services configured.";
|
|
12520
|
+
return `You are Spark, an AI assistant for the project "${projectName}".
|
|
12521
|
+
You can discuss the project, answer questions, and perform operations.
|
|
12522
|
+
|
|
12523
|
+
${issuesSection}
|
|
12524
|
+
|
|
12525
|
+
${servicesSection}
|
|
12526
|
+
|
|
12527
|
+
## Available Actions
|
|
12528
|
+
|
|
12529
|
+
To perform an operation, emit a fenced code block with the \`action\` language tag containing valid JSON:
|
|
12530
|
+
|
|
12531
|
+
\`\`\`action
|
|
12532
|
+
{ "type": "<action-type>", "payload": { ... } }
|
|
12533
|
+
\`\`\`
|
|
12534
|
+
|
|
12535
|
+
### Action types:
|
|
12536
|
+
|
|
12537
|
+
**Issue operations:**
|
|
12538
|
+
- \`create-issue\` \u2014 payload: \`{ "title": string, "description"?: string }\`
|
|
12539
|
+
- \`retry-issue\` \u2014 payload: \`{ "issueId": string, "feedback"?: string }\`
|
|
12540
|
+
- \`replan-issue\` \u2014 payload: \`{ "issueId": string }\`
|
|
12541
|
+
- \`approve-issue\` \u2014 payload: \`{ "issueId": string }\`
|
|
12542
|
+
- \`merge-issue\` \u2014 payload: \`{ "issueId": string }\`
|
|
12543
|
+
|
|
12544
|
+
**Service operations:**
|
|
12545
|
+
- \`start-service\` \u2014 payload: \`{ "id": string }\`
|
|
12546
|
+
- \`stop-service\` \u2014 payload: \`{ "id": string }\`
|
|
12547
|
+
- \`restart-service\` \u2014 payload: \`{ "id": string }\`
|
|
12548
|
+
|
|
12549
|
+
**Read operations:**
|
|
12550
|
+
- \`read-file\` \u2014 payload: \`{ "path": string }\`
|
|
12551
|
+
- \`read-service-log\` \u2014 payload: \`{ "id": string, "bytes"?: number }\`
|
|
12552
|
+
- \`list-issues\` \u2014 payload: \`{}\`
|
|
12553
|
+
- \`list-services\` \u2014 payload: \`{}\`
|
|
12554
|
+
|
|
12555
|
+
You may emit multiple action blocks in one response. Only emit actions when the user explicitly asks for an operation.
|
|
12556
|
+
|
|
12557
|
+
## Response format
|
|
12558
|
+
Always respond in **Markdown**. Use headings, bullet lists, numbered lists, bold, inline code, and code blocks to structure your answers. Keep responses concise and well-formatted.`;
|
|
12559
|
+
}
|
|
12560
|
+
|
|
12561
|
+
// src/agents/chat/action-parser.ts
|
|
12562
|
+
var VALID_ACTION_TYPES = /* @__PURE__ */ new Set([
|
|
12563
|
+
"create-issue",
|
|
12564
|
+
"retry-issue",
|
|
12565
|
+
"replan-issue",
|
|
12566
|
+
"approve-issue",
|
|
12567
|
+
"merge-issue",
|
|
12568
|
+
"start-service",
|
|
12569
|
+
"stop-service",
|
|
12570
|
+
"restart-service",
|
|
12571
|
+
"read-file",
|
|
12572
|
+
"read-service-log",
|
|
12573
|
+
"list-issues",
|
|
12574
|
+
"list-services"
|
|
12575
|
+
]);
|
|
12576
|
+
var ACTION_BLOCK_RE = /```action\n([\s\S]*?)```/g;
|
|
12577
|
+
function parseActionsFromResponse(text) {
|
|
12578
|
+
const actions = [];
|
|
12579
|
+
for (const match of text.matchAll(ACTION_BLOCK_RE)) {
|
|
12580
|
+
const raw = match[1]?.trim();
|
|
12581
|
+
if (!raw) continue;
|
|
12582
|
+
try {
|
|
12583
|
+
const parsed = JSON.parse(raw);
|
|
12584
|
+
const type = parsed.type;
|
|
12585
|
+
if (!VALID_ACTION_TYPES.has(type)) continue;
|
|
12586
|
+
actions.push({
|
|
12587
|
+
type,
|
|
12588
|
+
payload: parsed.payload ?? {}
|
|
12589
|
+
});
|
|
12590
|
+
} catch {
|
|
12591
|
+
}
|
|
12592
|
+
}
|
|
12593
|
+
return actions;
|
|
12594
|
+
}
|
|
12595
|
+
|
|
12596
|
+
// src/agents/chat/action-executor.ts
|
|
12597
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
12598
|
+
import { resolve as resolve4, normalize } from "path";
|
|
12599
|
+
async function executeChatAction(action, state) {
|
|
12600
|
+
try {
|
|
12601
|
+
switch (action.type) {
|
|
12602
|
+
case "list-issues":
|
|
12603
|
+
return {
|
|
12604
|
+
ok: true,
|
|
12605
|
+
result: state.issues.map((i) => ({
|
|
12606
|
+
id: i.id,
|
|
12607
|
+
identifier: i.identifier,
|
|
12608
|
+
title: i.title,
|
|
12609
|
+
state: i.state
|
|
12610
|
+
}))
|
|
12611
|
+
};
|
|
12612
|
+
case "list-services": {
|
|
12613
|
+
const fifonyDir = resolve4(TARGET_ROOT, ".fifony");
|
|
12614
|
+
const statuses = listServiceStatuses(state.config.services ?? [], fifonyDir);
|
|
12615
|
+
return {
|
|
12616
|
+
ok: true,
|
|
12617
|
+
result: statuses.map((s) => ({
|
|
12618
|
+
id: s.id,
|
|
12619
|
+
name: s.name,
|
|
12620
|
+
state: s.state,
|
|
12621
|
+
port: s.port,
|
|
12622
|
+
running: s.running
|
|
12623
|
+
}))
|
|
12624
|
+
};
|
|
12625
|
+
}
|
|
12626
|
+
case "read-file": {
|
|
12627
|
+
const rawPath = String(action.payload.path ?? "");
|
|
12628
|
+
const absPath = normalize(resolve4(TARGET_ROOT, rawPath));
|
|
12629
|
+
if (!absPath.startsWith(TARGET_ROOT)) {
|
|
12630
|
+
return { ok: false, result: null, error: "Path is outside the project root." };
|
|
12631
|
+
}
|
|
12632
|
+
const content = readFileSync16(absPath, "utf8");
|
|
12633
|
+
return { ok: true, result: content.length > 8e3 ? content.slice(0, 8e3) + "\n...(truncated)" : content };
|
|
12634
|
+
}
|
|
12635
|
+
case "read-service-log": {
|
|
12636
|
+
const id = String(action.payload.id ?? "");
|
|
12637
|
+
if (!id) return { ok: false, result: null, error: "Service id is required." };
|
|
12638
|
+
const fifonyDir = resolve4(TARGET_ROOT, ".fifony");
|
|
12639
|
+
const bytes = typeof action.payload.bytes === "number" ? action.payload.bytes : 8192;
|
|
12640
|
+
const log = readServiceLogTail(id, fifonyDir, bytes);
|
|
12641
|
+
return { ok: true, result: log || "(no log output)" };
|
|
12642
|
+
}
|
|
12643
|
+
case "start-service": {
|
|
12644
|
+
const id = String(action.payload.id ?? "");
|
|
12645
|
+
if (!id) return { ok: false, result: null, error: "Service id is required." };
|
|
12646
|
+
await startManagedService(id);
|
|
12647
|
+
return { ok: true, result: `Service ${id} start requested.` };
|
|
12648
|
+
}
|
|
12649
|
+
case "stop-service": {
|
|
12650
|
+
const id = String(action.payload.id ?? "");
|
|
12651
|
+
if (!id) return { ok: false, result: null, error: "Service id is required." };
|
|
12652
|
+
await stopManagedService(id);
|
|
12653
|
+
return { ok: true, result: `Service ${id} stop requested.` };
|
|
12654
|
+
}
|
|
12655
|
+
case "restart-service": {
|
|
12656
|
+
const id = String(action.payload.id ?? "");
|
|
12657
|
+
if (!id) return { ok: false, result: null, error: "Service id is required." };
|
|
12658
|
+
await stopManagedService(id);
|
|
12659
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
12660
|
+
await startManagedService(id);
|
|
12661
|
+
return { ok: true, result: `Service ${id} restart requested.` };
|
|
12662
|
+
}
|
|
12663
|
+
case "create-issue": {
|
|
12664
|
+
const { createIssueCommand: createIssueCommand2 } = await import("./create-issue.command-VAKYRECC.js");
|
|
12665
|
+
const container = getContainer();
|
|
12666
|
+
const payload = {
|
|
12667
|
+
title: String(action.payload.title ?? ""),
|
|
12668
|
+
description: String(action.payload.description ?? "")
|
|
12669
|
+
};
|
|
12670
|
+
if (!payload.title) return { ok: false, result: null, error: "Title is required." };
|
|
12671
|
+
const { issue } = await createIssueCommand2({ payload, state }, container);
|
|
12672
|
+
await persistState(state);
|
|
12673
|
+
return { ok: true, result: { id: issue.id, identifier: issue.identifier, title: issue.title, state: issue.state } };
|
|
12674
|
+
}
|
|
12675
|
+
case "retry-issue": {
|
|
12676
|
+
const { retryIssueCommand: retryIssueCommand2 } = await import("./retry-issue.command-GJBUUYDJ.js");
|
|
12677
|
+
const container = getContainer();
|
|
12678
|
+
const issue = findIssue3(state, String(action.payload.issueId ?? ""));
|
|
12679
|
+
if (!issue) return { ok: false, result: null, error: "Issue not found." };
|
|
12680
|
+
const feedback = action.payload.feedback ? String(action.payload.feedback) : void 0;
|
|
12681
|
+
await retryIssueCommand2({ issue, feedback }, container);
|
|
12682
|
+
await persistState(state);
|
|
12683
|
+
return { ok: true, result: `Retry requested for ${issue.identifier}.` };
|
|
12684
|
+
}
|
|
12685
|
+
case "replan-issue": {
|
|
12686
|
+
const { replanIssueCommand: replanIssueCommand2 } = await import("./replan-issue.command-2GQ3QXCR.js");
|
|
12687
|
+
const container = getContainer();
|
|
12688
|
+
const issue = findIssue3(state, String(action.payload.issueId ?? ""));
|
|
12689
|
+
if (!issue) return { ok: false, result: null, error: "Issue not found." };
|
|
12690
|
+
await replanIssueCommand2({ issue }, container);
|
|
12691
|
+
await persistState(state);
|
|
12692
|
+
return { ok: true, result: `Replan requested for ${issue.identifier}.` };
|
|
12693
|
+
}
|
|
12694
|
+
case "approve-issue": {
|
|
12695
|
+
const { approvePlanCommand: approvePlanCommand2 } = await import("./approve-plan.command-QGQZZXTQ.js");
|
|
12696
|
+
const container = getContainer();
|
|
12697
|
+
const issue = findIssue3(state, String(action.payload.issueId ?? ""));
|
|
12698
|
+
if (!issue) return { ok: false, result: null, error: "Issue not found." };
|
|
12699
|
+
await approvePlanCommand2({ issue }, container);
|
|
12700
|
+
await persistState(state);
|
|
12701
|
+
return { ok: true, result: `Plan approved for ${issue.identifier}.` };
|
|
12702
|
+
}
|
|
12703
|
+
case "merge-issue": {
|
|
12704
|
+
const { mergeWorkspaceCommand: mergeWorkspaceCommand2 } = await import("./merge-workspace.command-T2NIGR4M.js");
|
|
12705
|
+
const container = getContainer();
|
|
12706
|
+
const issue = findIssue3(state, String(action.payload.issueId ?? ""));
|
|
12707
|
+
if (!issue) return { ok: false, result: null, error: "Issue not found." };
|
|
12708
|
+
const result = await mergeWorkspaceCommand2({ issue, state }, container);
|
|
12709
|
+
await persistState(state);
|
|
12710
|
+
return { ok: true, result: { merged: true, conflicts: result.conflicts.length } };
|
|
12711
|
+
}
|
|
12712
|
+
default:
|
|
12713
|
+
return { ok: false, result: null, error: `Unknown action type: ${action.type}` };
|
|
12714
|
+
}
|
|
12715
|
+
} catch (err) {
|
|
12716
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12717
|
+
logger.error({ err, action: action.type }, "[Chat] Action execution failed");
|
|
12718
|
+
return { ok: false, result: null, error: msg };
|
|
12719
|
+
}
|
|
12720
|
+
}
|
|
12721
|
+
function findIssue3(state, issueId) {
|
|
12722
|
+
return state.issues.find((i) => i.id === issueId || i.identifier === issueId);
|
|
12723
|
+
}
|
|
12724
|
+
|
|
12725
|
+
// src/routes/chat.ts
|
|
12726
|
+
function registerChatRoutes(app, state) {
|
|
12727
|
+
app.get("/api/chat/sessions", async (c) => {
|
|
12728
|
+
try {
|
|
12729
|
+
const sessions = await listChatSessions();
|
|
12730
|
+
return c.json({ ok: true, sessions: sessions.map(sessionFileToMeta) });
|
|
12731
|
+
} catch (err) {
|
|
12732
|
+
return c.json({ ok: false, error: err instanceof Error ? err.message : String(err) }, 500);
|
|
12733
|
+
}
|
|
12734
|
+
});
|
|
12735
|
+
app.get("/api/chat/sessions/:id", async (c) => {
|
|
12736
|
+
const id = c.req.param("id");
|
|
12737
|
+
if (!id) return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
12738
|
+
try {
|
|
12739
|
+
const session = await loadChatSession(id);
|
|
12740
|
+
if (!session) return c.json({ ok: false, error: "No chat session for this issue." }, 404);
|
|
12741
|
+
return c.json({ ok: true, session: sessionFileToMeta(session) });
|
|
12742
|
+
} catch (err) {
|
|
12743
|
+
return c.json({ ok: false, error: err instanceof Error ? err.message : String(err) }, 500);
|
|
12744
|
+
}
|
|
12745
|
+
});
|
|
12746
|
+
app.delete("/api/chat/sessions/:id", async (c) => {
|
|
12747
|
+
const id = c.req.param("id");
|
|
12748
|
+
if (!id) return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
12749
|
+
try {
|
|
12750
|
+
await deleteChatSession(id);
|
|
12751
|
+
return c.json({ ok: true });
|
|
12752
|
+
} catch (err) {
|
|
12753
|
+
return c.json({ ok: false, error: err instanceof Error ? err.message : String(err) }, 500);
|
|
12754
|
+
}
|
|
12755
|
+
});
|
|
12756
|
+
app.post("/api/chat", async (c) => {
|
|
12757
|
+
try {
|
|
12758
|
+
const body = await c.req.json();
|
|
12759
|
+
const message = toStringValue(body.message, "").trim();
|
|
12760
|
+
if (!message) return c.json({ ok: false, error: "Message is required." }, 400);
|
|
12761
|
+
const issueId = typeof body.issueId === "string" ? body.issueId : void 0;
|
|
12762
|
+
const historyRaw = Array.isArray(body.history) ? body.history : [];
|
|
12763
|
+
const history = historyRaw.filter((t) => (t.role === "user" || t.role === "assistant") && typeof t.content === "string").map((t) => ({ role: t.role, content: t.content }));
|
|
12764
|
+
let session = null;
|
|
12765
|
+
if (issueId) {
|
|
12766
|
+
session = await loadChatSession(issueId);
|
|
12767
|
+
if (session) {
|
|
12768
|
+
const persistedHistory = session.turns.filter((t) => t.role === "user" || t.role === "assistant").map((t) => ({ role: t.role, content: t.content }));
|
|
12769
|
+
history.length = 0;
|
|
12770
|
+
history.push(...persistedHistory);
|
|
12771
|
+
}
|
|
12772
|
+
}
|
|
12773
|
+
const systemPrompt = buildGlobalChatPrompt(state);
|
|
12774
|
+
const issue = issueId ? state.issues.find((i) => i.id === issueId) : null;
|
|
12775
|
+
const result = await chatWithIssue(
|
|
12776
|
+
{
|
|
12777
|
+
issueId: issueId || "global-chat",
|
|
12778
|
+
title: issue?.title || "Global Chat",
|
|
12779
|
+
description: issue ? `${issue.description}
|
|
12780
|
+
|
|
12781
|
+
---
|
|
12782
|
+
|
|
12783
|
+
${systemPrompt}` : systemPrompt,
|
|
12784
|
+
plan: issue?.plan ?? null,
|
|
12785
|
+
message,
|
|
12786
|
+
history
|
|
12787
|
+
},
|
|
12788
|
+
state.config
|
|
12789
|
+
);
|
|
12790
|
+
const actions = parseActionsFromResponse(result.response);
|
|
12791
|
+
if (issueId) {
|
|
12792
|
+
if (!session) {
|
|
12793
|
+
session = await createIssueChat(issueId, { provider: result.provider });
|
|
12794
|
+
}
|
|
12795
|
+
appendTurn(session, { role: "user", content: message });
|
|
12796
|
+
appendTurn(session, {
|
|
12797
|
+
role: "assistant",
|
|
12798
|
+
content: result.response,
|
|
12799
|
+
actions: actions.length > 0 ? actions : void 0
|
|
12800
|
+
});
|
|
12801
|
+
session.cli = { provider: result.provider };
|
|
12802
|
+
await persistChatSession(session);
|
|
12803
|
+
}
|
|
12804
|
+
logger.info(
|
|
12805
|
+
{ issueId: issueId || "temporary", provider: result.provider, actions: actions.length },
|
|
12806
|
+
"[Chat] Message processed"
|
|
12807
|
+
);
|
|
12808
|
+
return c.json({
|
|
12809
|
+
ok: true,
|
|
12810
|
+
response: result.response,
|
|
12811
|
+
actions,
|
|
12812
|
+
issueId: issueId || null,
|
|
12813
|
+
provider: result.provider
|
|
12814
|
+
});
|
|
12815
|
+
} catch (err) {
|
|
12816
|
+
logger.error({ err }, "[Chat] POST /api/chat failed");
|
|
12817
|
+
return c.json({ ok: false, error: err instanceof Error ? err.message : String(err) }, 500);
|
|
12818
|
+
}
|
|
12819
|
+
});
|
|
12820
|
+
app.post("/api/chat/link", async (c) => {
|
|
12821
|
+
try {
|
|
12822
|
+
const body = await c.req.json();
|
|
12823
|
+
const issueId = typeof body.issueId === "string" ? body.issueId : "";
|
|
12824
|
+
if (!issueId) return c.json({ ok: false, error: "issueId is required." }, 400);
|
|
12825
|
+
const turns = Array.isArray(body.turns) ? body.turns : [];
|
|
12826
|
+
const provider = typeof body.provider === "string" ? body.provider : state.config.agentProvider;
|
|
12827
|
+
const session = await createIssueChat(issueId, { provider }, turns);
|
|
12828
|
+
logger.info({ issueId, turns: turns.length }, "[Chat] Linked temporary chat to issue");
|
|
12829
|
+
return c.json({ ok: true, session: sessionFileToMeta(session) });
|
|
12830
|
+
} catch (err) {
|
|
12831
|
+
return c.json({ ok: false, error: err instanceof Error ? err.message : String(err) }, 500);
|
|
12832
|
+
}
|
|
12833
|
+
});
|
|
12834
|
+
app.post("/api/chat/action", async (c) => {
|
|
12835
|
+
try {
|
|
12836
|
+
const body = await c.req.json();
|
|
12837
|
+
const action = body.action;
|
|
12838
|
+
if (!action?.type) return c.json({ ok: false, error: "Action is required." }, 400);
|
|
12839
|
+
const result = await executeChatAction(action, state);
|
|
12840
|
+
return c.json(result);
|
|
12841
|
+
} catch (err) {
|
|
12842
|
+
logger.error({ err }, "[Chat] Action execution failed");
|
|
12843
|
+
return c.json({ ok: false, error: err instanceof Error ? err.message : String(err) }, 500);
|
|
12844
|
+
}
|
|
12845
|
+
});
|
|
12846
|
+
}
|
|
12847
|
+
|
|
12471
12848
|
// src/persistence/plugins/api-server.ts
|
|
12472
12849
|
var RouteCollector = class {
|
|
12473
12850
|
routes = {};
|
|
@@ -12512,10 +12889,10 @@ async function startApiServer(state, port, _options) {
|
|
|
12512
12889
|
}
|
|
12513
12890
|
setApiRuntimeContext(state);
|
|
12514
12891
|
const serveTextFile = (filePath, contentType, cacheControl = "no-cache") => {
|
|
12515
|
-
if (!
|
|
12892
|
+
if (!existsSync19(filePath)) {
|
|
12516
12893
|
return new Response("Not found", { status: 404 });
|
|
12517
12894
|
}
|
|
12518
|
-
return new Response(
|
|
12895
|
+
return new Response(readFileSync17(filePath), {
|
|
12519
12896
|
headers: {
|
|
12520
12897
|
"content-type": contentType,
|
|
12521
12898
|
"cache-control": cacheControl
|
|
@@ -12530,17 +12907,41 @@ async function startApiServer(state, port, _options) {
|
|
|
12530
12907
|
headers: { location: `http://localhost:${devPort}${path ?? "/"}` }
|
|
12531
12908
|
});
|
|
12532
12909
|
}
|
|
12533
|
-
if (!
|
|
12910
|
+
if (!existsSync19(FRONTEND_INDEX)) {
|
|
12534
12911
|
return new Response("Not found", { status: 404 });
|
|
12535
12912
|
}
|
|
12536
|
-
|
|
12537
|
-
return new Response(html, {
|
|
12913
|
+
return new Response(readFileSync17(FRONTEND_INDEX, "utf8"), {
|
|
12538
12914
|
headers: {
|
|
12539
12915
|
"content-type": "text/html; charset=utf-8",
|
|
12540
12916
|
"cache-control": "no-cache"
|
|
12541
12917
|
}
|
|
12542
12918
|
});
|
|
12543
12919
|
};
|
|
12920
|
+
const rootStaticAssets = Object.fromEntries(
|
|
12921
|
+
[
|
|
12922
|
+
"favicon.png",
|
|
12923
|
+
"apple-touch-icon.png",
|
|
12924
|
+
"og-image.png",
|
|
12925
|
+
"dinofffaur.png",
|
|
12926
|
+
"dinofffaur.webp",
|
|
12927
|
+
"icon-16.png",
|
|
12928
|
+
"icon-32.png",
|
|
12929
|
+
"icon-48.png",
|
|
12930
|
+
"icon-72.png",
|
|
12931
|
+
"icon-96.png",
|
|
12932
|
+
"icon-128.png",
|
|
12933
|
+
"icon-144.png",
|
|
12934
|
+
"icon-152.png",
|
|
12935
|
+
"icon-192.png",
|
|
12936
|
+
"icon-384.png",
|
|
12937
|
+
"icon-512.png",
|
|
12938
|
+
"icon-maskable-192.png",
|
|
12939
|
+
"icon-maskable-512.png"
|
|
12940
|
+
].map((file) => [
|
|
12941
|
+
`GET /${file}`,
|
|
12942
|
+
() => serveTextFile(`${FRONTEND_DIR}/${file}`, "image/png", "public, max-age=604800, immutable")
|
|
12943
|
+
])
|
|
12944
|
+
);
|
|
12544
12945
|
const appShellRoutes = Object.fromEntries(
|
|
12545
12946
|
APP_SHELL_ROUTES.map((path) => [`GET ${path}`, () => serveAppShell(path)])
|
|
12546
12947
|
);
|
|
@@ -12554,8 +12955,10 @@ async function startApiServer(state, port, _options) {
|
|
|
12554
12955
|
registerReferenceRepositoryRoutes(collector);
|
|
12555
12956
|
registerMiscRoutes(collector, state);
|
|
12556
12957
|
registerServiceRoutes(collector, state);
|
|
12958
|
+
registerTrafficRoutes(collector, state);
|
|
12557
12959
|
registerVariableRoutes(collector, state);
|
|
12558
12960
|
registerDevProfileRoutes(collector);
|
|
12961
|
+
registerChatRoutes(collector, state);
|
|
12559
12962
|
const apiPlugin = new ApiPlugin({
|
|
12560
12963
|
port,
|
|
12561
12964
|
host: "0.0.0.0",
|
|
@@ -12577,7 +12980,7 @@ async function startApiServer(state, port, _options) {
|
|
|
12577
12980
|
static: [{
|
|
12578
12981
|
driver: "filesystem",
|
|
12579
12982
|
path: "/assets",
|
|
12580
|
-
root: FRONTEND_DIR
|
|
12983
|
+
root: `${FRONTEND_DIR}/assets`,
|
|
12581
12984
|
pwa: false,
|
|
12582
12985
|
config: { etag: true }
|
|
12583
12986
|
}],
|
|
@@ -12602,6 +13005,7 @@ async function startApiServer(state, port, _options) {
|
|
|
12602
13005
|
"GET /offline.html": () => serveTextFile(FRONTEND_OFFLINE_HTML, "text/html; charset=utf-8"),
|
|
12603
13006
|
"GET /icon.svg": () => serveTextFile(FRONTEND_ICON_SVG, "image/svg+xml", "public, max-age=604800, immutable"),
|
|
12604
13007
|
"GET /icon-maskable.svg": () => serveTextFile(FRONTEND_MASKABLE_ICON_SVG, "image/svg+xml", "public, max-age=604800, immutable"),
|
|
13008
|
+
...rootStaticAssets,
|
|
12605
13009
|
...appShellRoutes,
|
|
12606
13010
|
"GET /api/health": (c) => c.json({ status: state.booting ? "booting" : "ready" })
|
|
12607
13011
|
}
|
|
@@ -12631,6 +13035,7 @@ var variablesResource = null;
|
|
|
12631
13035
|
var contextFragmentResource = null;
|
|
12632
13036
|
var activeApiPlugin = null;
|
|
12633
13037
|
var activeStateMachinePlugin = null;
|
|
13038
|
+
var activeServiceStateMachinePlugin = null;
|
|
12634
13039
|
var activeEcPlugin = null;
|
|
12635
13040
|
function getStateDb() {
|
|
12636
13041
|
return stateDb;
|
|
@@ -12792,6 +13197,18 @@ async function initStateStore() {
|
|
|
12792
13197
|
} else {
|
|
12793
13198
|
logger.warn("StateMachinePlugin not available. Issue transitions will use local logic only.");
|
|
12794
13199
|
}
|
|
13200
|
+
if (StateMachinePlugin) {
|
|
13201
|
+
try {
|
|
13202
|
+
const serviceSmPlugin = await stateDb.usePlugin(
|
|
13203
|
+
new StateMachinePlugin(serviceStateMachineConfig),
|
|
13204
|
+
"service-state-machine"
|
|
13205
|
+
);
|
|
13206
|
+
activeServiceStateMachinePlugin = serviceSmPlugin;
|
|
13207
|
+
logger.info("Service StateMachinePlugin installed.");
|
|
13208
|
+
} catch (error) {
|
|
13209
|
+
logger.warn(`Service StateMachinePlugin failed to install: ${String(error)}`);
|
|
13210
|
+
}
|
|
13211
|
+
}
|
|
12795
13212
|
const { EventualConsistencyPlugin } = await loadS3dbModule();
|
|
12796
13213
|
if (EventualConsistencyPlugin) {
|
|
12797
13214
|
try {
|
|
@@ -12867,6 +13284,15 @@ async function initStateStore() {
|
|
|
12867
13284
|
});
|
|
12868
13285
|
debugBoot("initStateStore:resource-state-api-bound");
|
|
12869
13286
|
}
|
|
13287
|
+
if (serviceResource && serviceResource.state) {
|
|
13288
|
+
const svcStateApi = serviceResource.state;
|
|
13289
|
+
setServiceResourceStateApi({
|
|
13290
|
+
send: svcStateApi.send?.bind(svcStateApi),
|
|
13291
|
+
get: svcStateApi.get?.bind(svcStateApi),
|
|
13292
|
+
initialize: svcStateApi.initialize?.bind(svcStateApi)
|
|
13293
|
+
});
|
|
13294
|
+
debugBoot("initStateStore:service-resource-state-api-bound");
|
|
13295
|
+
}
|
|
12870
13296
|
debugBoot("initStateStore:resources-ready");
|
|
12871
13297
|
}
|
|
12872
13298
|
function isStateNotFoundError(error) {
|
|
@@ -13183,7 +13609,7 @@ async function closeStateStore() {
|
|
|
13183
13609
|
logger.info("[Store] Closing state store and plugins");
|
|
13184
13610
|
clearApiRuntimeContext();
|
|
13185
13611
|
try {
|
|
13186
|
-
const { stopQueueWorkers: stopQueueWorkers2 } = await import("./queue-workers-
|
|
13612
|
+
const { stopQueueWorkers: stopQueueWorkers2 } = await import("./queue-workers-V57BYXAY.js");
|
|
13187
13613
|
await stopQueueWorkers2();
|
|
13188
13614
|
} catch (error) {
|
|
13189
13615
|
logger.warn(`Failed to stop queue workers: ${String(error)}`);
|
|
@@ -13197,6 +13623,15 @@ async function closeStateStore() {
|
|
|
13197
13623
|
activeEcPlugin = null;
|
|
13198
13624
|
}
|
|
13199
13625
|
}
|
|
13626
|
+
if (activeServiceStateMachinePlugin?.stop) {
|
|
13627
|
+
try {
|
|
13628
|
+
await activeServiceStateMachinePlugin.stop();
|
|
13629
|
+
} catch (error) {
|
|
13630
|
+
logger.warn(`Failed to stop Service StateMachine plugin: ${String(error)}`);
|
|
13631
|
+
} finally {
|
|
13632
|
+
activeServiceStateMachinePlugin = null;
|
|
13633
|
+
}
|
|
13634
|
+
}
|
|
13200
13635
|
if (activeStateMachinePlugin?.stop) {
|
|
13201
13636
|
try {
|
|
13202
13637
|
await activeStateMachinePlugin.stop();
|
|
@@ -13275,6 +13710,13 @@ var SETTING_ID_ADAPTIVE_HARNESS_SELECTION = "runtime.adaptiveHarnessSelection";
|
|
|
13275
13710
|
var SETTING_ID_ADAPTIVE_REVIEW_ROUTING = "runtime.adaptiveReviewRouting";
|
|
13276
13711
|
var SETTING_ID_ADAPTIVE_POLICY_MIN_SAMPLES = "runtime.adaptivePolicyMinSamples";
|
|
13277
13712
|
var SETTING_ID_SERVICE_ENV = "runtime.serviceEnv";
|
|
13713
|
+
var SETTING_ID_MESH_ENABLED = "runtime.meshEnabled";
|
|
13714
|
+
var SETTING_ID_MESH_PROXY_PORT = "runtime.meshProxyPort";
|
|
13715
|
+
var SETTING_ID_MESH_BUFFER_SIZE = "runtime.meshBufferSize";
|
|
13716
|
+
var SETTING_ID_AUTO_APPROVE_TRIVIAL_PLANS = "runtime.autoApproveTrivialPlans";
|
|
13717
|
+
var SETTING_ID_AUTO_COMMIT_BEFORE_MERGE = "runtime.autoCommitBeforeMerge";
|
|
13718
|
+
var SETTING_ID_AUTO_RESOLVE_CONFLICTS = "runtime.autoResolveConflicts";
|
|
13719
|
+
var SETTING_ID_SANDBOX_EXECUTION = "runtime.sandboxExecution";
|
|
13278
13720
|
async function loadRuntimeSettings() {
|
|
13279
13721
|
return loadPersistedSettings();
|
|
13280
13722
|
}
|
|
@@ -13304,7 +13746,14 @@ var RUNTIME_CONFIG_SETTING_IDS = /* @__PURE__ */ new Set([
|
|
|
13304
13746
|
SETTING_ID_ADAPTIVE_HARNESS_SELECTION,
|
|
13305
13747
|
SETTING_ID_ADAPTIVE_REVIEW_ROUTING,
|
|
13306
13748
|
SETTING_ID_ADAPTIVE_POLICY_MIN_SAMPLES,
|
|
13307
|
-
SETTING_ID_SERVICE_ENV
|
|
13749
|
+
SETTING_ID_SERVICE_ENV,
|
|
13750
|
+
SETTING_ID_MESH_ENABLED,
|
|
13751
|
+
SETTING_ID_MESH_PROXY_PORT,
|
|
13752
|
+
SETTING_ID_MESH_BUFFER_SIZE,
|
|
13753
|
+
SETTING_ID_AUTO_APPROVE_TRIVIAL_PLANS,
|
|
13754
|
+
SETTING_ID_AUTO_COMMIT_BEFORE_MERGE,
|
|
13755
|
+
SETTING_ID_AUTO_RESOLVE_CONFLICTS,
|
|
13756
|
+
SETTING_ID_SANDBOX_EXECUTION
|
|
13308
13757
|
]);
|
|
13309
13758
|
var VALID_REASONING_EFFORTS = /* @__PURE__ */ new Set(["low", "medium", "high", "extra-high"]);
|
|
13310
13759
|
function parseIntegerSetting(value) {
|
|
@@ -13377,7 +13826,14 @@ function buildRuntimeConfigSettings(config, source) {
|
|
|
13377
13826
|
{ id: SETTING_ID_ADAPTIVE_HARNESS_SELECTION, scope: "runtime", value: config.adaptiveHarnessSelection !== false, source, updatedAt },
|
|
13378
13827
|
{ id: SETTING_ID_ADAPTIVE_REVIEW_ROUTING, scope: "runtime", value: config.adaptiveReviewRouting !== false, source, updatedAt },
|
|
13379
13828
|
{ id: SETTING_ID_ADAPTIVE_POLICY_MIN_SAMPLES, scope: "runtime", value: config.adaptivePolicyMinSamples ?? DEFAULT_ADAPTIVE_POLICY_MIN_SAMPLES, source, updatedAt },
|
|
13380
|
-
{ id: SETTING_ID_SERVICE_ENV, scope: "runtime", value: config.serviceEnv ?? {}, source, updatedAt }
|
|
13829
|
+
{ id: SETTING_ID_SERVICE_ENV, scope: "runtime", value: config.serviceEnv ?? {}, source, updatedAt },
|
|
13830
|
+
{ id: SETTING_ID_MESH_ENABLED, scope: "runtime", value: config.meshEnabled ?? false, source, updatedAt },
|
|
13831
|
+
{ id: SETTING_ID_MESH_PROXY_PORT, scope: "runtime", value: config.meshProxyPort ?? 0, source, updatedAt },
|
|
13832
|
+
{ id: SETTING_ID_MESH_BUFFER_SIZE, scope: "runtime", value: config.meshBufferSize ?? 1e3, source, updatedAt },
|
|
13833
|
+
{ id: SETTING_ID_AUTO_APPROVE_TRIVIAL_PLANS, scope: "runtime", value: config.autoApproveTrivialPlans ?? true, source, updatedAt },
|
|
13834
|
+
{ id: SETTING_ID_AUTO_COMMIT_BEFORE_MERGE, scope: "runtime", value: config.autoCommitBeforeMerge ?? true, source, updatedAt },
|
|
13835
|
+
{ id: SETTING_ID_AUTO_RESOLVE_CONFLICTS, scope: "runtime", value: config.autoResolveConflicts ?? false, source, updatedAt },
|
|
13836
|
+
{ id: SETTING_ID_SANDBOX_EXECUTION, scope: "runtime", value: config.sandboxExecution ?? false, source, updatedAt }
|
|
13381
13837
|
];
|
|
13382
13838
|
}
|
|
13383
13839
|
function applyPersistedSettings(config, settings) {
|
|
@@ -13544,6 +14000,40 @@ function applyPersistedSettings(config, settings) {
|
|
|
13544
14000
|
}
|
|
13545
14001
|
break;
|
|
13546
14002
|
}
|
|
14003
|
+
case SETTING_ID_MESH_ENABLED: {
|
|
14004
|
+
nextConfig.meshEnabled = setting.value === true || setting.value === "true";
|
|
14005
|
+
break;
|
|
14006
|
+
}
|
|
14007
|
+
case SETTING_ID_MESH_PROXY_PORT: {
|
|
14008
|
+
const parsed = Number(setting.value);
|
|
14009
|
+
if (!Number.isNaN(parsed) && parsed >= 0 && parsed <= 65535) {
|
|
14010
|
+
nextConfig.meshProxyPort = parsed;
|
|
14011
|
+
}
|
|
14012
|
+
break;
|
|
14013
|
+
}
|
|
14014
|
+
case SETTING_ID_MESH_BUFFER_SIZE: {
|
|
14015
|
+
const parsed = Number(setting.value);
|
|
14016
|
+
if (!Number.isNaN(parsed) && parsed >= 100 && parsed <= 1e4) {
|
|
14017
|
+
nextConfig.meshBufferSize = parsed;
|
|
14018
|
+
}
|
|
14019
|
+
break;
|
|
14020
|
+
}
|
|
14021
|
+
case SETTING_ID_AUTO_APPROVE_TRIVIAL_PLANS: {
|
|
14022
|
+
nextConfig.autoApproveTrivialPlans = toBooleanValue(setting.value, true);
|
|
14023
|
+
break;
|
|
14024
|
+
}
|
|
14025
|
+
case SETTING_ID_AUTO_COMMIT_BEFORE_MERGE: {
|
|
14026
|
+
nextConfig.autoCommitBeforeMerge = toBooleanValue(setting.value, true);
|
|
14027
|
+
break;
|
|
14028
|
+
}
|
|
14029
|
+
case SETTING_ID_AUTO_RESOLVE_CONFLICTS: {
|
|
14030
|
+
nextConfig.autoResolveConflicts = toBooleanValue(setting.value, false);
|
|
14031
|
+
break;
|
|
14032
|
+
}
|
|
14033
|
+
case SETTING_ID_SANDBOX_EXECUTION: {
|
|
14034
|
+
nextConfig.sandboxExecution = toBooleanValue(setting.value, false);
|
|
14035
|
+
break;
|
|
14036
|
+
}
|
|
13547
14037
|
default:
|
|
13548
14038
|
break;
|
|
13549
14039
|
}
|
|
@@ -13619,26 +14109,62 @@ function buildDefaultWorkflowConfig(detectedProviders, discoveredModels) {
|
|
|
13619
14109
|
const claudeDefault = { provider: "claude", model: claudeModel, effort: "medium" };
|
|
13620
14110
|
const codexDefault = { provider: "codex", model: codexModel, effort: codexEffort };
|
|
13621
14111
|
if (hasClaude && hasCodex) {
|
|
14112
|
+
const planConfig2 = { ...claudeDefault, effort: "high" };
|
|
13622
14113
|
return {
|
|
13623
|
-
|
|
14114
|
+
enhance: { ...planConfig2 },
|
|
14115
|
+
chat: { ...planConfig2, effort: "medium" },
|
|
14116
|
+
plan: planConfig2,
|
|
13624
14117
|
execute: { ...codexDefault },
|
|
13625
|
-
review: { ...claudeDefault }
|
|
14118
|
+
review: { ...claudeDefault },
|
|
14119
|
+
services: { ...planConfig2, effort: "medium" }
|
|
13626
14120
|
};
|
|
13627
14121
|
}
|
|
13628
14122
|
if (hasClaude) {
|
|
13629
|
-
|
|
14123
|
+
const planConfig2 = { ...claudeDefault, effort: "high" };
|
|
14124
|
+
return {
|
|
14125
|
+
enhance: { ...planConfig2 },
|
|
14126
|
+
chat: { ...planConfig2, effort: "medium" },
|
|
14127
|
+
plan: planConfig2,
|
|
14128
|
+
execute: claudeDefault,
|
|
14129
|
+
review: claudeDefault,
|
|
14130
|
+
services: { ...planConfig2, effort: "medium" }
|
|
14131
|
+
};
|
|
13630
14132
|
}
|
|
13631
14133
|
if (hasCodex) {
|
|
13632
|
-
|
|
14134
|
+
const planConfig2 = { ...codexDefault, effort: "high" };
|
|
14135
|
+
return {
|
|
14136
|
+
enhance: { ...planConfig2 },
|
|
14137
|
+
chat: { ...planConfig2, effort: "medium" },
|
|
14138
|
+
plan: planConfig2,
|
|
14139
|
+
execute: codexDefault,
|
|
14140
|
+
review: codexDefault,
|
|
14141
|
+
services: { ...planConfig2, effort: "medium" }
|
|
14142
|
+
};
|
|
13633
14143
|
}
|
|
13634
|
-
|
|
14144
|
+
const planConfig = { ...claudeDefault };
|
|
14145
|
+
return {
|
|
14146
|
+
enhance: { ...planConfig },
|
|
14147
|
+
chat: { ...planConfig, effort: "medium" },
|
|
14148
|
+
plan: planConfig,
|
|
14149
|
+
execute: codexDefault,
|
|
14150
|
+
review: claudeDefault,
|
|
14151
|
+
services: { ...planConfig, effort: "medium" }
|
|
14152
|
+
};
|
|
13635
14153
|
}
|
|
13636
14154
|
function getWorkflowConfig(settings) {
|
|
13637
14155
|
const setting = settings.find((s) => s.id === SETTING_ID_WORKFLOW_CONFIG);
|
|
13638
14156
|
if (!setting?.value || typeof setting.value !== "object") return null;
|
|
13639
14157
|
const wf = setting.value;
|
|
13640
14158
|
if (isValidStage(wf.plan) && isValidStage(wf.execute) && isValidStage(wf.review)) {
|
|
13641
|
-
|
|
14159
|
+
const config = {
|
|
14160
|
+
plan: wf.plan,
|
|
14161
|
+
execute: wf.execute,
|
|
14162
|
+
review: wf.review
|
|
14163
|
+
};
|
|
14164
|
+
if (isValidStage(wf.enhance)) config.enhance = wf.enhance;
|
|
14165
|
+
if (isValidStage(wf.chat)) config.chat = wf.chat;
|
|
14166
|
+
if (isValidStage(wf.services)) config.services = wf.services;
|
|
14167
|
+
return config;
|
|
13642
14168
|
}
|
|
13643
14169
|
return null;
|
|
13644
14170
|
}
|
|
@@ -13685,6 +14211,13 @@ export {
|
|
|
13685
14211
|
SETTING_ID_ADAPTIVE_REVIEW_ROUTING,
|
|
13686
14212
|
SETTING_ID_ADAPTIVE_POLICY_MIN_SAMPLES,
|
|
13687
14213
|
SETTING_ID_SERVICE_ENV,
|
|
14214
|
+
SETTING_ID_MESH_ENABLED,
|
|
14215
|
+
SETTING_ID_MESH_PROXY_PORT,
|
|
14216
|
+
SETTING_ID_MESH_BUFFER_SIZE,
|
|
14217
|
+
SETTING_ID_AUTO_APPROVE_TRIVIAL_PLANS,
|
|
14218
|
+
SETTING_ID_AUTO_COMMIT_BEFORE_MERGE,
|
|
14219
|
+
SETTING_ID_AUTO_RESOLVE_CONFLICTS,
|
|
14220
|
+
SETTING_ID_SANDBOX_EXECUTION,
|
|
13688
14221
|
loadRuntimeSettings,
|
|
13689
14222
|
RUNTIME_CONFIG_SETTING_IDS,
|
|
13690
14223
|
applyPersistedSettings,
|
|
@@ -13696,22 +14229,13 @@ export {
|
|
|
13696
14229
|
buildDefaultWorkflowConfig,
|
|
13697
14230
|
getWorkflowConfig,
|
|
13698
14231
|
persistWorkflowConfig,
|
|
13699
|
-
|
|
14232
|
+
resolveServicesStageConfig,
|
|
13700
14233
|
reconcileAgentStateTransitions,
|
|
13701
14234
|
startManagedAgentWatcher,
|
|
13702
14235
|
canDispatchManagedAgent,
|
|
13703
14236
|
runPlanningJob,
|
|
13704
14237
|
runManagedExecuteJob,
|
|
13705
14238
|
runManagedReviewJob,
|
|
13706
|
-
startServiceLogBroadcasting,
|
|
13707
|
-
stopServiceLogBroadcasting,
|
|
13708
|
-
wsClients,
|
|
13709
|
-
setAnalyticsOnSubscribeFn,
|
|
13710
|
-
analyticsRoomHasSubscribers,
|
|
13711
|
-
sendToAnalyticsRoom,
|
|
13712
|
-
issueLogRoomSize,
|
|
13713
|
-
sendToIssueLogRoom,
|
|
13714
|
-
broadcastToWebSocketClients,
|
|
13715
14239
|
isShuttingDown,
|
|
13716
14240
|
installGracefulShutdown,
|
|
13717
14241
|
analyzeParallelizability,
|
|
@@ -13730,6 +14254,8 @@ export {
|
|
|
13730
14254
|
runAgentSession,
|
|
13731
14255
|
runAgentPipeline,
|
|
13732
14256
|
issueHasResumableSession,
|
|
14257
|
+
createIssueCommand,
|
|
14258
|
+
mergeWorkspaceCommand,
|
|
13733
14259
|
listSettings,
|
|
13734
14260
|
getSetting,
|
|
13735
14261
|
updateSetting,
|
|
@@ -13737,7 +14263,6 @@ export {
|
|
|
13737
14263
|
listServiceStatuses,
|
|
13738
14264
|
startAutoConfiguredServices,
|
|
13739
14265
|
reconcileManagedServiceStates,
|
|
13740
|
-
initManagedServiceWatcher,
|
|
13741
14266
|
collectRuntimeHealthSnapshot,
|
|
13742
14267
|
runDoctorChecks,
|
|
13743
14268
|
buildProbeResult,
|
|
@@ -13781,4 +14306,4 @@ export {
|
|
|
13781
14306
|
getEcDailyLines,
|
|
13782
14307
|
closeStateStore
|
|
13783
14308
|
};
|
|
13784
|
-
//# sourceMappingURL=chunk-
|
|
14309
|
+
//# sourceMappingURL=chunk-K36BWMUV.js.map
|