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.
Files changed (65) hide show
  1. package/app/dist/assets/{CommandPalette-M4VAMxCU.js → CommandPalette-CL8p78lG.js} +1 -1
  2. package/app/dist/assets/{KeyboardShortcutsHelp-DkvPUXQq.js → KeyboardShortcutsHelp-CqEFfGcE.js} +1 -1
  3. package/app/dist/assets/OnboardingWizard-BmI50ZUv.js +1 -0
  4. package/app/dist/assets/analytics.lazy-CXGjZabc.js +1 -0
  5. package/app/dist/assets/{api-CkVfYg_m.js → api-CEr_D4e5.js} +1 -1
  6. package/app/dist/assets/{createLucideIcon-Dfk_Hxud.js → createLucideIcon-luywpIq4.js} +1 -1
  7. package/app/dist/assets/index-CEaccpYh.js +96 -0
  8. package/app/dist/assets/index-CzzWGzux.css +1 -0
  9. package/app/dist/assets/vendor-uqBx3VSC.js +9 -0
  10. package/app/dist/index.html +12 -12
  11. package/app/dist/service-worker.js +15 -5
  12. package/dist/agent/pty-daemon.js +3 -2
  13. package/dist/agent/run-local.js +71 -52
  14. package/dist/{agent-RMQTTUEC.js → agent-DFSFG6DG.js} +18 -12
  15. package/dist/{analytics-broadcaster-O6YBP66L.js → analytics-broadcaster-O4AE3RUK.js} +21 -14
  16. package/dist/approve-plan.command-QGQZZXTQ.js +17 -0
  17. package/dist/{chunk-E2EWEYA4.js → chunk-2PRRKBG6.js} +20 -10
  18. package/dist/chunk-5AMWD66T.js +38 -0
  19. package/dist/{chunk-QQQLP3PL.js → chunk-7TXZYZR5.js} +9 -37
  20. package/dist/chunk-AAVROEQC.js +859 -0
  21. package/dist/{chunk-ESWHDHH6.js → chunk-AAZKYWOY.js} +4 -4
  22. package/dist/chunk-EBCSQFPR.js +682 -0
  23. package/dist/{chunk-BRSR26VK.js → chunk-FH7HUPZX.js} +2 -2
  24. package/dist/chunk-HOIOVUHI.js +35 -0
  25. package/dist/{chunk-AILXZ2TD.js → chunk-JRLWLZOD.js} +20 -13
  26. package/dist/{chunk-YRSH2CLW.js → chunk-K36BWMUV.js} +1741 -1216
  27. package/dist/chunk-N4KFNX2G.js +370 -0
  28. package/dist/chunk-PACI3T4I.js +125 -0
  29. package/dist/{chunk-FJNH3G2Z.js → chunk-PI7Y77R3.js} +38 -663
  30. package/dist/{chunk-DVU3CXWA.js → chunk-PXTIWKLQ.js} +2 -1
  31. package/dist/{chunk-SOBLO4YZ.js → chunk-QH6VCTET.js} +316 -127
  32. package/dist/{chunk-MVTGAKQK.js → chunk-QHISYRXJ.js} +2 -2
  33. package/dist/{chunk-42AMQAJG.js → chunk-VM5QAYP5.js} +2 -2
  34. package/dist/cli.js +17 -11
  35. package/dist/create-issue.command-VAKYRECC.js +24 -0
  36. package/dist/{fsm-issue-YGGF7SIL.js → fsm-issue-EHTSKMFN.js} +9 -8
  37. package/dist/fsm-service-7O4AJG2R.js +32 -0
  38. package/dist/{helpers-L7NYO5XS.js → helpers-ON2S7UEF.js} +2 -2
  39. package/dist/{issue-log-broadcaster-WZAHISYB.js → issue-log-broadcaster-FZGVEEIX.js} +20 -13
  40. package/dist/{issues-3QRR7KM6.js → issues-3YNNTB4U.js} +10 -7
  41. package/dist/{log-analyzer-K7MXQB4T.js → log-analyzer-EIX6R6PP.js} +82 -18
  42. package/dist/logger-IFLXTQPS.js +11 -0
  43. package/dist/mcp/server.js +2 -2
  44. package/dist/merge-workspace.command-T2NIGR4M.js +24 -0
  45. package/dist/{parallel-executor-6INE6NDO.js → parallel-executor-DWESCNX3.js} +20 -14
  46. package/dist/queue-workers-V57BYXAY.js +38 -0
  47. package/dist/replan-issue.command-2GQ3QXCR.js +17 -0
  48. package/dist/retry-issue.command-GJBUUYDJ.js +17 -0
  49. package/dist/scheduler-KYILMWLD.js +32 -0
  50. package/dist/{settings-ZAWDCFP2.js → settings-SOTIS6ZD.js} +32 -12
  51. package/dist/settings.resource-JMD3JQOS.js +30 -0
  52. package/dist/{store-M6NCKMZY.js → store-S3NAYZ3S.js} +18 -12
  53. package/dist/{web-push-AX5IIK3P.js → web-push-QCTLS7EJ.js} +3 -3
  54. package/dist/websocket-T2Y3BY4B.js +61 -0
  55. package/dist/{workspace-CJTWFWTJ.js → workspace-OS7GPMCN.js} +7 -6
  56. package/package.json +8 -5
  57. package/app/dist/assets/OnboardingWizard-B7V9hoCR.js +0 -1
  58. package/app/dist/assets/analytics.lazy-zVJdF880.js +0 -1
  59. package/app/dist/assets/index-BpiCi7Ew.css +0 -1
  60. package/app/dist/assets/index-D2INW0zc.js +0 -47
  61. package/app/dist/assets/vendor-BEoYbFV1.js +0 -9
  62. package/dist/queue-workers-XFZK3TT5.js +0 -32
  63. package/dist/replan-issue.command-4UCWYHGZ.js +0 -15
  64. package/dist/scheduler-ZP7GOZDW.js +0 -26
  65. 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-BRSR26VK.js";
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-QQQLP3PL.js";
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-AILXZ2TD.js";
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-SOBLO4YZ.js";
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-E2EWEYA4.js";
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-FJNH3G2Z.js";
151
+ } from "./chunk-PI7Y77R3.js";
128
152
  import {
129
- logger
130
- } from "./chunk-DVU3CXWA.js";
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-ESWHDHH6.js";
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-42AMQAJG.js";
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 existsSync18,
265
- readFileSync as readFileSync14
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/pipeline",
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 join15 } from "path";
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 existsSync6,
1388
+ existsSync as existsSync7,
1336
1389
  mkdirSync as mkdirSync7,
1337
- readFileSync as readFileSync5,
1390
+ readFileSync as readFileSync6,
1338
1391
  rmSync as rmSync3,
1339
1392
  writeFileSync as writeFileSync9
1340
1393
  } from "fs";
1341
- import { join as join13 } from "path";
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: env3 } = await import("@huggingface/transformers");
1723
+ const { pipeline, env: env4 } = await import("@huggingface/transformers");
1671
1724
  mkdirSync2(EMBEDDING_LOCAL_CACHE_DIR, { recursive: true });
1672
- env3.cacheDir = EMBEDDING_LOCAL_CACHE_DIR;
1673
- env3.allowLocalModels = true;
1674
- env3.allowRemoteModels = true;
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
- try {
3403
- const envelope = JSON.parse(text.trim());
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 command = config.testCommand;
3838
- logger.info({ issueId: issue.id, command, cwd }, "[Validation] Running validation gate");
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-M6NCKMZY.js").then(
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-O6YBP66L.js").then(({ initAnalyticsBroadcaster, pushAllAnalytics }) => {
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-ZP7GOZDW.js");
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-RMQTTUEC.js");
4183
- const { addEvent: addEvent2 } = await import("./issues-3QRR7KM6.js");
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-RMQTTUEC.js");
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 join12 } from "path";
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 join12(workspacePath, BLUEPRINT_ARTIFACTS_DIRNAME);
4697
+ return join13(workspacePath, BLUEPRINT_ARTIFACTS_DIRNAME);
4466
4698
  }
4467
4699
  function ensureNodeDir(workspacePath, runId, nodeId) {
4468
- const dir = join12(resolveBlueprintArtifactsRoot(workspacePath), runId, nodeId);
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 = join12(dir, fileName);
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 join13(fifonyDir, `agent-${idToSafePath(issueId)}.job.json`);
4812
+ return join14(fifonyDir, `agent-${idToSafePath(issueId)}.job.json`);
4581
4813
  }
4582
4814
  function agentLogPath(workspacePath) {
4583
- return join13(workspacePath, "live-output.log");
4815
+ return join14(workspacePath, "live-output.log");
4584
4816
  }
4585
4817
  function readJobState(fifonyDir, issueId) {
4586
4818
  const path = jobStatePath(fifonyDir, issueId);
4587
- if (!existsSync6(path)) return null;
4819
+ if (!existsSync7(path)) return null;
4588
4820
  try {
4589
- return JSON.parse(readFileSync5(path, "utf8"));
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 && existsSync6(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 = join13(WORKSPACE_ROOT, safeId);
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-M6NCKMZY.js").then(({ persistState: persistState2 }) => persistState2(state).catch(() => {
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-M6NCKMZY.js");
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(join13(workspaceDir, `plan.v${pv}.json`), JSON.stringify(plan, null, 2), "utf8");
4878
- writeFileSync9(join13(workspaceDir, `plan.v${pv}.prompt.md`), prompt, "utf8");
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-3QRR7KM6.js");
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 match = text.match(/```json grading_report\n([\s\S]+?)```/);
4919
- if (!match) return null;
4920
- try {
4921
- return JSON.parse(match[1]);
4922
- } catch {
4923
- return null;
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
- return DEFAULT_MAX_TURNS_BY_MODE[mode] ?? DEFAULT_MAX_TURNS;
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 validation = await runValidationGate(issue, state.config);
5007
- if (validation) {
5008
- issue.validationResult = validation;
5009
- markIssueDirty(issue.id);
5010
- if (!validation.passed) {
5011
- addEvent(state, issue.id, "error", `Validation gate failed for ${issue.identifier}: ${validation.command}`);
5012
- logger.warn({ issueId: issue.id, command: validation.command }, "[AgentFSM] Validation gate failed after successful review path");
5013
- return;
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 = join13(stateRoot, "playwright-mcp.json");
5032
- if (!existsSync6(configPath)) {
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 join13(workspacePath, scope === "checkpoint" ? "checkpoint-review-prompt.md" : "review-prompt.md");
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 gradingReport = rawGradingReport ? applyHarnessReviewPolicy(issue, rawGradingReport, scope) : resolveHarnessMode(issue) === "contractual" ? applyHarnessReviewPolicy(issue, {
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 = join13(workspacePath, "execution-audit.json");
5275
- if (existsSync6(reviewPromptSrc)) {
5276
- writeFileSync9(join13(workspacePath, `${artifactPrefix}.prompt.md`), readFileSync5(reviewPromptSrc, "utf8"), "utf8");
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 (existsSync6(reviewAuditSrc)) {
5279
- writeFileSync9(join13(workspacePath, `${artifactPrefix}.audit.json`), readFileSync5(reviewAuditSrc, "utf8"), "utf8");
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-4UCWYHGZ.js");
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-4UCWYHGZ.js");
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.cancelledReason = `Max attempts reached (${issue.attempts}/${issue.maxAttempts}): reviewer failed or blocked.`;
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 = join13(workspacePath, "prompt.md");
5630
- const vExecAuditSrc = join13(workspacePath, "execution-audit.json");
5631
- if (existsSync6(vExecPromptSrc)) {
5632
- writeFileSync9(join13(workspacePath, `execute.v${epv}a${eea}.prompt.md`), readFileSync5(vExecPromptSrc, "utf8"), "utf8");
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 (existsSync6(vExecAuditSrc)) {
5635
- writeFileSync9(join13(workspacePath, `execute.v${epv}a${eea}.audit.json`), readFileSync5(vExecAuditSrc, "utf8"), "utf8");
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.cancelledReason = `Max attempts reached: pre-review gate failed repeatedly.`;
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-4UCWYHGZ.js");
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.cancelledReason = `Max attempts reached (${issue.attempts}/${issue.maxAttempts}): execution failed repeatedly.`;
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 ?? join13(WORKSPACE_ROOT, idToSafePath(issue.id));
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-WZAHISYB.js");
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.cancelledReason = `Max attempts reached (${issue.attempts}/${issue.maxAttempts}): unexpected failure \u2014 ${issue.lastError?.slice(0, 120) ?? "unknown error"}.`;
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-WZAHISYB.js").then(({ stopIssueLogBroadcasting }) => stopIssueLogBroadcasting(issue.id)).catch(() => {
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, active3, getCurrentIssue2, fifonyDir = STATE_ROOT, onTransition) {
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 ?? join13(WORKSPACE_ROOT, idToSafePath(issue.id));
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 (active3() && current) {
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-M6NCKMZY.js");
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-WZAHISYB.js");
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.cancelledReason = `Max attempts reached (${target.attempts}/${target.maxAttempts}): unexpected failure \u2014 ${target.lastError?.slice(0, 120) ?? "unknown error"}.`;
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-WZAHISYB.js").then(({ stopIssueLogBroadcasting }) => stopIssueLogBroadcasting(issue.id)).catch(() => {
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 || !existsSync6(job.workspacePath)) return null;
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 && existsSync6(job.workspacePath) ? readAgentPid(job.workspacePath) : null;
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 join14 } from "path";
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 = join14(workspacePath, "handoff.md");
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 = join15(workspacePath, `result-${provider.role}-${provider.provider}.json`);
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 : join15(workspacePath, `turn-${String(turnIndex).padStart(2, "0")}.md`);
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 = join15(workspacePath, "outputs");
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 = join15(outputsDir, outputFileName);
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(join15(workspacePath, "skills.md"), skillContext, "utf8");
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 = join15(workspacePath, ".compiled-env.sh");
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: readFileSync15, existsSync: existsSync19 } = await import("fs");
6357
- if (existsSync19(issue.lastHandoffFile)) {
6358
- const handoffContent = readFileSync15(issue.lastHandoffFile, "utf8");
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-6INE6NDO.js");
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 existsSync7, mkdirSync as mkdirSync9, renameSync } from "fs";
6636
- import { basename as basename2, join as join16 } from "path";
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 = join16(ATTACHMENTS_ROOT, issue.id);
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" && existsSync7(tempPath)) {
6647
- const dest = join16(issueAttachDir, basename2(tempPath));
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 existsSync8 } from "fs";
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 join17 } from "path";
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(join17(tmpdir3(), "fifony-conflict-"));
6778
- const promptFile = join17(tempDir, "fifony-conflict-prompt.md");
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 || !existsSync8(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 mergeResult = mergeWorkspace(
6898
- issue,
6899
- /* abortOnConflict */
6900
- false
6901
- );
6902
- result = mergeResult;
6903
- if (result.conflicts.length > 0) {
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
- deps.eventStore.addEvent(issue.id, "error", `Merge aborted \u2014 ${result.conflicts.length} conflict(s) remain: ${result.conflicts.join(", ")}`);
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 execFileSync2, execSync as execSync5 } from "child_process";
7325
+ import { execFileSync as execFileSync3, execSync as execSync5 } from "child_process";
7016
7326
  function isGhAvailable() {
7017
7327
  try {
7018
- execFileSync2("gh", ["--version"], { stdio: "pipe", timeout: 5e3 });
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 = execFileSync2(
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 execFileSync2(
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/retry-execution.command.ts
7128
- async function retryExecutionCommand(input, deps) {
7129
- const { issue, note } = input;
7130
- if (issue.state !== "Blocked") {
7131
- throw new Error(
7132
- `retryExecutionCommand requires Blocked state, got ${issue.state}. Use replanIssueCommand for re-planning or the generic /retry endpoint for other states.`
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: note ?? `Retry execution for ${issue.identifier} (attempt ${(issue.attempts ?? 0) + 1}).` },
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 existsSync9, mkdirSync as mkdirSync10, openSync, readFileSync as readFileSync6, readSync, readdirSync as readdirSync3, statSync as statSync2, writeFileSync as writeFileSync13 } from "fs";
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 join18 } from "path";
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-M6NCKMZY.js");
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-M6NCKMZY.js");
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-M6NCKMZY.js");
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-M6NCKMZY.js");
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-M6NCKMZY.js");
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-M6NCKMZY.js");
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-M6NCKMZY.js");
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-M6NCKMZY.js");
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-M6NCKMZY.js");
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-M6NCKMZY.js");
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-CJTWFWTJ.js");
7581
- const result = dryMerge(issue);
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-CJTWFWTJ.js");
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-M6NCKMZY.js");
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-M6NCKMZY.js");
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-M6NCKMZY.js");
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-M6NCKMZY.js");
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 = join18(ATTACHMENTS_ROOT, issue.id);
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 = join18(issueAttachDir, safeName);
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-M6NCKMZY.js");
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 = join18(ATTACHMENTS_ROOT, issueId, safeName);
7729
- if (!existsSync9(filePath)) return { status: 404, body: { ok: false, error: "Image not found." } };
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 = readFileSync6(filePath);
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-RMQTTUEC.js");
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 && existsSync9(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 || !existsSync9(lf)) return;
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 && existsSync9(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 ? existsSync9(join18(wp, "agent.sock")) : false;
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 && existsSync9(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 && existsSync9(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 || !existsSync9(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 (!existsSync9(SOURCE_ROOT)) {
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 = join18(wp, "outputs");
8117
- if (!existsSync9(outputsDir)) return { body: { ok: true, files: [] } };
8118
- const entries = readdirSync3(outputsDir).filter((file) => file.endsWith(".stdout.log")).map((file) => {
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(join18(outputsDir, file));
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 = join18(wp, "outputs", safeName);
8143
- if (!existsSync9(filePath)) return { status: 404, body: { ok: false, error: "Output file not found." } };
8144
- const content = readFileSync6(filePath, "utf8");
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-M6NCKMZY.js");
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-M6NCKMZY.js");
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-M6NCKMZY.js");
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 ENV_KEY_PATTERN2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
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 (!ENV_KEY_PATTERN2.test(key)) return { error: `Invalid variable name "${key}". Must match [A-Za-z_][A-Za-z0-9_]*.` };
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 existsSync12, readFileSync as readFileSync8, readdirSync as readdirSync4, realpathSync } from "fs";
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 (existsSync12(candidate)) {
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 (existsSync12(explicit)) return explicit;
9769
- const candidates = readdirSync4(codexDir).filter((name) => name.startsWith("state_") && name.endsWith(".sqlite")).sort().reverse();
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 now2 = /* @__PURE__ */ new Date();
9775
- const utcDay = now2.getUTCDay();
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(now2);
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 (!existsSync12(claudeDir)) return null;
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 (existsSync12(projectsDir)) {
9433
+ if (existsSync11(projectsDir)) {
9855
9434
  try {
9856
- const projectDirs = readdirSync4(projectsDir, { withFileTypes: true });
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 = readdirSync4(projectPath).filter((f) => f.endsWith(".jsonl"));
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 (existsSync12(settingsPath)) {
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 (!existsSync12(sessionsDir)) return [];
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 = readdirSync4(current, { withFileTypes: true });
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 (existsSync12(modelsCachePath)) {
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 (existsSync12(configPath)) {
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 (!existsSync12(geminiTmp)) return [];
9956
+ if (!existsSync11(geminiTmp)) return [];
10378
9957
  const usages = [];
10379
9958
  let entries = [];
10380
9959
  try {
10381
- entries = readdirSync4(geminiTmp, { withFileTypes: true });
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 (!existsSync12(chatsDir)) continue;
9967
+ if (!existsSync11(chatsDir)) continue;
10389
9968
  let sessions = [];
10390
9969
  try {
10391
- sessions = readdirSync4(chatsDir).filter((name) => name.startsWith("session-") && (name.endsWith(".json") || name.endsWith(".jsonl")));
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 sessionPath = join20(chatsDir, sessionFile);
9975
+ const sessionPath2 = join20(chatsDir, sessionFile);
10397
9976
  try {
10398
- const usage = aggregateGeminiSessionUsageFromJson(readFileSync8(sessionPath, "utf8"));
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 (existsSync12(accountsPath)) {
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 (existsSync12(modelsPath)) {
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 (existsSync12(settingsPath)) {
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(entry, targetRoot, fifonyDir, globalEnv) {
10598
- return cmdStart(entry, targetRoot, fifonyDir, globalEnv);
10176
+ async function startManagedService(id) {
10177
+ await sendServiceEvent(id, "START");
10599
10178
  }
10600
- function stopManagedService(id, fifonyDir) {
10601
- return cmdStop(id, fifonyDir);
10179
+ async function stopManagedService(id) {
10180
+ await sendServiceEvent(id, "STOP");
10602
10181
  }
10603
- function startAutoConfiguredServices(entries, targetRoot, fifonyDir, globalEnv) {
10604
- const transitions = [];
10182
+ async function startAutoConfiguredServices(entries) {
10183
+ const started = [];
10605
10184
  for (const entry of entries) {
10606
10185
  if (!entry.autoStart) continue;
10607
- transitions.push(startManagedService(entry, targetRoot, fifonyDir, globalEnv));
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 transitions;
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 existsSync13, mkdtempSync as mkdtempSync4, readFileSync as readFileSync9, rmSync as rmSync6, writeFileSync as writeFileSync15 } from "fs";
10903
- import { spawn as spawn3 } from "child_process";
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 (existsSync13(resultFile)) {
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
- writeFileSync15(promptFile, `${prompt}
10583
+ writeFileSync14(promptFile, `${prompt}
11002
10584
  `, "utf8");
11003
- writeFileSync15(issuePayloadFile, JSON.stringify({ title, description, field }, null, 2), "utf8");
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 = spawn3(effectiveCommand, {
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
- rmSync6(tempDir, { recursive: true, force: true });
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
- rmSync6(tempDir, { recursive: true, force: true });
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
- rmSync6(tempDir, { recursive: true, force: true });
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 resolvePlanStageConfig(config);
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-5CW456AZ.js");
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. Each stage needs provider, model, and effort." }, 400);
11071
+ return c.json({ ok: false, error: "Invalid workflow config. plan, execute, and review stages are required with a provider." }, 400);
11289
11072
  }
11290
- await persistWorkflowConfig(workflow);
11291
- addEvent(state, void 0, "manual", `Workflow config updated: plan=${workflow.plan.provider}/${workflow.plan.model}, execute=${workflow.execute.provider}/${workflow.execute.model}, review=${workflow.review.provider}/${workflow.review.model}.`);
11292
- return c.json({ ok: true, workflow });
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 readFileSync10, writeFileSync as writeFileSync16 } from "fs";
11421
- import { join as join22 } from "path";
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 = join22(targetRoot, "CLAUDE.md");
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 = readFileSync10(claudeMdPath, "utf8");
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 readFileSync11,
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 join23 } from "path";
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-XFZK3TT5.js");
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 = join23(TARGET_ROOT, ".gitignore");
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 = readFileSync11(gitignorePath, "utf-8");
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 = join23(TARGET_ROOT, ".gitignore");
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 = readFileSync11(gitignorePath, "utf-8");
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 = join23(ATTACHMENTS_ROOT, "temp", uploadId);
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 = join23(uploadDir, safeName);
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 closeSync4,
11550
+ closeSync as closeSync2,
11760
11551
  existsSync as existsSync16,
11761
- openSync as openSync4,
11762
- readFileSync as readFileSync12,
11763
- readSync as readSync4,
11764
- readdirSync as readdirSync5,
11765
- statSync as statSync5
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 join24 } from "path";
11558
+ import { join as join25 } from "path";
11768
11559
  function detectServices(targetRoot) {
11769
11560
  const suggestions = [];
11770
- if (existsSync16(join24(targetRoot, "turbo.json"))) {
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 = join24(targetRoot, "pnpm-workspace.yaml");
11774
- const rootPkgFile = join24(targetRoot, "package.json");
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 = readFileSync12(pnpmWorkspaceFile, "utf8");
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(readFileSync12(rootPkgFile, "utf8"));
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 = join24(targetRoot, parent);
11594
+ const parentAbs = join25(targetRoot, parent);
11804
11595
  if (!existsSync16(parentAbs)) continue;
11805
11596
  try {
11806
- const children = readdirSync5(parentAbs, { withFileTypes: true });
11597
+ const children = readdirSync6(parentAbs, { withFileTypes: true });
11807
11598
  for (const child of children) {
11808
11599
  if (!child.isDirectory()) continue;
11809
- const childPkg = join24(parentAbs, child.name, "package.json");
11600
+ const childPkg = join25(parentAbs, child.name, "package.json");
11810
11601
  if (!existsSync16(childPkg)) continue;
11811
11602
  try {
11812
- const pkg = JSON.parse(readFileSync12(childPkg, "utf8"));
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 = join24(targetRoot, "Makefile");
11624
+ const makefile = join25(targetRoot, "Makefile");
11834
11625
  if (existsSync16(makefile)) {
11835
11626
  try {
11836
- const content = readFileSync12(makefile, "utf8");
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(readFileSync12(rootPkgFile, "utf8"));
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(join24(targetRoot, "docker-compose.yml")) || existsSync16(join24(targetRoot, "docker-compose.yaml"))) {
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
- const globalVars = Object.fromEntries(
11900
- (state.variables ?? []).filter((v) => v.scope === "global").map((v) => [v.key, v.value])
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/stop", (c) => {
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
- const t = stopManagedService(id, STATE_ROOT);
11920
- if (t) broadcastTransition(t);
11921
- stopServiceLogBroadcasting(id);
11922
- return c.json({ ok: true, state: t?.to ?? "stopped" });
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 = statSync5(logFile).size;
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 = openSync4(logFile, "r");
11759
+ const fd = openSync2(logFile, "r");
11942
11760
  const buf = Buffer.alloc(readSize);
11943
- readSync4(fd, buf, 0, readSize, after);
11944
- closeSync4(fd);
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 = statSync5(logFile);
11790
+ const stat = statSync3(logFile);
11973
11791
  lastSize = stat.size;
11974
11792
  const readSize = Math.min(lastSize, 16384);
11975
- const fd = openSync4(logFile, "r");
11793
+ const fd = openSync2(logFile, "r");
11976
11794
  const buf = Buffer.alloc(readSize);
11977
- readSync4(fd, buf, 0, readSize, Math.max(0, lastSize - readSize));
11978
- closeSync4(fd);
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 = statSync5(logFile);
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 = openSync4(logFile, "r");
11812
+ const fd = openSync2(logFile, "r");
11995
11813
  const buf = Buffer.alloc(readSize);
11996
- readSync4(fd, buf, 0, readSize, 0);
11997
- closeSync4(fd);
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 = openSync4(logFile, "r");
11822
+ const fd = openSync2(logFile, "r");
12005
11823
  const buf = Buffer.alloc(readSize);
12006
- readSync4(fd, buf, 0, readSize, lastSize);
12007
- closeSync4(fd);
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-M6NCKMZY.js");
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
- const t = stopManagedService(id, STATE_ROOT);
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-M6NCKMZY.js");
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-M6NCKMZY.js");
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-K7MXQB4T.js");
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-M6NCKMZY.js");
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-K7MXQB4T.js");
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-M6NCKMZY.js");
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-M6NCKMZY.js");
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 readFileSync13,
12174
- readdirSync as readdirSync6,
12126
+ readFileSync as readFileSync14,
12127
+ readdirSync as readdirSync7,
12175
12128
  renameSync as renameSync2,
12176
12129
  rmSync as rmSync7,
12177
- statSync as statSync6,
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 join25, resolve as resolve4 } from "path";
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 = join25(stateRoot, "profiles", profileName);
12139
+ const profileRoot = join26(stateRoot, "profiles", profileName);
12187
12140
  return {
12188
12141
  profileName,
12189
12142
  profileRoot,
12190
- workspaceRoot: join25(profileRoot, "workspace"),
12191
- persistenceRoot: join25(profileRoot, ".fifony"),
12192
- trashRoot: join25(stateRoot, "trash", "dev-profiles"),
12193
- runbooksRoot: join25(profileRoot, "runbooks"),
12194
- bootstrapRoot: join25(profileRoot, "bootstrap"),
12195
- metadataFile: join25(profileRoot, "profile.json")
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(readFileSync13(paths.metadataFile, "utf8")) : {};
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 = join25(sourceRoot, dir);
12214
- const target = join25(workspaceRoot, dir);
12215
- if (!existsSync17(source) || existsSync17(target) || !statSync6(source).isDirectory()) continue;
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 = join25(sourceRoot, file);
12220
- const target = join25(workspaceRoot, file);
12172
+ const source = join26(sourceRoot, file);
12173
+ const target = join26(workspaceRoot, file);
12221
12174
  if (!existsSync17(source) || existsSync17(target)) continue;
12222
- writeFileSync18(target, readFileSync13(source));
12175
+ writeFileSync18(target, readFileSync14(source));
12223
12176
  }
12224
12177
  }
12225
12178
  function ensureWorktreeLocalExcludes(workspaceRoot) {
12226
12179
  try {
12227
- const gitFile = readFileSync13(join25(workspaceRoot, ".git"), "utf8").trim();
12180
+ const gitFile = readFileSync14(join26(workspaceRoot, ".git"), "utf8").trim();
12228
12181
  const gitDirPath = gitFile.replace(/^gitdir:\s*/, "").trim();
12229
- const resolvedGitDir = resolve4(workspaceRoot, gitDirPath);
12230
- const excludePath = join25(resolvedGitDir, "info", "exclude");
12231
- ensureDir(join25(resolvedGitDir, "info"));
12232
- const current = existsSync17(excludePath) ? readFileSync13(excludePath, "utf8") : "";
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(join25(paths.workspaceRoot, ".fifony-dev"));
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(join25(paths.workspaceRoot, "WORKFLOW.local.md"), `${workflowLocal}
12266
+ writeFileSync18(join26(paths.workspaceRoot, "WORKFLOW.local.md"), `${workflowLocal}
12314
12267
  `, "utf8");
12315
- writeFileSync18(join25(paths.workspaceRoot, "FIFONY.md"), `${fifonyMd}
12268
+ writeFileSync18(join26(paths.workspaceRoot, "FIFONY.md"), `${fifonyMd}
12316
12269
  `, "utf8");
12317
- writeFileSync18(join25(paths.runbooksRoot, "doctor.md"), `${doctorRunbook}
12270
+ writeFileSync18(join26(paths.runbooksRoot, "doctor.md"), `${doctorRunbook}
12318
12271
  `, "utf8");
12319
- writeFileSync18(join25(paths.runbooksRoot, "services.md"), `${servicesRunbook}
12272
+ writeFileSync18(join26(paths.runbooksRoot, "services.md"), `${servicesRunbook}
12320
12273
  `, "utf8");
12321
- writeFileSync18(join25(paths.runbooksRoot, "context.md"), `${contextRunbook}
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) ? readdirSync6(paths.trashRoot).filter((entry) => entry.startsWith(`${profileName}-`)).sort().reverse() : [];
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(join25(paths.workspaceRoot, "WORKFLOW.local.md")) && existsSync17(join25(paths.workspaceRoot, "FIFONY.md")),
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(join25(paths.workspaceRoot, "WORKFLOW.local.md")),
12362
- fifony: existsSync17(join25(paths.workspaceRoot, "FIFONY.md")),
12363
- runbooks: existsSync17(paths.runbooksRoot) ? readdirSync6(paths.runbooksRoot).sort() : []
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 = join25(paths.trashRoot, `${profileName}-${Date.now()}`);
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 (!existsSync18(filePath)) {
12892
+ if (!existsSync19(filePath)) {
12516
12893
  return new Response("Not found", { status: 404 });
12517
12894
  }
12518
- return new Response(readFileSync14(filePath), {
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 (!existsSync18(FRONTEND_INDEX)) {
12910
+ if (!existsSync19(FRONTEND_INDEX)) {
12534
12911
  return new Response("Not found", { status: 404 });
12535
12912
  }
12536
- const html = readFileSync14(FRONTEND_INDEX, "utf8").replace('href="/assets/manifest.webmanifest"', 'href="/manifest.webmanifest"').replaceAll('href="/assets/icon.svg"', 'href="/icon.svg"');
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-XFZK3TT5.js");
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
- plan: { ...claudeDefault, effort: "high" },
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
- return { plan: { ...claudeDefault, effort: "high" }, execute: claudeDefault, review: claudeDefault };
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
- return { plan: { ...codexDefault, effort: "high" }, execute: codexDefault, review: codexDefault };
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
- return { plan: claudeDefault, execute: codexDefault, review: claudeDefault };
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
- return wf;
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
- resolvePlanStageConfig,
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-YRSH2CLW.js.map
14309
+ //# sourceMappingURL=chunk-K36BWMUV.js.map