fifony 0.1.36 → 0.1.38
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-Ct6qhS6N.js} +1 -1
- package/app/dist/assets/{KeyboardShortcutsHelp-D71YmyfF.js → KeyboardShortcutsHelp-CuBWBOeU.js} +1 -1
- package/app/dist/assets/OnboardingWizard-9qzUVfYb.js +1 -0
- package/app/dist/assets/{analytics.lazy-D8vsSFxh.js → analytics.lazy-C25zZ3JA.js} +1 -1
- package/app/dist/assets/createLucideIcon-R47sXufx.js +1 -0
- package/app/dist/assets/index-BWB0OQnx.css +1 -0
- package/app/dist/assets/index-LC--nUBy.js +54 -0
- package/app/dist/index.html +3 -3
- package/app/dist/service-worker.js +1 -1
- package/dist/agent/run-local.js +6 -6
- package/dist/{agent-5AEC4SL7.js → agent-YAUMQQUS.js} +7 -7
- package/dist/{chunk-OONOOWNC.js → chunk-37N5OFHM.js} +4 -2
- package/dist/{chunk-GYVLPWYB.js → chunk-DYOLZYEC.js} +12 -12
- package/dist/{chunk-QBAR5JLY.js → chunk-FDR47R6J.js} +43 -8
- package/dist/{chunk-O5AEQXUV.js → chunk-T2YJOZ6N.js} +2 -2
- package/dist/{chunk-LUIPCPRO.js → chunk-VOA5UMXD.js} +44 -12
- package/dist/{chunk-A7BVAGPW.js → chunk-YBMQIQ2X.js} +604 -944
- package/dist/cli.js +6 -6
- package/dist/issue-runner-VVZVUMRF.js +15 -0
- package/dist/{issue-state-machine-JCGSR5QP.js → issue-state-machine-A6NMZG5W.js} +5 -5
- package/dist/{issues-2ENRFJHC.js → issues-2X2H3SBN.js} +7 -9
- package/dist/mcp/server.js +2 -2
- package/dist/{queue-workers-BQLDNMFQ.js → queue-workers-6MZWSPBW.js} +3 -3
- package/dist/{scheduler-5XTHGLCA.js → scheduler-OZRHGZIU.js} +7 -7
- package/dist/{store-44KLJAXC.js → store-O3UNJU26.js} +13 -7
- package/dist/{workspace-U43FRPEB.js → workspace-E26GGQES.js} +4 -4
- package/package.json +8 -8
- package/app/dist/assets/OnboardingWizard-TLlzqU2A.js +0 -1
- package/app/dist/assets/createLucideIcon-CBw-4t9s.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
|
@@ -14,14 +14,13 @@ import {
|
|
|
14
14
|
markAllIssuesDirty,
|
|
15
15
|
markEventDirty,
|
|
16
16
|
markIssueDirty,
|
|
17
|
-
markIssuePlanDirty,
|
|
18
17
|
setFsmEventEmitter,
|
|
19
18
|
setIssueResourceStateApi,
|
|
20
19
|
setIssueStateMachinePlugin,
|
|
21
20
|
snapshotAndClearDirtyEventIds,
|
|
22
21
|
snapshotAndClearDirtyIssueIds,
|
|
23
22
|
snapshotAndClearDirtyIssuePlanIds
|
|
24
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-FDR47R6J.js";
|
|
25
24
|
import {
|
|
26
25
|
ADAPTERS,
|
|
27
26
|
assertIssueHasGitWorktree,
|
|
@@ -49,10 +48,9 @@ import {
|
|
|
49
48
|
parseDiffStats,
|
|
50
49
|
prepareWorkspace,
|
|
51
50
|
readCodexConfig,
|
|
52
|
-
resolveAgentCommand,
|
|
53
51
|
runCommandWithTimeout,
|
|
54
52
|
runHook
|
|
55
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-VOA5UMXD.js";
|
|
56
54
|
import {
|
|
57
55
|
appendFileTail,
|
|
58
56
|
clamp,
|
|
@@ -74,15 +72,14 @@ import {
|
|
|
74
72
|
toStringArray,
|
|
75
73
|
toStringValue,
|
|
76
74
|
withRetryBackoff
|
|
77
|
-
} from "./chunk-
|
|
75
|
+
} from "./chunk-T2YJOZ6N.js";
|
|
78
76
|
import {
|
|
79
77
|
enqueue
|
|
80
|
-
} from "./chunk-
|
|
78
|
+
} from "./chunk-DYOLZYEC.js";
|
|
81
79
|
import {
|
|
82
80
|
logger
|
|
83
81
|
} from "./chunk-DVU3CXWA.js";
|
|
84
82
|
import {
|
|
85
|
-
ALLOWED_STATES,
|
|
86
83
|
ATTACHMENTS_ROOT,
|
|
87
84
|
COMPLETED_STATES,
|
|
88
85
|
EXECUTING_STATES,
|
|
@@ -94,6 +91,7 @@ import {
|
|
|
94
91
|
FRONTEND_OFFLINE_HTML,
|
|
95
92
|
FRONTEND_SERVICE_WORKER_JS,
|
|
96
93
|
PERSIST_EVENTS_MAX,
|
|
94
|
+
QUIET_MODE,
|
|
97
95
|
S3DB_AGENT_PIPELINE_RESOURCE,
|
|
98
96
|
S3DB_AGENT_SESSION_RESOURCE,
|
|
99
97
|
S3DB_DATABASE_PATH,
|
|
@@ -109,7 +107,7 @@ import {
|
|
|
109
107
|
TARGET_ROOT,
|
|
110
108
|
TERMINAL_STATES,
|
|
111
109
|
WORKSPACE_ROOT
|
|
112
|
-
} from "./chunk-
|
|
110
|
+
} from "./chunk-37N5OFHM.js";
|
|
113
111
|
|
|
114
112
|
// src/agents/issue-runner.ts
|
|
115
113
|
import {
|
|
@@ -293,20 +291,17 @@ function getAnalytics(topN = 20) {
|
|
|
293
291
|
}
|
|
294
292
|
|
|
295
293
|
// src/domains/project.ts
|
|
296
|
-
import { execFileSync as execFileSync2
|
|
294
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
297
295
|
import { createHash } from "crypto";
|
|
298
296
|
import {
|
|
299
297
|
existsSync as existsSync13,
|
|
300
298
|
mkdirSync as mkdirSync7,
|
|
301
|
-
mkdtempSync as mkdtempSync4,
|
|
302
299
|
readdirSync as readdirSync4,
|
|
303
300
|
readFileSync as readFileSync10,
|
|
304
|
-
rmSync as rmSync5,
|
|
305
301
|
writeFileSync as writeFileSync11
|
|
306
302
|
} from "fs";
|
|
307
|
-
import { homedir as homedir3
|
|
303
|
+
import { homedir as homedir3 } from "os";
|
|
308
304
|
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
305
|
|
|
311
306
|
// src/persistence/plugins/api-runtime-context.ts
|
|
312
307
|
var context = null;
|
|
@@ -1726,9 +1721,279 @@ async function cancelIssueCommand(input, deps) {
|
|
|
1726
1721
|
{ issue, target: "Cancelled", note: "Manual cancel requested." },
|
|
1727
1722
|
deps
|
|
1728
1723
|
);
|
|
1724
|
+
if (deps.state) {
|
|
1725
|
+
try {
|
|
1726
|
+
await cleanWorkspace(issue.id, issue, deps.state);
|
|
1727
|
+
issue.workspacePath = void 0;
|
|
1728
|
+
issue.worktreePath = void 0;
|
|
1729
|
+
} catch (error) {
|
|
1730
|
+
logger.warn({ issueId: issue.id, err: String(error) }, "[Command] Failed to clean workspace during cancel");
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1729
1733
|
deps.eventStore.addEvent(issue.id, "manual", `Manual cancel requested for ${issue.id}.`);
|
|
1730
1734
|
}
|
|
1731
1735
|
|
|
1736
|
+
// src/commands/merge-workspace.command.ts
|
|
1737
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1738
|
+
import { execSync } from "child_process";
|
|
1739
|
+
|
|
1740
|
+
// src/domains/validation.ts
|
|
1741
|
+
import { execFile } from "child_process";
|
|
1742
|
+
async function runValidationGate(issue, config) {
|
|
1743
|
+
if (!config.testCommand) return null;
|
|
1744
|
+
const cwd = issue.worktreePath ?? issue.workspacePath;
|
|
1745
|
+
if (!cwd) {
|
|
1746
|
+
logger.warn({ issueId: issue.id }, "[Validation] No workspace path \u2014 skipping gate");
|
|
1747
|
+
return null;
|
|
1748
|
+
}
|
|
1749
|
+
const command = config.testCommand;
|
|
1750
|
+
logger.info({ issueId: issue.id, command, cwd }, "[Validation] Running validation gate");
|
|
1751
|
+
return new Promise((resolve3) => {
|
|
1752
|
+
const child = execFile("sh", ["-c", command], {
|
|
1753
|
+
cwd,
|
|
1754
|
+
encoding: "utf8",
|
|
1755
|
+
timeout: 3e5,
|
|
1756
|
+
maxBuffer: 2 * 1024 * 1024
|
|
1757
|
+
}, (err, stdout, stderr) => {
|
|
1758
|
+
const combined = (stdout || "") + (stderr || "");
|
|
1759
|
+
if (!err) {
|
|
1760
|
+
logger.info({ issueId: issue.id }, "[Validation] Gate passed");
|
|
1761
|
+
resolve3({
|
|
1762
|
+
passed: true,
|
|
1763
|
+
output: combined.slice(-2048),
|
|
1764
|
+
command,
|
|
1765
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1766
|
+
});
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
logger.warn({ issueId: issue.id, exitCode: err.code }, "[Validation] Gate failed");
|
|
1770
|
+
resolve3({
|
|
1771
|
+
passed: false,
|
|
1772
|
+
output: combined.slice(-2048) || String(err).slice(0, 2048),
|
|
1773
|
+
command,
|
|
1774
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1775
|
+
});
|
|
1776
|
+
});
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// src/commands/merge-workspace.command.ts
|
|
1781
|
+
async function mergeWorkspaceCommand(input, deps) {
|
|
1782
|
+
const { issue, state, squashAlreadyApplied } = input;
|
|
1783
|
+
if (!["Approved", "Reviewing", "PendingDecision"].includes(issue.state)) {
|
|
1784
|
+
throw new Error(`Issue ${issue.identifier} is in state ${issue.state}. Merge is only allowed in Reviewing, PendingDecision, or Approved state.`);
|
|
1785
|
+
}
|
|
1786
|
+
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "merge issues");
|
|
1787
|
+
if (issue.state === "Reviewing" || issue.state === "PendingDecision") {
|
|
1788
|
+
await transitionIssueCommand(
|
|
1789
|
+
{ issue, target: "Approved", note: "Approved and merged by user." },
|
|
1790
|
+
deps
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
const wp = issue.worktreePath ?? issue.workspacePath;
|
|
1794
|
+
if (!wp || !existsSync6(wp)) {
|
|
1795
|
+
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.`);
|
|
1796
|
+
}
|
|
1797
|
+
if (issue.branchName && issue.baseBranch) {
|
|
1798
|
+
try {
|
|
1799
|
+
const stat = execSync(
|
|
1800
|
+
`git diff --stat "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
1801
|
+
{ encoding: "utf8", cwd: TARGET_ROOT, stdio: "pipe", maxBuffer: 512e3, timeout: 1e4 }
|
|
1802
|
+
);
|
|
1803
|
+
parseDiffStats(issue, stat);
|
|
1804
|
+
logger.info({ issueId: issue.id, linesAdded: issue.linesAdded, linesRemoved: issue.linesRemoved, filesChanged: issue.filesChanged }, "[Merge] Diff stats computed");
|
|
1805
|
+
} catch (err) {
|
|
1806
|
+
logger.warn({ err: String(err), issueId: issue.id }, "[Merge] Failed to compute diff stats");
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
const validation = await runValidationGate(issue, state.config);
|
|
1810
|
+
if (validation) {
|
|
1811
|
+
issue.validationResult = validation;
|
|
1812
|
+
if (!validation.passed) {
|
|
1813
|
+
throw new Error(`Validation gate failed (${validation.command}): ${validation.output.slice(0, 500)}`);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
let result;
|
|
1817
|
+
if (squashAlreadyApplied) {
|
|
1818
|
+
try {
|
|
1819
|
+
execSync("git add -A", { cwd: TARGET_ROOT, stdio: "pipe", timeout: 1e4 });
|
|
1820
|
+
execSync(
|
|
1821
|
+
`git commit -m "fifony: merge ${issue.identifier}"`,
|
|
1822
|
+
{ cwd: TARGET_ROOT, stdio: "pipe", timeout: 1e4 }
|
|
1823
|
+
);
|
|
1824
|
+
logger.info({ issueId: issue.id }, "[Merge] Committed existing test squash");
|
|
1825
|
+
} catch (err) {
|
|
1826
|
+
throw new Error(`Failed to commit test squash: ${err.stderr || err.stdout || String(err)}`);
|
|
1827
|
+
}
|
|
1828
|
+
issue.testApplied = false;
|
|
1829
|
+
result = { copied: [], deleted: [], skipped: [], conflicts: [] };
|
|
1830
|
+
} else {
|
|
1831
|
+
try {
|
|
1832
|
+
const indexStatus = execSync("git diff --cached --name-only", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
|
|
1833
|
+
const wtStatus = execSync("git diff --name-only", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
|
|
1834
|
+
if (indexStatus && !wtStatus) {
|
|
1835
|
+
execSync("git reset --hard HEAD", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
1836
|
+
logger.info({ issueId: issue.id }, "[Command] Cleared residual squash from index before merge");
|
|
1837
|
+
}
|
|
1838
|
+
} catch {
|
|
1839
|
+
}
|
|
1840
|
+
const mergeResult = mergeWorkspace(issue);
|
|
1841
|
+
result = mergeResult;
|
|
1842
|
+
}
|
|
1843
|
+
issue.mergeResult = {
|
|
1844
|
+
copied: result.copied.length,
|
|
1845
|
+
deleted: result.deleted.length,
|
|
1846
|
+
skipped: result.skipped.length,
|
|
1847
|
+
conflicts: result.conflicts.length,
|
|
1848
|
+
conflictFiles: result.conflicts.length > 0 ? result.conflicts : void 0
|
|
1849
|
+
};
|
|
1850
|
+
if (result.conflicts.length > 0) {
|
|
1851
|
+
deps.eventStore.addEvent(issue.id, "error", `Merge conflicts: ${result.conflicts.join(", ")}`);
|
|
1852
|
+
await deps.persistencePort.persistState(state);
|
|
1853
|
+
return result;
|
|
1854
|
+
}
|
|
1855
|
+
if (!issue.mergedReason) issue.mergedReason = squashAlreadyApplied ? "Approved and shipped after testing." : "Merged by user.";
|
|
1856
|
+
await transitionIssueCommand(
|
|
1857
|
+
{ issue, target: "Merged", note: `Workspace merged for ${issue.identifier}.` },
|
|
1858
|
+
deps
|
|
1859
|
+
);
|
|
1860
|
+
if (issue.workspacePath) {
|
|
1861
|
+
try {
|
|
1862
|
+
await cleanWorkspace(issue.id, issue, state);
|
|
1863
|
+
issue.workspacePath = void 0;
|
|
1864
|
+
issue.worktreePath = void 0;
|
|
1865
|
+
} catch {
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
await deps.persistencePort.persistState(state);
|
|
1869
|
+
return result;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// src/commands/push-workspace.command.ts
|
|
1873
|
+
import { execFileSync, execSync as execSync2 } from "child_process";
|
|
1874
|
+
function isGhAvailable() {
|
|
1875
|
+
try {
|
|
1876
|
+
execFileSync("gh", ["--version"], { stdio: "pipe", timeout: 5e3 });
|
|
1877
|
+
return true;
|
|
1878
|
+
} catch {
|
|
1879
|
+
return false;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
function getCompareUrl(branchName, baseBranch) {
|
|
1883
|
+
try {
|
|
1884
|
+
const remote = execSync2("git remote get-url origin", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
|
|
1885
|
+
const cleanRemote = remote.replace(/\.git$/, "");
|
|
1886
|
+
return `${cleanRemote}/compare/${baseBranch}...${branchName}`;
|
|
1887
|
+
} catch {
|
|
1888
|
+
return `(branch pushed: ${branchName})`;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
function findExistingPr(branchName) {
|
|
1892
|
+
try {
|
|
1893
|
+
const result = execFileSync(
|
|
1894
|
+
"gh",
|
|
1895
|
+
["pr", "view", branchName, "--json", "url,state", "--jq", 'select(.state == "OPEN") | .url'],
|
|
1896
|
+
{ cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe", timeout: 15e3 }
|
|
1897
|
+
).trim();
|
|
1898
|
+
return result || null;
|
|
1899
|
+
} catch {
|
|
1900
|
+
return null;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
function createPr(branchName, baseBranch, title, body) {
|
|
1904
|
+
return execFileSync(
|
|
1905
|
+
"gh",
|
|
1906
|
+
["pr", "create", "--head", branchName, "--base", baseBranch, "--title", title, "--body", body],
|
|
1907
|
+
{ cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe", timeout: 3e4 }
|
|
1908
|
+
).trim();
|
|
1909
|
+
}
|
|
1910
|
+
async function pushWorkspaceCommand(input, deps) {
|
|
1911
|
+
const { issue, state } = input;
|
|
1912
|
+
if (!["Approved", "Reviewing", "PendingDecision"].includes(issue.state)) {
|
|
1913
|
+
throw new Error(`Issue ${issue.identifier} is in state ${issue.state}. Push is only allowed in Reviewing, PendingDecision, or Approved state.`);
|
|
1914
|
+
}
|
|
1915
|
+
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "push issue branches");
|
|
1916
|
+
assertIssueHasGitWorktree(issue, "push");
|
|
1917
|
+
if (issue.testApplied) {
|
|
1918
|
+
try {
|
|
1919
|
+
execSync2("git reset --hard HEAD", { cwd: TARGET_ROOT, stdio: "pipe", timeout: 15e3 });
|
|
1920
|
+
execSync2("git clean -fd", { cwd: TARGET_ROOT, stdio: "pipe", timeout: 15e3 });
|
|
1921
|
+
issue.testApplied = false;
|
|
1922
|
+
deps.eventStore.addEvent(issue.id, "info", "Auto-reverted test squash before push.");
|
|
1923
|
+
logger.info({ issueId: issue.id }, "[Push] Auto-reverted test squash before push");
|
|
1924
|
+
} catch (err) {
|
|
1925
|
+
const msg = err.stderr || err.stdout || String(err);
|
|
1926
|
+
throw new Error(`Failed to revert test squash before push: ${msg}`);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
if (issue.state === "Reviewing" || issue.state === "PendingDecision") {
|
|
1930
|
+
await transitionIssueCommand(
|
|
1931
|
+
{ issue, target: "Approved", note: "Approved and pushed by user." },
|
|
1932
|
+
deps
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
ensureWorktreeCommitted(issue);
|
|
1936
|
+
const validation = await runValidationGate(issue, state.config);
|
|
1937
|
+
if (validation) {
|
|
1938
|
+
issue.validationResult = validation;
|
|
1939
|
+
if (!validation.passed) {
|
|
1940
|
+
throw new Error(`Validation gate failed (${validation.command}): ${validation.output.slice(0, 500)}`);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
computeDiffStats(issue);
|
|
1944
|
+
const planSummary = issue.plan?.summary ?? issue.title;
|
|
1945
|
+
let diffStat = "";
|
|
1946
|
+
try {
|
|
1947
|
+
diffStat = execSync2(
|
|
1948
|
+
`git diff --stat "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
1949
|
+
{ cwd: TARGET_ROOT, encoding: "utf8", maxBuffer: 512e3, timeout: 1e4, stdio: "pipe" }
|
|
1950
|
+
).trim();
|
|
1951
|
+
} catch {
|
|
1952
|
+
}
|
|
1953
|
+
const body = `## Summary
|
|
1954
|
+
${planSummary}
|
|
1955
|
+
|
|
1956
|
+
## Diff Stats
|
|
1957
|
+
\`\`\`
|
|
1958
|
+
${diffStat || "No diff stats available"}
|
|
1959
|
+
\`\`\`
|
|
1960
|
+
|
|
1961
|
+
*Automated by fifony*`;
|
|
1962
|
+
execSync2(`git push -u origin "${issue.branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
1963
|
+
const prBase = state.config.prBaseBranch || issue.baseBranch;
|
|
1964
|
+
const ghAvailable = isGhAvailable();
|
|
1965
|
+
let prUrl;
|
|
1966
|
+
if (!ghAvailable) {
|
|
1967
|
+
prUrl = getCompareUrl(issue.branchName, prBase);
|
|
1968
|
+
logger.info({ issueId: issue.id, prUrl }, "[Push] gh CLI not available \u2014 using compare URL");
|
|
1969
|
+
} else {
|
|
1970
|
+
const existingUrl = findExistingPr(issue.branchName);
|
|
1971
|
+
if (existingUrl) {
|
|
1972
|
+
prUrl = existingUrl;
|
|
1973
|
+
logger.info({ issueId: issue.id, prUrl }, "[Push] Existing open PR found");
|
|
1974
|
+
} else {
|
|
1975
|
+
try {
|
|
1976
|
+
prUrl = createPr(issue.branchName, prBase, issue.title, body);
|
|
1977
|
+
logger.info({ issueId: issue.id, prUrl }, "[Push] PR created");
|
|
1978
|
+
} catch (err) {
|
|
1979
|
+
const ghError = (err.stderr || err.stdout || String(err)).toString().slice(0, 500);
|
|
1980
|
+
logger.error({ issueId: issue.id, ghError }, "[Push] gh pr create failed");
|
|
1981
|
+
prUrl = getCompareUrl(issue.branchName, prBase);
|
|
1982
|
+
deps.eventStore.addEvent(issue.id, "error", `gh pr create failed: ${ghError}. Branch was pushed \u2014 use the compare URL to create the PR manually.`);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
issue.prUrl = prUrl;
|
|
1987
|
+
if (!issue.mergedReason) issue.mergedReason = "Pushed to origin and PR created.";
|
|
1988
|
+
await transitionIssueCommand(
|
|
1989
|
+
{ issue, target: "Merged", note: `Branch ${issue.branchName} pushed. PR: ${prUrl}` },
|
|
1990
|
+
deps
|
|
1991
|
+
);
|
|
1992
|
+
deps.eventStore.addEvent(issue.id, "merge", `PR created: ${prUrl}`);
|
|
1993
|
+
await deps.persistencePort.persistState(state);
|
|
1994
|
+
return { prUrl, ghAvailable };
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1732
1997
|
// src/persistence/resources/issues.resource.ts
|
|
1733
1998
|
function getIssueId(c) {
|
|
1734
1999
|
if (!c || typeof c !== "object" || !("req" in c) || !c.req || typeof c.req !== "object") {
|
|
@@ -1781,7 +2046,18 @@ async function patchIssueState(c) {
|
|
|
1781
2046
|
}
|
|
1782
2047
|
try {
|
|
1783
2048
|
const payload = await c.req.json();
|
|
1784
|
-
|
|
2049
|
+
const nextState = parseIssueState(payload.state);
|
|
2050
|
+
if (!nextState) {
|
|
2051
|
+
throw new Error(`Unsupported state: ${String(payload.state)}`);
|
|
2052
|
+
}
|
|
2053
|
+
const container = getContainer();
|
|
2054
|
+
const reason = payload.reason ? toStringValue(payload.reason) : void 0;
|
|
2055
|
+
logger.info({ issueId, identifier: issue.identifier, targetState: nextState }, "[API] POST /api/issues/:id/state");
|
|
2056
|
+
await transitionIssueCommand({
|
|
2057
|
+
issue,
|
|
2058
|
+
target: nextState,
|
|
2059
|
+
note: reason || `Manual state update: ${nextState}`
|
|
2060
|
+
}, container);
|
|
1785
2061
|
await persistState(context2.state);
|
|
1786
2062
|
return { body: { ok: true, issue } };
|
|
1787
2063
|
} catch (error) {
|
|
@@ -1798,17 +2074,41 @@ async function retryIssue(c) {
|
|
|
1798
2074
|
if (!issue) {
|
|
1799
2075
|
return { status: 404, body: { ok: false, error: "Issue not found" } };
|
|
1800
2076
|
}
|
|
2077
|
+
let feedback;
|
|
2078
|
+
try {
|
|
2079
|
+
const body = await c.req.json();
|
|
2080
|
+
if (body?.feedback) feedback = toStringValue(body.feedback);
|
|
2081
|
+
} catch {
|
|
2082
|
+
}
|
|
1801
2083
|
const container = getContainer();
|
|
2084
|
+
logger.info({ issueId, state: issue.state, lastFailedPhase: issue.lastFailedPhase, attempts: issue.attempts }, "[API] Retry \u2014 dispatching");
|
|
2085
|
+
const note = feedback ? `Rework requested for ${issue.identifier}: ${feedback.slice(0, 200)}` : `Manual retry for ${issue.identifier}.`;
|
|
1802
2086
|
if (TERMINAL_STATES.has(issue.state)) {
|
|
2087
|
+
await transitionIssueCommand({ issue, target: "Planning", note }, container);
|
|
2088
|
+
if (issue.plan?.steps?.length) {
|
|
2089
|
+
await transitionIssueCommand({ issue, target: "PendingApproval", note: "Existing plan found." }, container);
|
|
2090
|
+
await transitionIssueCommand({ issue, target: "Queued", note: "Auto-queued after plan approval." }, container);
|
|
2091
|
+
}
|
|
2092
|
+
} else if (issue.state === "Blocked" && issue.lastFailedPhase === "review") {
|
|
1803
2093
|
issue.lastError = void 0;
|
|
1804
|
-
issue.
|
|
1805
|
-
await transitionIssueCommand(
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
2094
|
+
issue.lastFailedPhase = void 0;
|
|
2095
|
+
await transitionIssueCommand({ issue, target: "Reviewing", note }, container);
|
|
2096
|
+
} else if (issue.state === "Blocked") {
|
|
2097
|
+
await transitionIssueCommand({ issue, target: "Queued", note }, container);
|
|
2098
|
+
} else if (issue.state === "Approved") {
|
|
2099
|
+
await transitionIssueCommand({ issue, target: "Planning", note }, container);
|
|
2100
|
+
if (issue.plan?.steps?.length) {
|
|
2101
|
+
await transitionIssueCommand({ issue, target: "PendingApproval", note: "Existing plan found." }, container);
|
|
2102
|
+
await transitionIssueCommand({ issue, target: "Queued", note: "Auto-queued for rework." }, container);
|
|
2103
|
+
}
|
|
2104
|
+
} else if (issue.state === "Reviewing" || issue.state === "PendingDecision") {
|
|
2105
|
+
const reworkNote = feedback || issue.lastError || "Manual rework request.";
|
|
2106
|
+
await transitionIssueCommand({ issue, target: "Queued", note: reworkNote }, container);
|
|
2107
|
+
} else if (issue.state === "PendingApproval") {
|
|
2108
|
+
await transitionIssueCommand({ issue, target: "Queued", note }, container);
|
|
1809
2109
|
} else {
|
|
1810
|
-
issue.nextRetryAt = void 0;
|
|
1811
2110
|
issue.lastError = void 0;
|
|
2111
|
+
issue.nextRetryAt = void 0;
|
|
1812
2112
|
issue.updatedAt = now();
|
|
1813
2113
|
}
|
|
1814
2114
|
addEvent(context2.state, issue.id, "manual", `Manual retry requested for ${issue.id}.`);
|
|
@@ -1841,19 +2141,57 @@ async function cancelIssue(c) {
|
|
|
1841
2141
|
}
|
|
1842
2142
|
await cancelIssueCommand(
|
|
1843
2143
|
{ issue },
|
|
1844
|
-
getContainer()
|
|
2144
|
+
{ ...getContainer(), state: context2.state }
|
|
1845
2145
|
);
|
|
1846
2146
|
addEvent(context2.state, issue.id, "manual", `Manual cancel requested for ${issue.id}.`);
|
|
1847
2147
|
await persistState(context2.state);
|
|
1848
2148
|
return { body: { ok: true, issue } };
|
|
1849
2149
|
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
2150
|
+
async function approveAndMerge(c) {
|
|
2151
|
+
const context2 = getApiRuntimeContextOrThrow();
|
|
2152
|
+
const issueId = getIssueId(c);
|
|
2153
|
+
if (!issueId) {
|
|
2154
|
+
return { status: 400, body: { ok: false, error: "Issue id is required." } };
|
|
2155
|
+
}
|
|
2156
|
+
const issue = findIssue(context2.state, issueId);
|
|
2157
|
+
if (!issue) {
|
|
2158
|
+
return { status: 404, body: { ok: false, error: "Issue not found" } };
|
|
2159
|
+
}
|
|
2160
|
+
if (issue.state !== "PendingDecision" && issue.state !== "Reviewing" && issue.state !== "Approved") {
|
|
2161
|
+
return { status: 400, body: { ok: false, error: `Cannot approve-and-merge from state ${issue.state}. Expected PendingDecision, Reviewing, or Approved.` } };
|
|
2162
|
+
}
|
|
2163
|
+
try {
|
|
2164
|
+
const container = getContainer();
|
|
2165
|
+
const mergeMode = context2.state.config.mergeMode;
|
|
2166
|
+
logger.info({ issueId, state: issue.state, testApplied: issue.testApplied, mergeMode }, "[API] POST /api/issues/:id/approve-and-merge");
|
|
2167
|
+
if (mergeMode === "push-pr") {
|
|
2168
|
+
if (issue.state !== "Approved") {
|
|
2169
|
+
await transitionIssueCommand(
|
|
2170
|
+
{ issue, target: "Approved", note: "Approved for push-pr." },
|
|
2171
|
+
container
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
2174
|
+
await pushWorkspaceCommand({ issue }, { ...container, state: context2.state });
|
|
2175
|
+
} else {
|
|
2176
|
+
await mergeWorkspaceCommand(
|
|
2177
|
+
{ issue, squashAlreadyApplied: issue.testApplied ?? false },
|
|
2178
|
+
{ ...container, state: context2.state }
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2181
|
+
addEvent(context2.state, issue.id, "manual", `Approved and ${mergeMode === "push-pr" ? "pushed PR for" : "merged"} ${issue.identifier}.`);
|
|
2182
|
+
await persistState(context2.state);
|
|
2183
|
+
return { body: { ok: true, issue } };
|
|
2184
|
+
} catch (error) {
|
|
2185
|
+
return { status: 409, body: { ok: false, error: error instanceof Error ? error.message : String(error) } };
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
var issues_resource_default = {
|
|
2189
|
+
name: S3DB_ISSUE_RESOURCE,
|
|
2190
|
+
attributes: {
|
|
2191
|
+
id: "string|required",
|
|
2192
|
+
identifier: "string|required",
|
|
2193
|
+
title: "string|required",
|
|
2194
|
+
description: "string|optional",
|
|
1857
2195
|
state: "string|required",
|
|
1858
2196
|
branchName: "string|optional",
|
|
1859
2197
|
url: "string|optional",
|
|
@@ -1886,11 +2224,10 @@ var issues_resource_default = {
|
|
|
1886
2224
|
commandOutputTail: "string|optional",
|
|
1887
2225
|
terminalWeek: "string|optional",
|
|
1888
2226
|
usage: "json|optional",
|
|
2227
|
+
testApplied: "boolean|optional",
|
|
1889
2228
|
tokenUsage: "json|optional",
|
|
1890
2229
|
tokensByPhase: "json|optional",
|
|
1891
2230
|
tokensByModel: "json|optional",
|
|
1892
|
-
plan: "json|optional",
|
|
1893
|
-
planHistory: "json|optional",
|
|
1894
2231
|
planVersion: "number|optional",
|
|
1895
2232
|
planningStatus: "string|optional",
|
|
1896
2233
|
planningStartedAt: "datetime|optional",
|
|
@@ -1965,6 +2302,13 @@ var issues_resource_default = {
|
|
|
1965
2302
|
return c.json(result.body, result.status);
|
|
1966
2303
|
}
|
|
1967
2304
|
return result.body;
|
|
2305
|
+
},
|
|
2306
|
+
"POST /:id/approve-and-merge": async (c) => {
|
|
2307
|
+
const result = await approveAndMerge(c);
|
|
2308
|
+
if (result.status) {
|
|
2309
|
+
return c.json(result.body, result.status);
|
|
2310
|
+
}
|
|
2311
|
+
return result.body;
|
|
1968
2312
|
}
|
|
1969
2313
|
}
|
|
1970
2314
|
};
|
|
@@ -1974,9 +2318,14 @@ var issue_plans_resource_default = {
|
|
|
1974
2318
|
name: S3DB_ISSUE_PLAN_RESOURCE,
|
|
1975
2319
|
attributes: {
|
|
1976
2320
|
id: "string|required",
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2321
|
+
issueId: "string|required",
|
|
2322
|
+
version: "number|required",
|
|
2323
|
+
current: "boolean|required",
|
|
2324
|
+
plan: "json|required"
|
|
2325
|
+
},
|
|
2326
|
+
partitions: {
|
|
2327
|
+
byIssue: { fields: { issueId: "string" } },
|
|
2328
|
+
byIssueCurrent: { fields: { issueId: "string", current: "boolean" } }
|
|
1980
2329
|
},
|
|
1981
2330
|
behavior: "body-overflow",
|
|
1982
2331
|
paranoid: false,
|
|
@@ -2353,13 +2702,13 @@ function hasTerminalQueue(state) {
|
|
|
2353
2702
|
}
|
|
2354
2703
|
|
|
2355
2704
|
// src/agents/providers-usage.ts
|
|
2356
|
-
import { execFile } from "child_process";
|
|
2705
|
+
import { execFile as execFile2 } from "child_process";
|
|
2357
2706
|
import { promisify } from "util";
|
|
2358
|
-
import { existsSync as
|
|
2707
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5, readdirSync as readdirSync2, realpathSync } from "fs";
|
|
2359
2708
|
import { join as join8, dirname } from "path";
|
|
2360
2709
|
import { homedir as homedir2 } from "os";
|
|
2361
2710
|
import { env } from "process";
|
|
2362
|
-
var execFileAsync = promisify(
|
|
2711
|
+
var execFileAsync = promisify(execFile2);
|
|
2363
2712
|
async function whichExists(cmd) {
|
|
2364
2713
|
try {
|
|
2365
2714
|
await execFileAsync("which", [cmd], { encoding: "utf8", timeout: 3e3 });
|
|
@@ -2394,7 +2743,7 @@ function resolveCodexHomeCandidates() {
|
|
|
2394
2743
|
}
|
|
2395
2744
|
function resolveCodexDir() {
|
|
2396
2745
|
for (const candidate of resolveCodexHomeCandidates()) {
|
|
2397
|
-
if (
|
|
2746
|
+
if (existsSync7(candidate)) {
|
|
2398
2747
|
return candidate;
|
|
2399
2748
|
}
|
|
2400
2749
|
}
|
|
@@ -2402,7 +2751,7 @@ function resolveCodexDir() {
|
|
|
2402
2751
|
}
|
|
2403
2752
|
function findLatestCodexDb(codexDir) {
|
|
2404
2753
|
const explicit = join8(codexDir, "state_5.sqlite");
|
|
2405
|
-
if (
|
|
2754
|
+
if (existsSync7(explicit)) return explicit;
|
|
2406
2755
|
const candidates = readdirSync2(codexDir).filter((name) => name.startsWith("state_") && name.endsWith(".sqlite")).sort().reverse();
|
|
2407
2756
|
if (candidates.length === 0) return null;
|
|
2408
2757
|
return join8(codexDir, candidates[0]);
|
|
@@ -2467,7 +2816,7 @@ function resolveClaudePlanKey(displayName) {
|
|
|
2467
2816
|
async function collectClaudeUsage() {
|
|
2468
2817
|
const home = homedir2();
|
|
2469
2818
|
const claudeDir = join8(home, ".claude");
|
|
2470
|
-
if (!
|
|
2819
|
+
if (!existsSync7(claudeDir)) return null;
|
|
2471
2820
|
const available = await whichExists("claude");
|
|
2472
2821
|
const projectsDir = join8(claudeDir, "projects");
|
|
2473
2822
|
let totalInputTokens = 0;
|
|
@@ -2488,7 +2837,7 @@ async function collectClaudeUsage() {
|
|
|
2488
2837
|
const weekMs = weekStart.getTime();
|
|
2489
2838
|
const last5hStart = computeLastHoursStart(5);
|
|
2490
2839
|
const last5hMs = last5hStart.getTime();
|
|
2491
|
-
if (
|
|
2840
|
+
if (existsSync7(projectsDir)) {
|
|
2492
2841
|
try {
|
|
2493
2842
|
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true });
|
|
2494
2843
|
for (const dir of projectDirs) {
|
|
@@ -2570,7 +2919,7 @@ async function collectClaudeUsage() {
|
|
|
2570
2919
|
let resetInfo = "Weekly reset (every Monday 00:00 UTC)";
|
|
2571
2920
|
let currentModel = "";
|
|
2572
2921
|
const settingsPath = join8(claudeDir, "settings.json");
|
|
2573
|
-
if (
|
|
2922
|
+
if (existsSync7(settingsPath)) {
|
|
2574
2923
|
try {
|
|
2575
2924
|
const settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
|
|
2576
2925
|
if (settings.plan === "max" || settings.plan === "max5x") {
|
|
@@ -2694,7 +3043,7 @@ function aggregateCodexSessionUsageFromJsonl(lines) {
|
|
|
2694
3043
|
}
|
|
2695
3044
|
function collectCodexSessionUsagesFromJsonl(codexDir) {
|
|
2696
3045
|
const sessionsDir = join8(codexDir, "sessions");
|
|
2697
|
-
if (!
|
|
3046
|
+
if (!existsSync7(sessionsDir)) return [];
|
|
2698
3047
|
const stack = [sessionsDir];
|
|
2699
3048
|
const usageByFile = [];
|
|
2700
3049
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2734,7 +3083,7 @@ async function collectCodexUsage() {
|
|
|
2734
3083
|
const models = [];
|
|
2735
3084
|
const modelsCachePath = join8(codexDir, "models_cache.json");
|
|
2736
3085
|
let currentModel = "";
|
|
2737
|
-
if (
|
|
3086
|
+
if (existsSync7(modelsCachePath)) {
|
|
2738
3087
|
try {
|
|
2739
3088
|
const cache = JSON.parse(readFileSync5(modelsCachePath, "utf8"));
|
|
2740
3089
|
for (const m of cache.models || []) {
|
|
@@ -2748,7 +3097,7 @@ async function collectCodexUsage() {
|
|
|
2748
3097
|
}
|
|
2749
3098
|
}
|
|
2750
3099
|
const configPath = join8(codexDir, "config.toml");
|
|
2751
|
-
if (
|
|
3100
|
+
if (existsSync7(configPath)) {
|
|
2752
3101
|
try {
|
|
2753
3102
|
const configContent = readFileSync5(configPath, "utf8");
|
|
2754
3103
|
const modelMatch = configContent.match(/^model\s*=\s*"([^"]+)"/m);
|
|
@@ -3011,7 +3360,7 @@ function aggregateGeminiSessionUsageFromJson(content) {
|
|
|
3011
3360
|
}
|
|
3012
3361
|
function collectGeminiSessionUsages() {
|
|
3013
3362
|
const geminiTmp = join8(homedir2(), ".gemini", "tmp");
|
|
3014
|
-
if (!
|
|
3363
|
+
if (!existsSync7(geminiTmp)) return [];
|
|
3015
3364
|
const usages = [];
|
|
3016
3365
|
let entries = [];
|
|
3017
3366
|
try {
|
|
@@ -3022,7 +3371,7 @@ function collectGeminiSessionUsages() {
|
|
|
3022
3371
|
for (const profile of entries) {
|
|
3023
3372
|
if (!profile.isDirectory()) continue;
|
|
3024
3373
|
const chatsDir = join8(geminiTmp, profile.name, "chats");
|
|
3025
|
-
if (!
|
|
3374
|
+
if (!existsSync7(chatsDir)) continue;
|
|
3026
3375
|
let sessions = [];
|
|
3027
3376
|
try {
|
|
3028
3377
|
sessions = readdirSync2(chatsDir).filter((name) => name.startsWith("session-") && (name.endsWith(".json") || name.endsWith(".jsonl")));
|
|
@@ -3052,7 +3401,7 @@ async function collectGeminiUsage() {
|
|
|
3052
3401
|
}
|
|
3053
3402
|
let account = null;
|
|
3054
3403
|
const accountsPath = join8(homedir2(), ".gemini", "google_accounts.json");
|
|
3055
|
-
if (
|
|
3404
|
+
if (existsSync7(accountsPath)) {
|
|
3056
3405
|
try {
|
|
3057
3406
|
const data = JSON.parse(readFileSync5(accountsPath, "utf8"));
|
|
3058
3407
|
if (typeof data.active === "string" && data.active.includes("@")) {
|
|
@@ -3070,7 +3419,7 @@ async function collectGeminiUsage() {
|
|
|
3070
3419
|
const { stdout: binPath } = await execFileAsync("which", ["gemini"], { encoding: "utf8", timeout: 3e3 });
|
|
3071
3420
|
const realBin = realpathSync(binPath.trim());
|
|
3072
3421
|
const modelsPath = join8(dirname(dirname(realBin)), "node_modules", "@google", "gemini-cli-core", "dist", "src", "config", "models.js");
|
|
3073
|
-
if (
|
|
3422
|
+
if (existsSync7(modelsPath)) {
|
|
3074
3423
|
const content = readFileSync5(modelsPath, "utf8");
|
|
3075
3424
|
const regex = /export const ([A-Z0-9_]+)\s*=\s*'(gemini-[^']+)';/g;
|
|
3076
3425
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3090,7 +3439,7 @@ async function collectGeminiUsage() {
|
|
|
3090
3439
|
}
|
|
3091
3440
|
let currentModel = "";
|
|
3092
3441
|
const settingsPath = join8(homedir2(), ".gemini", "settings.json");
|
|
3093
|
-
if (
|
|
3442
|
+
if (existsSync7(settingsPath)) {
|
|
3094
3443
|
try {
|
|
3095
3444
|
const settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
|
|
3096
3445
|
if (typeof settings.model === "string" && settings.model.trim()) {
|
|
@@ -3270,12 +3619,7 @@ async function replanIssueCommand(input, deps) {
|
|
|
3270
3619
|
if (issue.state === "Running" || issue.state === "Reviewing" || issue.state === "Queued") {
|
|
3271
3620
|
throw new Error(`Cannot replan issue in ${issue.state} state \u2014 wait for it to finish or cancel it first.`);
|
|
3272
3621
|
}
|
|
3273
|
-
|
|
3274
|
-
if (!Array.isArray(issue.planHistory)) issue.planHistory = [];
|
|
3275
|
-
issue.planHistory.push(issue.plan);
|
|
3276
|
-
issue.plan = void 0;
|
|
3277
|
-
markIssuePlanDirty(issue.id);
|
|
3278
|
-
}
|
|
3622
|
+
issue.plan = void 0;
|
|
3279
3623
|
issue.planVersion = (issue.planVersion ?? 0) + 1;
|
|
3280
3624
|
issue.executeAttempt = 0;
|
|
3281
3625
|
issue.reviewAttempt = 0;
|
|
@@ -3289,286 +3633,6 @@ async function replanIssueCommand(input, deps) {
|
|
|
3289
3633
|
deps.eventStore.addEvent(issue.id, "manual", `Replan requested for ${issue.identifier} \u2014 now at plan v${issue.planVersion}.`);
|
|
3290
3634
|
}
|
|
3291
3635
|
|
|
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
3636
|
// src/routes/state.ts
|
|
3573
3637
|
function getStateQuery(state, showAll = false) {
|
|
3574
3638
|
let issues = state.issues;
|
|
@@ -3606,168 +3670,40 @@ function registerStateRoutes(app, state) {
|
|
|
3606
3670
|
app.get(
|
|
3607
3671
|
"/api/status",
|
|
3608
3672
|
async (c) => c.json({
|
|
3609
|
-
status: "ok",
|
|
3610
|
-
updatedAt: state.updatedAt,
|
|
3611
|
-
config: state.config,
|
|
3612
|
-
trackerKind: state.trackerKind
|
|
3613
|
-
})
|
|
3614
|
-
);
|
|
3615
|
-
app.get("/api/providers", async (c) => {
|
|
3616
|
-
const providers = detectAvailableProviders();
|
|
3617
|
-
return c.json({ providers });
|
|
3618
|
-
});
|
|
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 });
|
|
3684
|
-
} catch (error) {
|
|
3685
|
-
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
3686
|
-
}
|
|
3687
|
-
});
|
|
3688
|
-
app.post("/api/issues/:id/retry", async (c) => {
|
|
3689
|
-
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/retry");
|
|
3690
|
-
let feedback;
|
|
3691
|
-
try {
|
|
3692
|
-
const body = await c.req.json();
|
|
3693
|
-
if (body?.feedback) feedback = toStringValue(body.feedback);
|
|
3694
|
-
} catch {
|
|
3695
|
-
}
|
|
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
|
-
});
|
|
3673
|
+
status: "ok",
|
|
3674
|
+
updatedAt: state.updatedAt,
|
|
3675
|
+
config: state.config,
|
|
3676
|
+
trackerKind: state.trackerKind
|
|
3677
|
+
})
|
|
3678
|
+
);
|
|
3679
|
+
app.get("/api/providers", async (c) => {
|
|
3680
|
+
const providers = detectAvailableProviders();
|
|
3681
|
+
return c.json({ providers });
|
|
3764
3682
|
});
|
|
3765
|
-
app.
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3683
|
+
app.get("/api/parallelism", async (c) => {
|
|
3684
|
+
return c.json(analyzeParallelizability(state.issues));
|
|
3685
|
+
});
|
|
3686
|
+
app.get("/api/providers/:slug/usage", async (c) => {
|
|
3687
|
+
const provider = c.req.param("slug") || "";
|
|
3688
|
+
try {
|
|
3689
|
+
const usage = await collectProviderUsage(provider);
|
|
3690
|
+
return c.json({
|
|
3691
|
+
providers: usage ? [usage] : [],
|
|
3692
|
+
collectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3693
|
+
});
|
|
3694
|
+
} catch (error) {
|
|
3695
|
+
logger.error({ err: error, provider }, "Failed to collect provider usage");
|
|
3696
|
+
return c.json({ providers: [] }, 500);
|
|
3697
|
+
}
|
|
3698
|
+
});
|
|
3699
|
+
app.get("/api/providers/usage", async (c) => {
|
|
3700
|
+
try {
|
|
3701
|
+
const usage = await collectProvidersUsage();
|
|
3702
|
+
return c.json(usage);
|
|
3703
|
+
} catch (error) {
|
|
3704
|
+
logger.error({ err: error }, "Failed to collect providers usage");
|
|
3705
|
+
return c.json({ providers: [] }, 500);
|
|
3706
|
+
}
|
|
3771
3707
|
});
|
|
3772
3708
|
app.post("/api/issues/:id/approve", async (c) => {
|
|
3773
3709
|
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/approve");
|
|
@@ -3802,7 +3738,7 @@ function registerStateRoutes(app, state) {
|
|
|
3802
3738
|
const result2 = await pushWorkspaceCommand({ issue, state }, container);
|
|
3803
3739
|
return c.json({ ok: true, prUrl: result2.prUrl, ghAvailable: result2.ghAvailable });
|
|
3804
3740
|
}
|
|
3805
|
-
const result = await mergeWorkspaceCommand({ issue, state }, container);
|
|
3741
|
+
const result = await mergeWorkspaceCommand({ issue, state, squashAlreadyApplied: issue.testApplied ?? false }, container);
|
|
3806
3742
|
return c.json({ ok: true, ...result });
|
|
3807
3743
|
} catch (error) {
|
|
3808
3744
|
const issueId = parseIssue(c);
|
|
@@ -3817,7 +3753,7 @@ function registerStateRoutes(app, state) {
|
|
|
3817
3753
|
if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
3818
3754
|
const issue = findIssue(state, issueId);
|
|
3819
3755
|
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
3820
|
-
const { dryMerge } = await import("./workspace-
|
|
3756
|
+
const { dryMerge } = await import("./workspace-E26GGQES.js");
|
|
3821
3757
|
const result = dryMerge(issue);
|
|
3822
3758
|
return c.json({ ok: true, ...result });
|
|
3823
3759
|
} catch (error) {
|
|
@@ -3832,7 +3768,7 @@ function registerStateRoutes(app, state) {
|
|
|
3832
3768
|
if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
3833
3769
|
const issue = findIssue(state, issueId);
|
|
3834
3770
|
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
3835
|
-
const { rebaseWorktree } = await import("./workspace-
|
|
3771
|
+
const { rebaseWorktree } = await import("./workspace-E26GGQES.js");
|
|
3836
3772
|
const result = rebaseWorktree(issue);
|
|
3837
3773
|
if (result.success) {
|
|
3838
3774
|
addEvent(state, issue.id, "info", `Branch ${issue.branchName} rebased onto ${issue.baseBranch}.`);
|
|
@@ -3862,6 +3798,8 @@ function registerStateRoutes(app, state) {
|
|
|
3862
3798
|
const msg = err.stderr || err.stdout || String(err);
|
|
3863
3799
|
throw new Error(`git merge --squash failed: ${msg}`);
|
|
3864
3800
|
}
|
|
3801
|
+
issue.testApplied = true;
|
|
3802
|
+
markIssueDirty(issue.id);
|
|
3865
3803
|
addEvent(state, issue.id, "manual", `Test squash applied to workspace: git merge --squash ${issue.branchName}`);
|
|
3866
3804
|
});
|
|
3867
3805
|
});
|
|
@@ -3875,6 +3813,8 @@ function registerStateRoutes(app, state) {
|
|
|
3875
3813
|
const msg = err.stderr || err.stdout || String(err);
|
|
3876
3814
|
throw new Error(`git reset/clean failed: ${msg}`);
|
|
3877
3815
|
}
|
|
3816
|
+
issue.testApplied = false;
|
|
3817
|
+
markIssueDirty(issue.id);
|
|
3878
3818
|
addEvent(state, issue.id, "manual", `Test reverted: git reset --hard HEAD && git clean -fd`);
|
|
3879
3819
|
});
|
|
3880
3820
|
});
|
|
@@ -3964,7 +3904,7 @@ function registerStateRoutes(app, state) {
|
|
|
3964
3904
|
const issue = findIssue(state, issueId);
|
|
3965
3905
|
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
3966
3906
|
try {
|
|
3967
|
-
const { getIssueTransitionHistory } = await import("./issue-state-machine-
|
|
3907
|
+
const { getIssueTransitionHistory } = await import("./issue-state-machine-A6NMZG5W.js");
|
|
3968
3908
|
const limit = parseInt(c.req.query("limit") ?? "50", 10);
|
|
3969
3909
|
const offset = parseInt(c.req.query("offset") ?? "0", 10);
|
|
3970
3910
|
const transitions = await getIssueTransitionHistory(issue.id, { limit, offset });
|
|
@@ -3975,7 +3915,7 @@ function registerStateRoutes(app, state) {
|
|
|
3975
3915
|
});
|
|
3976
3916
|
app.get("/api/state-machine/transitions", async (c) => {
|
|
3977
3917
|
try {
|
|
3978
|
-
const { getStateMachineTransitions } = await import("./issue-state-machine-
|
|
3918
|
+
const { getStateMachineTransitions } = await import("./issue-state-machine-A6NMZG5W.js");
|
|
3979
3919
|
return c.json({ ok: true, transitions: getStateMachineTransitions() });
|
|
3980
3920
|
} catch (error) {
|
|
3981
3921
|
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
@@ -3983,7 +3923,7 @@ function registerStateRoutes(app, state) {
|
|
|
3983
3923
|
});
|
|
3984
3924
|
app.get("/api/state-machine/visualize", async (c) => {
|
|
3985
3925
|
try {
|
|
3986
|
-
const { visualizeStateMachine } = await import("./issue-state-machine-
|
|
3926
|
+
const { visualizeStateMachine } = await import("./issue-state-machine-A6NMZG5W.js");
|
|
3987
3927
|
const dot = visualizeStateMachine();
|
|
3988
3928
|
if (!dot) return c.json({ ok: false, error: "Visualization not available." }, 404);
|
|
3989
3929
|
return c.json({ ok: true, dot });
|
|
@@ -4956,13 +4896,18 @@ function generatePlanInBackground(issue, config, _workflowDefinition, callbacks,
|
|
|
4956
4896
|
addEvent2(issue.id, "info", `${fast ? "Fast plan" : "Plan"} generation starting for ${issue.identifier} (provider detection in progress).`);
|
|
4957
4897
|
generatePlan(issue.title, issue.description, config, null, { fast }).then(async ({ plan, usage }) => {
|
|
4958
4898
|
issue.plan = plan;
|
|
4959
|
-
|
|
4899
|
+
issue.planVersion = Math.max(issue.planVersion ?? 0, 1);
|
|
4960
4900
|
issue.planningStatus = "idle";
|
|
4961
4901
|
issue.planningStartedAt = void 0;
|
|
4962
4902
|
issue.planningError = void 0;
|
|
4963
4903
|
issue.updatedAt = now();
|
|
4964
4904
|
applyUsage(issue, usage);
|
|
4965
4905
|
applySuggestions(issue, plan);
|
|
4906
|
+
try {
|
|
4907
|
+
const { savePlanForIssue: savePlanForIssue2 } = await import("./store-O3UNJU26.js");
|
|
4908
|
+
await savePlanForIssue2(issue.id, plan, issue.planVersion);
|
|
4909
|
+
} catch {
|
|
4910
|
+
}
|
|
4966
4911
|
addEvent2(issue.id, "progress", `${fast ? "Fast plan" : "Plan"} generated for ${issue.identifier}: ${plan.steps.length} steps, complexity: ${plan.estimatedComplexity}.`);
|
|
4967
4912
|
if (usage.totalTokens > 0) {
|
|
4968
4913
|
addEvent2(issue.id, "info", `Plan tokens (${issue.identifier}): ${usage.totalTokens.toLocaleString()} (in: ${usage.inputTokens.toLocaleString()}, out: ${usage.outputTokens.toLocaleString()}) [${usage.model}]`);
|
|
@@ -4988,13 +4933,18 @@ function refinePlanInBackground(issue, feedback, config, _workflowDefinition, ca
|
|
|
4988
4933
|
addEvent2(issue.id, "info", `Plan refinement starting for ${issue.identifier}: "${feedbackSnippet}".`);
|
|
4989
4934
|
refinePlan(issue, feedback, config, null).then(async ({ plan, usage }) => {
|
|
4990
4935
|
issue.plan = plan;
|
|
4991
|
-
|
|
4936
|
+
issue.planVersion = Math.max(issue.planVersion ?? 0, 1);
|
|
4992
4937
|
issue.planningStatus = "idle";
|
|
4993
4938
|
issue.planningStartedAt = void 0;
|
|
4994
4939
|
issue.planningError = void 0;
|
|
4995
4940
|
issue.updatedAt = now();
|
|
4996
4941
|
applyUsage(issue, usage);
|
|
4997
4942
|
applySuggestions(issue, plan);
|
|
4943
|
+
try {
|
|
4944
|
+
const { savePlanForIssue: savePlanForIssue2 } = await import("./store-O3UNJU26.js");
|
|
4945
|
+
await savePlanForIssue2(issue.id, plan, issue.planVersion);
|
|
4946
|
+
} catch {
|
|
4947
|
+
}
|
|
4998
4948
|
const feedbackPreview = feedback.length > 80 ? `${feedback.slice(0, 77)}...` : feedback;
|
|
4999
4949
|
addEvent2(issue.id, "progress", `Plan refined for ${issue.identifier}: "${feedbackPreview}" \u2192 ${plan.steps.length} steps, complexity: ${plan.estimatedComplexity}.`);
|
|
5000
4950
|
if (usage.totalTokens > 0) {
|
|
@@ -5018,10 +4968,6 @@ import { existsSync as existsSync9, mkdtempSync as mkdtempSync3, readFileSync as
|
|
|
5018
4968
|
import { spawn as spawn2 } from "child_process";
|
|
5019
4969
|
import { tmpdir as tmpdir3 } from "os";
|
|
5020
4970
|
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
4971
|
async function buildPrompt2(field, title, description, issueType, images) {
|
|
5026
4972
|
const context2 = {
|
|
5027
4973
|
title: title || "(empty)",
|
|
@@ -5072,8 +5018,9 @@ function parseCandidate(raw, expectedField) {
|
|
|
5072
5018
|
if (value && !isPlaceholder && (!field || field === expectedField)) {
|
|
5073
5019
|
return value;
|
|
5074
5020
|
}
|
|
5075
|
-
|
|
5076
|
-
|
|
5021
|
+
const nestedSource = typeof parsed.result === "string" ? parsed.result : typeof parsed.response === "string" ? parsed.response : void 0;
|
|
5022
|
+
if (nestedSource) {
|
|
5023
|
+
const nested = nestedSource.trim();
|
|
5077
5024
|
if (nested) {
|
|
5078
5025
|
const nestedClean = nested.replace(/^```(?:json)?\s*|\s*```$/g, "").trim();
|
|
5079
5026
|
for (const nestedCandidate of extractJsonObjects(nestedClean)) {
|
|
@@ -5169,65 +5116,51 @@ async function enhanceIssueField(payload, config, _workflowDefinition) {
|
|
|
5169
5116
|
const title = typeof payload.title === "string" ? payload.title.trim() : "";
|
|
5170
5117
|
const description = typeof payload.description === "string" ? payload.description.trim() : "";
|
|
5171
5118
|
const issueType = typeof payload.issueType === "string" ? payload.issueType.trim() : void 0;
|
|
5172
|
-
const
|
|
5173
|
-
|
|
5174
|
-
);
|
|
5119
|
+
const images = Array.isArray(payload.images) ? payload.images.filter((p) => typeof p === "string") : void 0;
|
|
5120
|
+
const { provider: selectedProvider, model: planModel } = await resolvePlanStageConfig(config);
|
|
5175
5121
|
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) {
|
|
5122
|
+
const isAvailable = providers.some((p) => p.name === selectedProvider && p.available);
|
|
5123
|
+
if (!isAvailable) {
|
|
5188
5124
|
const known = providers.map((entry) => `${entry.name}:${entry.available ? "available" : "missing"}`).join(", ");
|
|
5189
|
-
throw new Error(`
|
|
5125
|
+
throw new Error(`Configured plan provider "${selectedProvider}" is not available. Detected: ${known}`);
|
|
5190
5126
|
}
|
|
5191
|
-
const
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5127
|
+
const adapter = ADAPTERS[selectedProvider];
|
|
5128
|
+
if (!adapter) {
|
|
5129
|
+
throw new Error(`No adapter configured for plan provider "${selectedProvider}".`);
|
|
5130
|
+
}
|
|
5131
|
+
const ENHANCE_JSON_SCHEMA = JSON.stringify({
|
|
5195
5132
|
type: "object",
|
|
5196
5133
|
properties: {
|
|
5197
|
-
field: { type: "string" },
|
|
5134
|
+
field: { type: "string", enum: ["title", "description"] },
|
|
5198
5135
|
value: { type: "string" }
|
|
5199
5136
|
},
|
|
5200
5137
|
required: ["field", "value"],
|
|
5201
5138
|
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
|
-
}
|
|
5139
|
+
});
|
|
5140
|
+
const command = adapter.buildCommand({
|
|
5141
|
+
model: planModel,
|
|
5142
|
+
imagePaths: images,
|
|
5143
|
+
jsonSchema: selectedProvider === "claude" ? ENHANCE_JSON_SCHEMA : void 0,
|
|
5144
|
+
noToolAccess: selectedProvider === "claude"
|
|
5145
|
+
});
|
|
5146
|
+
if (!command) {
|
|
5147
|
+
throw new Error(`Adapter returned empty command for provider "${selectedProvider}".`);
|
|
5229
5148
|
}
|
|
5230
|
-
|
|
5149
|
+
const prompt = await buildPrompt2(field, title, description, issueType, images);
|
|
5150
|
+
const output = await runProviderCommand(
|
|
5151
|
+
command,
|
|
5152
|
+
selectedProvider,
|
|
5153
|
+
prompt,
|
|
5154
|
+
title,
|
|
5155
|
+
description,
|
|
5156
|
+
field,
|
|
5157
|
+
config.commandTimeoutMs,
|
|
5158
|
+
images
|
|
5159
|
+
);
|
|
5160
|
+
logger.info({ provider: selectedProvider, model: planModel, field, rawOutput: output.slice(0, 2e3) }, "Enhance raw output");
|
|
5161
|
+
const value = parseEnhancerOutput(output, field);
|
|
5162
|
+
logger.info({ provider: selectedProvider, field, parsedValue: value.slice(0, 500) }, "Enhance parsed value");
|
|
5163
|
+
return { field, value, provider: selectedProvider };
|
|
5231
5164
|
}
|
|
5232
5165
|
|
|
5233
5166
|
// src/routes/plan.ts
|
|
@@ -5540,17 +5473,6 @@ function registerScanningRoutes(app, state) {
|
|
|
5540
5473
|
return c.json({ ok: false, error: "Failed to scan project." }, 500);
|
|
5541
5474
|
}
|
|
5542
5475
|
});
|
|
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
5476
|
app.post("/api/boot/skip-scan", async (c) => {
|
|
5555
5477
|
broadcastToWebSocketClients({ type: "boot:scan:skipped" });
|
|
5556
5478
|
return c.json({ ok: true, message: "Scan skipped." });
|
|
@@ -6252,7 +6174,7 @@ async function startApiServer(state, port, options) {
|
|
|
6252
6174
|
docs: { enabled: true, title: "Fifony API", version: "1.0.0", description: "Local orchestration API for Fifony" },
|
|
6253
6175
|
cors: { enabled: true, origin: "*" },
|
|
6254
6176
|
security: { enabled: false },
|
|
6255
|
-
logging: { enabled:
|
|
6177
|
+
logging: { enabled: !QUIET_MODE, excludePaths: ["/health", "/status", "/**/*.js", "/**/*.css", "/**/*.svg"] },
|
|
6256
6178
|
compression: { enabled: true, threshold: 1024 },
|
|
6257
6179
|
health: { enabled: true },
|
|
6258
6180
|
resources: {
|
|
@@ -6290,6 +6212,7 @@ async function startApiServer(state, port, options) {
|
|
|
6290
6212
|
}
|
|
6291
6213
|
|
|
6292
6214
|
// src/persistence/store.ts
|
|
6215
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
6293
6216
|
var loadedS3dbModule = null;
|
|
6294
6217
|
var stateDb = null;
|
|
6295
6218
|
var runtimeStateResource = null;
|
|
@@ -6323,6 +6246,58 @@ function getAgentSessionResource() {
|
|
|
6323
6246
|
function getAgentPipelineResource() {
|
|
6324
6247
|
return agentPipelineResource;
|
|
6325
6248
|
}
|
|
6249
|
+
async function savePlanForIssue(issueId, plan, version) {
|
|
6250
|
+
if (!issuePlanResource) throw new Error("Issue plan resource not initialized");
|
|
6251
|
+
try {
|
|
6252
|
+
const existing = await issuePlanResource.list({
|
|
6253
|
+
partition: "byIssueCurrent",
|
|
6254
|
+
partitionValues: { issueId, current: true }
|
|
6255
|
+
});
|
|
6256
|
+
if (Array.isArray(existing)) {
|
|
6257
|
+
for (const old of existing) {
|
|
6258
|
+
if (old?.id) await issuePlanResource.patch(old.id, { current: false });
|
|
6259
|
+
}
|
|
6260
|
+
}
|
|
6261
|
+
} catch {
|
|
6262
|
+
}
|
|
6263
|
+
const planId = `plan-${randomUUID3()}`;
|
|
6264
|
+
await issuePlanResource.insert({
|
|
6265
|
+
id: planId,
|
|
6266
|
+
issueId,
|
|
6267
|
+
version,
|
|
6268
|
+
current: true,
|
|
6269
|
+
plan
|
|
6270
|
+
});
|
|
6271
|
+
return planId;
|
|
6272
|
+
}
|
|
6273
|
+
async function getCurrentPlanForIssue(issueId) {
|
|
6274
|
+
if (!issuePlanResource) return null;
|
|
6275
|
+
try {
|
|
6276
|
+
const results = await issuePlanResource.list({
|
|
6277
|
+
partition: "byIssueCurrent",
|
|
6278
|
+
partitionValues: { issueId, current: true },
|
|
6279
|
+
limit: 1
|
|
6280
|
+
});
|
|
6281
|
+
if (Array.isArray(results) && results.length > 0 && results[0]?.plan) {
|
|
6282
|
+
return { id: results[0].id, plan: results[0].plan, version: results[0].version ?? 1 };
|
|
6283
|
+
}
|
|
6284
|
+
} catch {
|
|
6285
|
+
}
|
|
6286
|
+
return null;
|
|
6287
|
+
}
|
|
6288
|
+
async function getPlansForIssue(issueId) {
|
|
6289
|
+
if (!issuePlanResource) return [];
|
|
6290
|
+
try {
|
|
6291
|
+
const results = await issuePlanResource.list({
|
|
6292
|
+
partition: "byIssue",
|
|
6293
|
+
partitionValues: { issueId }
|
|
6294
|
+
});
|
|
6295
|
+
if (!Array.isArray(results)) return [];
|
|
6296
|
+
return results.filter((r) => r?.id && r?.plan).map((r) => ({ id: r.id, plan: r.plan, version: r.version ?? 1, current: !!r.current })).sort((a, b) => a.version - b.version);
|
|
6297
|
+
} catch {
|
|
6298
|
+
return [];
|
|
6299
|
+
}
|
|
6300
|
+
}
|
|
6326
6301
|
function setActiveApiPlugin(plugin) {
|
|
6327
6302
|
activeApiPlugin = plugin;
|
|
6328
6303
|
}
|
|
@@ -6486,14 +6461,15 @@ async function recoverStateFromIssueResource() {
|
|
|
6486
6461
|
const issues = records.filter((r) => r?.id && r?.identifier && r?.state).map((r) => r);
|
|
6487
6462
|
if (issues.length === 0) return null;
|
|
6488
6463
|
logger.info(`Recovered ${issues.length} issue(s) from s3db issue resource.`);
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
} catch {
|
|
6464
|
+
for (const issue of issues) {
|
|
6465
|
+
try {
|
|
6466
|
+
const current = await getCurrentPlanForIssue(issue.id);
|
|
6467
|
+
if (current) {
|
|
6468
|
+
issue.plan = current.plan;
|
|
6469
|
+
logger.debug({ issueId: issue.id, version: current.version }, "[Recovery] Hydrated current plan");
|
|
6496
6470
|
}
|
|
6471
|
+
} catch (err) {
|
|
6472
|
+
logger.warn({ issueId: issue.id, err: String(err) }, "[Recovery] Failed to load plan");
|
|
6497
6473
|
}
|
|
6498
6474
|
}
|
|
6499
6475
|
return {
|
|
@@ -6555,22 +6531,7 @@ async function persistState(state) {
|
|
|
6555
6531
|
}
|
|
6556
6532
|
}
|
|
6557
6533
|
}
|
|
6558
|
-
|
|
6559
|
-
if (issuePlanResource && dirtyIssuePlans.size > 0) {
|
|
6560
|
-
for (const issue of state.issues) {
|
|
6561
|
-
if (!dirtyIssuePlans.has(issue.id)) continue;
|
|
6562
|
-
try {
|
|
6563
|
-
await issuePlanResource.replace(issue.id, {
|
|
6564
|
-
id: issue.id,
|
|
6565
|
-
plan: issue.plan,
|
|
6566
|
-
planHistory: issue.planHistory,
|
|
6567
|
-
planVersion: issue.planVersion ?? 0
|
|
6568
|
-
});
|
|
6569
|
-
} catch (error) {
|
|
6570
|
-
logger.warn(`Failed to persist issue plan ${issue.id}: ${String(error)}`);
|
|
6571
|
-
}
|
|
6572
|
-
}
|
|
6573
|
-
}
|
|
6534
|
+
snapshotAndClearDirtyIssuePlanIds();
|
|
6574
6535
|
const dirtyEvents = eventStateResource ? snapshotAndClearDirtyEventIds() : /* @__PURE__ */ new Set();
|
|
6575
6536
|
if (eventStateResource && dirtyEvents.size > 0) {
|
|
6576
6537
|
for (const event of state.events) {
|
|
@@ -6829,314 +6790,7 @@ function scanProjectFiles(targetRoot) {
|
|
|
6829
6790
|
packageDescription
|
|
6830
6791
|
};
|
|
6831
6792
|
}
|
|
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
6793
|
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
6794
|
var DEFAULT_REFERENCE_REPOSITORIES = [
|
|
7141
6795
|
{
|
|
7142
6796
|
id: "ring",
|
|
@@ -7575,7 +7229,7 @@ function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
|
|
|
7575
7229
|
}
|
|
7576
7230
|
|
|
7577
7231
|
// src/domains/config.ts
|
|
7578
|
-
import { env as
|
|
7232
|
+
import { env as env3 } from "process";
|
|
7579
7233
|
var VALID_EFFORTS = /* @__PURE__ */ new Set(["low", "medium", "high", "extra-high"]);
|
|
7580
7234
|
function parseEffortValue(value) {
|
|
7581
7235
|
const str = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
@@ -7637,21 +7291,21 @@ function deriveConfig(args) {
|
|
|
7637
7291
|
staleInProgressTimeoutMs: parseEnvNumber("FIFONY_STALE_IN_PROGRESS_MS", 24e5),
|
|
7638
7292
|
logLinesTail: parseEnvNumber("FIFONY_LOG_TAIL_CHARS", 12e3),
|
|
7639
7293
|
maxPreviousOutputChars: parseEnvNumber("FIFONY_PREVIOUS_OUTPUT_CHARS", 2e4),
|
|
7640
|
-
agentProvider: normalizeAgentProvider(
|
|
7641
|
-
agentCommand: toStringValue(
|
|
7294
|
+
agentProvider: normalizeAgentProvider(env3.FIFONY_AGENT_PROVIDER ?? "codex"),
|
|
7295
|
+
agentCommand: toStringValue(env3.FIFONY_AGENT_COMMAND, ""),
|
|
7642
7296
|
defaultEffort: {
|
|
7643
|
-
default: parseEffortValue(
|
|
7644
|
-
planner: parseEffortValue(
|
|
7645
|
-
executor: parseEffortValue(
|
|
7646
|
-
reviewer: parseEffortValue(
|
|
7297
|
+
default: parseEffortValue(env3.FIFONY_REASONING_EFFORT),
|
|
7298
|
+
planner: parseEffortValue(env3.FIFONY_PLANNER_EFFORT),
|
|
7299
|
+
executor: parseEffortValue(env3.FIFONY_EXECUTOR_EFFORT),
|
|
7300
|
+
reviewer: parseEffortValue(env3.FIFONY_REVIEWER_EFFORT)
|
|
7647
7301
|
},
|
|
7648
7302
|
maxConcurrentByState: {},
|
|
7649
7303
|
runMode: "filesystem",
|
|
7650
7304
|
autoReviewApproval: true,
|
|
7651
|
-
afterCreateHook:
|
|
7652
|
-
beforeRunHook:
|
|
7653
|
-
afterRunHook:
|
|
7654
|
-
beforeRemoveHook:
|
|
7305
|
+
afterCreateHook: env3.FIFONY_AFTER_CREATE_HOOK ?? "",
|
|
7306
|
+
beforeRunHook: env3.FIFONY_BEFORE_RUN_HOOK ?? "",
|
|
7307
|
+
afterRunHook: env3.FIFONY_AFTER_RUN_HOOK ?? "",
|
|
7308
|
+
beforeRemoveHook: env3.FIFONY_BEFORE_REMOVE_HOOK ?? ""
|
|
7655
7309
|
};
|
|
7656
7310
|
}
|
|
7657
7311
|
function applyWorkflowConfig(config, port) {
|
|
@@ -7873,35 +7527,33 @@ function getNextRetryAt(issue, baseMs) {
|
|
|
7873
7527
|
const nextDelay = withRetryBackoff(nextAttempt, baseMs);
|
|
7874
7528
|
return new Date(Date.now() + nextDelay).toISOString();
|
|
7875
7529
|
}
|
|
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;
|
|
7530
|
+
|
|
7531
|
+
// src/commands/request-rework.command.ts
|
|
7532
|
+
async function requestReworkCommand(input, deps) {
|
|
7533
|
+
const { issue, reviewerFeedback, note } = input;
|
|
7534
|
+
if (issue.state !== "Reviewing" && issue.state !== "PendingDecision") {
|
|
7535
|
+
throw new Error(
|
|
7536
|
+
`requestReworkCommand requires Reviewing or PendingDecision state, got ${issue.state}.`
|
|
7537
|
+
);
|
|
7900
7538
|
}
|
|
7901
|
-
|
|
7902
|
-
|
|
7539
|
+
issue.lastError = reviewerFeedback;
|
|
7540
|
+
issue.lastFailedPhase = "review";
|
|
7541
|
+
issue.attempts += 1;
|
|
7542
|
+
if (issue.state === "Reviewing") {
|
|
7543
|
+
await transitionIssueCommand(
|
|
7544
|
+
{ issue, target: "PendingDecision", note: `Reviewer completed for ${issue.identifier}.` },
|
|
7545
|
+
deps
|
|
7546
|
+
);
|
|
7903
7547
|
}
|
|
7904
|
-
|
|
7548
|
+
await transitionIssueCommand(
|
|
7549
|
+
{ issue, target: "Queued", note: note ?? `Reviewer requested rework for ${issue.identifier}.` },
|
|
7550
|
+
deps
|
|
7551
|
+
);
|
|
7552
|
+
deps.eventStore.addEvent(
|
|
7553
|
+
issue.id,
|
|
7554
|
+
"runner",
|
|
7555
|
+
`Issue ${issue.identifier} sent back for rework by reviewer.`
|
|
7556
|
+
);
|
|
7905
7557
|
}
|
|
7906
7558
|
|
|
7907
7559
|
// src/agents/issue-runner.ts
|
|
@@ -7924,8 +7576,14 @@ async function runPlanningJob(state, issue) {
|
|
|
7924
7576
|
{ persistSession: false }
|
|
7925
7577
|
);
|
|
7926
7578
|
issue.plan = plan;
|
|
7927
|
-
markIssuePlanDirty(issue.id);
|
|
7928
7579
|
issue.planVersion = Math.max(issue.planVersion ?? 0, 1);
|
|
7580
|
+
try {
|
|
7581
|
+
const { savePlanForIssue: savePlanForIssue2 } = await import("./store-O3UNJU26.js");
|
|
7582
|
+
await savePlanForIssue2(issue.id, plan, issue.planVersion);
|
|
7583
|
+
logger.debug({ issueId: issue.id, planVersion: issue.planVersion }, "[Agent] Plan saved to issue_plans resource");
|
|
7584
|
+
} catch (err) {
|
|
7585
|
+
logger.warn({ err: String(err), issueId: issue.id }, "[Agent] Failed to save plan");
|
|
7586
|
+
}
|
|
7929
7587
|
if (plan.suggestedPaths?.length && !issue.paths?.length) issue.paths = plan.suggestedPaths;
|
|
7930
7588
|
if (plan.suggestedEffort && !issue.effort) issue.effort = plan.suggestedEffort;
|
|
7931
7589
|
if (usage.totalTokens > 0) {
|
|
@@ -8156,7 +7814,7 @@ async function runIssueOnce(state, issue, running) {
|
|
|
8156
7814
|
const { workspacePath, promptText, promptFile } = await prepareWorkspace(issue, state, state.config.defaultBranch);
|
|
8157
7815
|
container.issueRepository.markDirty(issue.id);
|
|
8158
7816
|
try {
|
|
8159
|
-
const { getIssueStateResource: getIssueStateResource2 } = await import("./store-
|
|
7817
|
+
const { getIssueStateResource: getIssueStateResource2 } = await import("./store-O3UNJU26.js");
|
|
8160
7818
|
const res = getIssueStateResource2();
|
|
8161
7819
|
if (res) {
|
|
8162
7820
|
await res.patch(issue.id, {
|
|
@@ -8197,7 +7855,7 @@ async function runIssueOnce(state, issue, running) {
|
|
|
8197
7855
|
await container.persistencePort.persistState(state);
|
|
8198
7856
|
if (issue.state === "Reviewing") {
|
|
8199
7857
|
try {
|
|
8200
|
-
const { enqueue: enqueue2 } = await import("./queue-workers-
|
|
7858
|
+
const { enqueue: enqueue2 } = await import("./queue-workers-6MZWSPBW.js");
|
|
8201
7859
|
await enqueue2(issue, "review");
|
|
8202
7860
|
} catch {
|
|
8203
7861
|
}
|
|
@@ -8243,7 +7901,6 @@ export {
|
|
|
8243
7901
|
transitionIssue,
|
|
8244
7902
|
issueDependenciesResolved,
|
|
8245
7903
|
getNextRetryAt,
|
|
8246
|
-
handleStatePatch,
|
|
8247
7904
|
runAgentSession,
|
|
8248
7905
|
runAgentPipeline,
|
|
8249
7906
|
issueHasResumableSession,
|
|
@@ -8255,6 +7912,9 @@ export {
|
|
|
8255
7912
|
getSettingStateResource,
|
|
8256
7913
|
getAgentSessionResource,
|
|
8257
7914
|
getAgentPipelineResource,
|
|
7915
|
+
savePlanForIssue,
|
|
7916
|
+
getCurrentPlanForIssue,
|
|
7917
|
+
getPlansForIssue,
|
|
8258
7918
|
setActiveApiPlugin,
|
|
8259
7919
|
loadS3dbModule,
|
|
8260
7920
|
initStateStore,
|
|
@@ -8275,4 +7935,4 @@ export {
|
|
|
8275
7935
|
syncReferenceRepositories,
|
|
8276
7936
|
importReferenceArtifacts
|
|
8277
7937
|
};
|
|
8278
|
-
//# sourceMappingURL=chunk-
|
|
7938
|
+
//# sourceMappingURL=chunk-YBMQIQ2X.js.map
|