fifony 0.1.36 → 0.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/dist/assets/{CommandPalette-CyyF04a2.js → CommandPalette-jM1LVTly.js} +1 -1
- package/app/dist/assets/{KeyboardShortcutsHelp-D71YmyfF.js → KeyboardShortcutsHelp-DuMbbWoK.js} +1 -1
- package/app/dist/assets/OnboardingWizard-CRNV-Tiy.js +1 -0
- package/app/dist/assets/{analytics.lazy-D8vsSFxh.js → analytics.lazy-BoFSI6cI.js} +1 -1
- package/app/dist/assets/index-BWB0OQnx.css +1 -0
- package/app/dist/assets/index-ChQVBIk9.js +54 -0
- package/app/dist/index.html +2 -2
- package/app/dist/service-worker.js +1 -1
- package/dist/agent/run-local.js +6 -6
- package/dist/{agent-5AEC4SL7.js → agent-B3GQHWDL.js} +7 -7
- package/dist/{chunk-OONOOWNC.js → chunk-37N5OFHM.js} +4 -2
- package/dist/{chunk-QBAR5JLY.js → chunk-4RUGXGUX.js} +43 -8
- package/dist/{chunk-GYVLPWYB.js → chunk-6BUKDXKZ.js} +12 -12
- package/dist/{chunk-LUIPCPRO.js → chunk-AZYLLJVZ.js} +44 -12
- package/dist/{chunk-O5AEQXUV.js → chunk-T2YJOZ6N.js} +2 -2
- package/dist/{chunk-A7BVAGPW.js → chunk-YJHD4BE4.js} +547 -895
- package/dist/cli.js +6 -6
- package/dist/issue-runner-PNYGUY3S.js +15 -0
- package/dist/{issue-state-machine-JCGSR5QP.js → issue-state-machine-NCD3ZFOI.js} +5 -5
- package/dist/{issues-2ENRFJHC.js → issues-IW6CFOSR.js} +7 -9
- package/dist/mcp/server.js +2 -2
- package/dist/{queue-workers-BQLDNMFQ.js → queue-workers-2NJZE6L2.js} +3 -3
- package/dist/{scheduler-5XTHGLCA.js → scheduler-YMQ3JZUU.js} +7 -7
- package/dist/{store-44KLJAXC.js → store-GZVWNJ6V.js} +7 -7
- package/dist/{workspace-U43FRPEB.js → workspace-HJEJZMTO.js} +4 -4
- package/package.json +1 -1
- package/app/dist/assets/OnboardingWizard-TLlzqU2A.js +0 -1
- package/app/dist/assets/index-Ccu8chEN.js +0 -49
- package/app/dist/assets/index-rLcPCr9E.css +0 -1
- package/dist/issue-runner-VOW7MZEK.js +0 -15
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
snapshotAndClearDirtyEventIds,
|
|
22
22
|
snapshotAndClearDirtyIssueIds,
|
|
23
23
|
snapshotAndClearDirtyIssuePlanIds
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-4RUGXGUX.js";
|
|
25
25
|
import {
|
|
26
26
|
ADAPTERS,
|
|
27
27
|
assertIssueHasGitWorktree,
|
|
@@ -49,10 +49,9 @@ import {
|
|
|
49
49
|
parseDiffStats,
|
|
50
50
|
prepareWorkspace,
|
|
51
51
|
readCodexConfig,
|
|
52
|
-
resolveAgentCommand,
|
|
53
52
|
runCommandWithTimeout,
|
|
54
53
|
runHook
|
|
55
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-AZYLLJVZ.js";
|
|
56
55
|
import {
|
|
57
56
|
appendFileTail,
|
|
58
57
|
clamp,
|
|
@@ -74,15 +73,14 @@ import {
|
|
|
74
73
|
toStringArray,
|
|
75
74
|
toStringValue,
|
|
76
75
|
withRetryBackoff
|
|
77
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-T2YJOZ6N.js";
|
|
78
77
|
import {
|
|
79
78
|
enqueue
|
|
80
|
-
} from "./chunk-
|
|
79
|
+
} from "./chunk-6BUKDXKZ.js";
|
|
81
80
|
import {
|
|
82
81
|
logger
|
|
83
82
|
} from "./chunk-DVU3CXWA.js";
|
|
84
83
|
import {
|
|
85
|
-
ALLOWED_STATES,
|
|
86
84
|
ATTACHMENTS_ROOT,
|
|
87
85
|
COMPLETED_STATES,
|
|
88
86
|
EXECUTING_STATES,
|
|
@@ -94,6 +92,7 @@ import {
|
|
|
94
92
|
FRONTEND_OFFLINE_HTML,
|
|
95
93
|
FRONTEND_SERVICE_WORKER_JS,
|
|
96
94
|
PERSIST_EVENTS_MAX,
|
|
95
|
+
QUIET_MODE,
|
|
97
96
|
S3DB_AGENT_PIPELINE_RESOURCE,
|
|
98
97
|
S3DB_AGENT_SESSION_RESOURCE,
|
|
99
98
|
S3DB_DATABASE_PATH,
|
|
@@ -109,7 +108,7 @@ import {
|
|
|
109
108
|
TARGET_ROOT,
|
|
110
109
|
TERMINAL_STATES,
|
|
111
110
|
WORKSPACE_ROOT
|
|
112
|
-
} from "./chunk-
|
|
111
|
+
} from "./chunk-37N5OFHM.js";
|
|
113
112
|
|
|
114
113
|
// src/agents/issue-runner.ts
|
|
115
114
|
import {
|
|
@@ -293,20 +292,17 @@ function getAnalytics(topN = 20) {
|
|
|
293
292
|
}
|
|
294
293
|
|
|
295
294
|
// src/domains/project.ts
|
|
296
|
-
import { execFileSync as execFileSync2
|
|
295
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
297
296
|
import { createHash } from "crypto";
|
|
298
297
|
import {
|
|
299
298
|
existsSync as existsSync13,
|
|
300
299
|
mkdirSync as mkdirSync7,
|
|
301
|
-
mkdtempSync as mkdtempSync4,
|
|
302
300
|
readdirSync as readdirSync4,
|
|
303
301
|
readFileSync as readFileSync10,
|
|
304
|
-
rmSync as rmSync5,
|
|
305
302
|
writeFileSync as writeFileSync11
|
|
306
303
|
} from "fs";
|
|
307
|
-
import { homedir as homedir3
|
|
304
|
+
import { homedir as homedir3 } from "os";
|
|
308
305
|
import { basename as basename4, dirname as dirname2, join as join16, relative as relativePath, resolve as resolve2 } from "path";
|
|
309
|
-
import { env as env3 } from "process";
|
|
310
306
|
|
|
311
307
|
// src/persistence/plugins/api-runtime-context.ts
|
|
312
308
|
var context = null;
|
|
@@ -1726,9 +1722,279 @@ async function cancelIssueCommand(input, deps) {
|
|
|
1726
1722
|
{ issue, target: "Cancelled", note: "Manual cancel requested." },
|
|
1727
1723
|
deps
|
|
1728
1724
|
);
|
|
1725
|
+
if (deps.state) {
|
|
1726
|
+
try {
|
|
1727
|
+
await cleanWorkspace(issue.id, issue, deps.state);
|
|
1728
|
+
issue.workspacePath = void 0;
|
|
1729
|
+
issue.worktreePath = void 0;
|
|
1730
|
+
} catch (error) {
|
|
1731
|
+
logger.warn({ issueId: issue.id, err: String(error) }, "[Command] Failed to clean workspace during cancel");
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1729
1734
|
deps.eventStore.addEvent(issue.id, "manual", `Manual cancel requested for ${issue.id}.`);
|
|
1730
1735
|
}
|
|
1731
1736
|
|
|
1737
|
+
// src/commands/merge-workspace.command.ts
|
|
1738
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1739
|
+
import { execSync } from "child_process";
|
|
1740
|
+
|
|
1741
|
+
// src/domains/validation.ts
|
|
1742
|
+
import { execFile } from "child_process";
|
|
1743
|
+
async function runValidationGate(issue, config) {
|
|
1744
|
+
if (!config.testCommand) return null;
|
|
1745
|
+
const cwd = issue.worktreePath ?? issue.workspacePath;
|
|
1746
|
+
if (!cwd) {
|
|
1747
|
+
logger.warn({ issueId: issue.id }, "[Validation] No workspace path \u2014 skipping gate");
|
|
1748
|
+
return null;
|
|
1749
|
+
}
|
|
1750
|
+
const command = config.testCommand;
|
|
1751
|
+
logger.info({ issueId: issue.id, command, cwd }, "[Validation] Running validation gate");
|
|
1752
|
+
return new Promise((resolve3) => {
|
|
1753
|
+
const child = execFile("sh", ["-c", command], {
|
|
1754
|
+
cwd,
|
|
1755
|
+
encoding: "utf8",
|
|
1756
|
+
timeout: 3e5,
|
|
1757
|
+
maxBuffer: 2 * 1024 * 1024
|
|
1758
|
+
}, (err, stdout, stderr) => {
|
|
1759
|
+
const combined = (stdout || "") + (stderr || "");
|
|
1760
|
+
if (!err) {
|
|
1761
|
+
logger.info({ issueId: issue.id }, "[Validation] Gate passed");
|
|
1762
|
+
resolve3({
|
|
1763
|
+
passed: true,
|
|
1764
|
+
output: combined.slice(-2048),
|
|
1765
|
+
command,
|
|
1766
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1767
|
+
});
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
logger.warn({ issueId: issue.id, exitCode: err.code }, "[Validation] Gate failed");
|
|
1771
|
+
resolve3({
|
|
1772
|
+
passed: false,
|
|
1773
|
+
output: combined.slice(-2048) || String(err).slice(0, 2048),
|
|
1774
|
+
command,
|
|
1775
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1776
|
+
});
|
|
1777
|
+
});
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// src/commands/merge-workspace.command.ts
|
|
1782
|
+
async function mergeWorkspaceCommand(input, deps) {
|
|
1783
|
+
const { issue, state, squashAlreadyApplied } = input;
|
|
1784
|
+
if (!["Approved", "Reviewing", "PendingDecision"].includes(issue.state)) {
|
|
1785
|
+
throw new Error(`Issue ${issue.identifier} is in state ${issue.state}. Merge is only allowed in Reviewing, PendingDecision, or Approved state.`);
|
|
1786
|
+
}
|
|
1787
|
+
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "merge issues");
|
|
1788
|
+
if (issue.state === "Reviewing" || issue.state === "PendingDecision") {
|
|
1789
|
+
await transitionIssueCommand(
|
|
1790
|
+
{ issue, target: "Approved", note: "Approved and merged by user." },
|
|
1791
|
+
deps
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
const wp = issue.worktreePath ?? issue.workspacePath;
|
|
1795
|
+
if (!wp || !existsSync6(wp)) {
|
|
1796
|
+
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.`);
|
|
1797
|
+
}
|
|
1798
|
+
if (issue.branchName && issue.baseBranch) {
|
|
1799
|
+
try {
|
|
1800
|
+
const stat = execSync(
|
|
1801
|
+
`git diff --stat "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
1802
|
+
{ encoding: "utf8", cwd: TARGET_ROOT, stdio: "pipe", maxBuffer: 512e3, timeout: 1e4 }
|
|
1803
|
+
);
|
|
1804
|
+
parseDiffStats(issue, stat);
|
|
1805
|
+
logger.info({ issueId: issue.id, linesAdded: issue.linesAdded, linesRemoved: issue.linesRemoved, filesChanged: issue.filesChanged }, "[Merge] Diff stats computed");
|
|
1806
|
+
} catch (err) {
|
|
1807
|
+
logger.warn({ err: String(err), issueId: issue.id }, "[Merge] Failed to compute diff stats");
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
const validation = await runValidationGate(issue, state.config);
|
|
1811
|
+
if (validation) {
|
|
1812
|
+
issue.validationResult = validation;
|
|
1813
|
+
if (!validation.passed) {
|
|
1814
|
+
throw new Error(`Validation gate failed (${validation.command}): ${validation.output.slice(0, 500)}`);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
let result;
|
|
1818
|
+
if (squashAlreadyApplied) {
|
|
1819
|
+
try {
|
|
1820
|
+
execSync("git add -A", { cwd: TARGET_ROOT, stdio: "pipe", timeout: 1e4 });
|
|
1821
|
+
execSync(
|
|
1822
|
+
`git commit -m "fifony: merge ${issue.identifier}"`,
|
|
1823
|
+
{ cwd: TARGET_ROOT, stdio: "pipe", timeout: 1e4 }
|
|
1824
|
+
);
|
|
1825
|
+
logger.info({ issueId: issue.id }, "[Merge] Committed existing test squash");
|
|
1826
|
+
} catch (err) {
|
|
1827
|
+
throw new Error(`Failed to commit test squash: ${err.stderr || err.stdout || String(err)}`);
|
|
1828
|
+
}
|
|
1829
|
+
issue.testApplied = false;
|
|
1830
|
+
result = { copied: [], deleted: [], skipped: [], conflicts: [] };
|
|
1831
|
+
} else {
|
|
1832
|
+
try {
|
|
1833
|
+
const indexStatus = execSync("git diff --cached --name-only", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
|
|
1834
|
+
const wtStatus = execSync("git diff --name-only", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
|
|
1835
|
+
if (indexStatus && !wtStatus) {
|
|
1836
|
+
execSync("git reset --hard HEAD", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
1837
|
+
logger.info({ issueId: issue.id }, "[Command] Cleared residual squash from index before merge");
|
|
1838
|
+
}
|
|
1839
|
+
} catch {
|
|
1840
|
+
}
|
|
1841
|
+
const mergeResult = mergeWorkspace(issue);
|
|
1842
|
+
result = mergeResult;
|
|
1843
|
+
}
|
|
1844
|
+
issue.mergeResult = {
|
|
1845
|
+
copied: result.copied.length,
|
|
1846
|
+
deleted: result.deleted.length,
|
|
1847
|
+
skipped: result.skipped.length,
|
|
1848
|
+
conflicts: result.conflicts.length,
|
|
1849
|
+
conflictFiles: result.conflicts.length > 0 ? result.conflicts : void 0
|
|
1850
|
+
};
|
|
1851
|
+
if (result.conflicts.length > 0) {
|
|
1852
|
+
deps.eventStore.addEvent(issue.id, "error", `Merge conflicts: ${result.conflicts.join(", ")}`);
|
|
1853
|
+
await deps.persistencePort.persistState(state);
|
|
1854
|
+
return result;
|
|
1855
|
+
}
|
|
1856
|
+
if (!issue.mergedReason) issue.mergedReason = squashAlreadyApplied ? "Approved and shipped after testing." : "Merged by user.";
|
|
1857
|
+
await transitionIssueCommand(
|
|
1858
|
+
{ issue, target: "Merged", note: `Workspace merged for ${issue.identifier}.` },
|
|
1859
|
+
deps
|
|
1860
|
+
);
|
|
1861
|
+
if (issue.workspacePath) {
|
|
1862
|
+
try {
|
|
1863
|
+
await cleanWorkspace(issue.id, issue, state);
|
|
1864
|
+
issue.workspacePath = void 0;
|
|
1865
|
+
issue.worktreePath = void 0;
|
|
1866
|
+
} catch {
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
await deps.persistencePort.persistState(state);
|
|
1870
|
+
return result;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// src/commands/push-workspace.command.ts
|
|
1874
|
+
import { execFileSync, execSync as execSync2 } from "child_process";
|
|
1875
|
+
function isGhAvailable() {
|
|
1876
|
+
try {
|
|
1877
|
+
execFileSync("gh", ["--version"], { stdio: "pipe", timeout: 5e3 });
|
|
1878
|
+
return true;
|
|
1879
|
+
} catch {
|
|
1880
|
+
return false;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
function getCompareUrl(branchName, baseBranch) {
|
|
1884
|
+
try {
|
|
1885
|
+
const remote = execSync2("git remote get-url origin", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
|
|
1886
|
+
const cleanRemote = remote.replace(/\.git$/, "");
|
|
1887
|
+
return `${cleanRemote}/compare/${baseBranch}...${branchName}`;
|
|
1888
|
+
} catch {
|
|
1889
|
+
return `(branch pushed: ${branchName})`;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
function findExistingPr(branchName) {
|
|
1893
|
+
try {
|
|
1894
|
+
const result = execFileSync(
|
|
1895
|
+
"gh",
|
|
1896
|
+
["pr", "view", branchName, "--json", "url,state", "--jq", 'select(.state == "OPEN") | .url'],
|
|
1897
|
+
{ cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe", timeout: 15e3 }
|
|
1898
|
+
).trim();
|
|
1899
|
+
return result || null;
|
|
1900
|
+
} catch {
|
|
1901
|
+
return null;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
function createPr(branchName, baseBranch, title, body) {
|
|
1905
|
+
return execFileSync(
|
|
1906
|
+
"gh",
|
|
1907
|
+
["pr", "create", "--head", branchName, "--base", baseBranch, "--title", title, "--body", body],
|
|
1908
|
+
{ cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe", timeout: 3e4 }
|
|
1909
|
+
).trim();
|
|
1910
|
+
}
|
|
1911
|
+
async function pushWorkspaceCommand(input, deps) {
|
|
1912
|
+
const { issue, state } = input;
|
|
1913
|
+
if (!["Approved", "Reviewing", "PendingDecision"].includes(issue.state)) {
|
|
1914
|
+
throw new Error(`Issue ${issue.identifier} is in state ${issue.state}. Push is only allowed in Reviewing, PendingDecision, or Approved state.`);
|
|
1915
|
+
}
|
|
1916
|
+
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "push issue branches");
|
|
1917
|
+
assertIssueHasGitWorktree(issue, "push");
|
|
1918
|
+
if (issue.testApplied) {
|
|
1919
|
+
try {
|
|
1920
|
+
execSync2("git reset --hard HEAD", { cwd: TARGET_ROOT, stdio: "pipe", timeout: 15e3 });
|
|
1921
|
+
execSync2("git clean -fd", { cwd: TARGET_ROOT, stdio: "pipe", timeout: 15e3 });
|
|
1922
|
+
issue.testApplied = false;
|
|
1923
|
+
deps.eventStore.addEvent(issue.id, "info", "Auto-reverted test squash before push.");
|
|
1924
|
+
logger.info({ issueId: issue.id }, "[Push] Auto-reverted test squash before push");
|
|
1925
|
+
} catch (err) {
|
|
1926
|
+
const msg = err.stderr || err.stdout || String(err);
|
|
1927
|
+
throw new Error(`Failed to revert test squash before push: ${msg}`);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
if (issue.state === "Reviewing" || issue.state === "PendingDecision") {
|
|
1931
|
+
await transitionIssueCommand(
|
|
1932
|
+
{ issue, target: "Approved", note: "Approved and pushed by user." },
|
|
1933
|
+
deps
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
ensureWorktreeCommitted(issue);
|
|
1937
|
+
const validation = await runValidationGate(issue, state.config);
|
|
1938
|
+
if (validation) {
|
|
1939
|
+
issue.validationResult = validation;
|
|
1940
|
+
if (!validation.passed) {
|
|
1941
|
+
throw new Error(`Validation gate failed (${validation.command}): ${validation.output.slice(0, 500)}`);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
computeDiffStats(issue);
|
|
1945
|
+
const planSummary = issue.plan?.summary ?? issue.title;
|
|
1946
|
+
let diffStat = "";
|
|
1947
|
+
try {
|
|
1948
|
+
diffStat = execSync2(
|
|
1949
|
+
`git diff --stat "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
1950
|
+
{ cwd: TARGET_ROOT, encoding: "utf8", maxBuffer: 512e3, timeout: 1e4, stdio: "pipe" }
|
|
1951
|
+
).trim();
|
|
1952
|
+
} catch {
|
|
1953
|
+
}
|
|
1954
|
+
const body = `## Summary
|
|
1955
|
+
${planSummary}
|
|
1956
|
+
|
|
1957
|
+
## Diff Stats
|
|
1958
|
+
\`\`\`
|
|
1959
|
+
${diffStat || "No diff stats available"}
|
|
1960
|
+
\`\`\`
|
|
1961
|
+
|
|
1962
|
+
*Automated by fifony*`;
|
|
1963
|
+
execSync2(`git push -u origin "${issue.branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
1964
|
+
const prBase = state.config.prBaseBranch || issue.baseBranch;
|
|
1965
|
+
const ghAvailable = isGhAvailable();
|
|
1966
|
+
let prUrl;
|
|
1967
|
+
if (!ghAvailable) {
|
|
1968
|
+
prUrl = getCompareUrl(issue.branchName, prBase);
|
|
1969
|
+
logger.info({ issueId: issue.id, prUrl }, "[Push] gh CLI not available \u2014 using compare URL");
|
|
1970
|
+
} else {
|
|
1971
|
+
const existingUrl = findExistingPr(issue.branchName);
|
|
1972
|
+
if (existingUrl) {
|
|
1973
|
+
prUrl = existingUrl;
|
|
1974
|
+
logger.info({ issueId: issue.id, prUrl }, "[Push] Existing open PR found");
|
|
1975
|
+
} else {
|
|
1976
|
+
try {
|
|
1977
|
+
prUrl = createPr(issue.branchName, prBase, issue.title, body);
|
|
1978
|
+
logger.info({ issueId: issue.id, prUrl }, "[Push] PR created");
|
|
1979
|
+
} catch (err) {
|
|
1980
|
+
const ghError = (err.stderr || err.stdout || String(err)).toString().slice(0, 500);
|
|
1981
|
+
logger.error({ issueId: issue.id, ghError }, "[Push] gh pr create failed");
|
|
1982
|
+
prUrl = getCompareUrl(issue.branchName, prBase);
|
|
1983
|
+
deps.eventStore.addEvent(issue.id, "error", `gh pr create failed: ${ghError}. Branch was pushed \u2014 use the compare URL to create the PR manually.`);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
issue.prUrl = prUrl;
|
|
1988
|
+
if (!issue.mergedReason) issue.mergedReason = "Pushed to origin and PR created.";
|
|
1989
|
+
await transitionIssueCommand(
|
|
1990
|
+
{ issue, target: "Merged", note: `Branch ${issue.branchName} pushed. PR: ${prUrl}` },
|
|
1991
|
+
deps
|
|
1992
|
+
);
|
|
1993
|
+
deps.eventStore.addEvent(issue.id, "merge", `PR created: ${prUrl}`);
|
|
1994
|
+
await deps.persistencePort.persistState(state);
|
|
1995
|
+
return { prUrl, ghAvailable };
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1732
1998
|
// src/persistence/resources/issues.resource.ts
|
|
1733
1999
|
function getIssueId(c) {
|
|
1734
2000
|
if (!c || typeof c !== "object" || !("req" in c) || !c.req || typeof c.req !== "object") {
|
|
@@ -1781,7 +2047,18 @@ async function patchIssueState(c) {
|
|
|
1781
2047
|
}
|
|
1782
2048
|
try {
|
|
1783
2049
|
const payload = await c.req.json();
|
|
1784
|
-
|
|
2050
|
+
const nextState = parseIssueState(payload.state);
|
|
2051
|
+
if (!nextState) {
|
|
2052
|
+
throw new Error(`Unsupported state: ${String(payload.state)}`);
|
|
2053
|
+
}
|
|
2054
|
+
const container = getContainer();
|
|
2055
|
+
const reason = payload.reason ? toStringValue(payload.reason) : void 0;
|
|
2056
|
+
logger.info({ issueId, identifier: issue.identifier, targetState: nextState }, "[API] POST /api/issues/:id/state");
|
|
2057
|
+
await transitionIssueCommand({
|
|
2058
|
+
issue,
|
|
2059
|
+
target: nextState,
|
|
2060
|
+
note: reason || `Manual state update: ${nextState}`
|
|
2061
|
+
}, container);
|
|
1785
2062
|
await persistState(context2.state);
|
|
1786
2063
|
return { body: { ok: true, issue } };
|
|
1787
2064
|
} catch (error) {
|
|
@@ -1798,17 +2075,41 @@ async function retryIssue(c) {
|
|
|
1798
2075
|
if (!issue) {
|
|
1799
2076
|
return { status: 404, body: { ok: false, error: "Issue not found" } };
|
|
1800
2077
|
}
|
|
2078
|
+
let feedback;
|
|
2079
|
+
try {
|
|
2080
|
+
const body = await c.req.json();
|
|
2081
|
+
if (body?.feedback) feedback = toStringValue(body.feedback);
|
|
2082
|
+
} catch {
|
|
2083
|
+
}
|
|
1801
2084
|
const container = getContainer();
|
|
2085
|
+
logger.info({ issueId, state: issue.state, lastFailedPhase: issue.lastFailedPhase, attempts: issue.attempts }, "[API] Retry \u2014 dispatching");
|
|
2086
|
+
const note = feedback ? `Rework requested for ${issue.identifier}: ${feedback.slice(0, 200)}` : `Manual retry for ${issue.identifier}.`;
|
|
1802
2087
|
if (TERMINAL_STATES.has(issue.state)) {
|
|
2088
|
+
await transitionIssueCommand({ issue, target: "Planning", note }, container);
|
|
2089
|
+
if (issue.plan?.steps?.length) {
|
|
2090
|
+
await transitionIssueCommand({ issue, target: "PendingApproval", note: "Existing plan found." }, container);
|
|
2091
|
+
await transitionIssueCommand({ issue, target: "Queued", note: "Auto-queued after plan approval." }, container);
|
|
2092
|
+
}
|
|
2093
|
+
} else if (issue.state === "Blocked" && issue.lastFailedPhase === "review") {
|
|
1803
2094
|
issue.lastError = void 0;
|
|
1804
|
-
issue.
|
|
1805
|
-
await transitionIssueCommand(
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
2095
|
+
issue.lastFailedPhase = void 0;
|
|
2096
|
+
await transitionIssueCommand({ issue, target: "Reviewing", note }, container);
|
|
2097
|
+
} else if (issue.state === "Blocked") {
|
|
2098
|
+
await transitionIssueCommand({ issue, target: "Queued", note }, container);
|
|
2099
|
+
} else if (issue.state === "Approved") {
|
|
2100
|
+
await transitionIssueCommand({ issue, target: "Planning", note }, container);
|
|
2101
|
+
if (issue.plan?.steps?.length) {
|
|
2102
|
+
await transitionIssueCommand({ issue, target: "PendingApproval", note: "Existing plan found." }, container);
|
|
2103
|
+
await transitionIssueCommand({ issue, target: "Queued", note: "Auto-queued for rework." }, container);
|
|
2104
|
+
}
|
|
2105
|
+
} else if (issue.state === "Reviewing" || issue.state === "PendingDecision") {
|
|
2106
|
+
const reworkNote = feedback || issue.lastError || "Manual rework request.";
|
|
2107
|
+
await transitionIssueCommand({ issue, target: "Queued", note: reworkNote }, container);
|
|
2108
|
+
} else if (issue.state === "PendingApproval") {
|
|
2109
|
+
await transitionIssueCommand({ issue, target: "Queued", note }, container);
|
|
1809
2110
|
} else {
|
|
1810
|
-
issue.nextRetryAt = void 0;
|
|
1811
2111
|
issue.lastError = void 0;
|
|
2112
|
+
issue.nextRetryAt = void 0;
|
|
1812
2113
|
issue.updatedAt = now();
|
|
1813
2114
|
}
|
|
1814
2115
|
addEvent(context2.state, issue.id, "manual", `Manual retry requested for ${issue.id}.`);
|
|
@@ -1841,19 +2142,57 @@ async function cancelIssue(c) {
|
|
|
1841
2142
|
}
|
|
1842
2143
|
await cancelIssueCommand(
|
|
1843
2144
|
{ issue },
|
|
1844
|
-
getContainer()
|
|
2145
|
+
{ ...getContainer(), state: context2.state }
|
|
1845
2146
|
);
|
|
1846
2147
|
addEvent(context2.state, issue.id, "manual", `Manual cancel requested for ${issue.id}.`);
|
|
1847
2148
|
await persistState(context2.state);
|
|
1848
2149
|
return { body: { ok: true, issue } };
|
|
1849
2150
|
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
2151
|
+
async function approveAndMerge(c) {
|
|
2152
|
+
const context2 = getApiRuntimeContextOrThrow();
|
|
2153
|
+
const issueId = getIssueId(c);
|
|
2154
|
+
if (!issueId) {
|
|
2155
|
+
return { status: 400, body: { ok: false, error: "Issue id is required." } };
|
|
2156
|
+
}
|
|
2157
|
+
const issue = findIssue(context2.state, issueId);
|
|
2158
|
+
if (!issue) {
|
|
2159
|
+
return { status: 404, body: { ok: false, error: "Issue not found" } };
|
|
2160
|
+
}
|
|
2161
|
+
if (issue.state !== "PendingDecision" && issue.state !== "Reviewing" && issue.state !== "Approved") {
|
|
2162
|
+
return { status: 400, body: { ok: false, error: `Cannot approve-and-merge from state ${issue.state}. Expected PendingDecision, Reviewing, or Approved.` } };
|
|
2163
|
+
}
|
|
2164
|
+
try {
|
|
2165
|
+
const container = getContainer();
|
|
2166
|
+
const mergeMode = context2.state.config.mergeMode;
|
|
2167
|
+
logger.info({ issueId, state: issue.state, testApplied: issue.testApplied, mergeMode }, "[API] POST /api/issues/:id/approve-and-merge");
|
|
2168
|
+
if (mergeMode === "push-pr") {
|
|
2169
|
+
if (issue.state !== "Approved") {
|
|
2170
|
+
await transitionIssueCommand(
|
|
2171
|
+
{ issue, target: "Approved", note: "Approved for push-pr." },
|
|
2172
|
+
container
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
await pushWorkspaceCommand({ issue }, { ...container, state: context2.state });
|
|
2176
|
+
} else {
|
|
2177
|
+
await mergeWorkspaceCommand(
|
|
2178
|
+
{ issue, squashAlreadyApplied: issue.testApplied ?? false },
|
|
2179
|
+
{ ...container, state: context2.state }
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
2182
|
+
addEvent(context2.state, issue.id, "manual", `Approved and ${mergeMode === "push-pr" ? "pushed PR for" : "merged"} ${issue.identifier}.`);
|
|
2183
|
+
await persistState(context2.state);
|
|
2184
|
+
return { body: { ok: true, issue } };
|
|
2185
|
+
} catch (error) {
|
|
2186
|
+
return { status: 409, body: { ok: false, error: error instanceof Error ? error.message : String(error) } };
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
var issues_resource_default = {
|
|
2190
|
+
name: S3DB_ISSUE_RESOURCE,
|
|
2191
|
+
attributes: {
|
|
2192
|
+
id: "string|required",
|
|
2193
|
+
identifier: "string|required",
|
|
2194
|
+
title: "string|required",
|
|
2195
|
+
description: "string|optional",
|
|
1857
2196
|
state: "string|required",
|
|
1858
2197
|
branchName: "string|optional",
|
|
1859
2198
|
url: "string|optional",
|
|
@@ -1886,6 +2225,7 @@ var issues_resource_default = {
|
|
|
1886
2225
|
commandOutputTail: "string|optional",
|
|
1887
2226
|
terminalWeek: "string|optional",
|
|
1888
2227
|
usage: "json|optional",
|
|
2228
|
+
testApplied: "boolean|optional",
|
|
1889
2229
|
tokenUsage: "json|optional",
|
|
1890
2230
|
tokensByPhase: "json|optional",
|
|
1891
2231
|
tokensByModel: "json|optional",
|
|
@@ -1965,6 +2305,13 @@ var issues_resource_default = {
|
|
|
1965
2305
|
return c.json(result.body, result.status);
|
|
1966
2306
|
}
|
|
1967
2307
|
return result.body;
|
|
2308
|
+
},
|
|
2309
|
+
"POST /:id/approve-and-merge": async (c) => {
|
|
2310
|
+
const result = await approveAndMerge(c);
|
|
2311
|
+
if (result.status) {
|
|
2312
|
+
return c.json(result.body, result.status);
|
|
2313
|
+
}
|
|
2314
|
+
return result.body;
|
|
1968
2315
|
}
|
|
1969
2316
|
}
|
|
1970
2317
|
};
|
|
@@ -2353,13 +2700,13 @@ function hasTerminalQueue(state) {
|
|
|
2353
2700
|
}
|
|
2354
2701
|
|
|
2355
2702
|
// src/agents/providers-usage.ts
|
|
2356
|
-
import { execFile } from "child_process";
|
|
2703
|
+
import { execFile as execFile2 } from "child_process";
|
|
2357
2704
|
import { promisify } from "util";
|
|
2358
|
-
import { existsSync as
|
|
2705
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5, readdirSync as readdirSync2, realpathSync } from "fs";
|
|
2359
2706
|
import { join as join8, dirname } from "path";
|
|
2360
2707
|
import { homedir as homedir2 } from "os";
|
|
2361
2708
|
import { env } from "process";
|
|
2362
|
-
var execFileAsync = promisify(
|
|
2709
|
+
var execFileAsync = promisify(execFile2);
|
|
2363
2710
|
async function whichExists(cmd) {
|
|
2364
2711
|
try {
|
|
2365
2712
|
await execFileAsync("which", [cmd], { encoding: "utf8", timeout: 3e3 });
|
|
@@ -2394,7 +2741,7 @@ function resolveCodexHomeCandidates() {
|
|
|
2394
2741
|
}
|
|
2395
2742
|
function resolveCodexDir() {
|
|
2396
2743
|
for (const candidate of resolveCodexHomeCandidates()) {
|
|
2397
|
-
if (
|
|
2744
|
+
if (existsSync7(candidate)) {
|
|
2398
2745
|
return candidate;
|
|
2399
2746
|
}
|
|
2400
2747
|
}
|
|
@@ -2402,7 +2749,7 @@ function resolveCodexDir() {
|
|
|
2402
2749
|
}
|
|
2403
2750
|
function findLatestCodexDb(codexDir) {
|
|
2404
2751
|
const explicit = join8(codexDir, "state_5.sqlite");
|
|
2405
|
-
if (
|
|
2752
|
+
if (existsSync7(explicit)) return explicit;
|
|
2406
2753
|
const candidates = readdirSync2(codexDir).filter((name) => name.startsWith("state_") && name.endsWith(".sqlite")).sort().reverse();
|
|
2407
2754
|
if (candidates.length === 0) return null;
|
|
2408
2755
|
return join8(codexDir, candidates[0]);
|
|
@@ -2467,7 +2814,7 @@ function resolveClaudePlanKey(displayName) {
|
|
|
2467
2814
|
async function collectClaudeUsage() {
|
|
2468
2815
|
const home = homedir2();
|
|
2469
2816
|
const claudeDir = join8(home, ".claude");
|
|
2470
|
-
if (!
|
|
2817
|
+
if (!existsSync7(claudeDir)) return null;
|
|
2471
2818
|
const available = await whichExists("claude");
|
|
2472
2819
|
const projectsDir = join8(claudeDir, "projects");
|
|
2473
2820
|
let totalInputTokens = 0;
|
|
@@ -2488,7 +2835,7 @@ async function collectClaudeUsage() {
|
|
|
2488
2835
|
const weekMs = weekStart.getTime();
|
|
2489
2836
|
const last5hStart = computeLastHoursStart(5);
|
|
2490
2837
|
const last5hMs = last5hStart.getTime();
|
|
2491
|
-
if (
|
|
2838
|
+
if (existsSync7(projectsDir)) {
|
|
2492
2839
|
try {
|
|
2493
2840
|
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true });
|
|
2494
2841
|
for (const dir of projectDirs) {
|
|
@@ -2570,7 +2917,7 @@ async function collectClaudeUsage() {
|
|
|
2570
2917
|
let resetInfo = "Weekly reset (every Monday 00:00 UTC)";
|
|
2571
2918
|
let currentModel = "";
|
|
2572
2919
|
const settingsPath = join8(claudeDir, "settings.json");
|
|
2573
|
-
if (
|
|
2920
|
+
if (existsSync7(settingsPath)) {
|
|
2574
2921
|
try {
|
|
2575
2922
|
const settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
|
|
2576
2923
|
if (settings.plan === "max" || settings.plan === "max5x") {
|
|
@@ -2694,7 +3041,7 @@ function aggregateCodexSessionUsageFromJsonl(lines) {
|
|
|
2694
3041
|
}
|
|
2695
3042
|
function collectCodexSessionUsagesFromJsonl(codexDir) {
|
|
2696
3043
|
const sessionsDir = join8(codexDir, "sessions");
|
|
2697
|
-
if (!
|
|
3044
|
+
if (!existsSync7(sessionsDir)) return [];
|
|
2698
3045
|
const stack = [sessionsDir];
|
|
2699
3046
|
const usageByFile = [];
|
|
2700
3047
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2734,7 +3081,7 @@ async function collectCodexUsage() {
|
|
|
2734
3081
|
const models = [];
|
|
2735
3082
|
const modelsCachePath = join8(codexDir, "models_cache.json");
|
|
2736
3083
|
let currentModel = "";
|
|
2737
|
-
if (
|
|
3084
|
+
if (existsSync7(modelsCachePath)) {
|
|
2738
3085
|
try {
|
|
2739
3086
|
const cache = JSON.parse(readFileSync5(modelsCachePath, "utf8"));
|
|
2740
3087
|
for (const m of cache.models || []) {
|
|
@@ -2748,7 +3095,7 @@ async function collectCodexUsage() {
|
|
|
2748
3095
|
}
|
|
2749
3096
|
}
|
|
2750
3097
|
const configPath = join8(codexDir, "config.toml");
|
|
2751
|
-
if (
|
|
3098
|
+
if (existsSync7(configPath)) {
|
|
2752
3099
|
try {
|
|
2753
3100
|
const configContent = readFileSync5(configPath, "utf8");
|
|
2754
3101
|
const modelMatch = configContent.match(/^model\s*=\s*"([^"]+)"/m);
|
|
@@ -3011,7 +3358,7 @@ function aggregateGeminiSessionUsageFromJson(content) {
|
|
|
3011
3358
|
}
|
|
3012
3359
|
function collectGeminiSessionUsages() {
|
|
3013
3360
|
const geminiTmp = join8(homedir2(), ".gemini", "tmp");
|
|
3014
|
-
if (!
|
|
3361
|
+
if (!existsSync7(geminiTmp)) return [];
|
|
3015
3362
|
const usages = [];
|
|
3016
3363
|
let entries = [];
|
|
3017
3364
|
try {
|
|
@@ -3022,7 +3369,7 @@ function collectGeminiSessionUsages() {
|
|
|
3022
3369
|
for (const profile of entries) {
|
|
3023
3370
|
if (!profile.isDirectory()) continue;
|
|
3024
3371
|
const chatsDir = join8(geminiTmp, profile.name, "chats");
|
|
3025
|
-
if (!
|
|
3372
|
+
if (!existsSync7(chatsDir)) continue;
|
|
3026
3373
|
let sessions = [];
|
|
3027
3374
|
try {
|
|
3028
3375
|
sessions = readdirSync2(chatsDir).filter((name) => name.startsWith("session-") && (name.endsWith(".json") || name.endsWith(".jsonl")));
|
|
@@ -3052,7 +3399,7 @@ async function collectGeminiUsage() {
|
|
|
3052
3399
|
}
|
|
3053
3400
|
let account = null;
|
|
3054
3401
|
const accountsPath = join8(homedir2(), ".gemini", "google_accounts.json");
|
|
3055
|
-
if (
|
|
3402
|
+
if (existsSync7(accountsPath)) {
|
|
3056
3403
|
try {
|
|
3057
3404
|
const data = JSON.parse(readFileSync5(accountsPath, "utf8"));
|
|
3058
3405
|
if (typeof data.active === "string" && data.active.includes("@")) {
|
|
@@ -3070,7 +3417,7 @@ async function collectGeminiUsage() {
|
|
|
3070
3417
|
const { stdout: binPath } = await execFileAsync("which", ["gemini"], { encoding: "utf8", timeout: 3e3 });
|
|
3071
3418
|
const realBin = realpathSync(binPath.trim());
|
|
3072
3419
|
const modelsPath = join8(dirname(dirname(realBin)), "node_modules", "@google", "gemini-cli-core", "dist", "src", "config", "models.js");
|
|
3073
|
-
if (
|
|
3420
|
+
if (existsSync7(modelsPath)) {
|
|
3074
3421
|
const content = readFileSync5(modelsPath, "utf8");
|
|
3075
3422
|
const regex = /export const ([A-Z0-9_]+)\s*=\s*'(gemini-[^']+)';/g;
|
|
3076
3423
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3090,7 +3437,7 @@ async function collectGeminiUsage() {
|
|
|
3090
3437
|
}
|
|
3091
3438
|
let currentModel = "";
|
|
3092
3439
|
const settingsPath = join8(homedir2(), ".gemini", "settings.json");
|
|
3093
|
-
if (
|
|
3440
|
+
if (existsSync7(settingsPath)) {
|
|
3094
3441
|
try {
|
|
3095
3442
|
const settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
|
|
3096
3443
|
if (typeof settings.model === "string" && settings.model.trim()) {
|
|
@@ -3289,286 +3636,6 @@ async function replanIssueCommand(input, deps) {
|
|
|
3289
3636
|
deps.eventStore.addEvent(issue.id, "manual", `Replan requested for ${issue.identifier} \u2014 now at plan v${issue.planVersion}.`);
|
|
3290
3637
|
}
|
|
3291
3638
|
|
|
3292
|
-
// src/commands/request-rework.command.ts
|
|
3293
|
-
async function requestReworkCommand(input, deps) {
|
|
3294
|
-
const { issue, reviewerFeedback, note } = input;
|
|
3295
|
-
if (issue.state !== "Reviewing" && issue.state !== "PendingDecision") {
|
|
3296
|
-
throw new Error(
|
|
3297
|
-
`requestReworkCommand requires Reviewing or PendingDecision state, got ${issue.state}.`
|
|
3298
|
-
);
|
|
3299
|
-
}
|
|
3300
|
-
issue.lastError = reviewerFeedback;
|
|
3301
|
-
issue.lastFailedPhase = "review";
|
|
3302
|
-
issue.attempts += 1;
|
|
3303
|
-
if (issue.state === "Reviewing") {
|
|
3304
|
-
await transitionIssueCommand(
|
|
3305
|
-
{ issue, target: "PendingDecision", note: `Reviewer completed for ${issue.identifier}.` },
|
|
3306
|
-
deps
|
|
3307
|
-
);
|
|
3308
|
-
}
|
|
3309
|
-
await transitionIssueCommand(
|
|
3310
|
-
{ issue, target: "Queued", note: note ?? `Reviewer requested rework for ${issue.identifier}.` },
|
|
3311
|
-
deps
|
|
3312
|
-
);
|
|
3313
|
-
deps.eventStore.addEvent(
|
|
3314
|
-
issue.id,
|
|
3315
|
-
"runner",
|
|
3316
|
-
`Issue ${issue.identifier} sent back for rework by reviewer.`
|
|
3317
|
-
);
|
|
3318
|
-
}
|
|
3319
|
-
|
|
3320
|
-
// src/commands/merge-workspace.command.ts
|
|
3321
|
-
import { existsSync as existsSync7 } from "fs";
|
|
3322
|
-
import { execSync } from "child_process";
|
|
3323
|
-
|
|
3324
|
-
// src/domains/validation.ts
|
|
3325
|
-
import { execFile as execFile2 } from "child_process";
|
|
3326
|
-
async function runValidationGate(issue, config) {
|
|
3327
|
-
if (!config.testCommand) return null;
|
|
3328
|
-
const cwd = issue.worktreePath ?? issue.workspacePath;
|
|
3329
|
-
if (!cwd) {
|
|
3330
|
-
logger.warn({ issueId: issue.id }, "[Validation] No workspace path \u2014 skipping gate");
|
|
3331
|
-
return null;
|
|
3332
|
-
}
|
|
3333
|
-
const command = config.testCommand;
|
|
3334
|
-
logger.info({ issueId: issue.id, command, cwd }, "[Validation] Running validation gate");
|
|
3335
|
-
return new Promise((resolve3) => {
|
|
3336
|
-
const child = execFile2("sh", ["-c", command], {
|
|
3337
|
-
cwd,
|
|
3338
|
-
encoding: "utf8",
|
|
3339
|
-
timeout: 3e5,
|
|
3340
|
-
maxBuffer: 2 * 1024 * 1024
|
|
3341
|
-
}, (err, stdout, stderr) => {
|
|
3342
|
-
const combined = (stdout || "") + (stderr || "");
|
|
3343
|
-
if (!err) {
|
|
3344
|
-
logger.info({ issueId: issue.id }, "[Validation] Gate passed");
|
|
3345
|
-
resolve3({
|
|
3346
|
-
passed: true,
|
|
3347
|
-
output: combined.slice(-2048),
|
|
3348
|
-
command,
|
|
3349
|
-
ranAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3350
|
-
});
|
|
3351
|
-
return;
|
|
3352
|
-
}
|
|
3353
|
-
logger.warn({ issueId: issue.id, exitCode: err.code }, "[Validation] Gate failed");
|
|
3354
|
-
resolve3({
|
|
3355
|
-
passed: false,
|
|
3356
|
-
output: combined.slice(-2048) || String(err).slice(0, 2048),
|
|
3357
|
-
command,
|
|
3358
|
-
ranAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3359
|
-
});
|
|
3360
|
-
});
|
|
3361
|
-
});
|
|
3362
|
-
}
|
|
3363
|
-
|
|
3364
|
-
// src/commands/merge-workspace.command.ts
|
|
3365
|
-
async function mergeWorkspaceCommand(input, deps) {
|
|
3366
|
-
const { issue, state } = input;
|
|
3367
|
-
if (!["Approved", "Reviewing", "PendingDecision"].includes(issue.state)) {
|
|
3368
|
-
throw new Error(`Issue ${issue.identifier} is in state ${issue.state}. Merge is only allowed in Reviewing, PendingDecision, or Approved state.`);
|
|
3369
|
-
}
|
|
3370
|
-
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "merge issues");
|
|
3371
|
-
if (issue.state === "Reviewing" || issue.state === "PendingDecision") {
|
|
3372
|
-
await transitionIssueCommand(
|
|
3373
|
-
{ issue, target: "Approved", note: "Approved and merged by user." },
|
|
3374
|
-
deps
|
|
3375
|
-
);
|
|
3376
|
-
}
|
|
3377
|
-
const wp = issue.worktreePath ?? issue.workspacePath;
|
|
3378
|
-
if (!wp || !existsSync7(wp)) {
|
|
3379
|
-
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.`);
|
|
3380
|
-
}
|
|
3381
|
-
if (issue.branchName && issue.baseBranch) {
|
|
3382
|
-
try {
|
|
3383
|
-
const stat = execSync(
|
|
3384
|
-
`git diff --stat "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
3385
|
-
{ encoding: "utf8", cwd: TARGET_ROOT, stdio: "pipe", maxBuffer: 512e3, timeout: 1e4 }
|
|
3386
|
-
);
|
|
3387
|
-
parseDiffStats(issue, stat);
|
|
3388
|
-
logger.info({ issueId: issue.id, linesAdded: issue.linesAdded, linesRemoved: issue.linesRemoved, filesChanged: issue.filesChanged }, "[Merge] Diff stats computed");
|
|
3389
|
-
} catch (err) {
|
|
3390
|
-
logger.warn({ err: String(err), issueId: issue.id }, "[Merge] Failed to compute diff stats");
|
|
3391
|
-
}
|
|
3392
|
-
}
|
|
3393
|
-
try {
|
|
3394
|
-
const indexStatus = execSync("git diff --cached --name-only", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
|
|
3395
|
-
const wtStatus = execSync("git diff --name-only", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
|
|
3396
|
-
if (indexStatus && !wtStatus) {
|
|
3397
|
-
execSync("git reset --hard HEAD", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
3398
|
-
logger.info({ issueId: issue.id }, "[Command] Cleared residual squash from index before merge");
|
|
3399
|
-
}
|
|
3400
|
-
} catch {
|
|
3401
|
-
}
|
|
3402
|
-
const validation = await runValidationGate(issue, state.config);
|
|
3403
|
-
if (validation) {
|
|
3404
|
-
issue.validationResult = validation;
|
|
3405
|
-
if (!validation.passed) {
|
|
3406
|
-
throw new Error(`Validation gate failed (${validation.command}): ${validation.output.slice(0, 500)}`);
|
|
3407
|
-
}
|
|
3408
|
-
}
|
|
3409
|
-
const result = mergeWorkspace(issue);
|
|
3410
|
-
issue.mergeResult = {
|
|
3411
|
-
copied: result.copied.length,
|
|
3412
|
-
deleted: result.deleted.length,
|
|
3413
|
-
skipped: result.skipped.length,
|
|
3414
|
-
conflicts: result.conflicts.length,
|
|
3415
|
-
conflictFiles: result.conflicts.length > 0 ? result.conflicts : void 0
|
|
3416
|
-
};
|
|
3417
|
-
if (result.conflicts.length > 0) {
|
|
3418
|
-
deps.eventStore.addEvent(issue.id, "error", `Merge conflicts: ${result.conflicts.join(", ")}`);
|
|
3419
|
-
await deps.persistencePort.persistState(state);
|
|
3420
|
-
return result;
|
|
3421
|
-
}
|
|
3422
|
-
if (!issue.mergedReason) issue.mergedReason = "Merged by user via PreviewModal.";
|
|
3423
|
-
await transitionIssueCommand(
|
|
3424
|
-
{ issue, target: "Merged", note: `Workspace merged: ${result.copied.length} file(s) copied, ${result.deleted.length} deleted.` },
|
|
3425
|
-
deps
|
|
3426
|
-
);
|
|
3427
|
-
if (issue.workspacePath) {
|
|
3428
|
-
try {
|
|
3429
|
-
await cleanWorkspace(issue.id, issue, state);
|
|
3430
|
-
issue.workspacePath = void 0;
|
|
3431
|
-
issue.worktreePath = void 0;
|
|
3432
|
-
} catch {
|
|
3433
|
-
}
|
|
3434
|
-
}
|
|
3435
|
-
await deps.persistencePort.persistState(state);
|
|
3436
|
-
return result;
|
|
3437
|
-
}
|
|
3438
|
-
|
|
3439
|
-
// src/commands/push-workspace.command.ts
|
|
3440
|
-
import { execFileSync, execSync as execSync2 } from "child_process";
|
|
3441
|
-
function isGhAvailable() {
|
|
3442
|
-
try {
|
|
3443
|
-
execFileSync("gh", ["--version"], { stdio: "pipe", timeout: 5e3 });
|
|
3444
|
-
return true;
|
|
3445
|
-
} catch {
|
|
3446
|
-
return false;
|
|
3447
|
-
}
|
|
3448
|
-
}
|
|
3449
|
-
function getCompareUrl(branchName, baseBranch) {
|
|
3450
|
-
try {
|
|
3451
|
-
const remote = execSync2("git remote get-url origin", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
|
|
3452
|
-
const cleanRemote = remote.replace(/\.git$/, "");
|
|
3453
|
-
return `${cleanRemote}/compare/${baseBranch}...${branchName}`;
|
|
3454
|
-
} catch {
|
|
3455
|
-
return `(branch pushed: ${branchName})`;
|
|
3456
|
-
}
|
|
3457
|
-
}
|
|
3458
|
-
function findExistingPr(branchName) {
|
|
3459
|
-
try {
|
|
3460
|
-
const result = execFileSync(
|
|
3461
|
-
"gh",
|
|
3462
|
-
["pr", "view", branchName, "--json", "url,state", "--jq", 'select(.state == "OPEN") | .url'],
|
|
3463
|
-
{ cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe", timeout: 15e3 }
|
|
3464
|
-
).trim();
|
|
3465
|
-
return result || null;
|
|
3466
|
-
} catch {
|
|
3467
|
-
return null;
|
|
3468
|
-
}
|
|
3469
|
-
}
|
|
3470
|
-
function createPr(branchName, baseBranch, title, body) {
|
|
3471
|
-
return execFileSync(
|
|
3472
|
-
"gh",
|
|
3473
|
-
["pr", "create", "--head", branchName, "--base", baseBranch, "--title", title, "--body", body],
|
|
3474
|
-
{ cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe", timeout: 3e4 }
|
|
3475
|
-
).trim();
|
|
3476
|
-
}
|
|
3477
|
-
async function pushWorkspaceCommand(input, deps) {
|
|
3478
|
-
const { issue, state } = input;
|
|
3479
|
-
if (!["Approved", "Reviewing", "PendingDecision"].includes(issue.state)) {
|
|
3480
|
-
throw new Error(`Issue ${issue.identifier} is in state ${issue.state}. Push is only allowed in Reviewing, PendingDecision, or Approved state.`);
|
|
3481
|
-
}
|
|
3482
|
-
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "push issue branches");
|
|
3483
|
-
assertIssueHasGitWorktree(issue, "push");
|
|
3484
|
-
if (issue.state === "Reviewing" || issue.state === "PendingDecision") {
|
|
3485
|
-
await transitionIssueCommand(
|
|
3486
|
-
{ issue, target: "Approved", note: "Approved and pushed by user." },
|
|
3487
|
-
deps
|
|
3488
|
-
);
|
|
3489
|
-
}
|
|
3490
|
-
ensureWorktreeCommitted(issue);
|
|
3491
|
-
const validation = await runValidationGate(issue, state.config);
|
|
3492
|
-
if (validation) {
|
|
3493
|
-
issue.validationResult = validation;
|
|
3494
|
-
if (!validation.passed) {
|
|
3495
|
-
throw new Error(`Validation gate failed (${validation.command}): ${validation.output.slice(0, 500)}`);
|
|
3496
|
-
}
|
|
3497
|
-
}
|
|
3498
|
-
computeDiffStats(issue);
|
|
3499
|
-
const planSummary = issue.plan?.summary ?? issue.title;
|
|
3500
|
-
let diffStat = "";
|
|
3501
|
-
try {
|
|
3502
|
-
diffStat = execSync2(
|
|
3503
|
-
`git diff --stat "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
3504
|
-
{ cwd: TARGET_ROOT, encoding: "utf8", maxBuffer: 512e3, timeout: 1e4, stdio: "pipe" }
|
|
3505
|
-
).trim();
|
|
3506
|
-
} catch {
|
|
3507
|
-
}
|
|
3508
|
-
const body = `## Summary
|
|
3509
|
-
${planSummary}
|
|
3510
|
-
|
|
3511
|
-
## Diff Stats
|
|
3512
|
-
\`\`\`
|
|
3513
|
-
${diffStat || "No diff stats available"}
|
|
3514
|
-
\`\`\`
|
|
3515
|
-
|
|
3516
|
-
*Automated by fifony*`;
|
|
3517
|
-
execSync2(`git push -u origin "${issue.branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
3518
|
-
const prBase = state.config.prBaseBranch || issue.baseBranch;
|
|
3519
|
-
const ghAvailable = isGhAvailable();
|
|
3520
|
-
let prUrl;
|
|
3521
|
-
if (!ghAvailable) {
|
|
3522
|
-
prUrl = getCompareUrl(issue.branchName, prBase);
|
|
3523
|
-
logger.info({ issueId: issue.id, prUrl }, "[Push] gh CLI not available \u2014 using compare URL");
|
|
3524
|
-
} else {
|
|
3525
|
-
const existingUrl = findExistingPr(issue.branchName);
|
|
3526
|
-
if (existingUrl) {
|
|
3527
|
-
prUrl = existingUrl;
|
|
3528
|
-
logger.info({ issueId: issue.id, prUrl }, "[Push] Existing open PR found");
|
|
3529
|
-
} else {
|
|
3530
|
-
try {
|
|
3531
|
-
prUrl = createPr(issue.branchName, prBase, issue.title, body);
|
|
3532
|
-
logger.info({ issueId: issue.id, prUrl }, "[Push] PR created");
|
|
3533
|
-
} catch (err) {
|
|
3534
|
-
const ghError = (err.stderr || err.stdout || String(err)).toString().slice(0, 500);
|
|
3535
|
-
logger.error({ issueId: issue.id, ghError }, "[Push] gh pr create failed");
|
|
3536
|
-
prUrl = getCompareUrl(issue.branchName, prBase);
|
|
3537
|
-
deps.eventStore.addEvent(issue.id, "error", `gh pr create failed: ${ghError}. Branch was pushed \u2014 use the compare URL to create the PR manually.`);
|
|
3538
|
-
}
|
|
3539
|
-
}
|
|
3540
|
-
}
|
|
3541
|
-
issue.prUrl = prUrl;
|
|
3542
|
-
if (!issue.mergedReason) issue.mergedReason = "Pushed to origin and PR created.";
|
|
3543
|
-
await transitionIssueCommand(
|
|
3544
|
-
{ issue, target: "Merged", note: `Branch ${issue.branchName} pushed. PR: ${prUrl}` },
|
|
3545
|
-
deps
|
|
3546
|
-
);
|
|
3547
|
-
deps.eventStore.addEvent(issue.id, "merge", `PR created: ${prUrl}`);
|
|
3548
|
-
await deps.persistencePort.persistState(state);
|
|
3549
|
-
return { prUrl, ghAvailable };
|
|
3550
|
-
}
|
|
3551
|
-
|
|
3552
|
-
// src/commands/retry-execution.command.ts
|
|
3553
|
-
async function retryExecutionCommand(input, deps) {
|
|
3554
|
-
const { issue, note } = input;
|
|
3555
|
-
if (issue.state !== "Blocked") {
|
|
3556
|
-
throw new Error(
|
|
3557
|
-
`retryExecutionCommand requires Blocked state, got ${issue.state}. Use replanIssueCommand for re-planning or the generic /retry endpoint for other states.`
|
|
3558
|
-
);
|
|
3559
|
-
}
|
|
3560
|
-
issue.attempts += 1;
|
|
3561
|
-
await transitionIssueCommand(
|
|
3562
|
-
{ issue, target: "Queued", note: note ?? `Retry execution for ${issue.identifier} (attempt ${issue.attempts}).` },
|
|
3563
|
-
deps
|
|
3564
|
-
);
|
|
3565
|
-
deps.eventStore.addEvent(
|
|
3566
|
-
issue.id,
|
|
3567
|
-
"manual",
|
|
3568
|
-
`Execution retry requested for ${issue.identifier} \u2014 re-queued from Blocked.`
|
|
3569
|
-
);
|
|
3570
|
-
}
|
|
3571
|
-
|
|
3572
3639
|
// src/routes/state.ts
|
|
3573
3640
|
function getStateQuery(state, showAll = false) {
|
|
3574
3641
|
let issues = state.issues;
|
|
@@ -3616,158 +3683,30 @@ function registerStateRoutes(app, state) {
|
|
|
3616
3683
|
const providers = detectAvailableProviders();
|
|
3617
3684
|
return c.json({ providers });
|
|
3618
3685
|
});
|
|
3619
|
-
app.get("/api/parallelism", async (c) => {
|
|
3620
|
-
return c.json(analyzeParallelizability(state.issues));
|
|
3621
|
-
});
|
|
3622
|
-
app.get("/api/providers/:slug/usage", async (c) => {
|
|
3623
|
-
const provider = c.req.param("slug") || "";
|
|
3624
|
-
try {
|
|
3625
|
-
const usage = await collectProviderUsage(provider);
|
|
3626
|
-
return c.json({
|
|
3627
|
-
providers: usage ? [usage] : [],
|
|
3628
|
-
collectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3629
|
-
});
|
|
3630
|
-
} catch (error) {
|
|
3631
|
-
logger.error({ err: error, provider }, "Failed to collect provider usage");
|
|
3632
|
-
return c.json({ providers: [] }, 500);
|
|
3633
|
-
}
|
|
3634
|
-
});
|
|
3635
|
-
app.get("/api/providers/usage", async (c) => {
|
|
3636
|
-
try {
|
|
3637
|
-
const usage = await collectProvidersUsage();
|
|
3638
|
-
return c.json(usage);
|
|
3639
|
-
} catch (error) {
|
|
3640
|
-
logger.error({ err: error }, "Failed to collect providers usage");
|
|
3641
|
-
return c.json({ providers: [] }, 500);
|
|
3642
|
-
}
|
|
3643
|
-
});
|
|
3644
|
-
app.post("/api/issues/create", async (c) => {
|
|
3645
|
-
try {
|
|
3646
|
-
const payload = await c.req.json();
|
|
3647
|
-
logger.info({ title: (payload.title ?? "").toString().slice(0, 80) }, "[API] POST /api/issues/create");
|
|
3648
|
-
const container = getContainer();
|
|
3649
|
-
const result = await createIssueCommand({ payload, state }, container);
|
|
3650
|
-
return c.json({ ok: true, issue: result.issue }, 201);
|
|
3651
|
-
} catch (error) {
|
|
3652
|
-
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
3653
|
-
}
|
|
3654
|
-
});
|
|
3655
|
-
app.post("/api/issues/:id/state", async (c) => {
|
|
3656
|
-
const issueId = parseIssue(c);
|
|
3657
|
-
if (!issueId) {
|
|
3658
|
-
return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
3659
|
-
}
|
|
3660
|
-
const issue = findIssue(state, issueId);
|
|
3661
|
-
if (!issue) {
|
|
3662
|
-
return c.json({ ok: false, error: "Issue not found" }, 404);
|
|
3663
|
-
}
|
|
3664
|
-
try {
|
|
3665
|
-
const payload = await c.req.json();
|
|
3666
|
-
logger.info({ issueId, identifier: issue.identifier, targetState: payload.state }, "[API] POST /api/issues/:id/state");
|
|
3667
|
-
const nextState = parseIssueState(payload.state);
|
|
3668
|
-
if (!nextState) {
|
|
3669
|
-
throw new Error(`Unsupported state: ${String(payload.state)}`);
|
|
3670
|
-
}
|
|
3671
|
-
if (nextState === "Running" && issue.state !== "Queued") {
|
|
3672
|
-
return c.json({ ok: false, error: "Manual transition to Running is only supported from Queued." }, 400);
|
|
3673
|
-
}
|
|
3674
|
-
const container = getContainer();
|
|
3675
|
-
await transitionIssueCommand({ issue, target: nextState, note: `Manual state update: ${nextState}` }, container);
|
|
3676
|
-
if (nextState === "Running") {
|
|
3677
|
-
await enqueue(issue, "execute");
|
|
3678
|
-
}
|
|
3679
|
-
if (nextState === "Cancelled" && payload.reason) {
|
|
3680
|
-
issue.lastError = toStringValue(payload.reason);
|
|
3681
|
-
}
|
|
3682
|
-
await persistState(state);
|
|
3683
|
-
return c.json({ ok: true, issue });
|
|
3686
|
+
app.get("/api/parallelism", async (c) => {
|
|
3687
|
+
return c.json(analyzeParallelizability(state.issues));
|
|
3688
|
+
});
|
|
3689
|
+
app.get("/api/providers/:slug/usage", async (c) => {
|
|
3690
|
+
const provider = c.req.param("slug") || "";
|
|
3691
|
+
try {
|
|
3692
|
+
const usage = await collectProviderUsage(provider);
|
|
3693
|
+
return c.json({
|
|
3694
|
+
providers: usage ? [usage] : [],
|
|
3695
|
+
collectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3696
|
+
});
|
|
3684
3697
|
} catch (error) {
|
|
3685
|
-
|
|
3698
|
+
logger.error({ err: error, provider }, "Failed to collect provider usage");
|
|
3699
|
+
return c.json({ providers: [] }, 500);
|
|
3686
3700
|
}
|
|
3687
3701
|
});
|
|
3688
|
-
app.
|
|
3689
|
-
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/retry");
|
|
3690
|
-
let feedback;
|
|
3702
|
+
app.get("/api/providers/usage", async (c) => {
|
|
3691
3703
|
try {
|
|
3692
|
-
const
|
|
3693
|
-
|
|
3694
|
-
} catch {
|
|
3704
|
+
const usage = await collectProvidersUsage();
|
|
3705
|
+
return c.json(usage);
|
|
3706
|
+
} catch (error) {
|
|
3707
|
+
logger.error({ err: error }, "Failed to collect providers usage");
|
|
3708
|
+
return c.json({ providers: [] }, 500);
|
|
3695
3709
|
}
|
|
3696
|
-
return mutateIssueState(state, c, async (issue) => {
|
|
3697
|
-
const container = getContainer();
|
|
3698
|
-
if (TERMINAL_STATES.has(issue.state)) {
|
|
3699
|
-
await transitionIssueCommand(
|
|
3700
|
-
{ issue, target: "Planning", note: "Manual retry \u2014 reopened." },
|
|
3701
|
-
container
|
|
3702
|
-
);
|
|
3703
|
-
if (issue.plan?.steps?.length) {
|
|
3704
|
-
await transitionIssueCommand(
|
|
3705
|
-
{ issue, target: "PendingApproval", note: "Existing plan found." },
|
|
3706
|
-
container
|
|
3707
|
-
);
|
|
3708
|
-
await transitionIssueCommand(
|
|
3709
|
-
{ issue, target: "Queued", note: "Auto-queued after plan approval." },
|
|
3710
|
-
container
|
|
3711
|
-
);
|
|
3712
|
-
}
|
|
3713
|
-
} else if (issue.state === "Blocked") {
|
|
3714
|
-
if (issue.lastFailedPhase === "review") {
|
|
3715
|
-
issue.lastError = void 0;
|
|
3716
|
-
issue.lastFailedPhase = void 0;
|
|
3717
|
-
await transitionIssueCommand(
|
|
3718
|
-
{ issue, target: "Reviewing", note: "Retrying review (execution was already successful)." },
|
|
3719
|
-
container
|
|
3720
|
-
);
|
|
3721
|
-
} else {
|
|
3722
|
-
await retryExecutionCommand(
|
|
3723
|
-
{ issue, note: "Manual retry from Blocked." },
|
|
3724
|
-
container
|
|
3725
|
-
);
|
|
3726
|
-
}
|
|
3727
|
-
} else if (issue.state === "Approved") {
|
|
3728
|
-
issue.attempts += 1;
|
|
3729
|
-
await transitionIssueCommand(
|
|
3730
|
-
{ issue, target: "Planning", note: "Requeued for rework after merge conflicts." },
|
|
3731
|
-
container
|
|
3732
|
-
);
|
|
3733
|
-
if (issue.plan?.steps?.length) {
|
|
3734
|
-
await transitionIssueCommand(
|
|
3735
|
-
{ issue, target: "PendingApproval", note: "Existing plan found." },
|
|
3736
|
-
container
|
|
3737
|
-
);
|
|
3738
|
-
await transitionIssueCommand(
|
|
3739
|
-
{ issue, target: "Queued", note: "Auto-queued for rework." },
|
|
3740
|
-
container
|
|
3741
|
-
);
|
|
3742
|
-
}
|
|
3743
|
-
} else if (issue.state === "Reviewing" || issue.state === "PendingDecision") {
|
|
3744
|
-
await requestReworkCommand(
|
|
3745
|
-
{
|
|
3746
|
-
issue,
|
|
3747
|
-
reviewerFeedback: feedback || issue.lastError || "Manual rework request.",
|
|
3748
|
-
note: feedback ? `Rework requested for ${issue.identifier}: ${feedback.slice(0, 200)}` : `Manual rework requested for ${issue.identifier}.`
|
|
3749
|
-
},
|
|
3750
|
-
container
|
|
3751
|
-
);
|
|
3752
|
-
} else if (issue.state === "PendingApproval") {
|
|
3753
|
-
await transitionIssueCommand(
|
|
3754
|
-
{ issue, target: "Queued", note: "Manual retry \u2014 queued for execution." },
|
|
3755
|
-
container
|
|
3756
|
-
);
|
|
3757
|
-
} else {
|
|
3758
|
-
issue.lastError = void 0;
|
|
3759
|
-
issue.nextRetryAt = void 0;
|
|
3760
|
-
issue.updatedAt = now();
|
|
3761
|
-
}
|
|
3762
|
-
addEvent(state, issue.id, "manual", `Manual retry requested for ${issue.id}.`);
|
|
3763
|
-
});
|
|
3764
|
-
});
|
|
3765
|
-
app.post("/api/issues/:id/cancel", async (c) => {
|
|
3766
|
-
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/cancel");
|
|
3767
|
-
return mutateIssueState(state, c, async (issue) => {
|
|
3768
|
-
const container = getContainer();
|
|
3769
|
-
await cancelIssueCommand({ issue }, container);
|
|
3770
|
-
});
|
|
3771
3710
|
});
|
|
3772
3711
|
app.post("/api/issues/:id/approve", async (c) => {
|
|
3773
3712
|
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/approve");
|
|
@@ -3802,7 +3741,7 @@ function registerStateRoutes(app, state) {
|
|
|
3802
3741
|
const result2 = await pushWorkspaceCommand({ issue, state }, container);
|
|
3803
3742
|
return c.json({ ok: true, prUrl: result2.prUrl, ghAvailable: result2.ghAvailable });
|
|
3804
3743
|
}
|
|
3805
|
-
const result = await mergeWorkspaceCommand({ issue, state }, container);
|
|
3744
|
+
const result = await mergeWorkspaceCommand({ issue, state, squashAlreadyApplied: issue.testApplied ?? false }, container);
|
|
3806
3745
|
return c.json({ ok: true, ...result });
|
|
3807
3746
|
} catch (error) {
|
|
3808
3747
|
const issueId = parseIssue(c);
|
|
@@ -3817,7 +3756,7 @@ function registerStateRoutes(app, state) {
|
|
|
3817
3756
|
if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
3818
3757
|
const issue = findIssue(state, issueId);
|
|
3819
3758
|
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
3820
|
-
const { dryMerge } = await import("./workspace-
|
|
3759
|
+
const { dryMerge } = await import("./workspace-HJEJZMTO.js");
|
|
3821
3760
|
const result = dryMerge(issue);
|
|
3822
3761
|
return c.json({ ok: true, ...result });
|
|
3823
3762
|
} catch (error) {
|
|
@@ -3832,7 +3771,7 @@ function registerStateRoutes(app, state) {
|
|
|
3832
3771
|
if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
3833
3772
|
const issue = findIssue(state, issueId);
|
|
3834
3773
|
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
3835
|
-
const { rebaseWorktree } = await import("./workspace-
|
|
3774
|
+
const { rebaseWorktree } = await import("./workspace-HJEJZMTO.js");
|
|
3836
3775
|
const result = rebaseWorktree(issue);
|
|
3837
3776
|
if (result.success) {
|
|
3838
3777
|
addEvent(state, issue.id, "info", `Branch ${issue.branchName} rebased onto ${issue.baseBranch}.`);
|
|
@@ -3862,6 +3801,8 @@ function registerStateRoutes(app, state) {
|
|
|
3862
3801
|
const msg = err.stderr || err.stdout || String(err);
|
|
3863
3802
|
throw new Error(`git merge --squash failed: ${msg}`);
|
|
3864
3803
|
}
|
|
3804
|
+
issue.testApplied = true;
|
|
3805
|
+
markIssueDirty(issue.id);
|
|
3865
3806
|
addEvent(state, issue.id, "manual", `Test squash applied to workspace: git merge --squash ${issue.branchName}`);
|
|
3866
3807
|
});
|
|
3867
3808
|
});
|
|
@@ -3875,6 +3816,8 @@ function registerStateRoutes(app, state) {
|
|
|
3875
3816
|
const msg = err.stderr || err.stdout || String(err);
|
|
3876
3817
|
throw new Error(`git reset/clean failed: ${msg}`);
|
|
3877
3818
|
}
|
|
3819
|
+
issue.testApplied = false;
|
|
3820
|
+
markIssueDirty(issue.id);
|
|
3878
3821
|
addEvent(state, issue.id, "manual", `Test reverted: git reset --hard HEAD && git clean -fd`);
|
|
3879
3822
|
});
|
|
3880
3823
|
});
|
|
@@ -3964,7 +3907,7 @@ function registerStateRoutes(app, state) {
|
|
|
3964
3907
|
const issue = findIssue(state, issueId);
|
|
3965
3908
|
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
3966
3909
|
try {
|
|
3967
|
-
const { getIssueTransitionHistory } = await import("./issue-state-machine-
|
|
3910
|
+
const { getIssueTransitionHistory } = await import("./issue-state-machine-NCD3ZFOI.js");
|
|
3968
3911
|
const limit = parseInt(c.req.query("limit") ?? "50", 10);
|
|
3969
3912
|
const offset = parseInt(c.req.query("offset") ?? "0", 10);
|
|
3970
3913
|
const transitions = await getIssueTransitionHistory(issue.id, { limit, offset });
|
|
@@ -3975,7 +3918,7 @@ function registerStateRoutes(app, state) {
|
|
|
3975
3918
|
});
|
|
3976
3919
|
app.get("/api/state-machine/transitions", async (c) => {
|
|
3977
3920
|
try {
|
|
3978
|
-
const { getStateMachineTransitions } = await import("./issue-state-machine-
|
|
3921
|
+
const { getStateMachineTransitions } = await import("./issue-state-machine-NCD3ZFOI.js");
|
|
3979
3922
|
return c.json({ ok: true, transitions: getStateMachineTransitions() });
|
|
3980
3923
|
} catch (error) {
|
|
3981
3924
|
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
@@ -3983,7 +3926,7 @@ function registerStateRoutes(app, state) {
|
|
|
3983
3926
|
});
|
|
3984
3927
|
app.get("/api/state-machine/visualize", async (c) => {
|
|
3985
3928
|
try {
|
|
3986
|
-
const { visualizeStateMachine } = await import("./issue-state-machine-
|
|
3929
|
+
const { visualizeStateMachine } = await import("./issue-state-machine-NCD3ZFOI.js");
|
|
3987
3930
|
const dot = visualizeStateMachine();
|
|
3988
3931
|
if (!dot) return c.json({ ok: false, error: "Visualization not available." }, 404);
|
|
3989
3932
|
return c.json({ ok: true, dot });
|
|
@@ -4956,6 +4899,7 @@ function generatePlanInBackground(issue, config, _workflowDefinition, callbacks,
|
|
|
4956
4899
|
addEvent2(issue.id, "info", `${fast ? "Fast plan" : "Plan"} generation starting for ${issue.identifier} (provider detection in progress).`);
|
|
4957
4900
|
generatePlan(issue.title, issue.description, config, null, { fast }).then(async ({ plan, usage }) => {
|
|
4958
4901
|
issue.plan = plan;
|
|
4902
|
+
issue.planVersion = Math.max(issue.planVersion ?? 0, 1);
|
|
4959
4903
|
markIssuePlanDirty(issue.id);
|
|
4960
4904
|
issue.planningStatus = "idle";
|
|
4961
4905
|
issue.planningStartedAt = void 0;
|
|
@@ -4963,6 +4907,19 @@ function generatePlanInBackground(issue, config, _workflowDefinition, callbacks,
|
|
|
4963
4907
|
issue.updatedAt = now();
|
|
4964
4908
|
applyUsage(issue, usage);
|
|
4965
4909
|
applySuggestions(issue, plan);
|
|
4910
|
+
try {
|
|
4911
|
+
const { getIssuePlanResource: getIssuePlanResource2 } = await import("./store-GZVWNJ6V.js");
|
|
4912
|
+
const planRes = getIssuePlanResource2();
|
|
4913
|
+
if (planRes) {
|
|
4914
|
+
await planRes.replace(issue.id, {
|
|
4915
|
+
id: issue.id,
|
|
4916
|
+
plan: issue.plan,
|
|
4917
|
+
planHistory: issue.planHistory,
|
|
4918
|
+
planVersion: issue.planVersion
|
|
4919
|
+
});
|
|
4920
|
+
}
|
|
4921
|
+
} catch {
|
|
4922
|
+
}
|
|
4966
4923
|
addEvent2(issue.id, "progress", `${fast ? "Fast plan" : "Plan"} generated for ${issue.identifier}: ${plan.steps.length} steps, complexity: ${plan.estimatedComplexity}.`);
|
|
4967
4924
|
if (usage.totalTokens > 0) {
|
|
4968
4925
|
addEvent2(issue.id, "info", `Plan tokens (${issue.identifier}): ${usage.totalTokens.toLocaleString()} (in: ${usage.inputTokens.toLocaleString()}, out: ${usage.outputTokens.toLocaleString()}) [${usage.model}]`);
|
|
@@ -4988,6 +4945,7 @@ function refinePlanInBackground(issue, feedback, config, _workflowDefinition, ca
|
|
|
4988
4945
|
addEvent2(issue.id, "info", `Plan refinement starting for ${issue.identifier}: "${feedbackSnippet}".`);
|
|
4989
4946
|
refinePlan(issue, feedback, config, null).then(async ({ plan, usage }) => {
|
|
4990
4947
|
issue.plan = plan;
|
|
4948
|
+
issue.planVersion = Math.max(issue.planVersion ?? 0, 1);
|
|
4991
4949
|
markIssuePlanDirty(issue.id);
|
|
4992
4950
|
issue.planningStatus = "idle";
|
|
4993
4951
|
issue.planningStartedAt = void 0;
|
|
@@ -4995,6 +4953,19 @@ function refinePlanInBackground(issue, feedback, config, _workflowDefinition, ca
|
|
|
4995
4953
|
issue.updatedAt = now();
|
|
4996
4954
|
applyUsage(issue, usage);
|
|
4997
4955
|
applySuggestions(issue, plan);
|
|
4956
|
+
try {
|
|
4957
|
+
const { getIssuePlanResource: getIssuePlanResource2 } = await import("./store-GZVWNJ6V.js");
|
|
4958
|
+
const planRes = getIssuePlanResource2();
|
|
4959
|
+
if (planRes) {
|
|
4960
|
+
await planRes.replace(issue.id, {
|
|
4961
|
+
id: issue.id,
|
|
4962
|
+
plan: issue.plan,
|
|
4963
|
+
planHistory: issue.planHistory,
|
|
4964
|
+
planVersion: issue.planVersion
|
|
4965
|
+
});
|
|
4966
|
+
}
|
|
4967
|
+
} catch {
|
|
4968
|
+
}
|
|
4998
4969
|
const feedbackPreview = feedback.length > 80 ? `${feedback.slice(0, 77)}...` : feedback;
|
|
4999
4970
|
addEvent2(issue.id, "progress", `Plan refined for ${issue.identifier}: "${feedbackPreview}" \u2192 ${plan.steps.length} steps, complexity: ${plan.estimatedComplexity}.`);
|
|
5000
4971
|
if (usage.totalTokens > 0) {
|
|
@@ -5018,10 +4989,6 @@ import { existsSync as existsSync9, mkdtempSync as mkdtempSync3, readFileSync as
|
|
|
5018
4989
|
import { spawn as spawn2 } from "child_process";
|
|
5019
4990
|
import { tmpdir as tmpdir3 } from "os";
|
|
5020
4991
|
import { join as join13 } from "path";
|
|
5021
|
-
function getProviderCommand(provider, config) {
|
|
5022
|
-
const explicit = provider === config.agentProvider ? config.agentCommand || "" : "";
|
|
5023
|
-
return resolveAgentCommand(provider, explicit, "", "");
|
|
5024
|
-
}
|
|
5025
4992
|
async function buildPrompt2(field, title, description, issueType, images) {
|
|
5026
4993
|
const context2 = {
|
|
5027
4994
|
title: title || "(empty)",
|
|
@@ -5072,8 +5039,9 @@ function parseCandidate(raw, expectedField) {
|
|
|
5072
5039
|
if (value && !isPlaceholder && (!field || field === expectedField)) {
|
|
5073
5040
|
return value;
|
|
5074
5041
|
}
|
|
5075
|
-
|
|
5076
|
-
|
|
5042
|
+
const nestedSource = typeof parsed.result === "string" ? parsed.result : typeof parsed.response === "string" ? parsed.response : void 0;
|
|
5043
|
+
if (nestedSource) {
|
|
5044
|
+
const nested = nestedSource.trim();
|
|
5077
5045
|
if (nested) {
|
|
5078
5046
|
const nestedClean = nested.replace(/^```(?:json)?\s*|\s*```$/g, "").trim();
|
|
5079
5047
|
for (const nestedCandidate of extractJsonObjects(nestedClean)) {
|
|
@@ -5169,65 +5137,51 @@ async function enhanceIssueField(payload, config, _workflowDefinition) {
|
|
|
5169
5137
|
const title = typeof payload.title === "string" ? payload.title.trim() : "";
|
|
5170
5138
|
const description = typeof payload.description === "string" ? payload.description.trim() : "";
|
|
5171
5139
|
const issueType = typeof payload.issueType === "string" ? payload.issueType.trim() : void 0;
|
|
5172
|
-
const
|
|
5173
|
-
|
|
5174
|
-
);
|
|
5140
|
+
const images = Array.isArray(payload.images) ? payload.images.filter((p) => typeof p === "string") : void 0;
|
|
5141
|
+
const { provider: selectedProvider, model: planModel } = await resolvePlanStageConfig(config);
|
|
5175
5142
|
const providers = detectAvailableProviders();
|
|
5176
|
-
const
|
|
5177
|
-
|
|
5178
|
-
const addProvider = (candidate) => {
|
|
5179
|
-
if (availableSet.has(candidate) && !orderedProviders.includes(candidate)) {
|
|
5180
|
-
orderedProviders.push(candidate);
|
|
5181
|
-
}
|
|
5182
|
-
};
|
|
5183
|
-
addProvider(requestedProvider);
|
|
5184
|
-
for (const entry of providers) {
|
|
5185
|
-
if (entry.available) addProvider(entry.name);
|
|
5186
|
-
}
|
|
5187
|
-
if (!orderedProviders.length) {
|
|
5143
|
+
const isAvailable = providers.some((p) => p.name === selectedProvider && p.available);
|
|
5144
|
+
if (!isAvailable) {
|
|
5188
5145
|
const known = providers.map((entry) => `${entry.name}:${entry.available ? "available" : "missing"}`).join(", ");
|
|
5189
|
-
throw new Error(`
|
|
5146
|
+
throw new Error(`Configured plan provider "${selectedProvider}" is not available. Detected: ${known}`);
|
|
5190
5147
|
}
|
|
5191
|
-
const
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5148
|
+
const adapter = ADAPTERS[selectedProvider];
|
|
5149
|
+
if (!adapter) {
|
|
5150
|
+
throw new Error(`No adapter configured for plan provider "${selectedProvider}".`);
|
|
5151
|
+
}
|
|
5152
|
+
const ENHANCE_JSON_SCHEMA = JSON.stringify({
|
|
5195
5153
|
type: "object",
|
|
5196
5154
|
properties: {
|
|
5197
|
-
field: { type: "string" },
|
|
5155
|
+
field: { type: "string", enum: ["title", "description"] },
|
|
5198
5156
|
value: { type: "string" }
|
|
5199
5157
|
},
|
|
5200
5158
|
required: ["field", "value"],
|
|
5201
5159
|
additionalProperties: false
|
|
5202
|
-
};
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
command,
|
|
5212
|
-
selectedProvider,
|
|
5213
|
-
prompt,
|
|
5214
|
-
title,
|
|
5215
|
-
description,
|
|
5216
|
-
field,
|
|
5217
|
-
config.commandTimeoutMs,
|
|
5218
|
-
images
|
|
5219
|
-
);
|
|
5220
|
-
logger.info({ provider: selectedProvider, field, rawOutput: output.slice(0, 2e3) }, "Enhance raw output");
|
|
5221
|
-
const value = parseEnhancerOutput(output, field);
|
|
5222
|
-
logger.info({ provider: selectedProvider, field, parsedValue: value }, "Enhance parsed value");
|
|
5223
|
-
return { field, value, provider: selectedProvider };
|
|
5224
|
-
} catch (error) {
|
|
5225
|
-
errors.push(
|
|
5226
|
-
`Provider "${selectedProvider}" failed: ${error instanceof Error ? error.message : String(error)}`
|
|
5227
|
-
);
|
|
5228
|
-
}
|
|
5160
|
+
});
|
|
5161
|
+
const command = adapter.buildCommand({
|
|
5162
|
+
model: planModel,
|
|
5163
|
+
imagePaths: images,
|
|
5164
|
+
jsonSchema: selectedProvider === "claude" ? ENHANCE_JSON_SCHEMA : void 0,
|
|
5165
|
+
noToolAccess: selectedProvider === "claude"
|
|
5166
|
+
});
|
|
5167
|
+
if (!command) {
|
|
5168
|
+
throw new Error(`Adapter returned empty command for provider "${selectedProvider}".`);
|
|
5229
5169
|
}
|
|
5230
|
-
|
|
5170
|
+
const prompt = await buildPrompt2(field, title, description, issueType, images);
|
|
5171
|
+
const output = await runProviderCommand(
|
|
5172
|
+
command,
|
|
5173
|
+
selectedProvider,
|
|
5174
|
+
prompt,
|
|
5175
|
+
title,
|
|
5176
|
+
description,
|
|
5177
|
+
field,
|
|
5178
|
+
config.commandTimeoutMs,
|
|
5179
|
+
images
|
|
5180
|
+
);
|
|
5181
|
+
logger.info({ provider: selectedProvider, model: planModel, field, rawOutput: output.slice(0, 2e3) }, "Enhance raw output");
|
|
5182
|
+
const value = parseEnhancerOutput(output, field);
|
|
5183
|
+
logger.info({ provider: selectedProvider, field, parsedValue: value.slice(0, 500) }, "Enhance parsed value");
|
|
5184
|
+
return { field, value, provider: selectedProvider };
|
|
5231
5185
|
}
|
|
5232
5186
|
|
|
5233
5187
|
// src/routes/plan.ts
|
|
@@ -5540,17 +5494,6 @@ function registerScanningRoutes(app, state) {
|
|
|
5540
5494
|
return c.json({ ok: false, error: "Failed to scan project." }, 500);
|
|
5541
5495
|
}
|
|
5542
5496
|
});
|
|
5543
|
-
app.post("/api/scan/analyze", async (c) => {
|
|
5544
|
-
try {
|
|
5545
|
-
const payload = await c.req.json();
|
|
5546
|
-
const provider = typeof payload.provider === "string" ? payload.provider : state.config.agentProvider;
|
|
5547
|
-
const result = await analyzeProjectWithCli(provider, TARGET_ROOT);
|
|
5548
|
-
return c.json(result);
|
|
5549
|
-
} catch (error) {
|
|
5550
|
-
logger.error({ err: error }, "Failed to analyze project with CLI");
|
|
5551
|
-
return c.json({ ok: false, error: "Failed to analyze project." }, 500);
|
|
5552
|
-
}
|
|
5553
|
-
});
|
|
5554
5497
|
app.post("/api/boot/skip-scan", async (c) => {
|
|
5555
5498
|
broadcastToWebSocketClients({ type: "boot:scan:skipped" });
|
|
5556
5499
|
return c.json({ ok: true, message: "Scan skipped." });
|
|
@@ -6252,7 +6195,7 @@ async function startApiServer(state, port, options) {
|
|
|
6252
6195
|
docs: { enabled: true, title: "Fifony API", version: "1.0.0", description: "Local orchestration API for Fifony" },
|
|
6253
6196
|
cors: { enabled: true, origin: "*" },
|
|
6254
6197
|
security: { enabled: false },
|
|
6255
|
-
logging: { enabled:
|
|
6198
|
+
logging: { enabled: !QUIET_MODE, excludePaths: ["/health", "/status", "/**/*.js", "/**/*.css", "/**/*.svg"] },
|
|
6256
6199
|
compression: { enabled: true, threshold: 1024 },
|
|
6257
6200
|
health: { enabled: true },
|
|
6258
6201
|
resources: {
|
|
@@ -6490,9 +6433,13 @@ async function recoverStateFromIssueResource() {
|
|
|
6490
6433
|
for (const issue of issues) {
|
|
6491
6434
|
try {
|
|
6492
6435
|
const planRecord = await issuePlanResource.get(issue.id);
|
|
6493
|
-
if (planRecord?.plan)
|
|
6436
|
+
if (planRecord?.plan) {
|
|
6437
|
+
issue.plan = planRecord.plan;
|
|
6438
|
+
logger.debug({ issueId: issue.id, hasSteps: !!issue.plan?.steps?.length }, "[Recovery] Hydrated plan from issue_plans resource");
|
|
6439
|
+
}
|
|
6494
6440
|
if (planRecord?.planHistory) issue.planHistory = planRecord.planHistory;
|
|
6495
|
-
} catch {
|
|
6441
|
+
} catch (err) {
|
|
6442
|
+
logger.warn({ issueId: issue.id, err: String(err) }, "[Recovery] Failed to load plan from issue_plans resource");
|
|
6496
6443
|
}
|
|
6497
6444
|
}
|
|
6498
6445
|
}
|
|
@@ -6829,314 +6776,7 @@ function scanProjectFiles(targetRoot) {
|
|
|
6829
6776
|
packageDescription
|
|
6830
6777
|
};
|
|
6831
6778
|
}
|
|
6832
|
-
var BUILD_FILE_SIGNALS = {
|
|
6833
|
-
"package.json": { language: "javascript", stack: ["node"] },
|
|
6834
|
-
"Cargo.toml": { language: "rust", stack: ["cargo"] },
|
|
6835
|
-
"pyproject.toml": { language: "python", stack: ["python"] },
|
|
6836
|
-
"setup.py": { language: "python", stack: ["python"] },
|
|
6837
|
-
"requirements.txt": { language: "python", stack: ["pip"] },
|
|
6838
|
-
"Pipfile": { language: "python", stack: ["pipenv"] },
|
|
6839
|
-
"go.mod": { language: "go", stack: ["go"] },
|
|
6840
|
-
"build.gradle": { language: "java", stack: ["gradle"] },
|
|
6841
|
-
"build.gradle.kts": { language: "kotlin", stack: ["gradle"] },
|
|
6842
|
-
"pom.xml": { language: "java", stack: ["maven"] },
|
|
6843
|
-
"Gemfile": { language: "ruby", stack: ["bundler"] },
|
|
6844
|
-
"mix.exs": { language: "elixir", stack: ["mix"] },
|
|
6845
|
-
"pubspec.yaml": { language: "dart", stack: ["flutter"] },
|
|
6846
|
-
"CMakeLists.txt": { language: "c++", stack: ["cmake"] },
|
|
6847
|
-
"Makefile": { language: "unknown", stack: ["make"] },
|
|
6848
|
-
"Dockerfile": { language: "unknown", stack: ["docker"] },
|
|
6849
|
-
"composer.json": { language: "php", stack: ["composer"] },
|
|
6850
|
-
"Package.swift": { language: "swift", stack: ["spm"] },
|
|
6851
|
-
"deno.json": { language: "typescript", stack: ["deno"] },
|
|
6852
|
-
"bun.lockb": { language: "typescript", stack: ["bun"] }
|
|
6853
|
-
};
|
|
6854
|
-
function buildFallbackAnalysis(targetRoot) {
|
|
6855
|
-
let description = "";
|
|
6856
|
-
let readmeExcerpt = "";
|
|
6857
|
-
for (const readmeFile of ["README.md", "README.rst", "README.txt", "README"]) {
|
|
6858
|
-
const p = join16(targetRoot, readmeFile);
|
|
6859
|
-
if (existsSync13(p)) {
|
|
6860
|
-
try {
|
|
6861
|
-
readmeExcerpt = readFileSync10(p, "utf8").slice(0, 300).trim();
|
|
6862
|
-
break;
|
|
6863
|
-
} catch {
|
|
6864
|
-
}
|
|
6865
|
-
}
|
|
6866
|
-
}
|
|
6867
|
-
const pkgPath = join16(targetRoot, "package.json");
|
|
6868
|
-
if (existsSync13(pkgPath)) {
|
|
6869
|
-
try {
|
|
6870
|
-
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
6871
|
-
const name = typeof pkg.name === "string" ? pkg.name : "";
|
|
6872
|
-
const desc = typeof pkg.description === "string" ? pkg.description : "";
|
|
6873
|
-
if (desc) description = name ? `${name}: ${desc}` : desc;
|
|
6874
|
-
} catch {
|
|
6875
|
-
}
|
|
6876
|
-
}
|
|
6877
|
-
const cargoPath = join16(targetRoot, "Cargo.toml");
|
|
6878
|
-
if (!description && existsSync13(cargoPath)) {
|
|
6879
|
-
try {
|
|
6880
|
-
const content = readFileSync10(cargoPath, "utf8");
|
|
6881
|
-
const descMatch = content.match(/^description\s*=\s*"([^"]+)"/m);
|
|
6882
|
-
const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m);
|
|
6883
|
-
if (descMatch) description = nameMatch ? `${nameMatch[1]}: ${descMatch[1]}` : descMatch[1];
|
|
6884
|
-
} catch {
|
|
6885
|
-
}
|
|
6886
|
-
}
|
|
6887
|
-
const pyprojectPath = join16(targetRoot, "pyproject.toml");
|
|
6888
|
-
if (!description && existsSync13(pyprojectPath)) {
|
|
6889
|
-
try {
|
|
6890
|
-
const content = readFileSync10(pyprojectPath, "utf8");
|
|
6891
|
-
const descMatch = content.match(/^description\s*=\s*"([^"]+)"/m);
|
|
6892
|
-
const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m);
|
|
6893
|
-
if (descMatch) description = nameMatch ? `${nameMatch[1]}: ${descMatch[1]}` : descMatch[1];
|
|
6894
|
-
} catch {
|
|
6895
|
-
}
|
|
6896
|
-
}
|
|
6897
|
-
if (!description) {
|
|
6898
|
-
description = readmeExcerpt ? readmeExcerpt.split("\n").filter(Boolean).slice(0, 2).join(". ") : "A software project.";
|
|
6899
|
-
}
|
|
6900
|
-
let language = "unknown";
|
|
6901
|
-
const stack = [];
|
|
6902
|
-
for (const [file, signal] of Object.entries(BUILD_FILE_SIGNALS)) {
|
|
6903
|
-
if (existsSync13(join16(targetRoot, file))) {
|
|
6904
|
-
if (language === "unknown" && signal.language !== "unknown") {
|
|
6905
|
-
language = signal.language;
|
|
6906
|
-
}
|
|
6907
|
-
for (const s of signal.stack) {
|
|
6908
|
-
if (!stack.includes(s)) stack.push(s);
|
|
6909
|
-
}
|
|
6910
|
-
}
|
|
6911
|
-
}
|
|
6912
|
-
return {
|
|
6913
|
-
description,
|
|
6914
|
-
language,
|
|
6915
|
-
domains: [],
|
|
6916
|
-
stack: stack.length ? stack : [language],
|
|
6917
|
-
suggestedAgents: ["code-reviewer", "software-architect"],
|
|
6918
|
-
source: "fallback"
|
|
6919
|
-
};
|
|
6920
|
-
}
|
|
6921
|
-
function parseAnalysisOutput(raw) {
|
|
6922
|
-
const text = raw.trim();
|
|
6923
|
-
if (!text) return null;
|
|
6924
|
-
let jsonText = text;
|
|
6925
|
-
try {
|
|
6926
|
-
const envelope = JSON.parse(text);
|
|
6927
|
-
if (typeof envelope.result === "string") {
|
|
6928
|
-
jsonText = envelope.result.trim();
|
|
6929
|
-
} else if (envelope.description || envelope.domains) {
|
|
6930
|
-
return validateAnalysis(envelope);
|
|
6931
|
-
}
|
|
6932
|
-
} catch {
|
|
6933
|
-
}
|
|
6934
|
-
const fenced = jsonText.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
|
|
6935
|
-
if (fenced) {
|
|
6936
|
-
jsonText = fenced[1].trim();
|
|
6937
|
-
}
|
|
6938
|
-
try {
|
|
6939
|
-
const parsed = JSON.parse(jsonText);
|
|
6940
|
-
return validateAnalysis(parsed);
|
|
6941
|
-
} catch {
|
|
6942
|
-
const match = jsonText.match(/\{[\s\S]*\}/);
|
|
6943
|
-
if (match) {
|
|
6944
|
-
try {
|
|
6945
|
-
const parsed = JSON.parse(match[0]);
|
|
6946
|
-
return validateAnalysis(parsed);
|
|
6947
|
-
} catch {
|
|
6948
|
-
return null;
|
|
6949
|
-
}
|
|
6950
|
-
}
|
|
6951
|
-
return null;
|
|
6952
|
-
}
|
|
6953
|
-
}
|
|
6954
|
-
function validateAnalysis(parsed) {
|
|
6955
|
-
if (!parsed || typeof parsed !== "object") return null;
|
|
6956
|
-
const description = typeof parsed.description === "string" ? parsed.description.trim() : "";
|
|
6957
|
-
const language = typeof parsed.language === "string" ? parsed.language.trim().toLowerCase() : "";
|
|
6958
|
-
const domains = Array.isArray(parsed.domains) ? parsed.domains.filter((d) => typeof d === "string") : [];
|
|
6959
|
-
const stack = Array.isArray(parsed.stack) ? parsed.stack.filter((s) => typeof s === "string") : [];
|
|
6960
|
-
const suggestedAgents = Array.isArray(parsed.suggestedAgents) ? parsed.suggestedAgents.filter((a) => typeof a === "string") : [];
|
|
6961
|
-
if (!description && domains.length === 0 && stack.length === 0) return null;
|
|
6962
|
-
return {
|
|
6963
|
-
description: description || "A software project.",
|
|
6964
|
-
language,
|
|
6965
|
-
domains,
|
|
6966
|
-
stack,
|
|
6967
|
-
suggestedAgents,
|
|
6968
|
-
source: "cli"
|
|
6969
|
-
};
|
|
6970
|
-
}
|
|
6971
|
-
function isBlockedProjectAnalysisResponse(analysis) {
|
|
6972
|
-
const normalized = `${analysis.description || ""}`.toLowerCase();
|
|
6973
|
-
const indicators = [
|
|
6974
|
-
"could not inspect the repository files",
|
|
6975
|
-
"local command execution is blocked",
|
|
6976
|
-
"please provide access",
|
|
6977
|
-
"paste the key files",
|
|
6978
|
-
"failed to inspect",
|
|
6979
|
-
"unable to access the repository"
|
|
6980
|
-
];
|
|
6981
|
-
return indicators.some((indicator) => normalized.includes(indicator));
|
|
6982
|
-
}
|
|
6983
6779
|
var ANALYSIS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
6984
|
-
function computeProjectHash(targetRoot) {
|
|
6985
|
-
const buildFiles = Object.keys(BUILD_FILE_SIGNALS);
|
|
6986
|
-
const found = buildFiles.filter((f) => existsSync13(join16(targetRoot, f))).sort();
|
|
6987
|
-
return createHash("sha256").update(found.join(",")).digest("hex").slice(0, 16);
|
|
6988
|
-
}
|
|
6989
|
-
async function loadCachedAnalysis(targetRoot) {
|
|
6990
|
-
const resource = getSettingStateResource();
|
|
6991
|
-
if (!resource) return null;
|
|
6992
|
-
const hash = computeProjectHash(targetRoot);
|
|
6993
|
-
const key = `project-analysis:${hash}`;
|
|
6994
|
-
try {
|
|
6995
|
-
const record2 = await resource.get(key);
|
|
6996
|
-
if (!record2?.value) return null;
|
|
6997
|
-
const cached = record2.value;
|
|
6998
|
-
if (!cached.analysis || !cached.updatedAt) return null;
|
|
6999
|
-
if (Date.now() - Date.parse(cached.updatedAt) > ANALYSIS_CACHE_TTL_MS) return null;
|
|
7000
|
-
return cached.analysis;
|
|
7001
|
-
} catch {
|
|
7002
|
-
return null;
|
|
7003
|
-
}
|
|
7004
|
-
}
|
|
7005
|
-
async function saveCachedAnalysis(targetRoot, analysis) {
|
|
7006
|
-
const resource = getSettingStateResource();
|
|
7007
|
-
if (!resource) return;
|
|
7008
|
-
const hash = computeProjectHash(targetRoot);
|
|
7009
|
-
const key = `project-analysis:${hash}`;
|
|
7010
|
-
try {
|
|
7011
|
-
await resource.replace(key, {
|
|
7012
|
-
id: key,
|
|
7013
|
-
scope: "system",
|
|
7014
|
-
source: "detected",
|
|
7015
|
-
value: { analysis, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
7016
|
-
});
|
|
7017
|
-
} catch {
|
|
7018
|
-
}
|
|
7019
|
-
}
|
|
7020
|
-
async function analyzeProjectWithCli(provider, targetRoot, options) {
|
|
7021
|
-
if (!options?.forceRefresh) {
|
|
7022
|
-
const cached = await loadCachedAnalysis(targetRoot);
|
|
7023
|
-
if (cached) {
|
|
7024
|
-
logger.info("Using cached project analysis.");
|
|
7025
|
-
return cached;
|
|
7026
|
-
}
|
|
7027
|
-
}
|
|
7028
|
-
const normalizedProvider = provider.trim().toLowerCase();
|
|
7029
|
-
const providers = detectAvailableProviders();
|
|
7030
|
-
const providerInfo = providers.find((p) => p.name === normalizedProvider && p.available);
|
|
7031
|
-
if (!providerInfo) {
|
|
7032
|
-
logger.warn(
|
|
7033
|
-
{ provider: normalizedProvider },
|
|
7034
|
-
"Requested CLI provider not available, using fallback analysis"
|
|
7035
|
-
);
|
|
7036
|
-
return buildFallbackAnalysis(targetRoot);
|
|
7037
|
-
}
|
|
7038
|
-
const tempDir = mkdtempSync4(join16(tmpdir4(), "fifony-scan-"));
|
|
7039
|
-
const promptFile = join16(tempDir, "fifony-scan-prompt.txt");
|
|
7040
|
-
const analysisPrompt = await renderPrompt("project-analysis");
|
|
7041
|
-
writeFileSync11(promptFile, analysisPrompt, "utf8");
|
|
7042
|
-
const processEnv = {};
|
|
7043
|
-
for (const [key, value] of Object.entries(env3)) {
|
|
7044
|
-
if (typeof value === "string") processEnv[key] = value;
|
|
7045
|
-
}
|
|
7046
|
-
processEnv.FIFONY_PROMPT_FILE = promptFile;
|
|
7047
|
-
try {
|
|
7048
|
-
const output = await new Promise((resolve3, reject) => {
|
|
7049
|
-
let stdout = "";
|
|
7050
|
-
let stderr = "";
|
|
7051
|
-
let timedOut = false;
|
|
7052
|
-
let args;
|
|
7053
|
-
let command;
|
|
7054
|
-
if (normalizedProvider === "claude") {
|
|
7055
|
-
command = "claude";
|
|
7056
|
-
args = [
|
|
7057
|
-
"--print",
|
|
7058
|
-
"--no-session-persistence",
|
|
7059
|
-
"--output-format",
|
|
7060
|
-
"json",
|
|
7061
|
-
"-p",
|
|
7062
|
-
analysisPrompt
|
|
7063
|
-
];
|
|
7064
|
-
} else if (normalizedProvider === "codex") {
|
|
7065
|
-
command = "sh";
|
|
7066
|
-
args = ["-c", `codex exec --skip-git-repo-check < "${promptFile}"`];
|
|
7067
|
-
} else {
|
|
7068
|
-
reject(new Error(`Unsupported provider: ${normalizedProvider}`));
|
|
7069
|
-
return;
|
|
7070
|
-
}
|
|
7071
|
-
const child = spawn3(command, args, {
|
|
7072
|
-
cwd: targetRoot,
|
|
7073
|
-
env: processEnv,
|
|
7074
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
7075
|
-
});
|
|
7076
|
-
if (child.stdin) child.stdin.end();
|
|
7077
|
-
child.stdout?.on("data", (chunk) => {
|
|
7078
|
-
stdout = appendFileTail(stdout, chunk.toString("utf8"), 64e3);
|
|
7079
|
-
});
|
|
7080
|
-
child.stderr?.on("data", (chunk) => {
|
|
7081
|
-
stderr = appendFileTail(stderr, chunk.toString("utf8"), 16e3);
|
|
7082
|
-
});
|
|
7083
|
-
const timer = setTimeout(() => {
|
|
7084
|
-
timedOut = true;
|
|
7085
|
-
child.kill("SIGTERM");
|
|
7086
|
-
}, 12e4);
|
|
7087
|
-
child.on("error", (err) => {
|
|
7088
|
-
clearTimeout(timer);
|
|
7089
|
-
reject(new Error(`Failed to spawn ${normalizedProvider}: ${err.message}`));
|
|
7090
|
-
});
|
|
7091
|
-
child.on("close", (code) => {
|
|
7092
|
-
clearTimeout(timer);
|
|
7093
|
-
if (timedOut) {
|
|
7094
|
-
reject(new Error(`CLI analysis timed out after 120s`));
|
|
7095
|
-
return;
|
|
7096
|
-
}
|
|
7097
|
-
if (code !== 0) {
|
|
7098
|
-
logger.debug(
|
|
7099
|
-
{ provider: normalizedProvider, code, stderr: stderr.slice(0, 500) },
|
|
7100
|
-
"CLI analysis command exited with non-zero code"
|
|
7101
|
-
);
|
|
7102
|
-
}
|
|
7103
|
-
resolve3(stdout);
|
|
7104
|
-
});
|
|
7105
|
-
});
|
|
7106
|
-
const analysis = parseAnalysisOutput(output);
|
|
7107
|
-
if (analysis && !isBlockedProjectAnalysisResponse(analysis)) {
|
|
7108
|
-
logger.info(
|
|
7109
|
-
{ provider: normalizedProvider, domains: analysis.domains, stack: analysis.stack },
|
|
7110
|
-
"CLI project analysis completed"
|
|
7111
|
-
);
|
|
7112
|
-
await saveCachedAnalysis(targetRoot, analysis);
|
|
7113
|
-
return analysis;
|
|
7114
|
-
}
|
|
7115
|
-
if (!analysis) {
|
|
7116
|
-
logger.warn(
|
|
7117
|
-
{ provider: normalizedProvider, rawOutput: output.slice(0, 500) },
|
|
7118
|
-
"CLI returned unparseable output, using fallback"
|
|
7119
|
-
);
|
|
7120
|
-
} else {
|
|
7121
|
-
logger.warn(
|
|
7122
|
-
{ provider: normalizedProvider, blockedAnalysis: analysis.description },
|
|
7123
|
-
"CLI analysis returned blocked/insufficient context response, using fallback"
|
|
7124
|
-
);
|
|
7125
|
-
}
|
|
7126
|
-
return buildFallbackAnalysis(targetRoot);
|
|
7127
|
-
} catch (error) {
|
|
7128
|
-
logger.warn(
|
|
7129
|
-
{ err: error, provider: normalizedProvider },
|
|
7130
|
-
"CLI analysis failed, using fallback"
|
|
7131
|
-
);
|
|
7132
|
-
return buildFallbackAnalysis(targetRoot);
|
|
7133
|
-
} finally {
|
|
7134
|
-
try {
|
|
7135
|
-
rmSync5(tempDir, { recursive: true, force: true });
|
|
7136
|
-
} catch {
|
|
7137
|
-
}
|
|
7138
|
-
}
|
|
7139
|
-
}
|
|
7140
6780
|
var DEFAULT_REFERENCE_REPOSITORIES = [
|
|
7141
6781
|
{
|
|
7142
6782
|
id: "ring",
|
|
@@ -7575,7 +7215,7 @@ function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
|
|
|
7575
7215
|
}
|
|
7576
7216
|
|
|
7577
7217
|
// src/domains/config.ts
|
|
7578
|
-
import { env as
|
|
7218
|
+
import { env as env3 } from "process";
|
|
7579
7219
|
var VALID_EFFORTS = /* @__PURE__ */ new Set(["low", "medium", "high", "extra-high"]);
|
|
7580
7220
|
function parseEffortValue(value) {
|
|
7581
7221
|
const str = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
@@ -7637,21 +7277,21 @@ function deriveConfig(args) {
|
|
|
7637
7277
|
staleInProgressTimeoutMs: parseEnvNumber("FIFONY_STALE_IN_PROGRESS_MS", 24e5),
|
|
7638
7278
|
logLinesTail: parseEnvNumber("FIFONY_LOG_TAIL_CHARS", 12e3),
|
|
7639
7279
|
maxPreviousOutputChars: parseEnvNumber("FIFONY_PREVIOUS_OUTPUT_CHARS", 2e4),
|
|
7640
|
-
agentProvider: normalizeAgentProvider(
|
|
7641
|
-
agentCommand: toStringValue(
|
|
7280
|
+
agentProvider: normalizeAgentProvider(env3.FIFONY_AGENT_PROVIDER ?? "codex"),
|
|
7281
|
+
agentCommand: toStringValue(env3.FIFONY_AGENT_COMMAND, ""),
|
|
7642
7282
|
defaultEffort: {
|
|
7643
|
-
default: parseEffortValue(
|
|
7644
|
-
planner: parseEffortValue(
|
|
7645
|
-
executor: parseEffortValue(
|
|
7646
|
-
reviewer: parseEffortValue(
|
|
7283
|
+
default: parseEffortValue(env3.FIFONY_REASONING_EFFORT),
|
|
7284
|
+
planner: parseEffortValue(env3.FIFONY_PLANNER_EFFORT),
|
|
7285
|
+
executor: parseEffortValue(env3.FIFONY_EXECUTOR_EFFORT),
|
|
7286
|
+
reviewer: parseEffortValue(env3.FIFONY_REVIEWER_EFFORT)
|
|
7647
7287
|
},
|
|
7648
7288
|
maxConcurrentByState: {},
|
|
7649
7289
|
runMode: "filesystem",
|
|
7650
7290
|
autoReviewApproval: true,
|
|
7651
|
-
afterCreateHook:
|
|
7652
|
-
beforeRunHook:
|
|
7653
|
-
afterRunHook:
|
|
7654
|
-
beforeRemoveHook:
|
|
7291
|
+
afterCreateHook: env3.FIFONY_AFTER_CREATE_HOOK ?? "",
|
|
7292
|
+
beforeRunHook: env3.FIFONY_BEFORE_RUN_HOOK ?? "",
|
|
7293
|
+
afterRunHook: env3.FIFONY_AFTER_RUN_HOOK ?? "",
|
|
7294
|
+
beforeRemoveHook: env3.FIFONY_BEFORE_REMOVE_HOOK ?? ""
|
|
7655
7295
|
};
|
|
7656
7296
|
}
|
|
7657
7297
|
function applyWorkflowConfig(config, port) {
|
|
@@ -7873,35 +7513,33 @@ function getNextRetryAt(issue, baseMs) {
|
|
|
7873
7513
|
const nextDelay = withRetryBackoff(nextAttempt, baseMs);
|
|
7874
7514
|
return new Date(Date.now() + nextDelay).toISOString();
|
|
7875
7515
|
}
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
throw new Error(`No valid transition from '${issue.state}' to '${nextState}' for issue ${issue.id}.`);
|
|
7885
|
-
}
|
|
7886
|
-
for (const event of path) {
|
|
7887
|
-
await transitionIssue(issue, event, { note: `Manual state update: ${nextState}`, reason: toStringValue(payload.reason) });
|
|
7888
|
-
}
|
|
7889
|
-
if (nextState === "Running" && sourceState === "Queued") {
|
|
7890
|
-
try {
|
|
7891
|
-
const { enqueue: enqueue2 } = await import("./queue-workers-BQLDNMFQ.js");
|
|
7892
|
-
await enqueue2(issue, "execute");
|
|
7893
|
-
} catch (error) {
|
|
7894
|
-
logger.warn({ issueId: issue.id, err: error }, "[Issues] Failed to enqueue after manual Running transition");
|
|
7895
|
-
}
|
|
7896
|
-
}
|
|
7897
|
-
if (nextState === "PendingApproval") {
|
|
7898
|
-
issue.nextRetryAt = void 0;
|
|
7899
|
-
issue.lastError = void 0;
|
|
7516
|
+
|
|
7517
|
+
// src/commands/request-rework.command.ts
|
|
7518
|
+
async function requestReworkCommand(input, deps) {
|
|
7519
|
+
const { issue, reviewerFeedback, note } = input;
|
|
7520
|
+
if (issue.state !== "Reviewing" && issue.state !== "PendingDecision") {
|
|
7521
|
+
throw new Error(
|
|
7522
|
+
`requestReworkCommand requires Reviewing or PendingDecision state, got ${issue.state}.`
|
|
7523
|
+
);
|
|
7900
7524
|
}
|
|
7901
|
-
|
|
7902
|
-
|
|
7525
|
+
issue.lastError = reviewerFeedback;
|
|
7526
|
+
issue.lastFailedPhase = "review";
|
|
7527
|
+
issue.attempts += 1;
|
|
7528
|
+
if (issue.state === "Reviewing") {
|
|
7529
|
+
await transitionIssueCommand(
|
|
7530
|
+
{ issue, target: "PendingDecision", note: `Reviewer completed for ${issue.identifier}.` },
|
|
7531
|
+
deps
|
|
7532
|
+
);
|
|
7903
7533
|
}
|
|
7904
|
-
|
|
7534
|
+
await transitionIssueCommand(
|
|
7535
|
+
{ issue, target: "Queued", note: note ?? `Reviewer requested rework for ${issue.identifier}.` },
|
|
7536
|
+
deps
|
|
7537
|
+
);
|
|
7538
|
+
deps.eventStore.addEvent(
|
|
7539
|
+
issue.id,
|
|
7540
|
+
"runner",
|
|
7541
|
+
`Issue ${issue.identifier} sent back for rework by reviewer.`
|
|
7542
|
+
);
|
|
7905
7543
|
}
|
|
7906
7544
|
|
|
7907
7545
|
// src/agents/issue-runner.ts
|
|
@@ -7924,8 +7562,23 @@ async function runPlanningJob(state, issue) {
|
|
|
7924
7562
|
{ persistSession: false }
|
|
7925
7563
|
);
|
|
7926
7564
|
issue.plan = plan;
|
|
7927
|
-
markIssuePlanDirty(issue.id);
|
|
7928
7565
|
issue.planVersion = Math.max(issue.planVersion ?? 0, 1);
|
|
7566
|
+
markIssuePlanDirty(issue.id);
|
|
7567
|
+
try {
|
|
7568
|
+
const { getIssuePlanResource: getIssuePlanResource2 } = await import("./store-GZVWNJ6V.js");
|
|
7569
|
+
const planRes = getIssuePlanResource2();
|
|
7570
|
+
if (planRes) {
|
|
7571
|
+
await planRes.replace(issue.id, {
|
|
7572
|
+
id: issue.id,
|
|
7573
|
+
plan: issue.plan,
|
|
7574
|
+
planHistory: issue.planHistory,
|
|
7575
|
+
planVersion: issue.planVersion
|
|
7576
|
+
});
|
|
7577
|
+
logger.debug({ issueId: issue.id, planVersion: issue.planVersion }, "[Agent] Plan flushed to issue_plans resource");
|
|
7578
|
+
}
|
|
7579
|
+
} catch (flushErr) {
|
|
7580
|
+
logger.warn({ err: String(flushErr), issueId: issue.id }, "[Agent] Failed to flush plan to issue_plans resource");
|
|
7581
|
+
}
|
|
7929
7582
|
if (plan.suggestedPaths?.length && !issue.paths?.length) issue.paths = plan.suggestedPaths;
|
|
7930
7583
|
if (plan.suggestedEffort && !issue.effort) issue.effort = plan.suggestedEffort;
|
|
7931
7584
|
if (usage.totalTokens > 0) {
|
|
@@ -8156,7 +7809,7 @@ async function runIssueOnce(state, issue, running) {
|
|
|
8156
7809
|
const { workspacePath, promptText, promptFile } = await prepareWorkspace(issue, state, state.config.defaultBranch);
|
|
8157
7810
|
container.issueRepository.markDirty(issue.id);
|
|
8158
7811
|
try {
|
|
8159
|
-
const { getIssueStateResource: getIssueStateResource2 } = await import("./store-
|
|
7812
|
+
const { getIssueStateResource: getIssueStateResource2 } = await import("./store-GZVWNJ6V.js");
|
|
8160
7813
|
const res = getIssueStateResource2();
|
|
8161
7814
|
if (res) {
|
|
8162
7815
|
await res.patch(issue.id, {
|
|
@@ -8197,7 +7850,7 @@ async function runIssueOnce(state, issue, running) {
|
|
|
8197
7850
|
await container.persistencePort.persistState(state);
|
|
8198
7851
|
if (issue.state === "Reviewing") {
|
|
8199
7852
|
try {
|
|
8200
|
-
const { enqueue: enqueue2 } = await import("./queue-workers-
|
|
7853
|
+
const { enqueue: enqueue2 } = await import("./queue-workers-2NJZE6L2.js");
|
|
8201
7854
|
await enqueue2(issue, "review");
|
|
8202
7855
|
} catch {
|
|
8203
7856
|
}
|
|
@@ -8243,7 +7896,6 @@ export {
|
|
|
8243
7896
|
transitionIssue,
|
|
8244
7897
|
issueDependenciesResolved,
|
|
8245
7898
|
getNextRetryAt,
|
|
8246
|
-
handleStatePatch,
|
|
8247
7899
|
runAgentSession,
|
|
8248
7900
|
runAgentPipeline,
|
|
8249
7901
|
issueHasResumableSession,
|
|
@@ -8275,4 +7927,4 @@ export {
|
|
|
8275
7927
|
syncReferenceRepositories,
|
|
8276
7928
|
importReferenceArtifacts
|
|
8277
7929
|
};
|
|
8278
|
-
//# sourceMappingURL=chunk-
|
|
7930
|
+
//# sourceMappingURL=chunk-YJHD4BE4.js.map
|