fifony 0.1.27 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -29
- package/app/dist/assets/{KeyboardShortcutsHelp-NmaeCZMn.js → KeyboardShortcutsHelp-BF5KYX3E.js} +1 -1
- package/app/dist/assets/OnboardingWizard-Cweg4Ch0.js +1 -0
- package/app/dist/assets/{analytics.lazy-BpH26eA2.js → analytics.lazy-BlFXDncv.js} +1 -1
- package/app/dist/assets/{createLucideIcon-BWC-guQt.js → createLucideIcon-DgMTp0yx.js} +1 -1
- package/app/dist/assets/index-CSquFPSf.js +45 -0
- package/app/dist/assets/{index-DntTEHv8.css → index-ZlyvZ7KI.css} +1 -1
- package/app/dist/assets/vendor-D-IqxHHu.js +9 -0
- package/app/dist/index.html +4 -4
- package/app/dist/service-worker.js +1 -1
- package/dist/agent/run-local.js +64 -144
- package/dist/agent-FPUYBJZD.js +74 -0
- package/dist/chunk-2G6SRDOC.js +847 -0
- package/dist/{chunk-G7W4NEOA.js → chunk-3FCJI2GK.js} +1232 -633
- package/dist/chunk-O5AEQXUV.js +311 -0
- package/dist/chunk-OONOOWNC.js +123 -0
- package/dist/chunk-VOQT7RVT.js +295 -0
- package/dist/{chunk-XN2QKKMY.js → chunk-XVF6GOVS.js} +456 -814
- package/dist/cli.js +6 -4
- package/dist/issue-runner-MRHO5ZAB.js +15 -0
- package/dist/{issue-state-machine-SKODQ6MG.js → issue-state-machine-V2KPUYPW.js} +5 -3
- package/dist/issues-3PUMY63N.js +40 -0
- package/dist/mcp/server.js +23 -121
- package/dist/queue-workers-EGHCDDLB.js +23 -0
- package/dist/scheduler-V4GMCBTE.js +21 -0
- package/dist/{store-366NGWR4.js → store-RVKQ6UEY.js} +7 -5
- package/dist/workspace-KEHFITYR.js +52 -0
- package/package.json +6 -6
- package/app/dist/assets/OnboardingWizard-CwW6b_X4.js +0 -1
- package/app/dist/assets/index-D6jtlB7h.js +0 -43
- package/app/dist/assets/vendor-BTlTWMUF.js +0 -9
- package/dist/chunk-AMOGDOM7.js +0 -796
- package/dist/chunk-MT3S55TM.js +0 -91
- package/dist/issue-runner-MTAIYNVN.js +0 -13
- package/dist/queue-workers-Q3IWRFLI.js +0 -20
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computeDiffStats
|
|
3
|
+
} from "./chunk-XVF6GOVS.js";
|
|
4
|
+
import {
|
|
5
|
+
isoWeek,
|
|
6
|
+
now
|
|
7
|
+
} from "./chunk-O5AEQXUV.js";
|
|
8
|
+
import {
|
|
9
|
+
logger
|
|
10
|
+
} from "./chunk-DVU3CXWA.js";
|
|
11
|
+
import {
|
|
12
|
+
S3DB_ISSUE_RESOURCE,
|
|
13
|
+
TERMINAL_STATES
|
|
14
|
+
} from "./chunk-OONOOWNC.js";
|
|
15
|
+
|
|
16
|
+
// src/agents/failure-analyzer.ts
|
|
17
|
+
function extractFilePaths(output) {
|
|
18
|
+
const pathRegex = /(?:^|\s|["'(])([.\w/-]+\.(?:ts|tsx|js|jsx|mjs|cjs|json|css|html|vue|svelte))\b/gm;
|
|
19
|
+
const paths = /* @__PURE__ */ new Set();
|
|
20
|
+
let match;
|
|
21
|
+
while ((match = pathRegex.exec(output)) !== null) {
|
|
22
|
+
const p = match[1];
|
|
23
|
+
if (p && !p.startsWith("http") && !p.includes("node_modules") && p.includes("/")) {
|
|
24
|
+
paths.add(p);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return [...paths].slice(0, 10);
|
|
28
|
+
}
|
|
29
|
+
function extractErrorLine(output) {
|
|
30
|
+
const patterns = [
|
|
31
|
+
// Specific error codes first (most informative)
|
|
32
|
+
/error\s+TS\d+:\s*(.+)/m,
|
|
33
|
+
/^AssertionError.*:\s*(.+)$/m,
|
|
34
|
+
/^CONFLICT\s*(.+)$/m,
|
|
35
|
+
/^fatal:\s*(.+)$/m,
|
|
36
|
+
/ERR!\s*(.+)$/m,
|
|
37
|
+
// Generic error types
|
|
38
|
+
/^(?:Error|TypeError|ReferenceError|SyntaxError|RangeError):\s*(.+)$/m,
|
|
39
|
+
/^(?:FAIL|FAILED)\s+(.+)$/m,
|
|
40
|
+
/^error:\s*(.+)$/m
|
|
41
|
+
];
|
|
42
|
+
for (const pattern of patterns) {
|
|
43
|
+
const match = output.match(pattern);
|
|
44
|
+
if (match) return match[0].trim().slice(0, 200);
|
|
45
|
+
}
|
|
46
|
+
const lines = output.split("\n");
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
const trimmed = line.trim();
|
|
49
|
+
if (trimmed.length > 10 && /error|fail|cannot|unable|not found|unexpected/i.test(trimmed)) {
|
|
50
|
+
return trimmed.slice(0, 200);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
function detectFailedCommand(output) {
|
|
56
|
+
const scriptMatch = output.match(/(?:pnpm|npm|yarn)\s+(?:run\s+)?(\w+).*(?:failed|error|ELIFECYCLE)/im) || output.match(/(?:pnpm|npm|yarn)\s+(\w+)\s+failed/im);
|
|
57
|
+
if (scriptMatch) return `pnpm ${scriptMatch[1]}`;
|
|
58
|
+
if (/error\s+TS\d+/m.test(output)) return "tsc (TypeScript compiler)";
|
|
59
|
+
if (/eslint/i.test(output) && /error/i.test(output)) return "eslint";
|
|
60
|
+
if (/jest|vitest|mocha|node --test/i.test(output) && /fail/i.test(output)) return "test runner";
|
|
61
|
+
if (/git\s+(merge|rebase|push|pull)/i.test(output) && /fatal|error|conflict/i.test(output)) {
|
|
62
|
+
const gitMatch = output.match(/git\s+(merge|rebase|push|pull)/i);
|
|
63
|
+
return gitMatch ? `git ${gitMatch[1]}` : "git";
|
|
64
|
+
}
|
|
65
|
+
if (/SIGTERM|SIGKILL|timed?\s*out/i.test(output)) return "process (timeout/killed)";
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
function unwrapJsonOutput(raw) {
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(raw.trim());
|
|
71
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
72
|
+
if (typeof parsed.result === "string") return parsed.result;
|
|
73
|
+
if (typeof parsed.response === "string") return parsed.response;
|
|
74
|
+
if (parsed.structured_output?.summary) return String(parsed.structured_output.summary);
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
return raw;
|
|
79
|
+
}
|
|
80
|
+
function extractFailureInsights(output, exitCode) {
|
|
81
|
+
const normalizedOutput = output ? `${unwrapJsonOutput(output)}
|
|
82
|
+
${output}` : "";
|
|
83
|
+
let errorType = "unknown";
|
|
84
|
+
if (/error\s+TS\d+/m.test(normalizedOutput)) {
|
|
85
|
+
errorType = "typescript";
|
|
86
|
+
} else if (/eslint|ESLint/m.test(normalizedOutput) && /error/m.test(normalizedOutput)) {
|
|
87
|
+
errorType = "lint";
|
|
88
|
+
} else if (/(?:CONFLICT|Merge conflict)/im.test(normalizedOutput)) {
|
|
89
|
+
errorType = "git";
|
|
90
|
+
} else if (/(?:FAIL\s|AssertionError|test.*failed|expect.*received)/im.test(normalizedOutput)) {
|
|
91
|
+
errorType = "test";
|
|
92
|
+
} else if (/(?:SIGTERM|SIGKILL|timed?\s*out|killed)/im.test(normalizedOutput)) {
|
|
93
|
+
errorType = "timeout";
|
|
94
|
+
} else if (/(?:ELIFECYCLE|ERR!|build.*fail)/im.test(normalizedOutput)) {
|
|
95
|
+
errorType = "build";
|
|
96
|
+
} else if (/(?:fatal:\s)/m.test(normalizedOutput)) {
|
|
97
|
+
errorType = "git";
|
|
98
|
+
} else if (/(?:Error:|TypeError:|ReferenceError:|SyntaxError:)/m.test(normalizedOutput)) {
|
|
99
|
+
errorType = "runtime";
|
|
100
|
+
} else if (exitCode && exitCode !== 0) {
|
|
101
|
+
errorType = "process";
|
|
102
|
+
}
|
|
103
|
+
const errorMessage = extractErrorLine(normalizedOutput);
|
|
104
|
+
const failedCommand = detectFailedCommand(normalizedOutput);
|
|
105
|
+
const filesInvolved = extractFilePaths(normalizedOutput);
|
|
106
|
+
let rootCause;
|
|
107
|
+
switch (errorType) {
|
|
108
|
+
case "typescript":
|
|
109
|
+
rootCause = `TypeScript compilation failed${filesInvolved.length ? ` in ${filesInvolved[0]}` : ""}`;
|
|
110
|
+
break;
|
|
111
|
+
case "test":
|
|
112
|
+
rootCause = `Test assertion failed${filesInvolved.length ? ` in ${filesInvolved[0]}` : ""}`;
|
|
113
|
+
break;
|
|
114
|
+
case "lint":
|
|
115
|
+
rootCause = "Lint rules violated";
|
|
116
|
+
break;
|
|
117
|
+
case "timeout":
|
|
118
|
+
rootCause = "Process timed out or was killed";
|
|
119
|
+
break;
|
|
120
|
+
case "build":
|
|
121
|
+
rootCause = "Build/install step failed";
|
|
122
|
+
break;
|
|
123
|
+
case "git":
|
|
124
|
+
rootCause = "Git operation failed (possible conflict or dirty state)";
|
|
125
|
+
break;
|
|
126
|
+
case "runtime":
|
|
127
|
+
rootCause = `Runtime error: ${errorMessage.slice(0, 80)}`;
|
|
128
|
+
break;
|
|
129
|
+
case "process":
|
|
130
|
+
rootCause = `Process exited with code ${exitCode}`;
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
rootCause = errorMessage ? `Error: ${errorMessage.slice(0, 80)}` : "Unknown failure";
|
|
134
|
+
}
|
|
135
|
+
let suggestion;
|
|
136
|
+
switch (errorType) {
|
|
137
|
+
case "typescript":
|
|
138
|
+
suggestion = "Check type signatures and imports. The previous approach introduced a type error \u2014 try a different API surface.";
|
|
139
|
+
break;
|
|
140
|
+
case "test":
|
|
141
|
+
suggestion = "The implementation broke existing tests. Read the test file first, understand the expected behavior, then fix the implementation to match.";
|
|
142
|
+
break;
|
|
143
|
+
case "lint":
|
|
144
|
+
suggestion = "Run the linter before finishing. Fix formatting and rule violations.";
|
|
145
|
+
break;
|
|
146
|
+
case "timeout":
|
|
147
|
+
suggestion = "The previous approach was too slow or hung. Simplify the approach or break it into smaller steps.";
|
|
148
|
+
break;
|
|
149
|
+
case "build":
|
|
150
|
+
suggestion = "A dependency or build step failed. Check package.json and verify imports.";
|
|
151
|
+
break;
|
|
152
|
+
case "git":
|
|
153
|
+
suggestion = "A git operation failed. Check for merge conflicts, uncommitted changes, or branch state.";
|
|
154
|
+
break;
|
|
155
|
+
case "runtime":
|
|
156
|
+
suggestion = "The code threw an error at runtime. Check for null/undefined, missing imports, or wrong API usage.";
|
|
157
|
+
break;
|
|
158
|
+
default:
|
|
159
|
+
suggestion = "Review the full error output and try a fundamentally different approach.";
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
errorType,
|
|
163
|
+
errorMessage,
|
|
164
|
+
failedCommand,
|
|
165
|
+
filesInvolved,
|
|
166
|
+
rootCause,
|
|
167
|
+
suggestion
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/domains/metrics.ts
|
|
172
|
+
function computeMetrics(issues) {
|
|
173
|
+
let planning = 0;
|
|
174
|
+
let queued = 0;
|
|
175
|
+
let inProgress = 0;
|
|
176
|
+
let blocked = 0;
|
|
177
|
+
let done = 0;
|
|
178
|
+
let merged = 0;
|
|
179
|
+
let cancelled = 0;
|
|
180
|
+
const completionTimes = [];
|
|
181
|
+
for (const issue of issues) {
|
|
182
|
+
if (issue.state === "Merged") {
|
|
183
|
+
const duration = issue.durationMs;
|
|
184
|
+
const candidate = typeof duration === "number" && Number.isFinite(duration) ? duration : Number.isFinite(Date.parse(issue.startedAt ?? "")) && Number.isFinite(Date.parse(issue.completedAt ?? "")) ? Date.parse(issue.completedAt) - Date.parse(issue.startedAt) : NaN;
|
|
185
|
+
if (Number.isFinite(candidate) && candidate >= 0) {
|
|
186
|
+
completionTimes.push(candidate);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
switch (issue.state) {
|
|
190
|
+
case "Planning":
|
|
191
|
+
planning += 1;
|
|
192
|
+
break;
|
|
193
|
+
case "PendingApproval":
|
|
194
|
+
queued += 1;
|
|
195
|
+
break;
|
|
196
|
+
case "Queued":
|
|
197
|
+
case "Running":
|
|
198
|
+
case "Reviewing":
|
|
199
|
+
case "PendingDecision":
|
|
200
|
+
inProgress += 1;
|
|
201
|
+
break;
|
|
202
|
+
case "Blocked":
|
|
203
|
+
blocked += 1;
|
|
204
|
+
break;
|
|
205
|
+
case "Approved":
|
|
206
|
+
done += 1;
|
|
207
|
+
break;
|
|
208
|
+
case "Merged":
|
|
209
|
+
merged += 1;
|
|
210
|
+
break;
|
|
211
|
+
case "Cancelled":
|
|
212
|
+
cancelled += 1;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (completionTimes.length === 0) {
|
|
217
|
+
return {
|
|
218
|
+
total: issues.length,
|
|
219
|
+
planning,
|
|
220
|
+
queued,
|
|
221
|
+
inProgress,
|
|
222
|
+
blocked,
|
|
223
|
+
done,
|
|
224
|
+
merged,
|
|
225
|
+
cancelled,
|
|
226
|
+
activeWorkers: 0
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
const sortedCompletionTimes = completionTimes.slice().sort((a, b) => a - b);
|
|
230
|
+
const totalCompletionMs = sortedCompletionTimes.reduce((acc, value) => acc + value, 0);
|
|
231
|
+
const mid = Math.floor(sortedCompletionTimes.length / 2);
|
|
232
|
+
const medianCompletionMs = sortedCompletionTimes.length % 2 === 1 ? sortedCompletionTimes[mid] : Math.round((sortedCompletionTimes[mid - 1] + sortedCompletionTimes[mid]) / 2);
|
|
233
|
+
return {
|
|
234
|
+
total: issues.length,
|
|
235
|
+
planning,
|
|
236
|
+
queued,
|
|
237
|
+
inProgress,
|
|
238
|
+
blocked,
|
|
239
|
+
done,
|
|
240
|
+
merged,
|
|
241
|
+
cancelled,
|
|
242
|
+
activeWorkers: 0,
|
|
243
|
+
avgCompletionMs: Math.round(totalCompletionMs / completionTimes.length),
|
|
244
|
+
medianCompletionMs,
|
|
245
|
+
fastestCompletionMs: sortedCompletionTimes[0],
|
|
246
|
+
slowestCompletionMs: sortedCompletionTimes[sortedCompletionTimes.length - 1]
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/persistence/metrics-cache.ts
|
|
251
|
+
var cachedMetrics = null;
|
|
252
|
+
var metricsStale = true;
|
|
253
|
+
function invalidateMetrics() {
|
|
254
|
+
metricsStale = true;
|
|
255
|
+
}
|
|
256
|
+
function getMetrics(issues) {
|
|
257
|
+
if (!metricsStale && cachedMetrics) return cachedMetrics;
|
|
258
|
+
cachedMetrics = computeMetrics(issues);
|
|
259
|
+
metricsStale = false;
|
|
260
|
+
return cachedMetrics;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/persistence/dirty-tracker.ts
|
|
264
|
+
var dirtyIssueIds = /* @__PURE__ */ new Set();
|
|
265
|
+
var dirtyIssuePlanIds = /* @__PURE__ */ new Set();
|
|
266
|
+
var dirtyEventIds = /* @__PURE__ */ new Set();
|
|
267
|
+
function markIssueDirty(id) {
|
|
268
|
+
dirtyIssueIds.add(id);
|
|
269
|
+
}
|
|
270
|
+
function markIssuePlanDirty(id) {
|
|
271
|
+
dirtyIssuePlanIds.add(id);
|
|
272
|
+
}
|
|
273
|
+
function markEventDirty(id) {
|
|
274
|
+
dirtyEventIds.add(id);
|
|
275
|
+
}
|
|
276
|
+
function hasDirtyState() {
|
|
277
|
+
return dirtyIssueIds.size > 0 || dirtyEventIds.size > 0;
|
|
278
|
+
}
|
|
279
|
+
function getDirtyIssueIds() {
|
|
280
|
+
return dirtyIssueIds;
|
|
281
|
+
}
|
|
282
|
+
function getDirtyEventIds() {
|
|
283
|
+
return dirtyEventIds;
|
|
284
|
+
}
|
|
285
|
+
function snapshotAndClearDirtyIssueIds() {
|
|
286
|
+
const snapshot = new Set(dirtyIssueIds);
|
|
287
|
+
for (const id of snapshot) dirtyIssueIds.delete(id);
|
|
288
|
+
return snapshot;
|
|
289
|
+
}
|
|
290
|
+
function snapshotAndClearDirtyIssuePlanIds() {
|
|
291
|
+
const snapshot = new Set(dirtyIssuePlanIds);
|
|
292
|
+
for (const id of snapshot) dirtyIssuePlanIds.delete(id);
|
|
293
|
+
return snapshot;
|
|
294
|
+
}
|
|
295
|
+
function snapshotAndClearDirtyEventIds() {
|
|
296
|
+
const snapshot = new Set(dirtyEventIds);
|
|
297
|
+
for (const id of snapshot) dirtyEventIds.delete(id);
|
|
298
|
+
return snapshot;
|
|
299
|
+
}
|
|
300
|
+
function markAllIssuesDirty(ids) {
|
|
301
|
+
for (const id of ids) dirtyIssueIds.add(id);
|
|
302
|
+
}
|
|
303
|
+
function markAllIssuePlansDirty(ids) {
|
|
304
|
+
for (const id of ids) dirtyIssuePlanIds.add(id);
|
|
305
|
+
}
|
|
306
|
+
function markAllEventsDirty(ids) {
|
|
307
|
+
for (const id of ids) dirtyEventIds.add(id);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/persistence/plugins/issue-state-machine.ts
|
|
311
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
312
|
+
import { join } from "path";
|
|
313
|
+
var fsmEventEmitter = null;
|
|
314
|
+
function setFsmEventEmitter(emitter) {
|
|
315
|
+
fsmEventEmitter = emitter;
|
|
316
|
+
}
|
|
317
|
+
function emitFsmEvent(issueId, kind, message) {
|
|
318
|
+
if (fsmEventEmitter) {
|
|
319
|
+
try {
|
|
320
|
+
fsmEventEmitter(issueId, kind, message);
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async function lazyEnqueue(issue, job) {
|
|
326
|
+
const { enqueue } = await import("./queue-workers-EGHCDDLB.js");
|
|
327
|
+
return enqueue(issue, job);
|
|
328
|
+
}
|
|
329
|
+
var ISSUE_STATE_MACHINE_ID = "issue-lifecycle";
|
|
330
|
+
function markDirtyAndInvalidate(issueId) {
|
|
331
|
+
markIssueDirty(issueId);
|
|
332
|
+
invalidateMetrics();
|
|
333
|
+
}
|
|
334
|
+
function resolveIssue(context) {
|
|
335
|
+
return context.issue ?? null;
|
|
336
|
+
}
|
|
337
|
+
function issueResource(machine) {
|
|
338
|
+
return machine.database?.resources?.[S3DB_ISSUE_RESOURCE];
|
|
339
|
+
}
|
|
340
|
+
var STALE_TIMEOUT_MS = 24e5;
|
|
341
|
+
async function isStaleIssue(context, _entityId) {
|
|
342
|
+
const issue = resolveIssue(context);
|
|
343
|
+
if (!issue) return false;
|
|
344
|
+
return Date.now() - Date.parse(issue.updatedAt) > STALE_TIMEOUT_MS;
|
|
345
|
+
}
|
|
346
|
+
var issueStateMachineConfig = {
|
|
347
|
+
persistTransitions: true,
|
|
348
|
+
workerId: `fifony-${process.pid}`,
|
|
349
|
+
lockTimeout: 5e3,
|
|
350
|
+
lockTTL: 30,
|
|
351
|
+
stateMachines: {
|
|
352
|
+
[ISSUE_STATE_MACHINE_ID]: {
|
|
353
|
+
resource: S3DB_ISSUE_RESOURCE,
|
|
354
|
+
stateField: "state",
|
|
355
|
+
initialState: "Planning",
|
|
356
|
+
autoCleanup: false,
|
|
357
|
+
states: {
|
|
358
|
+
Planning: {
|
|
359
|
+
on: { PLANNED: "PendingApproval", CANCEL: "Cancelled" },
|
|
360
|
+
entry: "onEnterPlanning"
|
|
361
|
+
},
|
|
362
|
+
PendingApproval: {
|
|
363
|
+
on: { QUEUE: "Queued", REPLAN: "Planning", CANCEL: "Cancelled" },
|
|
364
|
+
entry: "onEnterPendingApproval"
|
|
365
|
+
},
|
|
366
|
+
Queued: {
|
|
367
|
+
on: { RUN: "Running" },
|
|
368
|
+
entry: "onEnterQueued"
|
|
369
|
+
},
|
|
370
|
+
Running: {
|
|
371
|
+
on: { REVIEW: "Reviewing", REQUEUE: "Queued", BLOCK: "Blocked" },
|
|
372
|
+
guards: { BLOCK: "requireBlockReason" },
|
|
373
|
+
triggers: [{
|
|
374
|
+
type: "cron",
|
|
375
|
+
cron: "*/10 * * * *",
|
|
376
|
+
sendEvent: "BLOCK",
|
|
377
|
+
condition: isStaleIssue
|
|
378
|
+
}]
|
|
379
|
+
},
|
|
380
|
+
Reviewing: {
|
|
381
|
+
on: { REVIEWED: "PendingDecision", REQUEUE: "Queued", BLOCK: "Blocked" },
|
|
382
|
+
entry: "onEnterReviewing",
|
|
383
|
+
guards: { BLOCK: "requireBlockReason" },
|
|
384
|
+
triggers: [{
|
|
385
|
+
type: "cron",
|
|
386
|
+
cron: "*/10 * * * *",
|
|
387
|
+
sendEvent: "BLOCK",
|
|
388
|
+
condition: isStaleIssue
|
|
389
|
+
}]
|
|
390
|
+
},
|
|
391
|
+
PendingDecision: {
|
|
392
|
+
on: { APPROVE: "Approved", REQUEUE: "Queued", REPLAN: "Planning", CANCEL: "Cancelled" }
|
|
393
|
+
},
|
|
394
|
+
Blocked: {
|
|
395
|
+
on: { UNBLOCK: "Queued", REPLAN: "Planning", CANCEL: "Cancelled" },
|
|
396
|
+
entry: "onEnterBlocked"
|
|
397
|
+
},
|
|
398
|
+
Approved: {
|
|
399
|
+
on: { MERGE: "Merged", REOPEN: "Planning" },
|
|
400
|
+
entry: "onEnterApproved"
|
|
401
|
+
},
|
|
402
|
+
Merged: {
|
|
403
|
+
on: { ARCHIVE: "Archived", REOPEN: "Planning" },
|
|
404
|
+
type: "final",
|
|
405
|
+
entry: "onEnterMerged"
|
|
406
|
+
},
|
|
407
|
+
Cancelled: {
|
|
408
|
+
on: { ARCHIVE: "Archived", REOPEN: "Planning" },
|
|
409
|
+
type: "final",
|
|
410
|
+
entry: "onEnterCancelled"
|
|
411
|
+
},
|
|
412
|
+
Archived: {
|
|
413
|
+
on: {},
|
|
414
|
+
type: "final",
|
|
415
|
+
entry: "onEnterArchived"
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
// ── Actions: (context, event, machine) ──────────────────────────────────
|
|
421
|
+
// context = payload from send()
|
|
422
|
+
// event = event name ("PLANNED", "BLOCK", etc.)
|
|
423
|
+
// machine = { database, machineId, entityId }
|
|
424
|
+
//
|
|
425
|
+
// Actions only mutate the in-memory issue + fire side effects (enqueue, s3db patch).
|
|
426
|
+
// Dirty tracking + metrics invalidation is done once in executeTransition() after send().
|
|
427
|
+
actions: {
|
|
428
|
+
onEnterPlanning: async (context, _event, _machine) => {
|
|
429
|
+
const issue = resolveIssue(context);
|
|
430
|
+
if (issue) {
|
|
431
|
+
issue.planningStatus = "idle";
|
|
432
|
+
issue.planningError = void 0;
|
|
433
|
+
issue.nextRetryAt = void 0;
|
|
434
|
+
issue.lastError = void 0;
|
|
435
|
+
emitFsmEvent(issue.id, "state", `${issue.identifier} entered Planning.`);
|
|
436
|
+
lazyEnqueue(issue, "plan").catch(() => {
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
onEnterPendingApproval: async (context, _event, _machine) => {
|
|
441
|
+
const issue = resolveIssue(context);
|
|
442
|
+
if (issue) {
|
|
443
|
+
issue.nextRetryAt = void 0;
|
|
444
|
+
issue.lastError = void 0;
|
|
445
|
+
emitFsmEvent(issue.id, "state", `Plan ready \u2014 ${issue.identifier} awaiting approval.`);
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
onEnterQueued: async (context, _event, _machine) => {
|
|
449
|
+
const issue = resolveIssue(context);
|
|
450
|
+
if (issue) {
|
|
451
|
+
if (issue.attempts > 0 && issue.lastError) {
|
|
452
|
+
let fullOutput = "";
|
|
453
|
+
let outputFile;
|
|
454
|
+
if (issue.workspacePath) {
|
|
455
|
+
try {
|
|
456
|
+
const outputsDir = join(issue.workspacePath, "outputs");
|
|
457
|
+
if (existsSync(outputsDir)) {
|
|
458
|
+
const files = readdirSync(outputsDir).filter((f) => f.endsWith(".stdout.log")).sort((a, b) => {
|
|
459
|
+
try {
|
|
460
|
+
return statSync(join(outputsDir, b)).mtimeMs - statSync(join(outputsDir, a)).mtimeMs;
|
|
461
|
+
} catch {
|
|
462
|
+
return 0;
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
if (files.length > 0) {
|
|
466
|
+
outputFile = files[0];
|
|
467
|
+
try {
|
|
468
|
+
fullOutput = readFileSync(join(outputsDir, files[0]), "utf8");
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const analysisSource = fullOutput || issue.commandOutputTail || issue.lastError || "";
|
|
477
|
+
const failureInsight = extractFailureInsights(analysisSource, issue.commandExitCode);
|
|
478
|
+
const summary = {
|
|
479
|
+
planVersion: issue.planVersion ?? 1,
|
|
480
|
+
executeAttempt: issue.executeAttempt ?? 1,
|
|
481
|
+
phase: issue.lastFailedPhase ?? void 0,
|
|
482
|
+
error: failureInsight.rootCause || (issue.lastError ?? "").slice(0, 500),
|
|
483
|
+
outputTail: (issue.commandOutputTail ?? "").slice(0, 500),
|
|
484
|
+
outputFile,
|
|
485
|
+
timestamp: now(),
|
|
486
|
+
insight: {
|
|
487
|
+
errorType: failureInsight.errorType,
|
|
488
|
+
rootCause: failureInsight.rootCause,
|
|
489
|
+
failedCommand: failureInsight.failedCommand,
|
|
490
|
+
filesInvolved: failureInsight.filesInvolved,
|
|
491
|
+
suggestion: failureInsight.suggestion
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
const prev = issue.previousAttemptSummaries ?? [];
|
|
495
|
+
issue.previousAttemptSummaries = [...prev.slice(-2), summary].slice(-3);
|
|
496
|
+
}
|
|
497
|
+
issue.nextRetryAt = void 0;
|
|
498
|
+
issue.lastError = void 0;
|
|
499
|
+
issue.lastFailedPhase = void 0;
|
|
500
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier }, "[FSM] onEnterQueued \u2014 enqueuing for execution");
|
|
501
|
+
emitFsmEvent(issue.id, "state", `${issue.identifier} queued for execution.`);
|
|
502
|
+
lazyEnqueue(issue, "execute").catch((err) => {
|
|
503
|
+
logger.error({ err, issueId: issue.id }, "[FSM] onEnterQueued \u2014 enqueue FAILED");
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
onEnterReviewing: async (context, _event, machine) => {
|
|
508
|
+
const issue = resolveIssue(context);
|
|
509
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
510
|
+
if (issue) {
|
|
511
|
+
issue.reviewingAt = ts;
|
|
512
|
+
issue.lastError = void 0;
|
|
513
|
+
emitFsmEvent(issue.id, "state", `${issue.identifier} moved to Reviewing.`);
|
|
514
|
+
lazyEnqueue(issue, "review").catch(() => {
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
const res = issueResource(machine);
|
|
518
|
+
if (res) {
|
|
519
|
+
res.patch(machine.entityId, { reviewingAt: ts }).catch(() => {
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
onEnterBlocked: async (context, _event, _machine) => {
|
|
524
|
+
const issue = resolveIssue(context);
|
|
525
|
+
const note = typeof context.note === "string" ? context.note : "Blocked";
|
|
526
|
+
if (issue) {
|
|
527
|
+
issue.lastError = note;
|
|
528
|
+
emitFsmEvent(issue.id, "error", `${issue.identifier} blocked: ${note}`);
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
onEnterApproved: async (context, _event, _machine) => {
|
|
532
|
+
const issue = resolveIssue(context);
|
|
533
|
+
if (issue) {
|
|
534
|
+
issue.nextRetryAt = void 0;
|
|
535
|
+
issue.lastError = void 0;
|
|
536
|
+
if (!issue.linesAdded && !issue.linesRemoved && issue.baseBranch && issue.branchName) {
|
|
537
|
+
computeDiffStats(issue);
|
|
538
|
+
}
|
|
539
|
+
emitFsmEvent(issue.id, "state", `${issue.identifier} approved \u2014 waiting for merge.`);
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
onEnterMerged: async (context, _event, machine) => {
|
|
543
|
+
const issue = resolveIssue(context);
|
|
544
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
545
|
+
const week = isoWeek();
|
|
546
|
+
if (issue) {
|
|
547
|
+
if (!issue.linesAdded && !issue.linesRemoved && issue.baseBranch && issue.branchName) {
|
|
548
|
+
computeDiffStats(issue);
|
|
549
|
+
}
|
|
550
|
+
issue.completedAt = ts;
|
|
551
|
+
issue.terminalWeek = week;
|
|
552
|
+
if (!issue.mergedAt) issue.mergedAt = ts;
|
|
553
|
+
issue.nextRetryAt = void 0;
|
|
554
|
+
issue.lastError = void 0;
|
|
555
|
+
emitFsmEvent(issue.id, "state", `${issue.identifier} merged.`);
|
|
556
|
+
}
|
|
557
|
+
const res = issueResource(machine);
|
|
558
|
+
if (res) {
|
|
559
|
+
const churnAdded = issue?.linesAdded ?? 0;
|
|
560
|
+
const churnRemoved = issue?.linesRemoved ?? 0;
|
|
561
|
+
const churnFiles = issue?.filesChanged ?? 0;
|
|
562
|
+
if (churnAdded || churnRemoved || churnFiles) {
|
|
563
|
+
await res.patch(machine.entityId, { linesAdded: 0, linesRemoved: 0, filesChanged: 0 }).catch(() => {
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
await res.patch(machine.entityId, {
|
|
567
|
+
completedAt: ts,
|
|
568
|
+
terminalWeek: week,
|
|
569
|
+
mergedAt: issue?.mergedAt ?? ts,
|
|
570
|
+
nextRetryAt: void 0,
|
|
571
|
+
lastError: void 0,
|
|
572
|
+
linesAdded: churnAdded,
|
|
573
|
+
linesRemoved: churnRemoved,
|
|
574
|
+
filesChanged: churnFiles,
|
|
575
|
+
branchName: issue?.branchName,
|
|
576
|
+
workspacePath: issue?.workspacePath,
|
|
577
|
+
worktreePath: issue?.worktreePath,
|
|
578
|
+
mergedReason: issue?.mergedReason
|
|
579
|
+
}).catch(() => {
|
|
580
|
+
});
|
|
581
|
+
if (churnAdded || churnRemoved || churnFiles) {
|
|
582
|
+
logger.info({ issueId: issue?.id, linesAdded: churnAdded, linesRemoved: churnRemoved, filesChanged: churnFiles }, "[FSM] EC diff stats patched on merge (reset\u2192set for delta tracking)");
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
onEnterCancelled: async (context, _event, machine) => {
|
|
587
|
+
const issue = resolveIssue(context);
|
|
588
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
589
|
+
const week = isoWeek();
|
|
590
|
+
if (issue) {
|
|
591
|
+
issue.completedAt = ts;
|
|
592
|
+
issue.terminalWeek = week;
|
|
593
|
+
issue.nextRetryAt = void 0;
|
|
594
|
+
issue.lastError = void 0;
|
|
595
|
+
emitFsmEvent(issue.id, "state", `${issue.identifier} cancelled.`);
|
|
596
|
+
}
|
|
597
|
+
const res = issueResource(machine);
|
|
598
|
+
if (res) {
|
|
599
|
+
res.patch(machine.entityId, {
|
|
600
|
+
completedAt: ts,
|
|
601
|
+
terminalWeek: week,
|
|
602
|
+
nextRetryAt: void 0,
|
|
603
|
+
lastError: void 0,
|
|
604
|
+
cancelledReason: issue?.cancelledReason
|
|
605
|
+
}).catch(() => {
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
onEnterArchived: async (context, _event, _machine) => {
|
|
610
|
+
const issue = resolveIssue(context);
|
|
611
|
+
if (issue) {
|
|
612
|
+
emitFsmEvent(issue.id, "state", `${issue.identifier} archived.`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
// ── Guards: (context, event, machine) ───────────────────────────────────
|
|
617
|
+
guards: {
|
|
618
|
+
requireBlockReason: async (context, _event, _machine) => {
|
|
619
|
+
return typeof context.note === "string" && context.note.trim().length > 0;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
var EVENT_TO_STATE = {
|
|
624
|
+
PLANNED: "PendingApproval",
|
|
625
|
+
QUEUE: "Queued",
|
|
626
|
+
RUN: "Running",
|
|
627
|
+
REVIEW: "Reviewing",
|
|
628
|
+
REVIEWED: "PendingDecision",
|
|
629
|
+
APPROVE: "Approved",
|
|
630
|
+
MERGE: "Merged",
|
|
631
|
+
CANCEL: "Cancelled",
|
|
632
|
+
BLOCK: "Blocked",
|
|
633
|
+
UNBLOCK: "Queued",
|
|
634
|
+
REPLAN: "Planning",
|
|
635
|
+
REQUEUE: "Queued",
|
|
636
|
+
REOPEN: "Planning",
|
|
637
|
+
ARCHIVE: "Archived"
|
|
638
|
+
};
|
|
639
|
+
function eventToTargetState(event) {
|
|
640
|
+
return EVENT_TO_STATE[event];
|
|
641
|
+
}
|
|
642
|
+
function getStatesFromConfig() {
|
|
643
|
+
const machine = issueStateMachineConfig.stateMachines[ISSUE_STATE_MACHINE_ID];
|
|
644
|
+
const result = {};
|
|
645
|
+
for (const [state, def] of Object.entries(machine.states)) {
|
|
646
|
+
result[state] = def.on ?? {};
|
|
647
|
+
}
|
|
648
|
+
return result;
|
|
649
|
+
}
|
|
650
|
+
function getStateMachineTransitions() {
|
|
651
|
+
const edges = getStatesFromConfig();
|
|
652
|
+
const result = {};
|
|
653
|
+
for (const [state, events] of Object.entries(edges)) {
|
|
654
|
+
const targets = [...new Set(Object.values(events))];
|
|
655
|
+
result[state] = targets;
|
|
656
|
+
}
|
|
657
|
+
return result;
|
|
658
|
+
}
|
|
659
|
+
function findIssueStateMachineTransitionPath(_machineDefinition, from, to) {
|
|
660
|
+
if (from === to) return [];
|
|
661
|
+
const edges = getStatesFromConfig();
|
|
662
|
+
if (!edges[from] || !edges[to]) return null;
|
|
663
|
+
const queue = [from];
|
|
664
|
+
const previousState = /* @__PURE__ */ new Map();
|
|
665
|
+
const previousEvent = /* @__PURE__ */ new Map();
|
|
666
|
+
previousState.set(from, "");
|
|
667
|
+
for (let i = 0; i < queue.length; i += 1) {
|
|
668
|
+
const current = queue[i];
|
|
669
|
+
const transitions = edges[current];
|
|
670
|
+
if (!transitions) continue;
|
|
671
|
+
for (const [evt, next] of Object.entries(transitions)) {
|
|
672
|
+
if (previousState.has(next)) continue;
|
|
673
|
+
previousState.set(next, current);
|
|
674
|
+
previousEvent.set(next, evt);
|
|
675
|
+
if (next === to) {
|
|
676
|
+
const events = [];
|
|
677
|
+
let cursor = next;
|
|
678
|
+
while (cursor !== from) {
|
|
679
|
+
const prev = previousState.get(cursor);
|
|
680
|
+
const e = previousEvent.get(cursor);
|
|
681
|
+
if (!prev || !e) return null;
|
|
682
|
+
events.unshift(e);
|
|
683
|
+
cursor = prev;
|
|
684
|
+
}
|
|
685
|
+
return events;
|
|
686
|
+
}
|
|
687
|
+
queue.push(next);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
var issueResourceStateApi = null;
|
|
693
|
+
function setIssueResourceStateApi(api) {
|
|
694
|
+
issueResourceStateApi = api;
|
|
695
|
+
}
|
|
696
|
+
function getIssueResourceStateApi() {
|
|
697
|
+
return issueResourceStateApi;
|
|
698
|
+
}
|
|
699
|
+
var issueStateMachinePlugin = null;
|
|
700
|
+
function setIssueStateMachinePlugin(plugin) {
|
|
701
|
+
issueStateMachinePlugin = plugin;
|
|
702
|
+
}
|
|
703
|
+
function getIssueStateMachinePlugin() {
|
|
704
|
+
return issueStateMachinePlugin;
|
|
705
|
+
}
|
|
706
|
+
function getIssueStateMachineDefinition() {
|
|
707
|
+
return issueStateMachinePlugin?.getMachineDefinition?.(ISSUE_STATE_MACHINE_ID) ?? issueStateMachineConfig.stateMachines[ISSUE_STATE_MACHINE_ID];
|
|
708
|
+
}
|
|
709
|
+
function getIssueStateMachineInitialState() {
|
|
710
|
+
return issueStateMachineConfig.stateMachines[ISSUE_STATE_MACHINE_ID].initialState;
|
|
711
|
+
}
|
|
712
|
+
async function executeTransition(issue, event, context = {}) {
|
|
713
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
714
|
+
const previous = issue.state;
|
|
715
|
+
const targetState = eventToTargetState(event);
|
|
716
|
+
if (!targetState) {
|
|
717
|
+
throw new Error(`Unknown FSM event '${event}' for issue ${issue.id}.`);
|
|
718
|
+
}
|
|
719
|
+
const resourceApi = getIssueResourceStateApi();
|
|
720
|
+
const plugin = getIssueStateMachinePlugin();
|
|
721
|
+
const sendContext = { ...context, issue };
|
|
722
|
+
if (resourceApi) {
|
|
723
|
+
try {
|
|
724
|
+
await resourceApi.send(issue.id, event, sendContext);
|
|
725
|
+
} catch (err) {
|
|
726
|
+
if (String(err).includes("not found") || String(err).includes("not initialized")) {
|
|
727
|
+
await resourceApi.initialize(issue.id, { issue, state: previous });
|
|
728
|
+
await resourceApi.send(issue.id, event, sendContext);
|
|
729
|
+
} else {
|
|
730
|
+
throw err;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
} else if (plugin?.send) {
|
|
734
|
+
try {
|
|
735
|
+
await plugin.send(ISSUE_STATE_MACHINE_ID, issue.id, event, sendContext);
|
|
736
|
+
} catch (err) {
|
|
737
|
+
if (plugin.initializeEntity && String(err).includes("not found")) {
|
|
738
|
+
await plugin.initializeEntity(ISSUE_STATE_MACHINE_ID, issue.id, { issue, state: previous });
|
|
739
|
+
await plugin.send(ISSUE_STATE_MACHINE_ID, issue.id, event, sendContext);
|
|
740
|
+
} else {
|
|
741
|
+
throw err;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
} else {
|
|
745
|
+
if (previous !== targetState) {
|
|
746
|
+
const edges = getStatesFromConfig();
|
|
747
|
+
const stateTransitions = edges[previous];
|
|
748
|
+
if (!stateTransitions || !stateTransitions[event]) {
|
|
749
|
+
throw new Error(`State machine does not allow event '${event}' from '${previous}' for issue ${issue.id}.`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const stateDef = issueStateMachineConfig.stateMachines[ISSUE_STATE_MACHINE_ID].states[previous];
|
|
753
|
+
if (stateDef && "guards" in stateDef && stateDef.guards?.[event]) {
|
|
754
|
+
const guardName = stateDef.guards[event];
|
|
755
|
+
const guardFn = issueStateMachineConfig.guards[guardName];
|
|
756
|
+
if (guardFn) {
|
|
757
|
+
const allowed = await guardFn(sendContext, event, { database: null, machineId: ISSUE_STATE_MACHINE_ID, entityId: issue.id });
|
|
758
|
+
if (!allowed) {
|
|
759
|
+
throw new Error(`Guard '${guardName}' rejected event '${event}' for issue ${issue.id}.`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const targetDef = issueStateMachineConfig.stateMachines[ISSUE_STATE_MACHINE_ID].states[targetState];
|
|
764
|
+
if (targetDef && "entry" in targetDef && typeof targetDef.entry === "string") {
|
|
765
|
+
const actionName = targetDef.entry;
|
|
766
|
+
const actionFn = issueStateMachineConfig.actions[actionName];
|
|
767
|
+
if (actionFn) {
|
|
768
|
+
await actionFn(sendContext, event, { database: null, machineId: ISSUE_STATE_MACHINE_ID, entityId: issue.id });
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
issue.state = targetState;
|
|
773
|
+
issue.updatedAt = ts;
|
|
774
|
+
const note = typeof context.note === "string" ? context.note : `${event}: ${previous} \u2192 ${targetState}`;
|
|
775
|
+
issue.history.push(`[${ts}] ${note}`);
|
|
776
|
+
if (TERMINAL_STATES.has(previous) && !TERMINAL_STATES.has(targetState)) {
|
|
777
|
+
issue.terminalWeek = "";
|
|
778
|
+
}
|
|
779
|
+
markDirtyAndInvalidate(issue.id);
|
|
780
|
+
return { previousState: previous };
|
|
781
|
+
}
|
|
782
|
+
async function getIssueTransitionHistory(issueId, options) {
|
|
783
|
+
const resourceApi = getIssueResourceStateApi();
|
|
784
|
+
if (resourceApi?.history) {
|
|
785
|
+
try {
|
|
786
|
+
return await resourceApi.history(issueId, options);
|
|
787
|
+
} catch {
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
const plugin = getIssueStateMachinePlugin();
|
|
791
|
+
if (plugin?.getTransitionHistory) {
|
|
792
|
+
try {
|
|
793
|
+
return await plugin.getTransitionHistory(ISSUE_STATE_MACHINE_ID, issueId, options);
|
|
794
|
+
} catch {
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return [];
|
|
798
|
+
}
|
|
799
|
+
async function canTransitionIssue(issueId, event) {
|
|
800
|
+
const resourceApi = getIssueResourceStateApi();
|
|
801
|
+
if (resourceApi?.canTransition) {
|
|
802
|
+
try {
|
|
803
|
+
return await resourceApi.canTransition(issueId, event);
|
|
804
|
+
} catch {
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
function visualizeStateMachine() {
|
|
810
|
+
const plugin = getIssueStateMachinePlugin();
|
|
811
|
+
if (!plugin?.visualize) return null;
|
|
812
|
+
return plugin.visualize(ISSUE_STATE_MACHINE_ID);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
export {
|
|
816
|
+
computeMetrics,
|
|
817
|
+
getMetrics,
|
|
818
|
+
markIssueDirty,
|
|
819
|
+
markIssuePlanDirty,
|
|
820
|
+
markEventDirty,
|
|
821
|
+
hasDirtyState,
|
|
822
|
+
getDirtyIssueIds,
|
|
823
|
+
getDirtyEventIds,
|
|
824
|
+
snapshotAndClearDirtyIssueIds,
|
|
825
|
+
snapshotAndClearDirtyIssuePlanIds,
|
|
826
|
+
snapshotAndClearDirtyEventIds,
|
|
827
|
+
markAllIssuesDirty,
|
|
828
|
+
markAllIssuePlansDirty,
|
|
829
|
+
markAllEventsDirty,
|
|
830
|
+
setFsmEventEmitter,
|
|
831
|
+
ISSUE_STATE_MACHINE_ID,
|
|
832
|
+
issueStateMachineConfig,
|
|
833
|
+
eventToTargetState,
|
|
834
|
+
getStateMachineTransitions,
|
|
835
|
+
findIssueStateMachineTransitionPath,
|
|
836
|
+
setIssueResourceStateApi,
|
|
837
|
+
getIssueResourceStateApi,
|
|
838
|
+
setIssueStateMachinePlugin,
|
|
839
|
+
getIssueStateMachinePlugin,
|
|
840
|
+
getIssueStateMachineDefinition,
|
|
841
|
+
getIssueStateMachineInitialState,
|
|
842
|
+
executeTransition,
|
|
843
|
+
getIssueTransitionHistory,
|
|
844
|
+
canTransitionIssue,
|
|
845
|
+
visualizeStateMachine
|
|
846
|
+
};
|
|
847
|
+
//# sourceMappingURL=chunk-2G6SRDOC.js.map
|