fifony 0.1.13 → 0.1.14-next.6f02449
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/{KeyboardShortcutsHelp-C7XipNeo.js → KeyboardShortcutsHelp-BHavWeoc.js} +1 -1
- package/app/dist/assets/OnboardingWizard-BaTEjLGf.js +1 -0
- package/app/dist/assets/analytics.lazy-Bt-CKC9Y.js +1 -0
- package/app/dist/assets/index-D9soGOIq.js +42 -0
- package/app/dist/assets/index-DACstix6.css +1 -0
- package/app/dist/assets/zap-BwZ-sPG7.js +1 -0
- package/app/dist/index.html +4 -3
- package/app/dist/service-worker.js +1 -1
- package/dist/cli.js +9 -4
- package/dist/cli.js.map +1 -1
- package/dist/runtime/run-local.js +801 -382
- package/dist/runtime/run-local.js.map +1 -1
- package/package.json +1 -1
- package/app/dist/assets/OnboardingWizard-D9Ps6ffD.js +0 -1
- package/app/dist/assets/analytics.lazy-CkRZNa5B.js +0 -1
- package/app/dist/assets/index-BVJ46xCh.js +0 -42
- package/app/dist/assets/index-CW9PqAUW.css +0 -1
- /package/app/dist/assets/{createLucideIcon-DDy-XBQG.js → createLucideIcon-DtZs0TX0.js} +0 -0
|
@@ -86,6 +86,10 @@ var ALLOWED_STATES = [
|
|
|
86
86
|
var TERMINAL_STATES = /* @__PURE__ */ new Set(["Done", "Cancelled"]);
|
|
87
87
|
var EXECUTING_STATES = /* @__PURE__ */ new Set(["Running", "In Review"]);
|
|
88
88
|
var PERSIST_EVENTS_MAX = 500;
|
|
89
|
+
var FAST_BOOT = CLI_ARGS.includes("--fast-boot");
|
|
90
|
+
var SKIP_SOURCE = FAST_BOOT || CLI_ARGS.includes("--skip-source");
|
|
91
|
+
var SKIP_SCAN = FAST_BOOT || CLI_ARGS.includes("--skip-scan");
|
|
92
|
+
var SKIP_RECOVERY = FAST_BOOT || CLI_ARGS.includes("--skip-recovery");
|
|
89
93
|
|
|
90
94
|
// src/runtime/helpers.ts
|
|
91
95
|
import { env as env2 } from "process";
|
|
@@ -296,7 +300,7 @@ var logger = {
|
|
|
296
300
|
};
|
|
297
301
|
|
|
298
302
|
// src/runtime/store.ts
|
|
299
|
-
import { mkdirSync as
|
|
303
|
+
import { mkdirSync as mkdirSync4 } from "fs";
|
|
300
304
|
|
|
301
305
|
// src/runtime/issues.ts
|
|
302
306
|
import { env as env4 } from "process";
|
|
@@ -463,6 +467,50 @@ function getAnalytics(topN = 20) {
|
|
|
463
467
|
};
|
|
464
468
|
}
|
|
465
469
|
|
|
470
|
+
// src/runtime/dirty-tracker.ts
|
|
471
|
+
var dirtyIssueIds = /* @__PURE__ */ new Set();
|
|
472
|
+
var dirtyEventIds = /* @__PURE__ */ new Set();
|
|
473
|
+
function markIssueDirty(id) {
|
|
474
|
+
dirtyIssueIds.add(id);
|
|
475
|
+
}
|
|
476
|
+
function markEventDirty(id) {
|
|
477
|
+
dirtyEventIds.add(id);
|
|
478
|
+
}
|
|
479
|
+
function hasDirtyState() {
|
|
480
|
+
return dirtyIssueIds.size > 0 || dirtyEventIds.size > 0;
|
|
481
|
+
}
|
|
482
|
+
function getDirtyIssueIds() {
|
|
483
|
+
return dirtyIssueIds;
|
|
484
|
+
}
|
|
485
|
+
function getDirtyEventIds() {
|
|
486
|
+
return dirtyEventIds;
|
|
487
|
+
}
|
|
488
|
+
function clearDirtyIssueIds() {
|
|
489
|
+
dirtyIssueIds.clear();
|
|
490
|
+
}
|
|
491
|
+
function clearDirtyEventIds() {
|
|
492
|
+
dirtyEventIds.clear();
|
|
493
|
+
}
|
|
494
|
+
function markAllIssuesDirty(ids) {
|
|
495
|
+
for (const id of ids) dirtyIssueIds.add(id);
|
|
496
|
+
}
|
|
497
|
+
function markAllEventsDirty(ids) {
|
|
498
|
+
for (const id of ids) dirtyEventIds.add(id);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/runtime/metrics-cache.ts
|
|
502
|
+
var cachedMetrics = null;
|
|
503
|
+
var metricsStale = true;
|
|
504
|
+
function invalidateMetrics() {
|
|
505
|
+
metricsStale = true;
|
|
506
|
+
}
|
|
507
|
+
function getMetrics(issues) {
|
|
508
|
+
if (!metricsStale && cachedMetrics) return cachedMetrics;
|
|
509
|
+
cachedMetrics = computeMetrics2(issues);
|
|
510
|
+
metricsStale = false;
|
|
511
|
+
return cachedMetrics;
|
|
512
|
+
}
|
|
513
|
+
|
|
466
514
|
// src/runtime/issue-state-machine.ts
|
|
467
515
|
var ISSUE_STATE_MACHINE_ID = "issue-lifecycle";
|
|
468
516
|
var ISSUE_STATE_MACHINE_DEFINITION = {
|
|
@@ -720,7 +768,13 @@ function getProviderDefaultCommand(provider, _reasoningEffort, model) {
|
|
|
720
768
|
if (provider === "claude") return buildClaudeCommand({ model, jsonSchema: CLAUDE_RESULT_SCHEMA });
|
|
721
769
|
return "";
|
|
722
770
|
}
|
|
771
|
+
var cachedProviders = null;
|
|
772
|
+
var providersCachedAt = 0;
|
|
773
|
+
var PROVIDER_CACHE_TTL = 6e4;
|
|
723
774
|
function detectAvailableProviders() {
|
|
775
|
+
if (cachedProviders && Date.now() - providersCachedAt < PROVIDER_CACHE_TTL) {
|
|
776
|
+
return cachedProviders;
|
|
777
|
+
}
|
|
724
778
|
const providers = [];
|
|
725
779
|
for (const name of ["claude", "codex"]) {
|
|
726
780
|
try {
|
|
@@ -730,6 +784,8 @@ function detectAvailableProviders() {
|
|
|
730
784
|
providers.push({ name, available: false, path: "" });
|
|
731
785
|
}
|
|
732
786
|
}
|
|
787
|
+
cachedProviders = providers;
|
|
788
|
+
providersCachedAt = Date.now();
|
|
733
789
|
return providers;
|
|
734
790
|
}
|
|
735
791
|
var modelCache = /* @__PURE__ */ new Map();
|
|
@@ -1257,7 +1313,7 @@ function buildRuntimeState(previous, config, definition) {
|
|
|
1257
1313
|
}
|
|
1258
1314
|
}
|
|
1259
1315
|
dedupHistoryEntries(mergedIssues);
|
|
1260
|
-
const metrics =
|
|
1316
|
+
const metrics = computeMetrics2(mergedIssues);
|
|
1261
1317
|
return {
|
|
1262
1318
|
startedAt: previous?.startedAt ?? now(),
|
|
1263
1319
|
updatedAt: now(),
|
|
@@ -1280,7 +1336,7 @@ function buildRuntimeState(previous, config, definition) {
|
|
|
1280
1336
|
]
|
|
1281
1337
|
};
|
|
1282
1338
|
}
|
|
1283
|
-
function
|
|
1339
|
+
function computeMetrics2(issues) {
|
|
1284
1340
|
let queued = 0;
|
|
1285
1341
|
let inProgress = 0;
|
|
1286
1342
|
let blocked = 0;
|
|
@@ -1361,6 +1417,7 @@ function addEvent(state, issueId, kind, message) {
|
|
|
1361
1417
|
at: now()
|
|
1362
1418
|
};
|
|
1363
1419
|
state.events = [event, ...state.events].slice(0, PERSIST_EVENTS_MAX);
|
|
1420
|
+
markEventDirty(event.id);
|
|
1364
1421
|
try {
|
|
1365
1422
|
recordEvent();
|
|
1366
1423
|
} catch {
|
|
@@ -1371,6 +1428,8 @@ function transition(issue, target, note) {
|
|
|
1371
1428
|
const previous = issue.state;
|
|
1372
1429
|
issue.state = target;
|
|
1373
1430
|
issue.updatedAt = now();
|
|
1431
|
+
markIssueDirty(issue.id);
|
|
1432
|
+
invalidateMetrics();
|
|
1374
1433
|
issue.history.push(`[${issue.updatedAt}] ${note}`);
|
|
1375
1434
|
if (previous === "Blocked" && target === "Todo") {
|
|
1376
1435
|
issue.lastError = void 0;
|
|
@@ -1519,12 +1578,14 @@ function getApiRuntimeContextOrThrow() {
|
|
|
1519
1578
|
// src/runtime/api-server.ts
|
|
1520
1579
|
import { execSync as execSync3 } from "child_process";
|
|
1521
1580
|
import {
|
|
1581
|
+
appendFileSync as appendFileSync2,
|
|
1522
1582
|
closeSync,
|
|
1523
|
-
existsSync as
|
|
1583
|
+
existsSync as existsSync10,
|
|
1524
1584
|
openSync,
|
|
1525
|
-
readFileSync as
|
|
1585
|
+
readFileSync as readFileSync9,
|
|
1526
1586
|
readSync,
|
|
1527
|
-
statSync as
|
|
1587
|
+
statSync as statSync3,
|
|
1588
|
+
writeFileSync as writeFileSync8
|
|
1528
1589
|
} from "fs";
|
|
1529
1590
|
|
|
1530
1591
|
// src/runtime/resources/runtime-state.resource.ts
|
|
@@ -1552,16 +1613,16 @@ var runtime_state_resource_default = {
|
|
|
1552
1613
|
import {
|
|
1553
1614
|
appendFileSync,
|
|
1554
1615
|
cpSync,
|
|
1555
|
-
existsSync as
|
|
1556
|
-
mkdirSync,
|
|
1557
|
-
readdirSync as
|
|
1558
|
-
readFileSync as
|
|
1616
|
+
existsSync as existsSync5,
|
|
1617
|
+
mkdirSync as mkdirSync2,
|
|
1618
|
+
readdirSync as readdirSync3,
|
|
1619
|
+
readFileSync as readFileSync4,
|
|
1559
1620
|
rmSync,
|
|
1560
|
-
statSync,
|
|
1561
|
-
writeFileSync as
|
|
1621
|
+
statSync as statSync2,
|
|
1622
|
+
writeFileSync as writeFileSync3
|
|
1562
1623
|
} from "fs";
|
|
1563
|
-
import { join as
|
|
1564
|
-
import { env as
|
|
1624
|
+
import { join as join8, relative } from "path";
|
|
1625
|
+
import { env as env6 } from "process";
|
|
1565
1626
|
import { execSync, spawn as spawn2 } from "child_process";
|
|
1566
1627
|
|
|
1567
1628
|
// src/runtime/skills.ts
|
|
@@ -1608,9 +1669,141 @@ ${skill.content}`
|
|
|
1608
1669
|
${sections.join("\n\n")}`;
|
|
1609
1670
|
}
|
|
1610
1671
|
|
|
1672
|
+
// src/runtime/workflow.ts
|
|
1673
|
+
import { existsSync as existsSync4, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync, writeFileSync } from "fs";
|
|
1674
|
+
import { copyFile, mkdir, readdir, stat, writeFile } from "fs/promises";
|
|
1675
|
+
import { extname } from "path";
|
|
1676
|
+
import { argv as argv2, exit } from "process";
|
|
1677
|
+
var sourceReadyPromise = null;
|
|
1678
|
+
var skipSourceFlag = false;
|
|
1679
|
+
function setSkipSource(skip) {
|
|
1680
|
+
skipSourceFlag = skip;
|
|
1681
|
+
}
|
|
1682
|
+
async function ensureSourceReady(onProgress) {
|
|
1683
|
+
if (skipSourceFlag) {
|
|
1684
|
+
onProgress?.("ready");
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
if (existsSync4(SOURCE_MARKER)) {
|
|
1688
|
+
onProgress?.("ready");
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
if (sourceReadyPromise) return sourceReadyPromise;
|
|
1692
|
+
sourceReadyPromise = (async () => {
|
|
1693
|
+
onProgress?.("copying");
|
|
1694
|
+
logger.info("Creating local source snapshot (async) for Fifony...");
|
|
1695
|
+
const skipDirs = /* @__PURE__ */ new Set([
|
|
1696
|
+
".git",
|
|
1697
|
+
".fifony",
|
|
1698
|
+
"node_modules",
|
|
1699
|
+
".venv",
|
|
1700
|
+
"data",
|
|
1701
|
+
"dist",
|
|
1702
|
+
"build",
|
|
1703
|
+
".turbo",
|
|
1704
|
+
".next",
|
|
1705
|
+
".nuxt",
|
|
1706
|
+
".tanstack",
|
|
1707
|
+
"coverage",
|
|
1708
|
+
"artifacts",
|
|
1709
|
+
"captures",
|
|
1710
|
+
"tmp",
|
|
1711
|
+
"temp"
|
|
1712
|
+
]);
|
|
1713
|
+
const shouldSkip = (relativePath) => {
|
|
1714
|
+
const parts = relativePath.split("/");
|
|
1715
|
+
if (parts.some((segment) => skipDirs.has(segment))) return true;
|
|
1716
|
+
const base = relativePath.split("/").at(-1) ?? "";
|
|
1717
|
+
if (base.startsWith("map_scan_") && extname(base) === ".json") return true;
|
|
1718
|
+
if (extname(base) === ".xlsx") return true;
|
|
1719
|
+
return false;
|
|
1720
|
+
};
|
|
1721
|
+
const copyRecursiveAsync = async (source, target, rel = "") => {
|
|
1722
|
+
await mkdir(target, { recursive: true });
|
|
1723
|
+
const items = await readdir(source, { withFileTypes: true });
|
|
1724
|
+
for (const item of items) {
|
|
1725
|
+
const nextRel = rel ? `${rel}/${item.name}` : item.name;
|
|
1726
|
+
if (shouldSkip(nextRel)) continue;
|
|
1727
|
+
const sourcePath = `${source}/${item.name}`;
|
|
1728
|
+
const targetPath = `${target}/${item.name}`;
|
|
1729
|
+
const itemStat = await stat(sourcePath);
|
|
1730
|
+
if (item.isDirectory()) {
|
|
1731
|
+
await copyRecursiveAsync(sourcePath, targetPath, nextRel);
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
if (item.isSymbolicLink() || itemStat.isSymbolicLink()) continue;
|
|
1735
|
+
if (itemStat.isFile() || itemStat.isFIFO()) {
|
|
1736
|
+
try {
|
|
1737
|
+
await copyFile(sourcePath, targetPath);
|
|
1738
|
+
} catch (error) {
|
|
1739
|
+
if (error.code === "ENOENT") {
|
|
1740
|
+
logger.debug(`Skipped missing source file: ${sourcePath}`);
|
|
1741
|
+
} else {
|
|
1742
|
+
throw error;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
};
|
|
1748
|
+
await mkdir(SOURCE_ROOT, { recursive: true });
|
|
1749
|
+
await copyRecursiveAsync(TARGET_ROOT, SOURCE_ROOT);
|
|
1750
|
+
await writeFile(SOURCE_MARKER, `${now()}
|
|
1751
|
+
`, "utf8");
|
|
1752
|
+
onProgress?.("ready");
|
|
1753
|
+
logger.info("Source snapshot ready (async).");
|
|
1754
|
+
})();
|
|
1755
|
+
return sourceReadyPromise;
|
|
1756
|
+
}
|
|
1757
|
+
function loadWorkflowDefinition() {
|
|
1758
|
+
const defaultPrompt = PROMPT_TEMPLATES["workflow-default"];
|
|
1759
|
+
return {
|
|
1760
|
+
workflowPath: "",
|
|
1761
|
+
rendered: "",
|
|
1762
|
+
config: {},
|
|
1763
|
+
promptTemplate: defaultPrompt,
|
|
1764
|
+
agentProvider: "codex",
|
|
1765
|
+
agentProfile: "",
|
|
1766
|
+
agentProfilePath: "",
|
|
1767
|
+
agentProfileInstructions: "",
|
|
1768
|
+
agentProviders: [],
|
|
1769
|
+
afterCreateHook: "",
|
|
1770
|
+
beforeRunHook: "",
|
|
1771
|
+
afterRunHook: "",
|
|
1772
|
+
beforeRemoveHook: ""
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
function parsePort(args) {
|
|
1776
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1777
|
+
const arg = args[i];
|
|
1778
|
+
if (arg === "--help" || arg === "-h") {
|
|
1779
|
+
console.log(
|
|
1780
|
+
`Usage: ${argv2[1]} [options]
|
|
1781
|
+
Options:
|
|
1782
|
+
--port <n> Start local dashboard (default: no UI and single batch run)
|
|
1783
|
+
--workspace <path> Target workspace root (default: current directory)
|
|
1784
|
+
--persistence <path> Persistence root (default: current directory)
|
|
1785
|
+
--concurrency <n> Maximum number of parallel issue runners
|
|
1786
|
+
--attempts <n> Maximum attempts per issue
|
|
1787
|
+
--poll <ms> Polling interval for the scheduler
|
|
1788
|
+
--once Run one local batch and exit
|
|
1789
|
+
--help Show this message`
|
|
1790
|
+
);
|
|
1791
|
+
exit(0);
|
|
1792
|
+
}
|
|
1793
|
+
if (arg === "--port") {
|
|
1794
|
+
const value = args[i + 1];
|
|
1795
|
+
if (!value || !/^\d+$/.test(value)) {
|
|
1796
|
+
fail(`Invalid value for --port: ${value ?? "<empty>"}`);
|
|
1797
|
+
}
|
|
1798
|
+
return parseIntArg(value, 4040);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
return void 0;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1611
1804
|
// src/runtime/adapters/index.ts
|
|
1612
|
-
import { writeFileSync } from "fs";
|
|
1613
|
-
import { join as
|
|
1805
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
1806
|
+
import { join as join7 } from "path";
|
|
1614
1807
|
|
|
1615
1808
|
// src/runtime/adapters/shared.ts
|
|
1616
1809
|
function buildPlanContextSection(plan) {
|
|
@@ -1862,7 +2055,7 @@ async function compileForClaude(issue, provider, plan, config, workspacePath, sk
|
|
|
1862
2055
|
}
|
|
1863
2056
|
|
|
1864
2057
|
// src/runtime/adapters/plan-to-codex.ts
|
|
1865
|
-
import { join as
|
|
2058
|
+
import { join as join6 } from "path";
|
|
1866
2059
|
var CODEX_RESULT_CONTRACT = `
|
|
1867
2060
|
Return a JSON object with this exact schema when finished:
|
|
1868
2061
|
{
|
|
@@ -1899,7 +2092,7 @@ async function compileForCodex(issue, provider, plan, config, workspacePath, ski
|
|
|
1899
2092
|
outputContract: CODEX_RESULT_CONTRACT
|
|
1900
2093
|
});
|
|
1901
2094
|
const relativeDirs = extractPlanDirs(plan);
|
|
1902
|
-
const absoluteDirs = relativeDirs.map((d) =>
|
|
2095
|
+
const absoluteDirs = relativeDirs.map((d) => join6(workspacePath, d));
|
|
1903
2096
|
const command = buildCodexCommand({
|
|
1904
2097
|
model: provider.model,
|
|
1905
2098
|
addDirs: absoluteDirs
|
|
@@ -1987,8 +2180,8 @@ function buildExecutionAudit(provider, compiled, issue, durationMs, result) {
|
|
|
1987
2180
|
}
|
|
1988
2181
|
function persistCompilationArtifacts(workspacePath, compiled) {
|
|
1989
2182
|
try {
|
|
1990
|
-
|
|
1991
|
-
|
|
2183
|
+
writeFileSync2(
|
|
2184
|
+
join7(workspacePath, "fifony-compiled-execution.json"),
|
|
1992
2185
|
JSON.stringify({
|
|
1993
2186
|
adapter: compiled.meta.adapter,
|
|
1994
2187
|
model: compiled.meta.model,
|
|
@@ -2010,8 +2203,8 @@ function persistCompilationArtifacts(workspacePath, compiled) {
|
|
|
2010
2203
|
}
|
|
2011
2204
|
if (compiled.payload) {
|
|
2012
2205
|
try {
|
|
2013
|
-
|
|
2014
|
-
|
|
2206
|
+
writeFileSync2(
|
|
2207
|
+
join7(workspacePath, "fifony-execution-payload.json"),
|
|
2015
2208
|
JSON.stringify(compiled.payload, null, 2),
|
|
2016
2209
|
"utf8"
|
|
2017
2210
|
);
|
|
@@ -2021,8 +2214,8 @@ function persistCompilationArtifacts(workspacePath, compiled) {
|
|
|
2021
2214
|
}
|
|
2022
2215
|
function persistExecutionAudit(workspacePath, audit) {
|
|
2023
2216
|
try {
|
|
2024
|
-
|
|
2025
|
-
|
|
2217
|
+
writeFileSync2(
|
|
2218
|
+
join7(workspacePath, "fifony-execution-audit.json"),
|
|
2026
2219
|
JSON.stringify(audit, null, 2),
|
|
2027
2220
|
"utf8"
|
|
2028
2221
|
);
|
|
@@ -2412,7 +2605,7 @@ function tryParseJsonOutput(output) {
|
|
|
2412
2605
|
}
|
|
2413
2606
|
function readAgentDirective(workspacePath, output, success) {
|
|
2414
2607
|
const fallbackStatus = success ? "done" : "failed";
|
|
2415
|
-
const resultFile =
|
|
2608
|
+
const resultFile = join8(workspacePath, "fifony-result.json");
|
|
2416
2609
|
let resultPayload = {};
|
|
2417
2610
|
const fullJson = (() => {
|
|
2418
2611
|
try {
|
|
@@ -2431,9 +2624,9 @@ function readAgentDirective(workspacePath, output, success) {
|
|
|
2431
2624
|
tokenUsage
|
|
2432
2625
|
};
|
|
2433
2626
|
}
|
|
2434
|
-
if (
|
|
2627
|
+
if (existsSync5(resultFile)) {
|
|
2435
2628
|
try {
|
|
2436
|
-
const parsed = JSON.parse(
|
|
2629
|
+
const parsed = JSON.parse(readFileSync4(resultFile, "utf8"));
|
|
2437
2630
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2438
2631
|
resultPayload = parsed;
|
|
2439
2632
|
}
|
|
@@ -2450,10 +2643,10 @@ function readAgentDirective(workspacePath, output, success) {
|
|
|
2450
2643
|
return { status, summary, nextPrompt, tokenUsage };
|
|
2451
2644
|
}
|
|
2452
2645
|
function readAgentPid(workspacePath) {
|
|
2453
|
-
const pidFile =
|
|
2454
|
-
if (!
|
|
2646
|
+
const pidFile = join8(workspacePath, "fifony-agent.pid");
|
|
2647
|
+
if (!existsSync5(pidFile)) return null;
|
|
2455
2648
|
try {
|
|
2456
|
-
const data = JSON.parse(
|
|
2649
|
+
const data = JSON.parse(readFileSync4(pidFile, "utf8"));
|
|
2457
2650
|
if (!data?.pid || typeof data.pid !== "number") return null;
|
|
2458
2651
|
return data;
|
|
2459
2652
|
} catch {
|
|
@@ -2470,7 +2663,7 @@ function isProcessAlive(pid) {
|
|
|
2470
2663
|
}
|
|
2471
2664
|
function isAgentStillRunning(issue) {
|
|
2472
2665
|
const wp = issue.workspacePath;
|
|
2473
|
-
if (!wp || !
|
|
2666
|
+
if (!wp || !existsSync5(wp)) return { alive: false, pid: null };
|
|
2474
2667
|
const pidInfo = readAgentPid(wp);
|
|
2475
2668
|
if (!pidInfo) return { alive: false, pid: null };
|
|
2476
2669
|
return { alive: isProcessAlive(pidInfo.pid), pid: pidInfo };
|
|
@@ -2480,7 +2673,7 @@ function cleanStalePidFile(workspacePath) {
|
|
|
2480
2673
|
if (!pidInfo) return;
|
|
2481
2674
|
if (!isProcessAlive(pidInfo.pid)) {
|
|
2482
2675
|
try {
|
|
2483
|
-
rmSync(
|
|
2676
|
+
rmSync(join8(workspacePath, "fifony-agent.pid"), { force: true });
|
|
2484
2677
|
} catch {
|
|
2485
2678
|
}
|
|
2486
2679
|
}
|
|
@@ -2522,33 +2715,33 @@ function shouldSkipRoutingPath(relativePath) {
|
|
|
2522
2715
|
return base === "WORKFLOW.local.md" || base === ".fifony-env.sh" || base.startsWith("fifony-") || base.startsWith("fifony_");
|
|
2523
2716
|
}
|
|
2524
2717
|
function inferChangedWorkspacePaths(workspacePath, limit = 32) {
|
|
2525
|
-
if (!workspacePath || !
|
|
2718
|
+
if (!workspacePath || !existsSync5(workspacePath) || !existsSync5(SOURCE_ROOT)) return [];
|
|
2526
2719
|
const changed = /* @__PURE__ */ new Set();
|
|
2527
2720
|
const walk = (currentRoot, relativeRoot = "") => {
|
|
2528
2721
|
if (changed.size >= limit) return;
|
|
2529
|
-
for (const item of
|
|
2722
|
+
for (const item of readdirSync3(currentRoot, { withFileTypes: true })) {
|
|
2530
2723
|
if (changed.size >= limit) return;
|
|
2531
2724
|
const nextRelative = relativeRoot ? `${relativeRoot}/${item.name}` : item.name;
|
|
2532
2725
|
if (shouldSkipRoutingPath(nextRelative)) continue;
|
|
2533
|
-
const currentPath =
|
|
2726
|
+
const currentPath = join8(currentRoot, item.name);
|
|
2534
2727
|
if (item.isDirectory()) {
|
|
2535
2728
|
walk(currentPath, nextRelative);
|
|
2536
2729
|
continue;
|
|
2537
2730
|
}
|
|
2538
2731
|
if (!item.isFile()) continue;
|
|
2539
|
-
const sourcePath =
|
|
2540
|
-
if (!
|
|
2732
|
+
const sourcePath = join8(SOURCE_ROOT, nextRelative);
|
|
2733
|
+
if (!existsSync5(sourcePath)) {
|
|
2541
2734
|
changed.add(nextRelative);
|
|
2542
2735
|
continue;
|
|
2543
2736
|
}
|
|
2544
|
-
const currentStat =
|
|
2545
|
-
const sourceStat =
|
|
2737
|
+
const currentStat = statSync2(currentPath);
|
|
2738
|
+
const sourceStat = statSync2(sourcePath);
|
|
2546
2739
|
if (currentStat.size !== sourceStat.size) {
|
|
2547
2740
|
changed.add(nextRelative);
|
|
2548
2741
|
continue;
|
|
2549
2742
|
}
|
|
2550
|
-
const currentFile =
|
|
2551
|
-
const sourceFile =
|
|
2743
|
+
const currentFile = readFileSync4(currentPath);
|
|
2744
|
+
const sourceFile = readFileSync4(sourcePath);
|
|
2552
2745
|
if (!currentFile.equals(sourceFile)) changed.add(nextRelative);
|
|
2553
2746
|
}
|
|
2554
2747
|
};
|
|
@@ -2557,7 +2750,7 @@ function inferChangedWorkspacePaths(workspacePath, limit = 32) {
|
|
|
2557
2750
|
}
|
|
2558
2751
|
function computeDiffStats(issue) {
|
|
2559
2752
|
const wp = issue.workspacePath;
|
|
2560
|
-
if (!wp || !
|
|
2753
|
+
if (!wp || !existsSync5(wp) || !existsSync5(SOURCE_ROOT)) return;
|
|
2561
2754
|
try {
|
|
2562
2755
|
let raw = "";
|
|
2563
2756
|
try {
|
|
@@ -2586,23 +2779,23 @@ function computeDiffStats(issue) {
|
|
|
2586
2779
|
}
|
|
2587
2780
|
}
|
|
2588
2781
|
function isConflict(relativePath) {
|
|
2589
|
-
const targetPath =
|
|
2590
|
-
const sourcePath =
|
|
2591
|
-
if (!
|
|
2592
|
-
if (!
|
|
2593
|
-
const targetStat =
|
|
2594
|
-
const sourceStat =
|
|
2782
|
+
const targetPath = join8(TARGET_ROOT, relativePath);
|
|
2783
|
+
const sourcePath = join8(SOURCE_ROOT, relativePath);
|
|
2784
|
+
if (!existsSync5(sourcePath)) return existsSync5(targetPath);
|
|
2785
|
+
if (!existsSync5(targetPath)) return false;
|
|
2786
|
+
const targetStat = statSync2(targetPath);
|
|
2787
|
+
const sourceStat = statSync2(sourcePath);
|
|
2595
2788
|
if (targetStat.size !== sourceStat.size) return true;
|
|
2596
|
-
return !
|
|
2789
|
+
return !readFileSync4(targetPath).equals(readFileSync4(sourcePath));
|
|
2597
2790
|
}
|
|
2598
2791
|
function mergeWorkspace(workspacePath) {
|
|
2599
2792
|
const result = { copied: [], deleted: [], skipped: [], conflicts: [] };
|
|
2600
|
-
if (!workspacePath || !
|
|
2793
|
+
if (!workspacePath || !existsSync5(workspacePath)) {
|
|
2601
2794
|
throw new Error(`Workspace not found: ${workspacePath}`);
|
|
2602
2795
|
}
|
|
2603
2796
|
const walkWorkspace = (dir) => {
|
|
2604
|
-
for (const item of
|
|
2605
|
-
const fullPath =
|
|
2797
|
+
for (const item of readdirSync3(dir, { withFileTypes: true })) {
|
|
2798
|
+
const fullPath = join8(dir, item.name);
|
|
2606
2799
|
const relativePath = relative(workspacePath, fullPath);
|
|
2607
2800
|
if (shouldSkipMergePath(relativePath)) {
|
|
2608
2801
|
result.skipped.push(relativePath);
|
|
@@ -2613,17 +2806,17 @@ function mergeWorkspace(workspacePath) {
|
|
|
2613
2806
|
continue;
|
|
2614
2807
|
}
|
|
2615
2808
|
if (!item.isFile()) continue;
|
|
2616
|
-
const sourcePath =
|
|
2617
|
-
const isNew = !
|
|
2809
|
+
const sourcePath = join8(SOURCE_ROOT, relativePath);
|
|
2810
|
+
const isNew = !existsSync5(sourcePath);
|
|
2618
2811
|
let isModified = false;
|
|
2619
2812
|
if (!isNew) {
|
|
2620
|
-
const wsStat =
|
|
2621
|
-
const srcStat =
|
|
2813
|
+
const wsStat = statSync2(fullPath);
|
|
2814
|
+
const srcStat = statSync2(sourcePath);
|
|
2622
2815
|
if (wsStat.size !== srcStat.size) {
|
|
2623
2816
|
isModified = true;
|
|
2624
2817
|
} else {
|
|
2625
|
-
const wsContent =
|
|
2626
|
-
const srcContent =
|
|
2818
|
+
const wsContent = readFileSync4(fullPath);
|
|
2819
|
+
const srcContent = readFileSync4(sourcePath);
|
|
2627
2820
|
isModified = !wsContent.equals(srcContent);
|
|
2628
2821
|
}
|
|
2629
2822
|
}
|
|
@@ -2632,18 +2825,18 @@ function mergeWorkspace(workspacePath) {
|
|
|
2632
2825
|
result.conflicts.push(relativePath);
|
|
2633
2826
|
continue;
|
|
2634
2827
|
}
|
|
2635
|
-
const targetDir =
|
|
2636
|
-
const targetPath =
|
|
2637
|
-
|
|
2828
|
+
const targetDir = join8(TARGET_ROOT, relative(workspacePath, dir));
|
|
2829
|
+
const targetPath = join8(TARGET_ROOT, relativePath);
|
|
2830
|
+
mkdirSync2(targetDir, { recursive: true });
|
|
2638
2831
|
cpSync(fullPath, targetPath, { force: true });
|
|
2639
2832
|
result.copied.push(relativePath);
|
|
2640
2833
|
}
|
|
2641
2834
|
}
|
|
2642
2835
|
};
|
|
2643
2836
|
const walkSource = (dir) => {
|
|
2644
|
-
if (!
|
|
2645
|
-
for (const item of
|
|
2646
|
-
const fullPath =
|
|
2837
|
+
if (!existsSync5(dir)) return;
|
|
2838
|
+
for (const item of readdirSync3(dir, { withFileTypes: true })) {
|
|
2839
|
+
const fullPath = join8(dir, item.name);
|
|
2647
2840
|
const relativePath = relative(SOURCE_ROOT, fullPath);
|
|
2648
2841
|
if (shouldSkipMergePath(relativePath)) continue;
|
|
2649
2842
|
if (item.isDirectory()) {
|
|
@@ -2651,10 +2844,10 @@ function mergeWorkspace(workspacePath) {
|
|
|
2651
2844
|
continue;
|
|
2652
2845
|
}
|
|
2653
2846
|
if (!item.isFile()) continue;
|
|
2654
|
-
const wsPath =
|
|
2655
|
-
if (!
|
|
2656
|
-
const targetPath =
|
|
2657
|
-
if (
|
|
2847
|
+
const wsPath = join8(workspacePath, relativePath);
|
|
2848
|
+
if (!existsSync5(wsPath)) {
|
|
2849
|
+
const targetPath = join8(TARGET_ROOT, relativePath);
|
|
2850
|
+
if (existsSync5(targetPath)) {
|
|
2658
2851
|
if (isConflict(relativePath)) {
|
|
2659
2852
|
result.conflicts.push(relativePath);
|
|
2660
2853
|
} else {
|
|
@@ -2976,16 +3169,16 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
|
|
|
2976
3169
|
};
|
|
2977
3170
|
for (const [key, value] of Object.entries(extraEnv)) {
|
|
2978
3171
|
if (value.length > 4e3) {
|
|
2979
|
-
const valFile =
|
|
2980
|
-
|
|
3172
|
+
const valFile = join8(workspacePath, `${key.toLowerCase()}.txt`);
|
|
3173
|
+
writeFileSync3(valFile, value, "utf8");
|
|
2981
3174
|
allVars[`${key}_FILE`] = valFile;
|
|
2982
3175
|
} else {
|
|
2983
3176
|
allVars[key] = value;
|
|
2984
3177
|
}
|
|
2985
3178
|
}
|
|
2986
|
-
const envFilePath =
|
|
3179
|
+
const envFilePath = join8(workspacePath, ".fifony-env.sh");
|
|
2987
3180
|
const envFileLines = Object.entries(allVars).map(([k, v]) => `export ${k}=${JSON.stringify(v)}`).join("\n");
|
|
2988
|
-
|
|
3181
|
+
writeFileSync3(envFilePath, envFileLines, "utf8");
|
|
2989
3182
|
const wrappedCommand = `. "${envFilePath}" && ${command}`;
|
|
2990
3183
|
const child = spawn2(wrappedCommand, {
|
|
2991
3184
|
shell: true,
|
|
@@ -2998,10 +3191,10 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
|
|
|
2998
3191
|
if (child.stdin) {
|
|
2999
3192
|
child.stdin.end();
|
|
3000
3193
|
}
|
|
3001
|
-
const pidFile =
|
|
3194
|
+
const pidFile = join8(workspacePath, "fifony-agent.pid");
|
|
3002
3195
|
const pid = child.pid;
|
|
3003
3196
|
if (pid) {
|
|
3004
|
-
|
|
3197
|
+
writeFileSync3(pidFile, JSON.stringify({
|
|
3005
3198
|
pid,
|
|
3006
3199
|
issueId: issue.id,
|
|
3007
3200
|
startedAt: new Date(started).toISOString(),
|
|
@@ -3011,8 +3204,8 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
|
|
|
3011
3204
|
let output = "";
|
|
3012
3205
|
let timedOut = false;
|
|
3013
3206
|
let outputBytes = 0;
|
|
3014
|
-
const liveLogFile =
|
|
3015
|
-
|
|
3207
|
+
const liveLogFile = join8(workspacePath, "fifony-live-output.log");
|
|
3208
|
+
writeFileSync3(liveLogFile, "", "utf8");
|
|
3016
3209
|
const onChunk = (chunk) => {
|
|
3017
3210
|
const text = String(chunk);
|
|
3018
3211
|
output = appendFileTail(output, text, config.logLinesTail);
|
|
@@ -3077,7 +3270,7 @@ async function runHook(command, workspacePath, issue, hookName, extraEnv = {}) {
|
|
|
3077
3270
|
retryDelayMs: 0,
|
|
3078
3271
|
staleInProgressTimeoutMs: 0,
|
|
3079
3272
|
logLinesTail: 12e3,
|
|
3080
|
-
agentProvider: normalizeAgentProvider(
|
|
3273
|
+
agentProvider: normalizeAgentProvider(env6.FIFONY_AGENT_PROVIDER ?? "codex"),
|
|
3081
3274
|
agentCommand: command,
|
|
3082
3275
|
maxTurns: 1,
|
|
3083
3276
|
runMode: "filesystem"
|
|
@@ -3088,8 +3281,8 @@ async function runHook(command, workspacePath, issue, hookName, extraEnv = {}) {
|
|
|
3088
3281
|
}
|
|
3089
3282
|
async function cleanWorkspace(issueId, workflowDefinition) {
|
|
3090
3283
|
const safeId = idToSafePath(issueId);
|
|
3091
|
-
const workspacePath =
|
|
3092
|
-
if (!
|
|
3284
|
+
const workspacePath = join8(WORKSPACE_ROOT, safeId);
|
|
3285
|
+
if (!existsSync5(workspacePath)) return;
|
|
3093
3286
|
if (workflowDefinition?.beforeRemoveHook) {
|
|
3094
3287
|
try {
|
|
3095
3288
|
const dummyIssue = { id: issueId, identifier: issueId };
|
|
@@ -3107,13 +3300,14 @@ async function cleanWorkspace(issueId, workflowDefinition) {
|
|
|
3107
3300
|
}
|
|
3108
3301
|
async function prepareWorkspace(issue, workflowDefinition) {
|
|
3109
3302
|
const safeId = idToSafePath(issue.id);
|
|
3110
|
-
const workspaceRoot =
|
|
3111
|
-
const createdNow = !
|
|
3303
|
+
const workspaceRoot = join8(WORKSPACE_ROOT, safeId);
|
|
3304
|
+
const createdNow = !existsSync5(workspaceRoot);
|
|
3112
3305
|
if (createdNow) {
|
|
3113
|
-
|
|
3306
|
+
mkdirSync2(workspaceRoot, { recursive: true });
|
|
3114
3307
|
if (workflowDefinition?.afterCreateHook) {
|
|
3115
3308
|
await runHook(workflowDefinition.afterCreateHook, workspaceRoot, issue, "after_create");
|
|
3116
3309
|
} else {
|
|
3310
|
+
await ensureSourceReady();
|
|
3117
3311
|
cpSync(SOURCE_ROOT, workspaceRoot, {
|
|
3118
3312
|
recursive: true,
|
|
3119
3313
|
force: true,
|
|
@@ -3121,11 +3315,11 @@ async function prepareWorkspace(issue, workflowDefinition) {
|
|
|
3121
3315
|
});
|
|
3122
3316
|
}
|
|
3123
3317
|
}
|
|
3124
|
-
const metaPath =
|
|
3318
|
+
const metaPath = join8(workspaceRoot, "fifony-issue.json");
|
|
3125
3319
|
const promptText = await buildPrompt(issue, workflowDefinition);
|
|
3126
|
-
const promptFile =
|
|
3127
|
-
|
|
3128
|
-
|
|
3320
|
+
const promptFile = join8(workspaceRoot, "fifony-prompt.md");
|
|
3321
|
+
writeFileSync3(metaPath, JSON.stringify({ ...issue, runtimeSource: SOURCE_ROOT, bootstrapAt: now() }, null, 2), "utf8");
|
|
3322
|
+
writeFileSync3(promptFile, `${promptText}
|
|
3129
3323
|
`, "utf8");
|
|
3130
3324
|
issue.workspacePath = workspaceRoot;
|
|
3131
3325
|
issue.workspacePreparedAt = now();
|
|
@@ -3142,7 +3336,7 @@ async function runAgentSession(state, issue, provider, cycle, workspacePath, bas
|
|
|
3142
3336
|
let nextPrompt = session.nextPrompt;
|
|
3143
3337
|
let lastCode = session.lastCode;
|
|
3144
3338
|
let lastOutput = session.lastOutput;
|
|
3145
|
-
const resultFile =
|
|
3339
|
+
const resultFile = join8(workspacePath, `fifony-result-${provider.role}-${provider.provider}.json`);
|
|
3146
3340
|
if (session.status === "done" && session.turns.length > 0) {
|
|
3147
3341
|
return { success: true, blocked: false, continueRequested: false, code: session.lastCode, output: session.lastOutput, turns: session.turns.length };
|
|
3148
3342
|
}
|
|
@@ -3155,8 +3349,8 @@ Agent requested additional turns beyond configured limit (${maxTurns}).`, state.
|
|
|
3155
3349
|
return { success: false, blocked: true, continueRequested: false, code: lastCode, output: session.lastOutput, turns: session.turns.length };
|
|
3156
3350
|
}
|
|
3157
3351
|
const turnPrompt = await buildTurnPrompt(issue, basePromptText, previousOutput, turnIndex, maxTurns, nextPrompt);
|
|
3158
|
-
const turnPromptFile = turnIndex === 1 ? basePromptFile :
|
|
3159
|
-
if (turnIndex > 1)
|
|
3352
|
+
const turnPromptFile = turnIndex === 1 ? basePromptFile : join8(workspacePath, `fifony-turn-${String(turnIndex).padStart(2, "0")}.md`);
|
|
3353
|
+
if (turnIndex > 1) writeFileSync3(turnPromptFile, `${turnPrompt}
|
|
3160
3354
|
`, "utf8");
|
|
3161
3355
|
session.status = "running";
|
|
3162
3356
|
session.lastPrompt = turnPrompt;
|
|
@@ -3267,7 +3461,7 @@ async function runAgentPipeline(state, issue, workspacePath, basePromptText, bas
|
|
|
3267
3461
|
const skills = discoverSkills(workspacePath);
|
|
3268
3462
|
const skillContext = buildSkillContext(skills);
|
|
3269
3463
|
if (skillContext) {
|
|
3270
|
-
|
|
3464
|
+
writeFileSync3(join8(workspacePath, "fifony-skills.md"), skillContext, "utf8");
|
|
3271
3465
|
}
|
|
3272
3466
|
const compiled = await compileExecution(issue, activeProvider, state.config, workspacePath, skillContext);
|
|
3273
3467
|
let providerPrompt;
|
|
@@ -3283,9 +3477,9 @@ async function runAgentPipeline(state, issue, workspacePath, basePromptText, bas
|
|
|
3283
3477
|
`Plan compiled for ${compiled.meta.adapter}: effort=${compiled.meta.reasoningEffort}, skills=[${compiled.meta.skillsActivated.join(",")}], subagents=[${compiled.meta.subagentsRequested.join(",")}].`
|
|
3284
3478
|
);
|
|
3285
3479
|
if (Object.keys(compiled.env).length > 0) {
|
|
3286
|
-
const envFile =
|
|
3480
|
+
const envFile = join8(workspacePath, ".fifony-compiled-env.sh");
|
|
3287
3481
|
const envLines = Object.entries(compiled.env).map(([k, v]) => `export ${k}=${JSON.stringify(v)}`).join("\n");
|
|
3288
|
-
|
|
3482
|
+
writeFileSync3(envFile, envLines, "utf8");
|
|
3289
3483
|
}
|
|
3290
3484
|
} else {
|
|
3291
3485
|
providerPrompt = await buildProviderBasePrompt(activeProvider, issue, basePromptText, workspacePath, skillContext);
|
|
@@ -3393,8 +3587,8 @@ async function runIssueOnce(state, issue, running, workflowDefinition) {
|
|
|
3393
3587
|
}
|
|
3394
3588
|
const compiled = await compileReview(issue, reviewer, workspacePath, diffSummary);
|
|
3395
3589
|
const effectiveReviewer = { ...reviewer, command: compiled.command || reviewer.command };
|
|
3396
|
-
const reviewPromptFile =
|
|
3397
|
-
|
|
3590
|
+
const reviewPromptFile = join8(workspacePath, "fifony-review-prompt.md");
|
|
3591
|
+
writeFileSync3(reviewPromptFile, `${compiled.prompt}
|
|
3398
3592
|
`, "utf8");
|
|
3399
3593
|
state._workflowDefinition = workflowDefinition;
|
|
3400
3594
|
const reviewResult = await runAgentSession(state, issue, effectiveReviewer, 1, workspacePath, compiled.prompt, reviewPromptFile);
|
|
@@ -3511,9 +3705,10 @@ async function runIssueOnce(state, issue, running, workflowDefinition) {
|
|
|
3511
3705
|
}
|
|
3512
3706
|
} finally {
|
|
3513
3707
|
issue.updatedAt = now();
|
|
3708
|
+
markIssueDirty(issue.id);
|
|
3514
3709
|
state.metrics.activeWorkers = Math.max(state.metrics.activeWorkers - 1, 0);
|
|
3515
3710
|
running.delete(issue.id);
|
|
3516
|
-
state.metrics =
|
|
3711
|
+
state.metrics = computeMetrics2(state.issues);
|
|
3517
3712
|
state.metrics.activeWorkers = Math.max(state.metrics.activeWorkers, 0);
|
|
3518
3713
|
state.updatedAt = now();
|
|
3519
3714
|
await persistState(state);
|
|
@@ -3863,43 +4058,43 @@ var NATIVE_RESOURCE_NAMES = NATIVE_RESOURCE_CONFIGS.map((resource) => resource.n
|
|
|
3863
4058
|
|
|
3864
4059
|
// src/runtime/providers-usage.ts
|
|
3865
4060
|
import { execSync as execSync2 } from "child_process";
|
|
3866
|
-
import { existsSync as
|
|
3867
|
-
import { join as
|
|
4061
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "fs";
|
|
4062
|
+
import { join as join9 } from "path";
|
|
3868
4063
|
import { homedir as homedir4 } from "os";
|
|
3869
|
-
import { env as
|
|
4064
|
+
import { env as env7 } from "process";
|
|
3870
4065
|
function resolveCodexHomeCandidates() {
|
|
3871
4066
|
const homePaths = /* @__PURE__ */ new Set([
|
|
3872
4067
|
homedir4(),
|
|
3873
|
-
|
|
3874
|
-
|
|
4068
|
+
env7.XDG_STATE_HOME?.trim() || "",
|
|
4069
|
+
env7.XDG_DATA_HOME?.trim() || ""
|
|
3875
4070
|
]);
|
|
3876
|
-
const sudoUser =
|
|
4071
|
+
const sudoUser = env7.SUDO_USER?.trim();
|
|
3877
4072
|
if (sudoUser && sudoUser !== "root") {
|
|
3878
4073
|
homePaths.add(`/home/${sudoUser}`);
|
|
3879
4074
|
}
|
|
3880
4075
|
const direct = /* @__PURE__ */ new Set([
|
|
3881
|
-
|
|
4076
|
+
env7.CODEX_HOME?.trim() || ""
|
|
3882
4077
|
]);
|
|
3883
4078
|
const candidates = [...homePaths, ...direct].filter(Boolean).flatMap((candidate) => {
|
|
3884
4079
|
if (candidate.endsWith("/.codex") || candidate.endsWith("/codex")) return [candidate];
|
|
3885
|
-
return [
|
|
4080
|
+
return [join9(candidate, ".codex"), join9(candidate, "codex")];
|
|
3886
4081
|
});
|
|
3887
4082
|
return [...new Set(candidates)];
|
|
3888
4083
|
}
|
|
3889
4084
|
function resolveCodexDir() {
|
|
3890
4085
|
for (const candidate of resolveCodexHomeCandidates()) {
|
|
3891
|
-
if (
|
|
4086
|
+
if (existsSync6(candidate)) {
|
|
3892
4087
|
return candidate;
|
|
3893
4088
|
}
|
|
3894
4089
|
}
|
|
3895
4090
|
return null;
|
|
3896
4091
|
}
|
|
3897
4092
|
function findLatestCodexDb(codexDir) {
|
|
3898
|
-
const explicit =
|
|
3899
|
-
if (
|
|
3900
|
-
const candidates =
|
|
4093
|
+
const explicit = join9(codexDir, "state_5.sqlite");
|
|
4094
|
+
if (existsSync6(explicit)) return explicit;
|
|
4095
|
+
const candidates = readdirSync4(codexDir).filter((name) => name.startsWith("state_") && name.endsWith(".sqlite")).sort().reverse();
|
|
3901
4096
|
if (candidates.length === 0) return null;
|
|
3902
|
-
return
|
|
4097
|
+
return join9(codexDir, candidates[0]);
|
|
3903
4098
|
}
|
|
3904
4099
|
function computeNextMonday() {
|
|
3905
4100
|
const now2 = /* @__PURE__ */ new Date();
|
|
@@ -3936,15 +4131,15 @@ var CLAUDE_PLAN_LIMITS = {
|
|
|
3936
4131
|
};
|
|
3937
4132
|
function collectClaudeUsage() {
|
|
3938
4133
|
const home = homedir4();
|
|
3939
|
-
const claudeDir =
|
|
3940
|
-
if (!
|
|
4134
|
+
const claudeDir = join9(home, ".claude");
|
|
4135
|
+
if (!existsSync6(claudeDir)) return null;
|
|
3941
4136
|
let available = false;
|
|
3942
4137
|
try {
|
|
3943
4138
|
execSync2("which claude", { encoding: "utf8", timeout: 3e3 });
|
|
3944
4139
|
available = true;
|
|
3945
4140
|
} catch {
|
|
3946
4141
|
}
|
|
3947
|
-
const projectsDir =
|
|
4142
|
+
const projectsDir = join9(claudeDir, "projects");
|
|
3948
4143
|
let totalInputTokens = 0;
|
|
3949
4144
|
let totalOutputTokens = 0;
|
|
3950
4145
|
let totalSessions = 0;
|
|
@@ -3958,23 +4153,23 @@ function collectClaudeUsage() {
|
|
|
3958
4153
|
const todayMs = todayStart.getTime();
|
|
3959
4154
|
const weekStart = computeWeekStart();
|
|
3960
4155
|
const weekMs = weekStart.getTime();
|
|
3961
|
-
if (
|
|
4156
|
+
if (existsSync6(projectsDir)) {
|
|
3962
4157
|
try {
|
|
3963
|
-
const projectDirs =
|
|
4158
|
+
const projectDirs = readdirSync4(projectsDir, { withFileTypes: true });
|
|
3964
4159
|
for (const dir of projectDirs) {
|
|
3965
4160
|
if (!dir.isDirectory()) continue;
|
|
3966
|
-
const projectPath =
|
|
4161
|
+
const projectPath = join9(projectsDir, dir.name);
|
|
3967
4162
|
let sessionFiles;
|
|
3968
4163
|
try {
|
|
3969
|
-
sessionFiles =
|
|
4164
|
+
sessionFiles = readdirSync4(projectPath).filter((f) => f.endsWith(".jsonl"));
|
|
3970
4165
|
} catch {
|
|
3971
4166
|
continue;
|
|
3972
4167
|
}
|
|
3973
4168
|
for (const file of sessionFiles) {
|
|
3974
|
-
const filePath =
|
|
4169
|
+
const filePath = join9(projectPath, file);
|
|
3975
4170
|
let content;
|
|
3976
4171
|
try {
|
|
3977
|
-
content =
|
|
4172
|
+
content = readFileSync5(filePath, "utf8");
|
|
3978
4173
|
} catch {
|
|
3979
4174
|
continue;
|
|
3980
4175
|
}
|
|
@@ -4028,10 +4223,10 @@ function collectClaudeUsage() {
|
|
|
4028
4223
|
];
|
|
4029
4224
|
let plan = "pro";
|
|
4030
4225
|
let resetInfo = "Weekly reset (every Monday 00:00 UTC)";
|
|
4031
|
-
const settingsPath =
|
|
4032
|
-
if (
|
|
4226
|
+
const settingsPath = join9(claudeDir, "settings.json");
|
|
4227
|
+
if (existsSync6(settingsPath)) {
|
|
4033
4228
|
try {
|
|
4034
|
-
const settings = JSON.parse(
|
|
4229
|
+
const settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
|
|
4035
4230
|
if (settings.plan === "max" || settings.plan === "max5x") {
|
|
4036
4231
|
plan = settings.plan;
|
|
4037
4232
|
resetInfo = `Plan: ${settings.plan.toUpperCase()} \u2014 Weekly token limit resets every Monday 00:00 UTC`;
|
|
@@ -4069,11 +4264,11 @@ function collectCodexUsage() {
|
|
|
4069
4264
|
} catch {
|
|
4070
4265
|
}
|
|
4071
4266
|
const models = [];
|
|
4072
|
-
const modelsCachePath =
|
|
4267
|
+
const modelsCachePath = join9(codexDir, "models_cache.json");
|
|
4073
4268
|
let currentModel = "";
|
|
4074
|
-
if (
|
|
4269
|
+
if (existsSync6(modelsCachePath)) {
|
|
4075
4270
|
try {
|
|
4076
|
-
const cache = JSON.parse(
|
|
4271
|
+
const cache = JSON.parse(readFileSync5(modelsCachePath, "utf8"));
|
|
4077
4272
|
for (const m of cache.models || []) {
|
|
4078
4273
|
models.push({
|
|
4079
4274
|
slug: m.slug,
|
|
@@ -4084,10 +4279,10 @@ function collectCodexUsage() {
|
|
|
4084
4279
|
} catch {
|
|
4085
4280
|
}
|
|
4086
4281
|
}
|
|
4087
|
-
const configPath =
|
|
4088
|
-
if (
|
|
4282
|
+
const configPath = join9(codexDir, "config.toml");
|
|
4283
|
+
if (existsSync6(configPath)) {
|
|
4089
4284
|
try {
|
|
4090
|
-
const configContent =
|
|
4285
|
+
const configContent = readFileSync5(configPath, "utf8");
|
|
4091
4286
|
const modelMatch = configContent.match(/^model\s*=\s*"([^"]+)"/m);
|
|
4092
4287
|
if (modelMatch) currentModel = modelMatch[1];
|
|
4093
4288
|
} catch {
|
|
@@ -4179,6 +4374,14 @@ function collectProvidersUsage() {
|
|
|
4179
4374
|
|
|
4180
4375
|
// src/runtime/scheduler.ts
|
|
4181
4376
|
var shuttingDown = false;
|
|
4377
|
+
var lastPersistAt = 0;
|
|
4378
|
+
var PERSIST_DEBOUNCE_MS = 5e3;
|
|
4379
|
+
var schedulerWakeResolve = null;
|
|
4380
|
+
function wakeScheduler() {
|
|
4381
|
+
schedulerWakeResolve?.();
|
|
4382
|
+
}
|
|
4383
|
+
var IDLE_POLL_MS = 5e3;
|
|
4384
|
+
var ACTIVE_POLL_MS = 500;
|
|
4182
4385
|
function installGracefulShutdown(state, running) {
|
|
4183
4386
|
const handler = async (signal) => {
|
|
4184
4387
|
if (shuttingDown) {
|
|
@@ -4197,7 +4400,7 @@ function installGracefulShutdown(state, running) {
|
|
|
4197
4400
|
}
|
|
4198
4401
|
}
|
|
4199
4402
|
state.updatedAt = now();
|
|
4200
|
-
state.metrics =
|
|
4403
|
+
state.metrics = computeMetrics2(state.issues);
|
|
4201
4404
|
try {
|
|
4202
4405
|
await persistState(state);
|
|
4203
4406
|
logger.info("State persisted.");
|
|
@@ -4275,6 +4478,7 @@ async function ensureNotStale(state, staleTimeoutMs) {
|
|
|
4275
4478
|
issue.attempts += 1;
|
|
4276
4479
|
issue.nextRetryAt = getNextRetryAt(issue, state.config.retryDelayMs);
|
|
4277
4480
|
issue.startedAt = void 0;
|
|
4481
|
+
markIssueDirty(issue.id);
|
|
4278
4482
|
await transitionIssueState(issue, "Blocked", `Issue state auto-recovered from stale execution.`);
|
|
4279
4483
|
}
|
|
4280
4484
|
}
|
|
@@ -4347,9 +4551,20 @@ async function scheduler(state, running, runForever, workflowDefinition) {
|
|
|
4347
4551
|
}
|
|
4348
4552
|
}
|
|
4349
4553
|
state.updatedAt = now();
|
|
4350
|
-
|
|
4554
|
+
const shouldPersist = hasDirtyState() || Date.now() - lastPersistAt > PERSIST_DEBOUNCE_MS;
|
|
4555
|
+
if (shouldPersist) {
|
|
4556
|
+
await persistState(state);
|
|
4557
|
+
lastPersistAt = Date.now();
|
|
4558
|
+
}
|
|
4351
4559
|
logger.debug("Scheduler tick completed.");
|
|
4352
|
-
|
|
4560
|
+
const effectivePoll = running.size > 0 ? ACTIVE_POLL_MS : IDLE_POLL_MS;
|
|
4561
|
+
await Promise.race([
|
|
4562
|
+
sleep(effectivePoll),
|
|
4563
|
+
new Promise((resolve4) => {
|
|
4564
|
+
schedulerWakeResolve = resolve4;
|
|
4565
|
+
})
|
|
4566
|
+
]);
|
|
4567
|
+
schedulerWakeResolve = null;
|
|
4353
4568
|
}
|
|
4354
4569
|
return;
|
|
4355
4570
|
}
|
|
@@ -4381,11 +4596,11 @@ async function scheduler(state, running, runForever, workflowDefinition) {
|
|
|
4381
4596
|
}
|
|
4382
4597
|
|
|
4383
4598
|
// src/runtime/issue-enhancer.ts
|
|
4384
|
-
import { env as
|
|
4385
|
-
import { existsSync as
|
|
4599
|
+
import { env as env8 } from "process";
|
|
4600
|
+
import { existsSync as existsSync7, mkdtempSync, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
4386
4601
|
import { spawn as spawn3 } from "child_process";
|
|
4387
4602
|
import { tmpdir } from "os";
|
|
4388
|
-
import { join as
|
|
4603
|
+
import { join as join10 } from "path";
|
|
4389
4604
|
function getProviderCommand(provider, config, workflowDefinition) {
|
|
4390
4605
|
const workflowConfig = workflowDefinition ? workflowDefinition.config : {};
|
|
4391
4606
|
const codexCommand = getNestedString(getNestedRecord(workflowConfig, "codex"), "command");
|
|
@@ -4455,23 +4670,23 @@ function parseCandidate(raw, expectedField) {
|
|
|
4455
4670
|
return "";
|
|
4456
4671
|
}
|
|
4457
4672
|
function readProviderOutput(resultFile, fallback) {
|
|
4458
|
-
if (
|
|
4673
|
+
if (existsSync7(resultFile)) {
|
|
4459
4674
|
try {
|
|
4460
|
-
return
|
|
4675
|
+
return readFileSync6(resultFile, "utf8").trim();
|
|
4461
4676
|
} catch {
|
|
4462
4677
|
}
|
|
4463
4678
|
}
|
|
4464
4679
|
return fallback;
|
|
4465
4680
|
}
|
|
4466
4681
|
async function runProviderCommand(command, provider, prompt, title, description, field, timeoutMs) {
|
|
4467
|
-
const tempDir = mkdtempSync(
|
|
4468
|
-
const promptFile =
|
|
4469
|
-
const issuePayloadFile =
|
|
4470
|
-
const resultFile =
|
|
4471
|
-
const envFile =
|
|
4472
|
-
|
|
4682
|
+
const tempDir = mkdtempSync(join10(tmpdir(), "fifony-enhance-"));
|
|
4683
|
+
const promptFile = join10(tempDir, "fifony-enhance-prompt.md");
|
|
4684
|
+
const issuePayloadFile = join10(tempDir, "fifony-issue.json");
|
|
4685
|
+
const resultFile = join10(tempDir, "fifony-result.txt");
|
|
4686
|
+
const envFile = join10(tempDir, "fifony-enhance-env.sh");
|
|
4687
|
+
writeFileSync4(promptFile, `${prompt}
|
|
4473
4688
|
`, "utf8");
|
|
4474
|
-
|
|
4689
|
+
writeFileSync4(issuePayloadFile, JSON.stringify({ title, description, field }, null, 2), "utf8");
|
|
4475
4690
|
const envLines = [
|
|
4476
4691
|
`export FIFONY_ISSUE_TITLE=${JSON.stringify(title)}`,
|
|
4477
4692
|
`export FIFONY_ISSUE_DESCRIPTION=${JSON.stringify(description)}`,
|
|
@@ -4482,11 +4697,11 @@ async function runProviderCommand(command, provider, prompt, title, description,
|
|
|
4482
4697
|
"export FIFONY_AGENT_PROVIDER=" + JSON.stringify(provider),
|
|
4483
4698
|
"export FIFONY_RESULT_FILE=" + JSON.stringify(resultFile)
|
|
4484
4699
|
];
|
|
4485
|
-
const processEnv = Object.entries(
|
|
4700
|
+
const processEnv = Object.entries(env8).map(([key, value]) => {
|
|
4486
4701
|
if (typeof value !== "string") return `export ${key}=${JSON.stringify("")}`;
|
|
4487
4702
|
return `export ${key}=${JSON.stringify(value)}`;
|
|
4488
4703
|
}).join("\n");
|
|
4489
|
-
|
|
4704
|
+
writeFileSync4(envFile, `${processEnv}
|
|
4490
4705
|
${envLines.join("\n")}
|
|
4491
4706
|
`, "utf8");
|
|
4492
4707
|
const wrappedCommand = `. "${envFile}" && ${command}`;
|
|
@@ -4587,9 +4802,9 @@ async function enhanceIssueField(payload, config, workflowDefinition) {
|
|
|
4587
4802
|
}
|
|
4588
4803
|
|
|
4589
4804
|
// src/runtime/issue-planner.ts
|
|
4590
|
-
import { mkdtempSync as mkdtempSync2, writeFileSync as
|
|
4805
|
+
import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync5, rmSync as rmSync3 } from "fs";
|
|
4591
4806
|
import { spawn as spawn4 } from "child_process";
|
|
4592
|
-
import { join as
|
|
4807
|
+
import { join as join11 } from "path";
|
|
4593
4808
|
import { tmpdir as tmpdir2 } from "os";
|
|
4594
4809
|
var PLANNING_SETTING_ID = "planning:active";
|
|
4595
4810
|
function emptySession() {
|
|
@@ -4876,12 +5091,12 @@ async function generatePlan(title, description, config, workflowDefinition, opti
|
|
|
4876
5091
|
};
|
|
4877
5092
|
await persistSession(session);
|
|
4878
5093
|
const prompt = await buildPlanPrompt(title, description, fast);
|
|
4879
|
-
const tempDir = mkdtempSync2(
|
|
4880
|
-
const promptFile =
|
|
4881
|
-
const envFile =
|
|
4882
|
-
|
|
5094
|
+
const tempDir = mkdtempSync2(join11(tmpdir2(), "fifony-plan-"));
|
|
5095
|
+
const promptFile = join11(tempDir, "fifony-plan-prompt.md");
|
|
5096
|
+
const envFile = join11(tempDir, "fifony-plan-env.sh");
|
|
5097
|
+
writeFileSync5(promptFile, `${prompt}
|
|
4883
5098
|
`, "utf8");
|
|
4884
|
-
|
|
5099
|
+
writeFileSync5(envFile, [
|
|
4885
5100
|
`export FIFONY_PROMPT_FILE=${JSON.stringify(promptFile)}`,
|
|
4886
5101
|
`export FIFONY_AGENT_PROVIDER=${JSON.stringify(preferred)}`
|
|
4887
5102
|
].join("\n"), "utf8");
|
|
@@ -5013,12 +5228,12 @@ async function refinePlan(issue, feedback, config, workflowDefinition) {
|
|
|
5013
5228
|
if (!command) throw new Error(`No command configured for provider ${preferred}.`);
|
|
5014
5229
|
const refineStartMs = Date.now();
|
|
5015
5230
|
const prompt = await buildRefinePrompt(issue.title, issue.description, issue.plan, feedback);
|
|
5016
|
-
const tempDir = mkdtempSync2(
|
|
5017
|
-
const promptFile =
|
|
5018
|
-
const envFile =
|
|
5019
|
-
|
|
5231
|
+
const tempDir = mkdtempSync2(join11(tmpdir2(), "fifony-refine-"));
|
|
5232
|
+
const promptFile = join11(tempDir, "fifony-refine-prompt.md");
|
|
5233
|
+
const envFile = join11(tempDir, "fifony-refine-env.sh");
|
|
5234
|
+
writeFileSync5(promptFile, `${prompt}
|
|
5020
5235
|
`, "utf8");
|
|
5021
|
-
|
|
5236
|
+
writeFileSync5(envFile, [
|
|
5022
5237
|
`export FIFONY_PROMPT_FILE=${JSON.stringify(promptFile)}`,
|
|
5023
5238
|
`export FIFONY_AGENT_PROVIDER=${JSON.stringify(preferred)}`
|
|
5024
5239
|
].join("\n"), "utf8");
|
|
@@ -5158,19 +5373,20 @@ function refinePlanInBackground(issue, feedback, config, workflowDefinition, cal
|
|
|
5158
5373
|
|
|
5159
5374
|
// src/runtime/project-scanner.ts
|
|
5160
5375
|
import {
|
|
5161
|
-
existsSync as
|
|
5376
|
+
existsSync as existsSync8,
|
|
5162
5377
|
mkdtempSync as mkdtempSync3,
|
|
5163
|
-
readdirSync as
|
|
5164
|
-
readFileSync as
|
|
5378
|
+
readdirSync as readdirSync5,
|
|
5379
|
+
readFileSync as readFileSync7,
|
|
5165
5380
|
rmSync as rmSync4,
|
|
5166
|
-
writeFileSync as
|
|
5381
|
+
writeFileSync as writeFileSync6
|
|
5167
5382
|
} from "fs";
|
|
5168
|
-
import { join as
|
|
5383
|
+
import { join as join12, basename as basename2 } from "path";
|
|
5169
5384
|
import { spawn as spawn5 } from "child_process";
|
|
5170
5385
|
import { tmpdir as tmpdir3 } from "os";
|
|
5171
|
-
import { env as
|
|
5386
|
+
import { env as env9 } from "process";
|
|
5387
|
+
import { createHash } from "crypto";
|
|
5172
5388
|
function scanProjectFiles(targetRoot) {
|
|
5173
|
-
const check = (rel) =>
|
|
5389
|
+
const check = (rel) => existsSync8(join12(targetRoot, rel));
|
|
5174
5390
|
const files = {
|
|
5175
5391
|
claudeMd: check("CLAUDE.md"),
|
|
5176
5392
|
claudeDir: check(".claude"),
|
|
@@ -5192,10 +5408,10 @@ function scanProjectFiles(targetRoot) {
|
|
|
5192
5408
|
};
|
|
5193
5409
|
const existingAgents = [];
|
|
5194
5410
|
for (const agentDir of [".claude/agents", ".codex/agents"]) {
|
|
5195
|
-
const fullPath =
|
|
5196
|
-
if (!
|
|
5411
|
+
const fullPath = join12(targetRoot, agentDir);
|
|
5412
|
+
if (!existsSync8(fullPath)) continue;
|
|
5197
5413
|
try {
|
|
5198
|
-
const entries =
|
|
5414
|
+
const entries = readdirSync5(fullPath);
|
|
5199
5415
|
for (const entry of entries) {
|
|
5200
5416
|
if (entry.endsWith(".md")) {
|
|
5201
5417
|
existingAgents.push(basename2(entry, ".md"));
|
|
@@ -5206,13 +5422,13 @@ function scanProjectFiles(targetRoot) {
|
|
|
5206
5422
|
}
|
|
5207
5423
|
const existingSkills = [];
|
|
5208
5424
|
for (const skillDir of [".claude/skills", ".codex/skills"]) {
|
|
5209
|
-
const fullPath =
|
|
5210
|
-
if (!
|
|
5425
|
+
const fullPath = join12(targetRoot, skillDir);
|
|
5426
|
+
if (!existsSync8(fullPath)) continue;
|
|
5211
5427
|
try {
|
|
5212
|
-
const entries =
|
|
5428
|
+
const entries = readdirSync5(fullPath);
|
|
5213
5429
|
for (const entry of entries) {
|
|
5214
|
-
const skillFile =
|
|
5215
|
-
if (
|
|
5430
|
+
const skillFile = join12(fullPath, entry, "SKILL.md");
|
|
5431
|
+
if (existsSync8(skillFile)) {
|
|
5216
5432
|
existingSkills.push(entry);
|
|
5217
5433
|
}
|
|
5218
5434
|
}
|
|
@@ -5220,20 +5436,20 @@ function scanProjectFiles(targetRoot) {
|
|
|
5220
5436
|
}
|
|
5221
5437
|
}
|
|
5222
5438
|
let readmeExcerpt = "";
|
|
5223
|
-
const readmePath =
|
|
5224
|
-
if (
|
|
5439
|
+
const readmePath = join12(targetRoot, "README.md");
|
|
5440
|
+
if (existsSync8(readmePath)) {
|
|
5225
5441
|
try {
|
|
5226
|
-
const content =
|
|
5442
|
+
const content = readFileSync7(readmePath, "utf8");
|
|
5227
5443
|
readmeExcerpt = content.slice(0, 200).trim();
|
|
5228
5444
|
} catch {
|
|
5229
5445
|
}
|
|
5230
5446
|
}
|
|
5231
5447
|
let packageName = "";
|
|
5232
5448
|
let packageDescription = "";
|
|
5233
|
-
const pkgPath =
|
|
5234
|
-
if (
|
|
5449
|
+
const pkgPath = join12(targetRoot, "package.json");
|
|
5450
|
+
if (existsSync8(pkgPath)) {
|
|
5235
5451
|
try {
|
|
5236
|
-
const pkg = JSON.parse(
|
|
5452
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
|
|
5237
5453
|
packageName = typeof pkg.name === "string" ? pkg.name : "";
|
|
5238
5454
|
packageDescription = typeof pkg.description === "string" ? pkg.description : "";
|
|
5239
5455
|
} catch {
|
|
@@ -5275,39 +5491,39 @@ function buildFallbackAnalysis(targetRoot) {
|
|
|
5275
5491
|
let description = "";
|
|
5276
5492
|
let readmeExcerpt = "";
|
|
5277
5493
|
for (const readmeFile of ["README.md", "README.rst", "README.txt", "README"]) {
|
|
5278
|
-
const p =
|
|
5279
|
-
if (
|
|
5494
|
+
const p = join12(targetRoot, readmeFile);
|
|
5495
|
+
if (existsSync8(p)) {
|
|
5280
5496
|
try {
|
|
5281
|
-
readmeExcerpt =
|
|
5497
|
+
readmeExcerpt = readFileSync7(p, "utf8").slice(0, 300).trim();
|
|
5282
5498
|
break;
|
|
5283
5499
|
} catch {
|
|
5284
5500
|
}
|
|
5285
5501
|
}
|
|
5286
5502
|
}
|
|
5287
|
-
const pkgPath =
|
|
5288
|
-
if (
|
|
5503
|
+
const pkgPath = join12(targetRoot, "package.json");
|
|
5504
|
+
if (existsSync8(pkgPath)) {
|
|
5289
5505
|
try {
|
|
5290
|
-
const pkg = JSON.parse(
|
|
5506
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
|
|
5291
5507
|
const name = typeof pkg.name === "string" ? pkg.name : "";
|
|
5292
5508
|
const desc = typeof pkg.description === "string" ? pkg.description : "";
|
|
5293
5509
|
if (desc) description = name ? `${name}: ${desc}` : desc;
|
|
5294
5510
|
} catch {
|
|
5295
5511
|
}
|
|
5296
5512
|
}
|
|
5297
|
-
const cargoPath =
|
|
5298
|
-
if (!description &&
|
|
5513
|
+
const cargoPath = join12(targetRoot, "Cargo.toml");
|
|
5514
|
+
if (!description && existsSync8(cargoPath)) {
|
|
5299
5515
|
try {
|
|
5300
|
-
const content =
|
|
5516
|
+
const content = readFileSync7(cargoPath, "utf8");
|
|
5301
5517
|
const descMatch = content.match(/^description\s*=\s*"([^"]+)"/m);
|
|
5302
5518
|
const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m);
|
|
5303
5519
|
if (descMatch) description = nameMatch ? `${nameMatch[1]}: ${descMatch[1]}` : descMatch[1];
|
|
5304
5520
|
} catch {
|
|
5305
5521
|
}
|
|
5306
5522
|
}
|
|
5307
|
-
const pyprojectPath =
|
|
5308
|
-
if (!description &&
|
|
5523
|
+
const pyprojectPath = join12(targetRoot, "pyproject.toml");
|
|
5524
|
+
if (!description && existsSync8(pyprojectPath)) {
|
|
5309
5525
|
try {
|
|
5310
|
-
const content =
|
|
5526
|
+
const content = readFileSync7(pyprojectPath, "utf8");
|
|
5311
5527
|
const descMatch = content.match(/^description\s*=\s*"([^"]+)"/m);
|
|
5312
5528
|
const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m);
|
|
5313
5529
|
if (descMatch) description = nameMatch ? `${nameMatch[1]}: ${descMatch[1]}` : descMatch[1];
|
|
@@ -5320,7 +5536,7 @@ function buildFallbackAnalysis(targetRoot) {
|
|
|
5320
5536
|
let language = "unknown";
|
|
5321
5537
|
const stack = [];
|
|
5322
5538
|
for (const [file, signal] of Object.entries(BUILD_FILE_SIGNALS)) {
|
|
5323
|
-
if (
|
|
5539
|
+
if (existsSync8(join12(targetRoot, file))) {
|
|
5324
5540
|
if (language === "unknown" && signal.language !== "unknown") {
|
|
5325
5541
|
language = signal.language;
|
|
5326
5542
|
}
|
|
@@ -5388,7 +5604,51 @@ function validateAnalysis(parsed) {
|
|
|
5388
5604
|
source: "cli"
|
|
5389
5605
|
};
|
|
5390
5606
|
}
|
|
5391
|
-
|
|
5607
|
+
var ANALYSIS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
5608
|
+
function computeProjectHash(targetRoot) {
|
|
5609
|
+
const buildFiles = Object.keys(BUILD_FILE_SIGNALS);
|
|
5610
|
+
const found = buildFiles.filter((f) => existsSync8(join12(targetRoot, f))).sort();
|
|
5611
|
+
return createHash("sha256").update(found.join(",")).digest("hex").slice(0, 16);
|
|
5612
|
+
}
|
|
5613
|
+
async function loadCachedAnalysis(targetRoot) {
|
|
5614
|
+
const resource = getSettingStateResource();
|
|
5615
|
+
if (!resource) return null;
|
|
5616
|
+
const hash = computeProjectHash(targetRoot);
|
|
5617
|
+
const key = `project-analysis:${hash}`;
|
|
5618
|
+
try {
|
|
5619
|
+
const record2 = await resource.get(key);
|
|
5620
|
+
if (!record2?.value) return null;
|
|
5621
|
+
const cached = record2.value;
|
|
5622
|
+
if (!cached.analysis || !cached.updatedAt) return null;
|
|
5623
|
+
if (Date.now() - Date.parse(cached.updatedAt) > ANALYSIS_CACHE_TTL_MS) return null;
|
|
5624
|
+
return cached.analysis;
|
|
5625
|
+
} catch {
|
|
5626
|
+
return null;
|
|
5627
|
+
}
|
|
5628
|
+
}
|
|
5629
|
+
async function saveCachedAnalysis(targetRoot, analysis) {
|
|
5630
|
+
const resource = getSettingStateResource();
|
|
5631
|
+
if (!resource) return;
|
|
5632
|
+
const hash = computeProjectHash(targetRoot);
|
|
5633
|
+
const key = `project-analysis:${hash}`;
|
|
5634
|
+
try {
|
|
5635
|
+
await resource.replace(key, {
|
|
5636
|
+
id: key,
|
|
5637
|
+
scope: "system",
|
|
5638
|
+
source: "detected",
|
|
5639
|
+
value: { analysis, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
5640
|
+
});
|
|
5641
|
+
} catch {
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5644
|
+
async function analyzeProjectWithCli(provider, targetRoot, options) {
|
|
5645
|
+
if (!options?.forceRefresh) {
|
|
5646
|
+
const cached = await loadCachedAnalysis(targetRoot);
|
|
5647
|
+
if (cached) {
|
|
5648
|
+
logger.info("Using cached project analysis.");
|
|
5649
|
+
return cached;
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5392
5652
|
const normalizedProvider = provider.trim().toLowerCase();
|
|
5393
5653
|
const providers = detectAvailableProviders();
|
|
5394
5654
|
const providerInfo = providers.find((p) => p.name === normalizedProvider && p.available);
|
|
@@ -5399,12 +5659,12 @@ async function analyzeProjectWithCli(provider, targetRoot) {
|
|
|
5399
5659
|
);
|
|
5400
5660
|
return buildFallbackAnalysis(targetRoot);
|
|
5401
5661
|
}
|
|
5402
|
-
const tempDir = mkdtempSync3(
|
|
5403
|
-
const promptFile =
|
|
5662
|
+
const tempDir = mkdtempSync3(join12(tmpdir3(), "fifony-scan-"));
|
|
5663
|
+
const promptFile = join12(tempDir, "fifony-scan-prompt.txt");
|
|
5404
5664
|
const analysisPrompt = await renderPrompt("project-analysis");
|
|
5405
|
-
|
|
5665
|
+
writeFileSync6(promptFile, analysisPrompt, "utf8");
|
|
5406
5666
|
const processEnv = {};
|
|
5407
|
-
for (const [key, value] of Object.entries(
|
|
5667
|
+
for (const [key, value] of Object.entries(env9)) {
|
|
5408
5668
|
if (typeof value === "string") processEnv[key] = value;
|
|
5409
5669
|
}
|
|
5410
5670
|
processEnv.FIFONY_PROMPT_FILE = promptFile;
|
|
@@ -5473,6 +5733,7 @@ async function analyzeProjectWithCli(provider, targetRoot) {
|
|
|
5473
5733
|
{ provider: normalizedProvider, domains: analysis.domains, stack: analysis.stack },
|
|
5474
5734
|
"CLI project analysis completed"
|
|
5475
5735
|
);
|
|
5736
|
+
await saveCachedAnalysis(targetRoot, analysis);
|
|
5476
5737
|
return analysis;
|
|
5477
5738
|
}
|
|
5478
5739
|
logger.warn(
|
|
@@ -5494,27 +5755,180 @@ async function analyzeProjectWithCli(provider, targetRoot) {
|
|
|
5494
5755
|
}
|
|
5495
5756
|
}
|
|
5496
5757
|
|
|
5758
|
+
// src/runtime/issue-scanner.ts
|
|
5759
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
5760
|
+
var SCAN_PATTERN = /\b(TODO|FIXME|HACK|XXX)\b[:\s]*(.*)/i;
|
|
5761
|
+
var EXCLUDE_DIRS = [
|
|
5762
|
+
"node_modules",
|
|
5763
|
+
".git",
|
|
5764
|
+
".fifony",
|
|
5765
|
+
"dist",
|
|
5766
|
+
"build",
|
|
5767
|
+
".turbo",
|
|
5768
|
+
".next",
|
|
5769
|
+
".nuxt",
|
|
5770
|
+
"coverage",
|
|
5771
|
+
".venv",
|
|
5772
|
+
"vendor",
|
|
5773
|
+
"tmp",
|
|
5774
|
+
"temp",
|
|
5775
|
+
"artifacts"
|
|
5776
|
+
];
|
|
5777
|
+
function scanForTodos(targetRoot) {
|
|
5778
|
+
const excludeArgs = EXCLUDE_DIRS.flatMap((dir) => ["--exclude-dir", dir]);
|
|
5779
|
+
let output;
|
|
5780
|
+
try {
|
|
5781
|
+
output = execFileSync2("grep", [
|
|
5782
|
+
"-rn",
|
|
5783
|
+
"-E",
|
|
5784
|
+
"\\b(TODO|FIXME|HACK|XXX)\\b",
|
|
5785
|
+
...excludeArgs,
|
|
5786
|
+
"--include=*.ts",
|
|
5787
|
+
"--include=*.tsx",
|
|
5788
|
+
"--include=*.js",
|
|
5789
|
+
"--include=*.jsx",
|
|
5790
|
+
"--include=*.py",
|
|
5791
|
+
"--include=*.rs",
|
|
5792
|
+
"--include=*.go",
|
|
5793
|
+
"--include=*.java",
|
|
5794
|
+
"--include=*.rb",
|
|
5795
|
+
"--include=*.php",
|
|
5796
|
+
"--include=*.cs",
|
|
5797
|
+
"--include=*.swift",
|
|
5798
|
+
"--include=*.kt",
|
|
5799
|
+
"--include=*.vue",
|
|
5800
|
+
"--include=*.svelte",
|
|
5801
|
+
targetRoot
|
|
5802
|
+
], {
|
|
5803
|
+
encoding: "utf8",
|
|
5804
|
+
timeout: 15e3,
|
|
5805
|
+
maxBuffer: 5e6
|
|
5806
|
+
});
|
|
5807
|
+
} catch (error) {
|
|
5808
|
+
if (error.status === 1) return [];
|
|
5809
|
+
if (error.stdout) output = error.stdout;
|
|
5810
|
+
else {
|
|
5811
|
+
logger.warn(`TODO scan failed: ${String(error)}`);
|
|
5812
|
+
return [];
|
|
5813
|
+
}
|
|
5814
|
+
}
|
|
5815
|
+
const results = [];
|
|
5816
|
+
const lines = output.split("\n").filter(Boolean);
|
|
5817
|
+
for (const line of lines) {
|
|
5818
|
+
const match = line.match(/^(.+?):(\d+):(.+)$/);
|
|
5819
|
+
if (!match) continue;
|
|
5820
|
+
const [, file, lineNo, content] = match;
|
|
5821
|
+
const todoMatch = content.match(SCAN_PATTERN);
|
|
5822
|
+
if (!todoMatch) continue;
|
|
5823
|
+
const [, tag, text] = todoMatch;
|
|
5824
|
+
const source = tag.toLowerCase();
|
|
5825
|
+
const trimmedText = text.trim();
|
|
5826
|
+
if (!trimmedText || trimmedText.length < 5) continue;
|
|
5827
|
+
const relativePath = file.startsWith(targetRoot) ? file.slice(targetRoot.length + 1) : file;
|
|
5828
|
+
results.push({
|
|
5829
|
+
source: source === "xxx" ? "hack" : source,
|
|
5830
|
+
title: trimmedText.length > 120 ? `${trimmedText.slice(0, 117)}...` : trimmedText,
|
|
5831
|
+
file: relativePath,
|
|
5832
|
+
line: parseInt(lineNo, 10),
|
|
5833
|
+
context: content.trim()
|
|
5834
|
+
});
|
|
5835
|
+
}
|
|
5836
|
+
return results;
|
|
5837
|
+
}
|
|
5838
|
+
function categorizeScannedIssues(issues, workflowDefinition) {
|
|
5839
|
+
const options = getCapabilityRoutingOptions(workflowDefinition);
|
|
5840
|
+
return issues.map((issue) => {
|
|
5841
|
+
const resolution = resolveTaskCapabilities({
|
|
5842
|
+
id: `scan-${issue.file}:${issue.line}`,
|
|
5843
|
+
identifier: `${issue.source}:${issue.file}:${issue.line}`,
|
|
5844
|
+
title: issue.title,
|
|
5845
|
+
description: issue.context,
|
|
5846
|
+
labels: [issue.source],
|
|
5847
|
+
paths: [issue.file]
|
|
5848
|
+
}, options);
|
|
5849
|
+
return {
|
|
5850
|
+
...issue,
|
|
5851
|
+
category: resolution.category,
|
|
5852
|
+
overlays: resolution.overlays,
|
|
5853
|
+
rationale: resolution.rationale,
|
|
5854
|
+
suggestedLabels: [
|
|
5855
|
+
issue.source,
|
|
5856
|
+
resolution.category ? `capability:${resolution.category}` : ""
|
|
5857
|
+
].filter(Boolean),
|
|
5858
|
+
suggestedPaths: [issue.file]
|
|
5859
|
+
};
|
|
5860
|
+
});
|
|
5861
|
+
}
|
|
5862
|
+
|
|
5863
|
+
// src/runtime/github-sync.ts
|
|
5864
|
+
import { execFile } from "child_process";
|
|
5865
|
+
async function fetchGitHubIssues(targetRoot) {
|
|
5866
|
+
return new Promise((resolve4) => {
|
|
5867
|
+
execFile(
|
|
5868
|
+
"gh",
|
|
5869
|
+
[
|
|
5870
|
+
"issue",
|
|
5871
|
+
"list",
|
|
5872
|
+
"--json",
|
|
5873
|
+
"number,title,body,labels,state,url",
|
|
5874
|
+
"--state",
|
|
5875
|
+
"open",
|
|
5876
|
+
"--limit",
|
|
5877
|
+
"50"
|
|
5878
|
+
],
|
|
5879
|
+
{
|
|
5880
|
+
cwd: targetRoot,
|
|
5881
|
+
timeout: 15e3,
|
|
5882
|
+
maxBuffer: 2e6
|
|
5883
|
+
},
|
|
5884
|
+
(error, stdout2) => {
|
|
5885
|
+
if (error) {
|
|
5886
|
+
logger.warn(`Failed to fetch GitHub issues: ${String(error)}`);
|
|
5887
|
+
resolve4([]);
|
|
5888
|
+
return;
|
|
5889
|
+
}
|
|
5890
|
+
try {
|
|
5891
|
+
const issues = JSON.parse(stdout2.trim());
|
|
5892
|
+
const results = issues.map((issue) => ({
|
|
5893
|
+
source: "github",
|
|
5894
|
+
title: issue.title,
|
|
5895
|
+
file: "",
|
|
5896
|
+
line: 0,
|
|
5897
|
+
context: (issue.body || "").slice(0, 500),
|
|
5898
|
+
suggestedLabels: issue.labels.map((l) => l.name),
|
|
5899
|
+
suggestedPaths: []
|
|
5900
|
+
}));
|
|
5901
|
+
resolve4(results);
|
|
5902
|
+
} catch (parseError) {
|
|
5903
|
+
logger.warn(`Failed to parse GitHub issues: ${String(parseError)}`);
|
|
5904
|
+
resolve4([]);
|
|
5905
|
+
}
|
|
5906
|
+
}
|
|
5907
|
+
);
|
|
5908
|
+
});
|
|
5909
|
+
}
|
|
5910
|
+
|
|
5497
5911
|
// src/runtime/catalog.ts
|
|
5498
|
-
import { existsSync as
|
|
5499
|
-
import { join as
|
|
5912
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
5913
|
+
import { join as join13, dirname as dirname2 } from "path";
|
|
5500
5914
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5501
5915
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
5502
5916
|
var __dirname2 = dirname2(__filename2);
|
|
5503
5917
|
function resolveFixturePath(filename) {
|
|
5504
5918
|
const candidates = [
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5919
|
+
join13(__dirname2, "..", "fixtures", filename),
|
|
5920
|
+
join13(__dirname2, "../..", "src", "fixtures", filename),
|
|
5921
|
+
join13(__dirname2, "../../..", "src", "fixtures", filename)
|
|
5508
5922
|
];
|
|
5509
5923
|
for (const candidate of candidates) {
|
|
5510
|
-
if (
|
|
5924
|
+
if (existsSync9(candidate)) return candidate;
|
|
5511
5925
|
}
|
|
5512
5926
|
return candidates[0];
|
|
5513
5927
|
}
|
|
5514
5928
|
function loadAgentCatalog() {
|
|
5515
5929
|
try {
|
|
5516
5930
|
const filePath = resolveFixturePath("agent-catalog.json");
|
|
5517
|
-
const raw =
|
|
5931
|
+
const raw = readFileSync8(filePath, "utf8");
|
|
5518
5932
|
return JSON.parse(raw);
|
|
5519
5933
|
} catch (error) {
|
|
5520
5934
|
logger.error({ err: error }, "Failed to load agent catalog");
|
|
@@ -5524,7 +5938,7 @@ function loadAgentCatalog() {
|
|
|
5524
5938
|
function loadSkillCatalog() {
|
|
5525
5939
|
try {
|
|
5526
5940
|
const filePath = resolveFixturePath("skill-catalog.json");
|
|
5527
|
-
const raw =
|
|
5941
|
+
const raw = readFileSync8(filePath, "utf8");
|
|
5528
5942
|
return JSON.parse(raw);
|
|
5529
5943
|
} catch (error) {
|
|
5530
5944
|
logger.error({ err: error }, "Failed to load skill catalog");
|
|
@@ -5543,9 +5957,9 @@ function filterByDomains(catalog, domains) {
|
|
|
5543
5957
|
function installAgents(targetRoot, agentNames, catalog) {
|
|
5544
5958
|
const result = { installed: [], skipped: [], errors: [] };
|
|
5545
5959
|
const catalogMap = new Map(catalog.map((entry) => [entry.name, entry]));
|
|
5546
|
-
const agentsDir =
|
|
5960
|
+
const agentsDir = join13(targetRoot, ".claude", "agents");
|
|
5547
5961
|
try {
|
|
5548
|
-
|
|
5962
|
+
mkdirSync3(agentsDir, { recursive: true });
|
|
5549
5963
|
} catch (error) {
|
|
5550
5964
|
logger.error({ err: error, path: agentsDir }, "Failed to create agents directory");
|
|
5551
5965
|
result.errors.push({ name: "_directory", error: `Failed to create ${agentsDir}` });
|
|
@@ -5557,13 +5971,13 @@ function installAgents(targetRoot, agentNames, catalog) {
|
|
|
5557
5971
|
result.errors.push({ name, error: "Agent not found in catalog" });
|
|
5558
5972
|
continue;
|
|
5559
5973
|
}
|
|
5560
|
-
const filePath =
|
|
5561
|
-
if (
|
|
5974
|
+
const filePath = join13(agentsDir, `${name}.md`);
|
|
5975
|
+
if (existsSync9(filePath)) {
|
|
5562
5976
|
result.skipped.push(name);
|
|
5563
5977
|
continue;
|
|
5564
5978
|
}
|
|
5565
5979
|
try {
|
|
5566
|
-
|
|
5980
|
+
writeFileSync7(filePath, entry.content, "utf8");
|
|
5567
5981
|
result.installed.push(name);
|
|
5568
5982
|
logger.info({ agent: name, path: filePath }, "Agent installed");
|
|
5569
5983
|
} catch (error) {
|
|
@@ -5578,9 +5992,9 @@ function installAgents(targetRoot, agentNames, catalog) {
|
|
|
5578
5992
|
function installSkills(targetRoot, skillNames, catalog) {
|
|
5579
5993
|
const result = { installed: [], skipped: [], errors: [] };
|
|
5580
5994
|
const catalogMap = new Map(catalog.map((entry) => [entry.name, entry]));
|
|
5581
|
-
const skillsDir =
|
|
5995
|
+
const skillsDir = join13(targetRoot, ".claude", "skills");
|
|
5582
5996
|
try {
|
|
5583
|
-
|
|
5997
|
+
mkdirSync3(skillsDir, { recursive: true });
|
|
5584
5998
|
} catch (error) {
|
|
5585
5999
|
logger.error({ err: error, path: skillsDir }, "Failed to create skills directory");
|
|
5586
6000
|
result.errors.push({ name: "_directory", error: `Failed to create ${skillsDir}` });
|
|
@@ -5592,16 +6006,16 @@ function installSkills(targetRoot, skillNames, catalog) {
|
|
|
5592
6006
|
result.errors.push({ name, error: "Skill not found in catalog" });
|
|
5593
6007
|
continue;
|
|
5594
6008
|
}
|
|
5595
|
-
const skillDir =
|
|
5596
|
-
const filePath =
|
|
5597
|
-
if (
|
|
6009
|
+
const skillDir = join13(skillsDir, name);
|
|
6010
|
+
const filePath = join13(skillDir, "SKILL.md");
|
|
6011
|
+
if (existsSync9(filePath)) {
|
|
5598
6012
|
result.skipped.push(name);
|
|
5599
6013
|
continue;
|
|
5600
6014
|
}
|
|
5601
6015
|
try {
|
|
5602
|
-
|
|
6016
|
+
mkdirSync3(skillDir, { recursive: true });
|
|
5603
6017
|
if (entry.installType === "bundled" && entry.content) {
|
|
5604
|
-
|
|
6018
|
+
writeFileSync7(filePath, entry.content, "utf8");
|
|
5605
6019
|
} else {
|
|
5606
6020
|
const referenceContent = [
|
|
5607
6021
|
`# ${entry.displayName}`,
|
|
@@ -5613,7 +6027,7 @@ function installSkills(targetRoot, skillNames, catalog) {
|
|
|
5613
6027
|
"",
|
|
5614
6028
|
`> This skill references an external resource. Install it from the source above.`
|
|
5615
6029
|
].filter(Boolean).join("\n");
|
|
5616
|
-
|
|
6030
|
+
writeFileSync7(filePath, referenceContent, "utf8");
|
|
5617
6031
|
}
|
|
5618
6032
|
result.installed.push(name);
|
|
5619
6033
|
logger.info({ skill: name, path: filePath, type: entry.installType }, "Skill installed");
|
|
@@ -5628,6 +6042,7 @@ function installSkills(targetRoot, skillNames, catalog) {
|
|
|
5628
6042
|
}
|
|
5629
6043
|
|
|
5630
6044
|
// src/runtime/api-server.ts
|
|
6045
|
+
import { join as join14 } from "path";
|
|
5631
6046
|
var wsClients = /* @__PURE__ */ new Map();
|
|
5632
6047
|
var broadcastSeq = 0;
|
|
5633
6048
|
var lastBroadcastIssueSnapshot = /* @__PURE__ */ new Map();
|
|
@@ -5767,6 +6182,7 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
5767
6182
|
}
|
|
5768
6183
|
await updater(issue);
|
|
5769
6184
|
await persistState(state);
|
|
6185
|
+
wakeScheduler();
|
|
5770
6186
|
return c.json({ ok: true, issue });
|
|
5771
6187
|
};
|
|
5772
6188
|
const resourceConfigs = Object.fromEntries(
|
|
@@ -5787,10 +6203,10 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
5787
6203
|
}
|
|
5788
6204
|
setApiRuntimeContext(state, workflowDefinition);
|
|
5789
6205
|
const serveTextFile = (filePath, contentType, cacheControl = "no-cache") => {
|
|
5790
|
-
if (!
|
|
6206
|
+
if (!existsSync10(filePath)) {
|
|
5791
6207
|
return new Response("Not found", { status: 404 });
|
|
5792
6208
|
}
|
|
5793
|
-
return new Response(
|
|
6209
|
+
return new Response(readFileSync9(filePath), {
|
|
5794
6210
|
headers: {
|
|
5795
6211
|
"content-type": contentType,
|
|
5796
6212
|
"cache-control": cacheControl
|
|
@@ -5798,10 +6214,10 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
5798
6214
|
});
|
|
5799
6215
|
};
|
|
5800
6216
|
const serveAppShell = () => {
|
|
5801
|
-
if (!
|
|
6217
|
+
if (!existsSync10(FRONTEND_INDEX)) {
|
|
5802
6218
|
return new Response("Not found", { status: 404 });
|
|
5803
6219
|
}
|
|
5804
|
-
const html =
|
|
6220
|
+
const html = readFileSync9(FRONTEND_INDEX, "utf8").replace('href="/assets/manifest.webmanifest"', 'href="/manifest.webmanifest"').replaceAll('href="/assets/icon.svg"', 'href="/icon.svg"');
|
|
5805
6221
|
return new Response(html, {
|
|
5806
6222
|
headers: {
|
|
5807
6223
|
"content-type": "text/html; charset=utf-8",
|
|
@@ -5830,7 +6246,7 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
5830
6246
|
type: "connected",
|
|
5831
6247
|
seq: broadcastSeq,
|
|
5832
6248
|
timestamp: now(),
|
|
5833
|
-
metrics:
|
|
6249
|
+
metrics: computeMetrics2(state.issues),
|
|
5834
6250
|
capabilities: computeCapabilityCounts(state.issues),
|
|
5835
6251
|
issues: state.issues,
|
|
5836
6252
|
events: state.events.slice(0, 50)
|
|
@@ -5883,12 +6299,14 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
5883
6299
|
"GET /icon-maskable.svg": () => serveTextFile(FRONTEND_MASKABLE_ICON_SVG, "image/svg+xml", "public, max-age=604800, immutable"),
|
|
5884
6300
|
"GET /kanban": () => serveAppShell(),
|
|
5885
6301
|
"GET /issues": () => serveAppShell(),
|
|
6302
|
+
"GET /discover": () => serveAppShell(),
|
|
5886
6303
|
"GET /agents": () => serveAppShell(),
|
|
5887
6304
|
"GET /settings": () => serveAppShell(),
|
|
5888
6305
|
"GET /settings/general": () => serveAppShell(),
|
|
5889
6306
|
"GET /settings/notifications": () => serveAppShell(),
|
|
5890
6307
|
"GET /settings/workflow": () => serveAppShell(),
|
|
5891
6308
|
"GET /settings/providers": () => serveAppShell(),
|
|
6309
|
+
"GET /api/health": (c) => c.json({ status: state.booting ? "booting" : "ready" }),
|
|
5892
6310
|
"GET /api/state": async (c) => {
|
|
5893
6311
|
const showAll = c.req.query("all") === "1";
|
|
5894
6312
|
let issues = state.issues;
|
|
@@ -5907,7 +6325,7 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
5907
6325
|
...state,
|
|
5908
6326
|
issues,
|
|
5909
6327
|
capabilities: computeCapabilityCounts(issues),
|
|
5910
|
-
metrics:
|
|
6328
|
+
metrics: computeMetrics2(issues),
|
|
5911
6329
|
_filter: showAll ? "all" : "recent",
|
|
5912
6330
|
_totalIssues: state.issues.length
|
|
5913
6331
|
});
|
|
@@ -6066,11 +6484,13 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6066
6484
|
const payload = await c.req.json();
|
|
6067
6485
|
const issue = createIssueFromPayload(payload, state.issues, workflowDefinition);
|
|
6068
6486
|
state.issues.push(issue);
|
|
6487
|
+
markIssueDirty(issue.id);
|
|
6069
6488
|
addEvent(state, issue.id, "info", `Issue ${issue.identifier} created via API.`);
|
|
6070
6489
|
if (issue.plan) {
|
|
6071
6490
|
addEvent(state, issue.id, "info", `Plan: ${issue.plan.steps.length} steps, complexity: ${issue.plan.estimatedComplexity}.`);
|
|
6072
6491
|
}
|
|
6073
6492
|
await persistState(state);
|
|
6493
|
+
wakeScheduler();
|
|
6074
6494
|
return c.json({ ok: true, issue }, 201);
|
|
6075
6495
|
} catch (error) {
|
|
6076
6496
|
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
@@ -6113,6 +6533,7 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6113
6533
|
const payload = await c.req.json();
|
|
6114
6534
|
await handleStatePatch(state, issue, payload);
|
|
6115
6535
|
await persistState(state);
|
|
6536
|
+
wakeScheduler();
|
|
6116
6537
|
return c.json({ ok: true, issue });
|
|
6117
6538
|
} catch (error) {
|
|
6118
6539
|
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
@@ -6171,7 +6592,7 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6171
6592
|
const issue = findIssue2(issueId);
|
|
6172
6593
|
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
6173
6594
|
const wp = issue.workspacePath;
|
|
6174
|
-
if (!wp || !
|
|
6595
|
+
if (!wp || !existsSync10(wp)) {
|
|
6175
6596
|
return c.json({ ok: false, error: "No workspace found for this issue." }, 400);
|
|
6176
6597
|
}
|
|
6177
6598
|
const result = mergeWorkspace(wp);
|
|
@@ -6239,10 +6660,10 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6239
6660
|
const liveLog = wp ? `${wp}/fifony-live-output.log` : null;
|
|
6240
6661
|
let logTail = "";
|
|
6241
6662
|
let logSize = 0;
|
|
6242
|
-
if (liveLog &&
|
|
6663
|
+
if (liveLog && existsSync10(liveLog)) {
|
|
6243
6664
|
try {
|
|
6244
|
-
const
|
|
6245
|
-
logSize =
|
|
6665
|
+
const stat2 = statSync3(liveLog);
|
|
6666
|
+
logSize = stat2.size;
|
|
6246
6667
|
const fd = openSync(liveLog, "r");
|
|
6247
6668
|
const readSize = Math.min(logSize, 8192);
|
|
6248
6669
|
const buf = Buffer.alloc(readSize);
|
|
@@ -6279,10 +6700,10 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6279
6700
|
const issue = findIssue2(issueId);
|
|
6280
6701
|
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
6281
6702
|
const wp = issue.workspacePath;
|
|
6282
|
-
if (!wp || !
|
|
6703
|
+
if (!wp || !existsSync10(wp)) {
|
|
6283
6704
|
return c.json({ ok: true, files: [], diff: "", message: "No workspace found." });
|
|
6284
6705
|
}
|
|
6285
|
-
if (!
|
|
6706
|
+
if (!existsSync10(SOURCE_ROOT)) {
|
|
6286
6707
|
return c.json({ ok: true, files: [], diff: "", message: "Source root not found." });
|
|
6287
6708
|
}
|
|
6288
6709
|
let raw = "";
|
|
@@ -6348,6 +6769,46 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6348
6769
|
});
|
|
6349
6770
|
return c.json({ events: events.slice(0, 200) });
|
|
6350
6771
|
},
|
|
6772
|
+
// ── Onboarding: gitignore check ────────────────────────────────────
|
|
6773
|
+
"GET /api/gitignore/status": async (c) => {
|
|
6774
|
+
try {
|
|
6775
|
+
const gitignorePath = join14(TARGET_ROOT, ".gitignore");
|
|
6776
|
+
if (!existsSync10(gitignorePath)) {
|
|
6777
|
+
return c.json({ exists: false, hasFifony: false });
|
|
6778
|
+
}
|
|
6779
|
+
const content = readFileSync9(gitignorePath, "utf-8");
|
|
6780
|
+
const lines = content.split("\n").map((l) => l.trim());
|
|
6781
|
+
const hasFifony = lines.some((l) => l === ".fifony" || l === ".fifony/" || l === "/.fifony" || l === "/.fifony/");
|
|
6782
|
+
return c.json({ exists: true, hasFifony });
|
|
6783
|
+
} catch (error) {
|
|
6784
|
+
logger.error({ err: error }, "Failed to check .gitignore");
|
|
6785
|
+
return c.json({ exists: false, hasFifony: false, error: "Failed to check .gitignore" }, 500);
|
|
6786
|
+
}
|
|
6787
|
+
},
|
|
6788
|
+
"POST /api/gitignore/add": async (c) => {
|
|
6789
|
+
try {
|
|
6790
|
+
const gitignorePath = join14(TARGET_ROOT, ".gitignore");
|
|
6791
|
+
if (!existsSync10(gitignorePath)) {
|
|
6792
|
+
writeFileSync8(gitignorePath, "# Fifony state directory\n.fifony/\n", "utf-8");
|
|
6793
|
+
return c.json({ ok: true, created: true });
|
|
6794
|
+
}
|
|
6795
|
+
const content = readFileSync9(gitignorePath, "utf-8");
|
|
6796
|
+
const lines = content.split("\n").map((l) => l.trim());
|
|
6797
|
+
const hasFifony = lines.some((l) => l === ".fifony" || l === ".fifony/" || l === "/.fifony" || l === "/.fifony/");
|
|
6798
|
+
if (hasFifony) {
|
|
6799
|
+
return c.json({ ok: true, alreadyPresent: true });
|
|
6800
|
+
}
|
|
6801
|
+
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
6802
|
+
appendFileSync2(gitignorePath, `${suffix}
|
|
6803
|
+
# Fifony state directory
|
|
6804
|
+
.fifony/
|
|
6805
|
+
`, "utf-8");
|
|
6806
|
+
return c.json({ ok: true, added: true });
|
|
6807
|
+
} catch (error) {
|
|
6808
|
+
logger.error({ err: error }, "Failed to update .gitignore");
|
|
6809
|
+
return c.json({ ok: false, error: "Failed to update .gitignore" }, 500);
|
|
6810
|
+
}
|
|
6811
|
+
},
|
|
6351
6812
|
// ── Onboarding: project scanning & catalog ─────────────────────────
|
|
6352
6813
|
"GET /api/scan/project": async (c) => {
|
|
6353
6814
|
try {
|
|
@@ -6369,6 +6830,30 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6369
6830
|
return c.json({ ok: false, error: "Failed to analyze project." }, 500);
|
|
6370
6831
|
}
|
|
6371
6832
|
},
|
|
6833
|
+
"GET /api/scan/issues": async (c) => {
|
|
6834
|
+
try {
|
|
6835
|
+
const todos = scanForTodos(TARGET_ROOT);
|
|
6836
|
+
const categorized = categorizeScannedIssues(todos, workflowDefinition);
|
|
6837
|
+
return c.json({ ok: true, issues: categorized, total: categorized.length });
|
|
6838
|
+
} catch (error) {
|
|
6839
|
+
logger.error({ err: error }, "Failed to scan for TODOs");
|
|
6840
|
+
return c.json({ ok: false, error: "Failed to scan for issues." }, 500);
|
|
6841
|
+
}
|
|
6842
|
+
},
|
|
6843
|
+
"POST /api/boot/skip-scan": async (c) => {
|
|
6844
|
+
broadcastToWebSocketClients({ type: "boot:scan:skipped" });
|
|
6845
|
+
return c.json({ ok: true, message: "Scan skipped." });
|
|
6846
|
+
},
|
|
6847
|
+
"GET /api/scan/github-issues": async (c) => {
|
|
6848
|
+
try {
|
|
6849
|
+
const issues = await fetchGitHubIssues(TARGET_ROOT);
|
|
6850
|
+
const categorized = categorizeScannedIssues(issues, workflowDefinition);
|
|
6851
|
+
return c.json({ ok: true, issues: categorized, total: categorized.length });
|
|
6852
|
+
} catch (error) {
|
|
6853
|
+
logger.error({ err: error }, "Failed to fetch GitHub issues");
|
|
6854
|
+
return c.json({ ok: false, error: "Failed to fetch GitHub issues." }, 500);
|
|
6855
|
+
}
|
|
6856
|
+
},
|
|
6372
6857
|
"GET /api/catalog/agents": async (c) => {
|
|
6373
6858
|
const domainsParam = c.req.query("domains");
|
|
6374
6859
|
const domains = typeof domainsParam === "string" ? domainsParam.split(",").map((d) => d.trim()).filter(Boolean) : [];
|
|
@@ -6495,7 +6980,7 @@ async function initStateStore() {
|
|
|
6495
6980
|
debugBoot("initStateStore:start");
|
|
6496
6981
|
const { S3db, FileSystemClient, StateMachinePlugin } = await loadS3dbModule();
|
|
6497
6982
|
debugBoot("initStateStore:module-loaded");
|
|
6498
|
-
|
|
6983
|
+
mkdirSync4(S3DB_DATABASE_PATH, { recursive: true });
|
|
6499
6984
|
stateDb = new S3db({
|
|
6500
6985
|
client: new FileSystemClient({
|
|
6501
6986
|
basePath: S3DB_DATABASE_PATH,
|
|
@@ -6634,20 +7119,25 @@ async function recoverStateFromIssueResource() {
|
|
|
6634
7119
|
}
|
|
6635
7120
|
async function persistState(state) {
|
|
6636
7121
|
state.metrics = {
|
|
6637
|
-
...
|
|
7122
|
+
...getMetrics(state.issues),
|
|
6638
7123
|
activeWorkers: state.metrics.activeWorkers
|
|
6639
7124
|
};
|
|
6640
7125
|
if (!runtimeStateResource) return;
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
7126
|
+
const dirty = hasDirtyState();
|
|
7127
|
+
if (dirty) {
|
|
7128
|
+
await runtimeStateResource.replace(S3DB_RUNTIME_RECORD_ID, {
|
|
7129
|
+
id: S3DB_RUNTIME_RECORD_ID,
|
|
7130
|
+
schemaVersion: S3DB_RUNTIME_SCHEMA_VERSION,
|
|
7131
|
+
trackerKind: "filesystem",
|
|
7132
|
+
runtimeTag: "local-only",
|
|
7133
|
+
updatedAt: now(),
|
|
7134
|
+
state
|
|
7135
|
+
});
|
|
7136
|
+
}
|
|
7137
|
+
const dirtyIssues = getDirtyIssueIds();
|
|
7138
|
+
if (issueStateResource && dirtyIssues.size > 0) {
|
|
6650
7139
|
for (const issue of state.issues) {
|
|
7140
|
+
if (!dirtyIssues.has(issue.id)) continue;
|
|
6651
7141
|
const clean = {
|
|
6652
7142
|
...issue,
|
|
6653
7143
|
nextRetryAt: issue.nextRetryAt || void 0,
|
|
@@ -6662,11 +7152,15 @@ async function persistState(state) {
|
|
|
6662
7152
|
logger.warn(`Failed to persist issue ${issue.id}: ${String(error)}`);
|
|
6663
7153
|
}
|
|
6664
7154
|
}
|
|
7155
|
+
clearDirtyIssueIds();
|
|
6665
7156
|
}
|
|
6666
|
-
|
|
7157
|
+
const dirtyEvents = getDirtyEventIds();
|
|
7158
|
+
if (eventStateResource && dirtyEvents.size > 0) {
|
|
6667
7159
|
for (const event of state.events) {
|
|
7160
|
+
if (!dirtyEvents.has(event.id)) continue;
|
|
6668
7161
|
await eventStateResource.replace(event.id, event);
|
|
6669
7162
|
}
|
|
7163
|
+
clearDirtyEventIds();
|
|
6670
7164
|
}
|
|
6671
7165
|
broadcastToWebSocketClients({
|
|
6672
7166
|
type: "state:update",
|
|
@@ -6677,6 +7171,11 @@ async function persistState(state) {
|
|
|
6677
7171
|
updatedAt: state.updatedAt
|
|
6678
7172
|
});
|
|
6679
7173
|
}
|
|
7174
|
+
async function persistStateFull(state) {
|
|
7175
|
+
markAllIssuesDirty(state.issues.map((i) => i.id));
|
|
7176
|
+
markAllEventsDirty(state.events.map((e) => e.id));
|
|
7177
|
+
await persistState(state);
|
|
7178
|
+
}
|
|
6680
7179
|
async function loadPersistedSettings() {
|
|
6681
7180
|
if (!settingStateResource?.list) return [];
|
|
6682
7181
|
try {
|
|
@@ -6750,119 +7249,6 @@ async function closeStateStore() {
|
|
|
6750
7249
|
}
|
|
6751
7250
|
}
|
|
6752
7251
|
|
|
6753
|
-
// src/runtime/workflow.ts
|
|
6754
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync4, readdirSync as readdirSync5, readFileSync as readFileSync9, statSync as statSync3, writeFileSync as writeFileSync7 } from "fs";
|
|
6755
|
-
import { extname } from "path";
|
|
6756
|
-
import { argv as argv2, exit } from "process";
|
|
6757
|
-
function bootstrapSource() {
|
|
6758
|
-
if (existsSync10(SOURCE_MARKER)) return;
|
|
6759
|
-
logger.info("Creating local source snapshot for Fifony (local-only runtime)...");
|
|
6760
|
-
const skipDirs = /* @__PURE__ */ new Set([
|
|
6761
|
-
".git",
|
|
6762
|
-
".fifony",
|
|
6763
|
-
"node_modules",
|
|
6764
|
-
".venv",
|
|
6765
|
-
"data",
|
|
6766
|
-
"dist",
|
|
6767
|
-
"build",
|
|
6768
|
-
".turbo",
|
|
6769
|
-
".next",
|
|
6770
|
-
".nuxt",
|
|
6771
|
-
".tanstack",
|
|
6772
|
-
"coverage",
|
|
6773
|
-
"artifacts",
|
|
6774
|
-
"captures",
|
|
6775
|
-
"tmp",
|
|
6776
|
-
"temp"
|
|
6777
|
-
]);
|
|
6778
|
-
const shouldSkip = (relativePath) => {
|
|
6779
|
-
const parts = relativePath.split("/");
|
|
6780
|
-
if (parts.some((segment) => skipDirs.has(segment))) return true;
|
|
6781
|
-
const base = relativePath.split("/").at(-1) ?? "";
|
|
6782
|
-
if (base.startsWith("map_scan_") && extname(base) === ".json") return true;
|
|
6783
|
-
if (extname(base) === ".xlsx") return true;
|
|
6784
|
-
return false;
|
|
6785
|
-
};
|
|
6786
|
-
const copyRecursive = (source, target, rel = "") => {
|
|
6787
|
-
mkdirSync4(target, { recursive: true });
|
|
6788
|
-
const items = readdirSync5(source, { withFileTypes: true });
|
|
6789
|
-
for (const item of items) {
|
|
6790
|
-
const nextRel = rel ? `${rel}/${item.name}` : item.name;
|
|
6791
|
-
if (shouldSkip(nextRel)) continue;
|
|
6792
|
-
const sourcePath = `${source}/${item.name}`;
|
|
6793
|
-
const targetPath = `${target}/${item.name}`;
|
|
6794
|
-
const itemStat = statSync3(sourcePath);
|
|
6795
|
-
if (item.isDirectory()) {
|
|
6796
|
-
copyRecursive(sourcePath, targetPath, nextRel);
|
|
6797
|
-
continue;
|
|
6798
|
-
}
|
|
6799
|
-
if (item.isSymbolicLink() || itemStat.isSymbolicLink()) continue;
|
|
6800
|
-
if (itemStat.isFile() || itemStat.isFIFO()) {
|
|
6801
|
-
try {
|
|
6802
|
-
const file = readFileSync9(sourcePath);
|
|
6803
|
-
writeFileSync7(targetPath, file);
|
|
6804
|
-
} catch (error) {
|
|
6805
|
-
if (error.code === "ENOENT") {
|
|
6806
|
-
logger.debug(`Skipped missing source file: ${sourcePath}`);
|
|
6807
|
-
} else {
|
|
6808
|
-
throw error;
|
|
6809
|
-
}
|
|
6810
|
-
}
|
|
6811
|
-
}
|
|
6812
|
-
}
|
|
6813
|
-
};
|
|
6814
|
-
mkdirSync4(SOURCE_ROOT, { recursive: true });
|
|
6815
|
-
copyRecursive(TARGET_ROOT, SOURCE_ROOT);
|
|
6816
|
-
writeFileSync7(SOURCE_MARKER, `${now()}
|
|
6817
|
-
`, "utf8");
|
|
6818
|
-
}
|
|
6819
|
-
function loadWorkflowDefinition() {
|
|
6820
|
-
const defaultPrompt = PROMPT_TEMPLATES["workflow-default"];
|
|
6821
|
-
return {
|
|
6822
|
-
workflowPath: "",
|
|
6823
|
-
rendered: "",
|
|
6824
|
-
config: {},
|
|
6825
|
-
promptTemplate: defaultPrompt,
|
|
6826
|
-
agentProvider: "codex",
|
|
6827
|
-
agentProfile: "",
|
|
6828
|
-
agentProfilePath: "",
|
|
6829
|
-
agentProfileInstructions: "",
|
|
6830
|
-
agentProviders: [],
|
|
6831
|
-
afterCreateHook: "",
|
|
6832
|
-
beforeRunHook: "",
|
|
6833
|
-
afterRunHook: "",
|
|
6834
|
-
beforeRemoveHook: ""
|
|
6835
|
-
};
|
|
6836
|
-
}
|
|
6837
|
-
function parsePort(args) {
|
|
6838
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
6839
|
-
const arg = args[i];
|
|
6840
|
-
if (arg === "--help" || arg === "-h") {
|
|
6841
|
-
console.log(
|
|
6842
|
-
`Usage: ${argv2[1]} [options]
|
|
6843
|
-
Options:
|
|
6844
|
-
--port <n> Start local dashboard (default: no UI and single batch run)
|
|
6845
|
-
--workspace <path> Target workspace root (default: current directory)
|
|
6846
|
-
--persistence <path> Persistence root (default: current directory)
|
|
6847
|
-
--concurrency <n> Maximum number of parallel issue runners
|
|
6848
|
-
--attempts <n> Maximum attempts per issue
|
|
6849
|
-
--poll <ms> Polling interval for the scheduler
|
|
6850
|
-
--once Run one local batch and exit
|
|
6851
|
-
--help Show this message`
|
|
6852
|
-
);
|
|
6853
|
-
exit(0);
|
|
6854
|
-
}
|
|
6855
|
-
if (arg === "--port") {
|
|
6856
|
-
const value = args[i + 1];
|
|
6857
|
-
if (!value || !/^\d+$/.test(value)) {
|
|
6858
|
-
fail(`Invalid value for --port: ${value ?? "<empty>"}`);
|
|
6859
|
-
}
|
|
6860
|
-
return parseIntArg(value, 4040);
|
|
6861
|
-
}
|
|
6862
|
-
}
|
|
6863
|
-
return void 0;
|
|
6864
|
-
}
|
|
6865
|
-
|
|
6866
7252
|
// src/runtime/dev-server.ts
|
|
6867
7253
|
import { resolve as resolve3 } from "path";
|
|
6868
7254
|
var VITE_CONFIG_PATH = resolve3(PACKAGE_ROOT, "app/vite.config.js");
|
|
@@ -6915,6 +7301,10 @@ Options:
|
|
|
6915
7301
|
--timeout <ms> Agent command timeout in ms (default: 1800000)
|
|
6916
7302
|
--dev Start Vite dev server alongside API (HMR on port+1)
|
|
6917
7303
|
--once Process once and exit
|
|
7304
|
+
--skip-source Skip source snapshot copy
|
|
7305
|
+
--skip-scan Skip project analysis
|
|
7306
|
+
--skip-recovery Skip orphaned agent recovery
|
|
7307
|
+
--fast-boot Equivalent to --skip-source --skip-scan --skip-recovery
|
|
6918
7308
|
`
|
|
6919
7309
|
);
|
|
6920
7310
|
}
|
|
@@ -6937,6 +7327,9 @@ async function main() {
|
|
|
6937
7327
|
const interfaceMode = (env10.FIFONY_INTERFACE ?? "cli").trim().toLowerCase();
|
|
6938
7328
|
const runOnce = args.includes("--once");
|
|
6939
7329
|
const devMode = args.includes("--dev") || env10.NODE_ENV === "development";
|
|
7330
|
+
const fastBoot = args.includes("--fast-boot");
|
|
7331
|
+
const skipSource = fastBoot || args.includes("--skip-source");
|
|
7332
|
+
if (skipSource) setSkipSource(true);
|
|
6940
7333
|
debugBoot("main:state-root-ready");
|
|
6941
7334
|
const workflowDefinition = loadWorkflowDefinition();
|
|
6942
7335
|
debugBoot("main:workflow-loaded");
|
|
@@ -6953,14 +7346,40 @@ async function main() {
|
|
|
6953
7346
|
}
|
|
6954
7347
|
}
|
|
6955
7348
|
const dashboardPort = port ?? (config.dashboardPort ? Number.parseInt(config.dashboardPort, 10) : void 0);
|
|
6956
|
-
|
|
6957
|
-
debugBoot("main:
|
|
7349
|
+
const skipRecovery = args.includes("--skip-recovery") || args.includes("--fast-boot");
|
|
7350
|
+
debugBoot("main:phase-b-start");
|
|
6958
7351
|
await initStateStore();
|
|
6959
7352
|
debugBoot("main:store-initialized");
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
7353
|
+
const earlyState = {
|
|
7354
|
+
startedAt: now(),
|
|
7355
|
+
updatedAt: now(),
|
|
7356
|
+
trackerKind: "filesystem",
|
|
7357
|
+
sourceRepoUrl: "",
|
|
7358
|
+
sourceRef: "workspace",
|
|
7359
|
+
workflowPath: "",
|
|
7360
|
+
config,
|
|
7361
|
+
issues: [],
|
|
7362
|
+
events: [],
|
|
7363
|
+
metrics: { total: 0, queued: 0, inProgress: 0, blocked: 0, done: 0, cancelled: 0, activeWorkers: 0 },
|
|
7364
|
+
notes: [],
|
|
7365
|
+
booting: true
|
|
7366
|
+
};
|
|
7367
|
+
let apiState = earlyState;
|
|
7368
|
+
if (dashboardPort) {
|
|
7369
|
+
await startApiServer(apiState, dashboardPort, workflowDefinition);
|
|
7370
|
+
debugBoot("main:api-server-early-start");
|
|
7371
|
+
if (devMode) {
|
|
7372
|
+
const devPort = dashboardPort + 1;
|
|
7373
|
+
await startDevFrontend(dashboardPort, devPort);
|
|
7374
|
+
}
|
|
7375
|
+
}
|
|
7376
|
+
debugBoot("main:phase-c-start");
|
|
7377
|
+
const [previous, persistedSettings] = await Promise.all([
|
|
7378
|
+
loadPersistedState(),
|
|
7379
|
+
loadRuntimeSettings(),
|
|
7380
|
+
persistDetectedProvidersSetting(detectedProviders),
|
|
7381
|
+
recoverPlanningSession()
|
|
7382
|
+
]);
|
|
6964
7383
|
debugBoot("main:state-loaded");
|
|
6965
7384
|
config = applyPersistedSettings(config, persistedSettings);
|
|
6966
7385
|
await syncRuntimeConfigSettings(config, persistedSettings);
|
|
@@ -6969,6 +7388,7 @@ async function main() {
|
|
|
6969
7388
|
state.config.dashboardPort = dashboardPort ? String(dashboardPort) : void 0;
|
|
6970
7389
|
state.workflowPath = WORKFLOW_RENDERED;
|
|
6971
7390
|
state.updatedAt = now();
|
|
7391
|
+
state.booting = false;
|
|
6972
7392
|
if (state.config.agentCommand) {
|
|
6973
7393
|
state.notes.push(`Using agent command: ${state.config.agentCommand}`);
|
|
6974
7394
|
}
|
|
@@ -6998,25 +7418,31 @@ async function main() {
|
|
|
6998
7418
|
logger.info("Background workspace cleanup complete.");
|
|
6999
7419
|
});
|
|
7000
7420
|
}
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
issue.state
|
|
7012
|
-
|
|
7013
|
-
|
|
7421
|
+
if (!skipRecovery) {
|
|
7422
|
+
for (const issue of state.issues) {
|
|
7423
|
+
if (issue.state === "Running" || issue.state === "Interrupted" || issue.state === "Queued") {
|
|
7424
|
+
const { alive, pid } = isAgentStillRunning(issue);
|
|
7425
|
+
if (alive && pid) {
|
|
7426
|
+
logger.info(`Agent for ${issue.identifier} still alive (PID ${pid.pid}), keeping state as Running.`);
|
|
7427
|
+
issue.state = "Running";
|
|
7428
|
+
addEvent(state, issue.id, "info", `Orphaned agent detected (PID ${pid.pid}), still alive \u2014 tracking resumed.`);
|
|
7429
|
+
} else {
|
|
7430
|
+
if (issue.workspacePath) cleanStalePidFile(issue.workspacePath);
|
|
7431
|
+
if (issue.state === "Running") {
|
|
7432
|
+
issue.state = "Interrupted";
|
|
7433
|
+
issue.history.push(`[${now()}] Agent process not found on boot \u2014 marked Interrupted.`);
|
|
7434
|
+
addEvent(state, issue.id, "info", `Agent for ${issue.identifier} not found, marked Interrupted.`);
|
|
7435
|
+
}
|
|
7014
7436
|
}
|
|
7015
7437
|
}
|
|
7016
7438
|
}
|
|
7017
7439
|
}
|
|
7018
|
-
state.metrics =
|
|
7019
|
-
await
|
|
7440
|
+
state.metrics = computeMetrics2(state.issues);
|
|
7441
|
+
await persistStateFull(state);
|
|
7442
|
+
if (dashboardPort) {
|
|
7443
|
+
Object.assign(apiState, state);
|
|
7444
|
+
debugBoot("main:api-state-swapped");
|
|
7445
|
+
}
|
|
7020
7446
|
const running = /* @__PURE__ */ new Set();
|
|
7021
7447
|
installGracefulShutdown(state, running);
|
|
7022
7448
|
logger.info(`Rendered local workflow: ${WORKFLOW_RENDERED}`);
|
|
@@ -7027,13 +7453,6 @@ async function main() {
|
|
|
7027
7453
|
logger.info(`Max turns: ${state.config.maxTurns}`);
|
|
7028
7454
|
logger.info(`Agent provider: ${state.config.agentProvider}`);
|
|
7029
7455
|
logger.info(`Interface mode: ${interfaceMode}`);
|
|
7030
|
-
if (dashboardPort) {
|
|
7031
|
-
await startApiServer(state, dashboardPort, workflowDefinition);
|
|
7032
|
-
if (devMode) {
|
|
7033
|
-
const devPort = dashboardPort + 1;
|
|
7034
|
-
await startDevFrontend(dashboardPort, devPort);
|
|
7035
|
-
}
|
|
7036
|
-
}
|
|
7037
7456
|
try {
|
|
7038
7457
|
addEvent(state, void 0, "info", `Runtime started in local-only mode (filesystem tracker).`);
|
|
7039
7458
|
const runForever = !runOnce && (Boolean(dashboardPort) || interfaceMode === "mcp");
|
|
@@ -7045,8 +7464,8 @@ async function main() {
|
|
|
7045
7464
|
throw error;
|
|
7046
7465
|
} finally {
|
|
7047
7466
|
state.updatedAt = now();
|
|
7048
|
-
state.metrics =
|
|
7049
|
-
await
|
|
7467
|
+
state.metrics = computeMetrics2(state.issues);
|
|
7468
|
+
await persistStateFull(state);
|
|
7050
7469
|
await closeStateStore();
|
|
7051
7470
|
}
|
|
7052
7471
|
}
|