fifony 0.1.13 → 0.1.14-next.045d1e1
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/FIFONY.md +1 -1
- package/app/dist/assets/KeyboardShortcutsHelp-B3nNQIvc.js +1 -0
- package/app/dist/assets/OnboardingWizard-Fcby1Iao.js +1 -0
- package/app/dist/assets/analytics.lazy-KVdi1hkh.js +1 -0
- package/app/dist/assets/index-C8eXy-p8.js +42 -0
- package/app/dist/assets/index-Cvp6YUcB.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 +11 -3
- package/app/public/service-worker.js +10 -2
- package/dist/{runtime → agent}/run-local.js +1034 -444
- package/dist/agent/run-local.js.map +1 -0
- package/dist/cli.js +15 -8
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/app/dist/assets/KeyboardShortcutsHelp-C7XipNeo.js +0 -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/dist/runtime/run-local.js.map +0 -1
- /package/app/dist/assets/{createLucideIcon-DDy-XBQG.js → createLucideIcon-DtZs0TX0.js} +0 -0
|
@@ -8,11 +8,11 @@ import {
|
|
|
8
8
|
resolveTaskCapabilities
|
|
9
9
|
} from "../chunk-JUSVR3DW.js";
|
|
10
10
|
|
|
11
|
-
// src/
|
|
11
|
+
// src/agent/run-local.ts
|
|
12
12
|
import { mkdirSync as mkdirSync5 } from "fs";
|
|
13
13
|
import { env as env10, exit as exit2, argv as argv3 } from "process";
|
|
14
14
|
|
|
15
|
-
// src/
|
|
15
|
+
// src/agent/constants.ts
|
|
16
16
|
import { existsSync } from "fs";
|
|
17
17
|
import { basename, dirname, join, resolve } from "path";
|
|
18
18
|
import { fileURLToPath } from "url";
|
|
@@ -86,8 +86,12 @@ 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
|
-
// src/
|
|
94
|
+
// src/agent/helpers.ts
|
|
91
95
|
import { env as env2 } from "process";
|
|
92
96
|
import { parse as parseYaml } from "yaml";
|
|
93
97
|
function now() {
|
|
@@ -224,8 +228,55 @@ function extractJsonObjects(text) {
|
|
|
224
228
|
}
|
|
225
229
|
return results;
|
|
226
230
|
}
|
|
231
|
+
function repairTruncatedJson(text) {
|
|
232
|
+
const firstBrace = text.indexOf("{");
|
|
233
|
+
if (firstBrace < 0) return null;
|
|
234
|
+
let json = text.slice(firstBrace);
|
|
235
|
+
let inStr = false;
|
|
236
|
+
let esc = false;
|
|
237
|
+
const stack = [];
|
|
238
|
+
for (let i = 0; i < json.length; i++) {
|
|
239
|
+
const ch = json[i];
|
|
240
|
+
if (inStr) {
|
|
241
|
+
if (esc) {
|
|
242
|
+
esc = false;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (ch === "\\") {
|
|
246
|
+
esc = true;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (ch === '"') {
|
|
250
|
+
inStr = false;
|
|
251
|
+
}
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (ch === '"') {
|
|
255
|
+
inStr = true;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (ch === "{") stack.push("{");
|
|
259
|
+
else if (ch === "[") stack.push("[");
|
|
260
|
+
else if (ch === "}") {
|
|
261
|
+
if (stack.length > 0 && stack[stack.length - 1] === "{") stack.pop();
|
|
262
|
+
} else if (ch === "]") {
|
|
263
|
+
if (stack.length > 0 && stack[stack.length - 1] === "[") stack.pop();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (!inStr && stack.length === 0) return json;
|
|
267
|
+
if (inStr) {
|
|
268
|
+
if (json.endsWith("\\")) json = json.slice(0, -1);
|
|
269
|
+
json += '"';
|
|
270
|
+
}
|
|
271
|
+
json = json.replace(/[,:\s]+$/, "");
|
|
272
|
+
while (stack.length > 0) {
|
|
273
|
+
const open = stack.pop();
|
|
274
|
+
json += open === "{" ? "}" : "]";
|
|
275
|
+
}
|
|
276
|
+
return json;
|
|
277
|
+
}
|
|
227
278
|
|
|
228
|
-
// src/
|
|
279
|
+
// src/agent/logger.ts
|
|
229
280
|
import pino from "pino";
|
|
230
281
|
import { env as env3, stdout } from "process";
|
|
231
282
|
import { join as join2 } from "path";
|
|
@@ -295,13 +346,13 @@ var logger = {
|
|
|
295
346
|
}
|
|
296
347
|
};
|
|
297
348
|
|
|
298
|
-
// src/
|
|
299
|
-
import { mkdirSync as
|
|
349
|
+
// src/agent/store.ts
|
|
350
|
+
import { mkdirSync as mkdirSync4 } from "fs";
|
|
300
351
|
|
|
301
|
-
// src/
|
|
352
|
+
// src/agent/issues.ts
|
|
302
353
|
import { env as env4 } from "process";
|
|
303
354
|
|
|
304
|
-
// src/
|
|
355
|
+
// src/agent/token-ledger.ts
|
|
305
356
|
var EMPTY = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
306
357
|
var overall = { ...EMPTY };
|
|
307
358
|
var byPhase = /* @__PURE__ */ new Map();
|
|
@@ -463,7 +514,51 @@ function getAnalytics(topN = 20) {
|
|
|
463
514
|
};
|
|
464
515
|
}
|
|
465
516
|
|
|
466
|
-
// src/
|
|
517
|
+
// src/agent/dirty-tracker.ts
|
|
518
|
+
var dirtyIssueIds = /* @__PURE__ */ new Set();
|
|
519
|
+
var dirtyEventIds = /* @__PURE__ */ new Set();
|
|
520
|
+
function markIssueDirty(id) {
|
|
521
|
+
dirtyIssueIds.add(id);
|
|
522
|
+
}
|
|
523
|
+
function markEventDirty(id) {
|
|
524
|
+
dirtyEventIds.add(id);
|
|
525
|
+
}
|
|
526
|
+
function hasDirtyState() {
|
|
527
|
+
return dirtyIssueIds.size > 0 || dirtyEventIds.size > 0;
|
|
528
|
+
}
|
|
529
|
+
function getDirtyIssueIds() {
|
|
530
|
+
return dirtyIssueIds;
|
|
531
|
+
}
|
|
532
|
+
function getDirtyEventIds() {
|
|
533
|
+
return dirtyEventIds;
|
|
534
|
+
}
|
|
535
|
+
function clearDirtyIssueIds() {
|
|
536
|
+
dirtyIssueIds.clear();
|
|
537
|
+
}
|
|
538
|
+
function clearDirtyEventIds() {
|
|
539
|
+
dirtyEventIds.clear();
|
|
540
|
+
}
|
|
541
|
+
function markAllIssuesDirty(ids) {
|
|
542
|
+
for (const id of ids) dirtyIssueIds.add(id);
|
|
543
|
+
}
|
|
544
|
+
function markAllEventsDirty(ids) {
|
|
545
|
+
for (const id of ids) dirtyEventIds.add(id);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// src/agent/metrics-cache.ts
|
|
549
|
+
var cachedMetrics = null;
|
|
550
|
+
var metricsStale = true;
|
|
551
|
+
function invalidateMetrics() {
|
|
552
|
+
metricsStale = true;
|
|
553
|
+
}
|
|
554
|
+
function getMetrics(issues) {
|
|
555
|
+
if (!metricsStale && cachedMetrics) return cachedMetrics;
|
|
556
|
+
cachedMetrics = computeMetrics(issues);
|
|
557
|
+
metricsStale = false;
|
|
558
|
+
return cachedMetrics;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/agent/issue-state-machine.ts
|
|
467
562
|
var ISSUE_STATE_MACHINE_ID = "issue-lifecycle";
|
|
468
563
|
var ISSUE_STATE_MACHINE_DEFINITION = {
|
|
469
564
|
initialState: "Planning",
|
|
@@ -592,13 +687,13 @@ function findIssueStateMachineTransitionPath(machineDefinition, from, to) {
|
|
|
592
687
|
return null;
|
|
593
688
|
}
|
|
594
689
|
|
|
595
|
-
// src/
|
|
690
|
+
// src/agent/providers.ts
|
|
596
691
|
import { execFileSync, spawn } from "child_process";
|
|
597
692
|
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
598
693
|
import { join as join3 } from "path";
|
|
599
694
|
import { homedir as homedir2 } from "os";
|
|
600
695
|
|
|
601
|
-
// src/
|
|
696
|
+
// src/agent/adapters/commands.ts
|
|
602
697
|
var CLAUDE_RESULT_SCHEMA = JSON.stringify({
|
|
603
698
|
type: "object",
|
|
604
699
|
properties: {
|
|
@@ -666,7 +761,7 @@ function extractPlanDirs(plan) {
|
|
|
666
761
|
return [...dirs];
|
|
667
762
|
}
|
|
668
763
|
|
|
669
|
-
// src/
|
|
764
|
+
// src/agent/providers.ts
|
|
670
765
|
function resolveAgentProfile(name) {
|
|
671
766
|
const normalized = name.trim();
|
|
672
767
|
if (!normalized) return { profilePath: "", instructions: "" };
|
|
@@ -720,7 +815,13 @@ function getProviderDefaultCommand(provider, _reasoningEffort, model) {
|
|
|
720
815
|
if (provider === "claude") return buildClaudeCommand({ model, jsonSchema: CLAUDE_RESULT_SCHEMA });
|
|
721
816
|
return "";
|
|
722
817
|
}
|
|
818
|
+
var cachedProviders = null;
|
|
819
|
+
var providersCachedAt = 0;
|
|
820
|
+
var PROVIDER_CACHE_TTL = 6e4;
|
|
723
821
|
function detectAvailableProviders() {
|
|
822
|
+
if (cachedProviders && Date.now() - providersCachedAt < PROVIDER_CACHE_TTL) {
|
|
823
|
+
return cachedProviders;
|
|
824
|
+
}
|
|
724
825
|
const providers = [];
|
|
725
826
|
for (const name of ["claude", "codex"]) {
|
|
726
827
|
try {
|
|
@@ -730,6 +831,8 @@ function detectAvailableProviders() {
|
|
|
730
831
|
providers.push({ name, available: false, path: "" });
|
|
731
832
|
}
|
|
732
833
|
}
|
|
834
|
+
cachedProviders = providers;
|
|
835
|
+
providersCachedAt = Date.now();
|
|
733
836
|
return providers;
|
|
734
837
|
}
|
|
735
838
|
var modelCache = /* @__PURE__ */ new Map();
|
|
@@ -1056,7 +1159,7 @@ function getEffectiveAgentProviders(state, issue, workflowDefinition, workflowCo
|
|
|
1056
1159
|
return applyWorkflowConfigToProviders(merged, workflowConfig ?? null);
|
|
1057
1160
|
}
|
|
1058
1161
|
|
|
1059
|
-
// src/
|
|
1162
|
+
// src/agent/issues.ts
|
|
1060
1163
|
var VALID_EFFORTS = /* @__PURE__ */ new Set(["low", "medium", "high", "extra-high"]);
|
|
1061
1164
|
function parseEffortValue(value) {
|
|
1062
1165
|
const str = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
@@ -1091,6 +1194,7 @@ function nextLocalIssueId(issues) {
|
|
|
1091
1194
|
function createIssueFromPayload(payload, issues, workflowDefinition) {
|
|
1092
1195
|
const identifier = toStringValue(payload.identifier, nextLocalIssueId(issues));
|
|
1093
1196
|
const id = toStringValue(payload.id, identifier.replace(/^#/, "issue-"));
|
|
1197
|
+
logger.info({ id, identifier, title: toStringValue(payload.title, "").slice(0, 80) }, "[Issues] Creating new issue");
|
|
1094
1198
|
const createdAt = now();
|
|
1095
1199
|
const blockedBy = toStringArray(payload.blockedBy);
|
|
1096
1200
|
const legacyBlockedBy = toStringArray(payload.blocked_by);
|
|
@@ -1281,6 +1385,7 @@ function buildRuntimeState(previous, config, definition) {
|
|
|
1281
1385
|
};
|
|
1282
1386
|
}
|
|
1283
1387
|
function computeMetrics(issues) {
|
|
1388
|
+
let planning = 0;
|
|
1284
1389
|
let queued = 0;
|
|
1285
1390
|
let inProgress = 0;
|
|
1286
1391
|
let blocked = 0;
|
|
@@ -1296,6 +1401,9 @@ function computeMetrics(issues) {
|
|
|
1296
1401
|
}
|
|
1297
1402
|
}
|
|
1298
1403
|
switch (issue.state) {
|
|
1404
|
+
case "Planning":
|
|
1405
|
+
planning += 1;
|
|
1406
|
+
break;
|
|
1299
1407
|
case "Todo":
|
|
1300
1408
|
queued += 1;
|
|
1301
1409
|
break;
|
|
@@ -1319,6 +1427,7 @@ function computeMetrics(issues) {
|
|
|
1319
1427
|
if (completionTimes.length === 0) {
|
|
1320
1428
|
return {
|
|
1321
1429
|
total: issues.length,
|
|
1430
|
+
planning,
|
|
1322
1431
|
queued,
|
|
1323
1432
|
inProgress,
|
|
1324
1433
|
blocked,
|
|
@@ -1333,6 +1442,7 @@ function computeMetrics(issues) {
|
|
|
1333
1442
|
const medianCompletionMs = sortedCompletionTimes.length % 2 === 1 ? sortedCompletionTimes[mid] : Math.round((sortedCompletionTimes[mid - 1] + sortedCompletionTimes[mid]) / 2);
|
|
1334
1443
|
return {
|
|
1335
1444
|
total: issues.length,
|
|
1445
|
+
planning,
|
|
1336
1446
|
queued,
|
|
1337
1447
|
inProgress,
|
|
1338
1448
|
blocked,
|
|
@@ -1361,6 +1471,7 @@ function addEvent(state, issueId, kind, message) {
|
|
|
1361
1471
|
at: now()
|
|
1362
1472
|
};
|
|
1363
1473
|
state.events = [event, ...state.events].slice(0, PERSIST_EVENTS_MAX);
|
|
1474
|
+
markEventDirty(event.id);
|
|
1364
1475
|
try {
|
|
1365
1476
|
recordEvent();
|
|
1366
1477
|
} catch {
|
|
@@ -1369,8 +1480,11 @@ function addEvent(state, issueId, kind, message) {
|
|
|
1369
1480
|
}
|
|
1370
1481
|
function transition(issue, target, note) {
|
|
1371
1482
|
const previous = issue.state;
|
|
1483
|
+
logger.debug({ issueId: issue.id, identifier: issue.identifier, from: previous, to: target, note }, "[State] Issue transition");
|
|
1372
1484
|
issue.state = target;
|
|
1373
1485
|
issue.updatedAt = now();
|
|
1486
|
+
markIssueDirty(issue.id);
|
|
1487
|
+
invalidateMetrics();
|
|
1374
1488
|
issue.history.push(`[${issue.updatedAt}] ${note}`);
|
|
1375
1489
|
if (previous === "Blocked" && target === "Todo") {
|
|
1376
1490
|
issue.lastError = void 0;
|
|
@@ -1501,7 +1615,7 @@ async function handleStatePatch(state, issue, payload) {
|
|
|
1501
1615
|
addEvent(state, issue.id, "manual", `Manual state transition to ${nextState}`);
|
|
1502
1616
|
}
|
|
1503
1617
|
|
|
1504
|
-
// src/
|
|
1618
|
+
// src/agent/api-runtime-context.ts
|
|
1505
1619
|
var context = null;
|
|
1506
1620
|
function setApiRuntimeContext(state, workflowDefinition) {
|
|
1507
1621
|
context = { state, workflowDefinition };
|
|
@@ -1516,18 +1630,20 @@ function getApiRuntimeContextOrThrow() {
|
|
|
1516
1630
|
return context;
|
|
1517
1631
|
}
|
|
1518
1632
|
|
|
1519
|
-
// src/
|
|
1633
|
+
// src/agent/api-server.ts
|
|
1520
1634
|
import { execSync as execSync3 } from "child_process";
|
|
1521
1635
|
import {
|
|
1636
|
+
appendFileSync as appendFileSync2,
|
|
1522
1637
|
closeSync,
|
|
1523
|
-
existsSync as
|
|
1638
|
+
existsSync as existsSync10,
|
|
1524
1639
|
openSync,
|
|
1525
|
-
readFileSync as
|
|
1640
|
+
readFileSync as readFileSync9,
|
|
1526
1641
|
readSync,
|
|
1527
|
-
statSync as
|
|
1642
|
+
statSync as statSync3,
|
|
1643
|
+
writeFileSync as writeFileSync8
|
|
1528
1644
|
} from "fs";
|
|
1529
1645
|
|
|
1530
|
-
// src/
|
|
1646
|
+
// src/agent/resources/runtime-state.resource.ts
|
|
1531
1647
|
var runtime_state_resource_default = {
|
|
1532
1648
|
name: S3DB_RUNTIME_RESOURCE,
|
|
1533
1649
|
attributes: {
|
|
@@ -1548,23 +1664,23 @@ var runtime_state_resource_default = {
|
|
|
1548
1664
|
}
|
|
1549
1665
|
};
|
|
1550
1666
|
|
|
1551
|
-
// src/
|
|
1667
|
+
// src/agent/agent.ts
|
|
1552
1668
|
import {
|
|
1553
1669
|
appendFileSync,
|
|
1554
1670
|
cpSync,
|
|
1555
|
-
existsSync as
|
|
1556
|
-
mkdirSync,
|
|
1557
|
-
readdirSync as
|
|
1558
|
-
readFileSync as
|
|
1671
|
+
existsSync as existsSync5,
|
|
1672
|
+
mkdirSync as mkdirSync2,
|
|
1673
|
+
readdirSync as readdirSync3,
|
|
1674
|
+
readFileSync as readFileSync4,
|
|
1559
1675
|
rmSync,
|
|
1560
|
-
statSync,
|
|
1561
|
-
writeFileSync as
|
|
1676
|
+
statSync as statSync2,
|
|
1677
|
+
writeFileSync as writeFileSync3
|
|
1562
1678
|
} from "fs";
|
|
1563
|
-
import { join as
|
|
1564
|
-
import { env as
|
|
1679
|
+
import { join as join8, relative } from "path";
|
|
1680
|
+
import { env as env6 } from "process";
|
|
1565
1681
|
import { execSync, spawn as spawn2 } from "child_process";
|
|
1566
1682
|
|
|
1567
|
-
// src/
|
|
1683
|
+
// src/agent/skills.ts
|
|
1568
1684
|
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
1569
1685
|
import { homedir as homedir3 } from "os";
|
|
1570
1686
|
import { join as join4, resolve as resolve2 } from "path";
|
|
@@ -1608,11 +1724,143 @@ ${skill.content}`
|
|
|
1608
1724
|
${sections.join("\n\n")}`;
|
|
1609
1725
|
}
|
|
1610
1726
|
|
|
1611
|
-
// src/
|
|
1612
|
-
import { writeFileSync } from "fs";
|
|
1613
|
-
import {
|
|
1727
|
+
// src/agent/workflow.ts
|
|
1728
|
+
import { existsSync as existsSync4, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync, writeFileSync } from "fs";
|
|
1729
|
+
import { copyFile, mkdir, readdir, stat, writeFile } from "fs/promises";
|
|
1730
|
+
import { extname } from "path";
|
|
1731
|
+
import { argv as argv2, exit } from "process";
|
|
1732
|
+
var sourceReadyPromise = null;
|
|
1733
|
+
var skipSourceFlag = false;
|
|
1734
|
+
function setSkipSource(skip) {
|
|
1735
|
+
skipSourceFlag = skip;
|
|
1736
|
+
}
|
|
1737
|
+
async function ensureSourceReady(onProgress) {
|
|
1738
|
+
if (skipSourceFlag) {
|
|
1739
|
+
onProgress?.("ready");
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
if (existsSync4(SOURCE_MARKER)) {
|
|
1743
|
+
onProgress?.("ready");
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
if (sourceReadyPromise) return sourceReadyPromise;
|
|
1747
|
+
sourceReadyPromise = (async () => {
|
|
1748
|
+
onProgress?.("copying");
|
|
1749
|
+
logger.info("Creating local source snapshot (async) for Fifony...");
|
|
1750
|
+
const skipDirs = /* @__PURE__ */ new Set([
|
|
1751
|
+
".git",
|
|
1752
|
+
".fifony",
|
|
1753
|
+
"node_modules",
|
|
1754
|
+
".venv",
|
|
1755
|
+
"data",
|
|
1756
|
+
"dist",
|
|
1757
|
+
"build",
|
|
1758
|
+
".turbo",
|
|
1759
|
+
".next",
|
|
1760
|
+
".nuxt",
|
|
1761
|
+
".tanstack",
|
|
1762
|
+
"coverage",
|
|
1763
|
+
"artifacts",
|
|
1764
|
+
"captures",
|
|
1765
|
+
"tmp",
|
|
1766
|
+
"temp"
|
|
1767
|
+
]);
|
|
1768
|
+
const shouldSkip = (relativePath) => {
|
|
1769
|
+
const parts = relativePath.split("/");
|
|
1770
|
+
if (parts.some((segment) => skipDirs.has(segment))) return true;
|
|
1771
|
+
const base = relativePath.split("/").at(-1) ?? "";
|
|
1772
|
+
if (base.startsWith("map_scan_") && extname(base) === ".json") return true;
|
|
1773
|
+
if (extname(base) === ".xlsx") return true;
|
|
1774
|
+
return false;
|
|
1775
|
+
};
|
|
1776
|
+
const copyRecursiveAsync = async (source, target, rel = "") => {
|
|
1777
|
+
await mkdir(target, { recursive: true });
|
|
1778
|
+
const items = await readdir(source, { withFileTypes: true });
|
|
1779
|
+
for (const item of items) {
|
|
1780
|
+
const nextRel = rel ? `${rel}/${item.name}` : item.name;
|
|
1781
|
+
if (shouldSkip(nextRel)) continue;
|
|
1782
|
+
const sourcePath = `${source}/${item.name}`;
|
|
1783
|
+
const targetPath = `${target}/${item.name}`;
|
|
1784
|
+
const itemStat = await stat(sourcePath);
|
|
1785
|
+
if (item.isDirectory()) {
|
|
1786
|
+
await copyRecursiveAsync(sourcePath, targetPath, nextRel);
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
if (item.isSymbolicLink() || itemStat.isSymbolicLink()) continue;
|
|
1790
|
+
if (itemStat.isFile() || itemStat.isFIFO()) {
|
|
1791
|
+
try {
|
|
1792
|
+
await copyFile(sourcePath, targetPath);
|
|
1793
|
+
} catch (error) {
|
|
1794
|
+
if (error.code === "ENOENT") {
|
|
1795
|
+
logger.debug(`Skipped missing source file: ${sourcePath}`);
|
|
1796
|
+
} else {
|
|
1797
|
+
throw error;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
await mkdir(SOURCE_ROOT, { recursive: true });
|
|
1804
|
+
await copyRecursiveAsync(TARGET_ROOT, SOURCE_ROOT);
|
|
1805
|
+
await writeFile(SOURCE_MARKER, `${now()}
|
|
1806
|
+
`, "utf8");
|
|
1807
|
+
onProgress?.("ready");
|
|
1808
|
+
logger.info("Source snapshot ready (async).");
|
|
1809
|
+
})();
|
|
1810
|
+
return sourceReadyPromise;
|
|
1811
|
+
}
|
|
1812
|
+
function loadWorkflowDefinition() {
|
|
1813
|
+
const defaultPrompt = PROMPT_TEMPLATES["workflow-default"];
|
|
1814
|
+
return {
|
|
1815
|
+
workflowPath: "",
|
|
1816
|
+
rendered: "",
|
|
1817
|
+
config: {},
|
|
1818
|
+
promptTemplate: defaultPrompt,
|
|
1819
|
+
agentProvider: "codex",
|
|
1820
|
+
agentProfile: "",
|
|
1821
|
+
agentProfilePath: "",
|
|
1822
|
+
agentProfileInstructions: "",
|
|
1823
|
+
agentProviders: [],
|
|
1824
|
+
afterCreateHook: "",
|
|
1825
|
+
beforeRunHook: "",
|
|
1826
|
+
afterRunHook: "",
|
|
1827
|
+
beforeRemoveHook: ""
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
function parsePort(args) {
|
|
1831
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1832
|
+
const arg = args[i];
|
|
1833
|
+
if (arg === "--help" || arg === "-h") {
|
|
1834
|
+
console.log(
|
|
1835
|
+
`Usage: ${argv2[1]} [options]
|
|
1836
|
+
Options:
|
|
1837
|
+
--port <n> Start local dashboard (default: no UI and single batch run)
|
|
1838
|
+
--workspace <path> Target workspace root (default: current directory)
|
|
1839
|
+
--persistence <path> Persistence root (default: current directory)
|
|
1840
|
+
--concurrency <n> Maximum number of parallel issue runners
|
|
1841
|
+
--attempts <n> Maximum attempts per issue
|
|
1842
|
+
--poll <ms> Polling interval for the scheduler
|
|
1843
|
+
--once Run one local batch and exit
|
|
1844
|
+
--help Show this message`
|
|
1845
|
+
);
|
|
1846
|
+
exit(0);
|
|
1847
|
+
}
|
|
1848
|
+
if (arg === "--port") {
|
|
1849
|
+
const value = args[i + 1];
|
|
1850
|
+
if (!value || !/^\d+$/.test(value)) {
|
|
1851
|
+
fail(`Invalid value for --port: ${value ?? "<empty>"}`);
|
|
1852
|
+
}
|
|
1853
|
+
return parseIntArg(value, 4040);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
return void 0;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// src/agent/adapters/index.ts
|
|
1860
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
1861
|
+
import { join as join7 } from "path";
|
|
1614
1862
|
|
|
1615
|
-
// src/
|
|
1863
|
+
// src/agent/adapters/shared.ts
|
|
1616
1864
|
function buildPlanContextSection(plan) {
|
|
1617
1865
|
const parts = ["## Plan Context", "", `**Summary:** ${plan.summary}`];
|
|
1618
1866
|
if (plan.assumptions?.length) {
|
|
@@ -1810,7 +2058,7 @@ function buildExecutionPayload(issue, provider, plan, workspacePath) {
|
|
|
1810
2058
|
};
|
|
1811
2059
|
}
|
|
1812
2060
|
|
|
1813
|
-
// src/
|
|
2061
|
+
// src/agent/adapters/plan-to-claude.ts
|
|
1814
2062
|
async function compileForClaude(issue, provider, plan, config, workspacePath, skillContext) {
|
|
1815
2063
|
const effort = resolveEffortForProvider(plan, provider.role, config.defaultEffort);
|
|
1816
2064
|
const prompt = await renderPrompt("compile-execution-claude", {
|
|
@@ -1861,8 +2109,8 @@ async function compileForClaude(issue, provider, plan, config, workspacePath, sk
|
|
|
1861
2109
|
};
|
|
1862
2110
|
}
|
|
1863
2111
|
|
|
1864
|
-
// src/
|
|
1865
|
-
import { join as
|
|
2112
|
+
// src/agent/adapters/plan-to-codex.ts
|
|
2113
|
+
import { join as join6 } from "path";
|
|
1866
2114
|
var CODEX_RESULT_CONTRACT = `
|
|
1867
2115
|
Return a JSON object with this exact schema when finished:
|
|
1868
2116
|
{
|
|
@@ -1899,7 +2147,7 @@ async function compileForCodex(issue, provider, plan, config, workspacePath, ski
|
|
|
1899
2147
|
outputContract: CODEX_RESULT_CONTRACT
|
|
1900
2148
|
});
|
|
1901
2149
|
const relativeDirs = extractPlanDirs(plan);
|
|
1902
|
-
const absoluteDirs = relativeDirs.map((d) =>
|
|
2150
|
+
const absoluteDirs = relativeDirs.map((d) => join6(workspacePath, d));
|
|
1903
2151
|
const command = buildCodexCommand({
|
|
1904
2152
|
model: provider.model,
|
|
1905
2153
|
addDirs: absoluteDirs
|
|
@@ -1931,7 +2179,7 @@ async function compileForCodex(issue, provider, plan, config, workspacePath, ski
|
|
|
1931
2179
|
};
|
|
1932
2180
|
}
|
|
1933
2181
|
|
|
1934
|
-
// src/
|
|
2182
|
+
// src/agent/adapters/index.ts
|
|
1935
2183
|
async function compileExecution(issue, provider, config, workspacePath, skillContext) {
|
|
1936
2184
|
const plan = issue.plan;
|
|
1937
2185
|
if (!plan?.steps?.length) return null;
|
|
@@ -1987,8 +2235,8 @@ function buildExecutionAudit(provider, compiled, issue, durationMs, result) {
|
|
|
1987
2235
|
}
|
|
1988
2236
|
function persistCompilationArtifacts(workspacePath, compiled) {
|
|
1989
2237
|
try {
|
|
1990
|
-
|
|
1991
|
-
|
|
2238
|
+
writeFileSync2(
|
|
2239
|
+
join7(workspacePath, "fifony-compiled-execution.json"),
|
|
1992
2240
|
JSON.stringify({
|
|
1993
2241
|
adapter: compiled.meta.adapter,
|
|
1994
2242
|
model: compiled.meta.model,
|
|
@@ -2010,8 +2258,8 @@ function persistCompilationArtifacts(workspacePath, compiled) {
|
|
|
2010
2258
|
}
|
|
2011
2259
|
if (compiled.payload) {
|
|
2012
2260
|
try {
|
|
2013
|
-
|
|
2014
|
-
|
|
2261
|
+
writeFileSync2(
|
|
2262
|
+
join7(workspacePath, "fifony-execution-payload.json"),
|
|
2015
2263
|
JSON.stringify(compiled.payload, null, 2),
|
|
2016
2264
|
"utf8"
|
|
2017
2265
|
);
|
|
@@ -2021,8 +2269,8 @@ function persistCompilationArtifacts(workspacePath, compiled) {
|
|
|
2021
2269
|
}
|
|
2022
2270
|
function persistExecutionAudit(workspacePath, audit) {
|
|
2023
2271
|
try {
|
|
2024
|
-
|
|
2025
|
-
|
|
2272
|
+
writeFileSync2(
|
|
2273
|
+
join7(workspacePath, "fifony-execution-audit.json"),
|
|
2026
2274
|
JSON.stringify(audit, null, 2),
|
|
2027
2275
|
"utf8"
|
|
2028
2276
|
);
|
|
@@ -2030,7 +2278,7 @@ function persistExecutionAudit(workspacePath, audit) {
|
|
|
2030
2278
|
}
|
|
2031
2279
|
}
|
|
2032
2280
|
|
|
2033
|
-
// src/
|
|
2281
|
+
// src/agent/settings.ts
|
|
2034
2282
|
var SETTING_ID_POLL_INTERVAL_MS = "runtime.pollIntervalMs";
|
|
2035
2283
|
var SETTING_ID_WORKER_CONCURRENCY = "runtime.workerConcurrency";
|
|
2036
2284
|
var SETTING_ID_COMMAND_TIMEOUT_MS = "runtime.commandTimeoutMs";
|
|
@@ -2311,7 +2559,7 @@ async function persistWorkflowConfig(config) {
|
|
|
2311
2559
|
await persistSetting(SETTING_ID_WORKFLOW_CONFIG, config, { scope: "runtime", source: "user" });
|
|
2312
2560
|
}
|
|
2313
2561
|
|
|
2314
|
-
// src/
|
|
2562
|
+
// src/agent/agent.ts
|
|
2315
2563
|
function normalizeAgentDirectiveStatus(value, fallback) {
|
|
2316
2564
|
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
2317
2565
|
if (normalized === "done" || normalized === "continue" || normalized === "blocked" || normalized === "failed") {
|
|
@@ -2412,7 +2660,7 @@ function tryParseJsonOutput(output) {
|
|
|
2412
2660
|
}
|
|
2413
2661
|
function readAgentDirective(workspacePath, output, success) {
|
|
2414
2662
|
const fallbackStatus = success ? "done" : "failed";
|
|
2415
|
-
const resultFile =
|
|
2663
|
+
const resultFile = join8(workspacePath, "fifony-result.json");
|
|
2416
2664
|
let resultPayload = {};
|
|
2417
2665
|
const fullJson = (() => {
|
|
2418
2666
|
try {
|
|
@@ -2431,9 +2679,9 @@ function readAgentDirective(workspacePath, output, success) {
|
|
|
2431
2679
|
tokenUsage
|
|
2432
2680
|
};
|
|
2433
2681
|
}
|
|
2434
|
-
if (
|
|
2682
|
+
if (existsSync5(resultFile)) {
|
|
2435
2683
|
try {
|
|
2436
|
-
const parsed = JSON.parse(
|
|
2684
|
+
const parsed = JSON.parse(readFileSync4(resultFile, "utf8"));
|
|
2437
2685
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2438
2686
|
resultPayload = parsed;
|
|
2439
2687
|
}
|
|
@@ -2450,10 +2698,10 @@ function readAgentDirective(workspacePath, output, success) {
|
|
|
2450
2698
|
return { status, summary, nextPrompt, tokenUsage };
|
|
2451
2699
|
}
|
|
2452
2700
|
function readAgentPid(workspacePath) {
|
|
2453
|
-
const pidFile =
|
|
2454
|
-
if (!
|
|
2701
|
+
const pidFile = join8(workspacePath, "fifony-agent.pid");
|
|
2702
|
+
if (!existsSync5(pidFile)) return null;
|
|
2455
2703
|
try {
|
|
2456
|
-
const data = JSON.parse(
|
|
2704
|
+
const data = JSON.parse(readFileSync4(pidFile, "utf8"));
|
|
2457
2705
|
if (!data?.pid || typeof data.pid !== "number") return null;
|
|
2458
2706
|
return data;
|
|
2459
2707
|
} catch {
|
|
@@ -2470,7 +2718,7 @@ function isProcessAlive(pid) {
|
|
|
2470
2718
|
}
|
|
2471
2719
|
function isAgentStillRunning(issue) {
|
|
2472
2720
|
const wp = issue.workspacePath;
|
|
2473
|
-
if (!wp || !
|
|
2721
|
+
if (!wp || !existsSync5(wp)) return { alive: false, pid: null };
|
|
2474
2722
|
const pidInfo = readAgentPid(wp);
|
|
2475
2723
|
if (!pidInfo) return { alive: false, pid: null };
|
|
2476
2724
|
return { alive: isProcessAlive(pidInfo.pid), pid: pidInfo };
|
|
@@ -2480,7 +2728,7 @@ function cleanStalePidFile(workspacePath) {
|
|
|
2480
2728
|
if (!pidInfo) return;
|
|
2481
2729
|
if (!isProcessAlive(pidInfo.pid)) {
|
|
2482
2730
|
try {
|
|
2483
|
-
rmSync(
|
|
2731
|
+
rmSync(join8(workspacePath, "fifony-agent.pid"), { force: true });
|
|
2484
2732
|
} catch {
|
|
2485
2733
|
}
|
|
2486
2734
|
}
|
|
@@ -2490,13 +2738,22 @@ function canRunIssue(issue, running, state) {
|
|
|
2490
2738
|
if (running.has(issue.id)) return false;
|
|
2491
2739
|
if (TERMINAL_STATES.has(issue.state)) return false;
|
|
2492
2740
|
const { alive } = isAgentStillRunning(issue);
|
|
2493
|
-
if (alive)
|
|
2741
|
+
if (alive) {
|
|
2742
|
+
logger.debug({ issueId: issue.id, identifier: issue.identifier }, "[Agent] Skipping issue \u2014 agent still alive from previous session");
|
|
2743
|
+
return false;
|
|
2744
|
+
}
|
|
2494
2745
|
if (issue.state === "Blocked") {
|
|
2495
2746
|
if (!issue.nextRetryAt) return false;
|
|
2496
|
-
if (issue.attempts >= issue.maxAttempts)
|
|
2747
|
+
if (issue.attempts >= issue.maxAttempts) {
|
|
2748
|
+
logger.debug({ issueId: issue.id, identifier: issue.identifier, attempts: issue.attempts, maxAttempts: issue.maxAttempts }, "[Agent] Skipping blocked issue \u2014 max attempts reached");
|
|
2749
|
+
return false;
|
|
2750
|
+
}
|
|
2497
2751
|
if (Date.parse(issue.nextRetryAt) > Date.now()) return false;
|
|
2498
2752
|
}
|
|
2499
|
-
if (!issueDepsResolved(issue, state.issues))
|
|
2753
|
+
if (!issueDepsResolved(issue, state.issues)) {
|
|
2754
|
+
logger.debug({ issueId: issue.id, identifier: issue.identifier, blockedBy: issue.blockedBy }, "[Agent] Skipping issue \u2014 unresolved dependencies");
|
|
2755
|
+
return false;
|
|
2756
|
+
}
|
|
2500
2757
|
if (issue.state === "Todo") return true;
|
|
2501
2758
|
if (issue.state === "Queued") return true;
|
|
2502
2759
|
if (issue.state === "Blocked") return true;
|
|
@@ -2522,33 +2779,33 @@ function shouldSkipRoutingPath(relativePath) {
|
|
|
2522
2779
|
return base === "WORKFLOW.local.md" || base === ".fifony-env.sh" || base.startsWith("fifony-") || base.startsWith("fifony_");
|
|
2523
2780
|
}
|
|
2524
2781
|
function inferChangedWorkspacePaths(workspacePath, limit = 32) {
|
|
2525
|
-
if (!workspacePath || !
|
|
2782
|
+
if (!workspacePath || !existsSync5(workspacePath) || !existsSync5(SOURCE_ROOT)) return [];
|
|
2526
2783
|
const changed = /* @__PURE__ */ new Set();
|
|
2527
2784
|
const walk = (currentRoot, relativeRoot = "") => {
|
|
2528
2785
|
if (changed.size >= limit) return;
|
|
2529
|
-
for (const item of
|
|
2786
|
+
for (const item of readdirSync3(currentRoot, { withFileTypes: true })) {
|
|
2530
2787
|
if (changed.size >= limit) return;
|
|
2531
2788
|
const nextRelative = relativeRoot ? `${relativeRoot}/${item.name}` : item.name;
|
|
2532
2789
|
if (shouldSkipRoutingPath(nextRelative)) continue;
|
|
2533
|
-
const currentPath =
|
|
2790
|
+
const currentPath = join8(currentRoot, item.name);
|
|
2534
2791
|
if (item.isDirectory()) {
|
|
2535
2792
|
walk(currentPath, nextRelative);
|
|
2536
2793
|
continue;
|
|
2537
2794
|
}
|
|
2538
2795
|
if (!item.isFile()) continue;
|
|
2539
|
-
const sourcePath =
|
|
2540
|
-
if (!
|
|
2796
|
+
const sourcePath = join8(SOURCE_ROOT, nextRelative);
|
|
2797
|
+
if (!existsSync5(sourcePath)) {
|
|
2541
2798
|
changed.add(nextRelative);
|
|
2542
2799
|
continue;
|
|
2543
2800
|
}
|
|
2544
|
-
const currentStat =
|
|
2545
|
-
const sourceStat =
|
|
2801
|
+
const currentStat = statSync2(currentPath);
|
|
2802
|
+
const sourceStat = statSync2(sourcePath);
|
|
2546
2803
|
if (currentStat.size !== sourceStat.size) {
|
|
2547
2804
|
changed.add(nextRelative);
|
|
2548
2805
|
continue;
|
|
2549
2806
|
}
|
|
2550
|
-
const currentFile =
|
|
2551
|
-
const sourceFile =
|
|
2807
|
+
const currentFile = readFileSync4(currentPath);
|
|
2808
|
+
const sourceFile = readFileSync4(sourcePath);
|
|
2552
2809
|
if (!currentFile.equals(sourceFile)) changed.add(nextRelative);
|
|
2553
2810
|
}
|
|
2554
2811
|
};
|
|
@@ -2557,7 +2814,7 @@ function inferChangedWorkspacePaths(workspacePath, limit = 32) {
|
|
|
2557
2814
|
}
|
|
2558
2815
|
function computeDiffStats(issue) {
|
|
2559
2816
|
const wp = issue.workspacePath;
|
|
2560
|
-
if (!wp || !
|
|
2817
|
+
if (!wp || !existsSync5(wp) || !existsSync5(SOURCE_ROOT)) return;
|
|
2561
2818
|
try {
|
|
2562
2819
|
let raw = "";
|
|
2563
2820
|
try {
|
|
@@ -2586,23 +2843,23 @@ function computeDiffStats(issue) {
|
|
|
2586
2843
|
}
|
|
2587
2844
|
}
|
|
2588
2845
|
function isConflict(relativePath) {
|
|
2589
|
-
const targetPath =
|
|
2590
|
-
const sourcePath =
|
|
2591
|
-
if (!
|
|
2592
|
-
if (!
|
|
2593
|
-
const targetStat =
|
|
2594
|
-
const sourceStat =
|
|
2846
|
+
const targetPath = join8(TARGET_ROOT, relativePath);
|
|
2847
|
+
const sourcePath = join8(SOURCE_ROOT, relativePath);
|
|
2848
|
+
if (!existsSync5(sourcePath)) return existsSync5(targetPath);
|
|
2849
|
+
if (!existsSync5(targetPath)) return false;
|
|
2850
|
+
const targetStat = statSync2(targetPath);
|
|
2851
|
+
const sourceStat = statSync2(sourcePath);
|
|
2595
2852
|
if (targetStat.size !== sourceStat.size) return true;
|
|
2596
|
-
return !
|
|
2853
|
+
return !readFileSync4(targetPath).equals(readFileSync4(sourcePath));
|
|
2597
2854
|
}
|
|
2598
2855
|
function mergeWorkspace(workspacePath) {
|
|
2599
2856
|
const result = { copied: [], deleted: [], skipped: [], conflicts: [] };
|
|
2600
|
-
if (!workspacePath || !
|
|
2857
|
+
if (!workspacePath || !existsSync5(workspacePath)) {
|
|
2601
2858
|
throw new Error(`Workspace not found: ${workspacePath}`);
|
|
2602
2859
|
}
|
|
2603
2860
|
const walkWorkspace = (dir) => {
|
|
2604
|
-
for (const item of
|
|
2605
|
-
const fullPath =
|
|
2861
|
+
for (const item of readdirSync3(dir, { withFileTypes: true })) {
|
|
2862
|
+
const fullPath = join8(dir, item.name);
|
|
2606
2863
|
const relativePath = relative(workspacePath, fullPath);
|
|
2607
2864
|
if (shouldSkipMergePath(relativePath)) {
|
|
2608
2865
|
result.skipped.push(relativePath);
|
|
@@ -2613,17 +2870,17 @@ function mergeWorkspace(workspacePath) {
|
|
|
2613
2870
|
continue;
|
|
2614
2871
|
}
|
|
2615
2872
|
if (!item.isFile()) continue;
|
|
2616
|
-
const sourcePath =
|
|
2617
|
-
const isNew = !
|
|
2873
|
+
const sourcePath = join8(SOURCE_ROOT, relativePath);
|
|
2874
|
+
const isNew = !existsSync5(sourcePath);
|
|
2618
2875
|
let isModified = false;
|
|
2619
2876
|
if (!isNew) {
|
|
2620
|
-
const wsStat =
|
|
2621
|
-
const srcStat =
|
|
2877
|
+
const wsStat = statSync2(fullPath);
|
|
2878
|
+
const srcStat = statSync2(sourcePath);
|
|
2622
2879
|
if (wsStat.size !== srcStat.size) {
|
|
2623
2880
|
isModified = true;
|
|
2624
2881
|
} else {
|
|
2625
|
-
const wsContent =
|
|
2626
|
-
const srcContent =
|
|
2882
|
+
const wsContent = readFileSync4(fullPath);
|
|
2883
|
+
const srcContent = readFileSync4(sourcePath);
|
|
2627
2884
|
isModified = !wsContent.equals(srcContent);
|
|
2628
2885
|
}
|
|
2629
2886
|
}
|
|
@@ -2632,18 +2889,18 @@ function mergeWorkspace(workspacePath) {
|
|
|
2632
2889
|
result.conflicts.push(relativePath);
|
|
2633
2890
|
continue;
|
|
2634
2891
|
}
|
|
2635
|
-
const targetDir =
|
|
2636
|
-
const targetPath =
|
|
2637
|
-
|
|
2892
|
+
const targetDir = join8(TARGET_ROOT, relative(workspacePath, dir));
|
|
2893
|
+
const targetPath = join8(TARGET_ROOT, relativePath);
|
|
2894
|
+
mkdirSync2(targetDir, { recursive: true });
|
|
2638
2895
|
cpSync(fullPath, targetPath, { force: true });
|
|
2639
2896
|
result.copied.push(relativePath);
|
|
2640
2897
|
}
|
|
2641
2898
|
}
|
|
2642
2899
|
};
|
|
2643
2900
|
const walkSource = (dir) => {
|
|
2644
|
-
if (!
|
|
2645
|
-
for (const item of
|
|
2646
|
-
const fullPath =
|
|
2901
|
+
if (!existsSync5(dir)) return;
|
|
2902
|
+
for (const item of readdirSync3(dir, { withFileTypes: true })) {
|
|
2903
|
+
const fullPath = join8(dir, item.name);
|
|
2647
2904
|
const relativePath = relative(SOURCE_ROOT, fullPath);
|
|
2648
2905
|
if (shouldSkipMergePath(relativePath)) continue;
|
|
2649
2906
|
if (item.isDirectory()) {
|
|
@@ -2651,10 +2908,10 @@ function mergeWorkspace(workspacePath) {
|
|
|
2651
2908
|
continue;
|
|
2652
2909
|
}
|
|
2653
2910
|
if (!item.isFile()) continue;
|
|
2654
|
-
const wsPath =
|
|
2655
|
-
if (!
|
|
2656
|
-
const targetPath =
|
|
2657
|
-
if (
|
|
2911
|
+
const wsPath = join8(workspacePath, relativePath);
|
|
2912
|
+
if (!existsSync5(wsPath)) {
|
|
2913
|
+
const targetPath = join8(TARGET_ROOT, relativePath);
|
|
2914
|
+
if (existsSync5(targetPath)) {
|
|
2658
2915
|
if (isConflict(relativePath)) {
|
|
2659
2916
|
result.conflicts.push(relativePath);
|
|
2660
2917
|
} else {
|
|
@@ -2976,16 +3233,16 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
|
|
|
2976
3233
|
};
|
|
2977
3234
|
for (const [key, value] of Object.entries(extraEnv)) {
|
|
2978
3235
|
if (value.length > 4e3) {
|
|
2979
|
-
const valFile =
|
|
2980
|
-
|
|
3236
|
+
const valFile = join8(workspacePath, `${key.toLowerCase()}.txt`);
|
|
3237
|
+
writeFileSync3(valFile, value, "utf8");
|
|
2981
3238
|
allVars[`${key}_FILE`] = valFile;
|
|
2982
3239
|
} else {
|
|
2983
3240
|
allVars[key] = value;
|
|
2984
3241
|
}
|
|
2985
3242
|
}
|
|
2986
|
-
const envFilePath =
|
|
3243
|
+
const envFilePath = join8(workspacePath, ".fifony-env.sh");
|
|
2987
3244
|
const envFileLines = Object.entries(allVars).map(([k, v]) => `export ${k}=${JSON.stringify(v)}`).join("\n");
|
|
2988
|
-
|
|
3245
|
+
writeFileSync3(envFilePath, envFileLines, "utf8");
|
|
2989
3246
|
const wrappedCommand = `. "${envFilePath}" && ${command}`;
|
|
2990
3247
|
const child = spawn2(wrappedCommand, {
|
|
2991
3248
|
shell: true,
|
|
@@ -2998,10 +3255,11 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
|
|
|
2998
3255
|
if (child.stdin) {
|
|
2999
3256
|
child.stdin.end();
|
|
3000
3257
|
}
|
|
3001
|
-
const pidFile =
|
|
3258
|
+
const pidFile = join8(workspacePath, "fifony-agent.pid");
|
|
3002
3259
|
const pid = child.pid;
|
|
3003
3260
|
if (pid) {
|
|
3004
|
-
|
|
3261
|
+
logger.debug({ issueId: issue.id, pid, command: command.slice(0, 120), cwd: workspacePath }, "[Agent] Process spawned");
|
|
3262
|
+
writeFileSync3(pidFile, JSON.stringify({
|
|
3005
3263
|
pid,
|
|
3006
3264
|
issueId: issue.id,
|
|
3007
3265
|
startedAt: new Date(started).toISOString(),
|
|
@@ -3011,8 +3269,8 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
|
|
|
3011
3269
|
let output = "";
|
|
3012
3270
|
let timedOut = false;
|
|
3013
3271
|
let outputBytes = 0;
|
|
3014
|
-
const liveLogFile =
|
|
3015
|
-
|
|
3272
|
+
const liveLogFile = join8(workspacePath, "fifony-live-output.log");
|
|
3273
|
+
writeFileSync3(liveLogFile, "", "utf8");
|
|
3016
3274
|
const onChunk = (chunk) => {
|
|
3017
3275
|
const text = String(chunk);
|
|
3018
3276
|
output = appendFileTail(output, text, config.logLinesTail);
|
|
@@ -3077,7 +3335,7 @@ async function runHook(command, workspacePath, issue, hookName, extraEnv = {}) {
|
|
|
3077
3335
|
retryDelayMs: 0,
|
|
3078
3336
|
staleInProgressTimeoutMs: 0,
|
|
3079
3337
|
logLinesTail: 12e3,
|
|
3080
|
-
agentProvider: normalizeAgentProvider(
|
|
3338
|
+
agentProvider: normalizeAgentProvider(env6.FIFONY_AGENT_PROVIDER ?? "codex"),
|
|
3081
3339
|
agentCommand: command,
|
|
3082
3340
|
maxTurns: 1,
|
|
3083
3341
|
runMode: "filesystem"
|
|
@@ -3088,8 +3346,8 @@ async function runHook(command, workspacePath, issue, hookName, extraEnv = {}) {
|
|
|
3088
3346
|
}
|
|
3089
3347
|
async function cleanWorkspace(issueId, workflowDefinition) {
|
|
3090
3348
|
const safeId = idToSafePath(issueId);
|
|
3091
|
-
const workspacePath =
|
|
3092
|
-
if (!
|
|
3349
|
+
const workspacePath = join8(WORKSPACE_ROOT, safeId);
|
|
3350
|
+
if (!existsSync5(workspacePath)) return;
|
|
3093
3351
|
if (workflowDefinition?.beforeRemoveHook) {
|
|
3094
3352
|
try {
|
|
3095
3353
|
const dummyIssue = { id: issueId, identifier: issueId };
|
|
@@ -3107,25 +3365,30 @@ async function cleanWorkspace(issueId, workflowDefinition) {
|
|
|
3107
3365
|
}
|
|
3108
3366
|
async function prepareWorkspace(issue, workflowDefinition) {
|
|
3109
3367
|
const safeId = idToSafePath(issue.id);
|
|
3110
|
-
const workspaceRoot =
|
|
3111
|
-
const createdNow = !
|
|
3368
|
+
const workspaceRoot = join8(WORKSPACE_ROOT, safeId);
|
|
3369
|
+
const createdNow = !existsSync5(workspaceRoot);
|
|
3112
3370
|
if (createdNow) {
|
|
3113
|
-
|
|
3371
|
+
logger.debug({ issueId: issue.id, identifier: issue.identifier, workspacePath: workspaceRoot }, "[Agent] Creating new workspace");
|
|
3372
|
+
mkdirSync2(workspaceRoot, { recursive: true });
|
|
3114
3373
|
if (workflowDefinition?.afterCreateHook) {
|
|
3115
3374
|
await runHook(workflowDefinition.afterCreateHook, workspaceRoot, issue, "after_create");
|
|
3116
3375
|
} else {
|
|
3376
|
+
await ensureSourceReady();
|
|
3117
3377
|
cpSync(SOURCE_ROOT, workspaceRoot, {
|
|
3118
3378
|
recursive: true,
|
|
3119
3379
|
force: true,
|
|
3120
3380
|
filter: (sourcePath) => !sourcePath.startsWith(WORKSPACE_ROOT)
|
|
3121
3381
|
});
|
|
3122
3382
|
}
|
|
3383
|
+
logger.debug({ issueId: issue.id, workspacePath: workspaceRoot }, "[Agent] Workspace created");
|
|
3384
|
+
} else {
|
|
3385
|
+
logger.debug({ issueId: issue.id, workspacePath: workspaceRoot }, "[Agent] Reusing existing workspace");
|
|
3123
3386
|
}
|
|
3124
|
-
const metaPath =
|
|
3387
|
+
const metaPath = join8(workspaceRoot, "fifony-issue.json");
|
|
3125
3388
|
const promptText = await buildPrompt(issue, workflowDefinition);
|
|
3126
|
-
const promptFile =
|
|
3127
|
-
|
|
3128
|
-
|
|
3389
|
+
const promptFile = join8(workspaceRoot, "fifony-prompt.md");
|
|
3390
|
+
writeFileSync3(metaPath, JSON.stringify({ ...issue, runtimeSource: SOURCE_ROOT, bootstrapAt: now() }, null, 2), "utf8");
|
|
3391
|
+
writeFileSync3(promptFile, `${promptText}
|
|
3129
3392
|
`, "utf8");
|
|
3130
3393
|
issue.workspacePath = workspaceRoot;
|
|
3131
3394
|
issue.workspacePreparedAt = now();
|
|
@@ -3142,8 +3405,9 @@ async function runAgentSession(state, issue, provider, cycle, workspacePath, bas
|
|
|
3142
3405
|
let nextPrompt = session.nextPrompt;
|
|
3143
3406
|
let lastCode = session.lastCode;
|
|
3144
3407
|
let lastOutput = session.lastOutput;
|
|
3145
|
-
const resultFile =
|
|
3408
|
+
const resultFile = join8(workspacePath, `fifony-result-${provider.role}-${provider.provider}.json`);
|
|
3146
3409
|
if (session.status === "done" && session.turns.length > 0) {
|
|
3410
|
+
logger.debug({ issueId: issue.id, identifier: issue.identifier, provider: provider.provider, role: provider.role }, "[Agent] Session already completed, returning cached result");
|
|
3147
3411
|
return { success: true, blocked: false, continueRequested: false, code: session.lastCode, output: session.lastOutput, turns: session.turns.length };
|
|
3148
3412
|
}
|
|
3149
3413
|
const turnIndex = session.turns.length + 1;
|
|
@@ -3155,14 +3419,15 @@ Agent requested additional turns beyond configured limit (${maxTurns}).`, state.
|
|
|
3155
3419
|
return { success: false, blocked: true, continueRequested: false, code: lastCode, output: session.lastOutput, turns: session.turns.length };
|
|
3156
3420
|
}
|
|
3157
3421
|
const turnPrompt = await buildTurnPrompt(issue, basePromptText, previousOutput, turnIndex, maxTurns, nextPrompt);
|
|
3158
|
-
const turnPromptFile = turnIndex === 1 ? basePromptFile :
|
|
3159
|
-
if (turnIndex > 1)
|
|
3422
|
+
const turnPromptFile = turnIndex === 1 ? basePromptFile : join8(workspacePath, `fifony-turn-${String(turnIndex).padStart(2, "0")}.md`);
|
|
3423
|
+
if (turnIndex > 1) writeFileSync3(turnPromptFile, `${turnPrompt}
|
|
3160
3424
|
`, "utf8");
|
|
3161
3425
|
session.status = "running";
|
|
3162
3426
|
session.lastPrompt = turnPrompt;
|
|
3163
3427
|
session.lastPromptFile = turnPromptFile;
|
|
3164
3428
|
session.maxTurns = maxTurns;
|
|
3165
3429
|
await persistAgentSessionState(sessionKey, issue, provider, cycle, session);
|
|
3430
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier, turn: turnIndex, maxTurns, provider: provider.provider, role: provider.role, cycle, command: provider.command.slice(0, 120) }, "[Agent] Spawning agent command");
|
|
3166
3431
|
const turnStartedAt = now();
|
|
3167
3432
|
const turnEnv = {
|
|
3168
3433
|
FIFONY_AGENT_PROVIDER: provider.provider,
|
|
@@ -3195,6 +3460,7 @@ Agent requested additional turns beyond configured limit (${maxTurns}).`, state.
|
|
|
3195
3460
|
FIFONY_PRESERVE_RESULT_FILE: "1"
|
|
3196
3461
|
});
|
|
3197
3462
|
}
|
|
3463
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier, turn: turnIndex, exitCode: turnResult.code, success: turnResult.success, outputBytes: turnResult.output.length }, "[Agent] Agent command finished");
|
|
3198
3464
|
const directive = readAgentDirective(workspacePath, turnResult.output, turnResult.success);
|
|
3199
3465
|
lastCode = turnResult.code;
|
|
3200
3466
|
lastOutput = turnResult.output;
|
|
@@ -3240,20 +3506,24 @@ Agent requested additional turns beyond configured limit (${maxTurns}).`, state.
|
|
|
3240
3506
|
const directiveSummary = directive.summary ? ` ${directive.summary}` : "";
|
|
3241
3507
|
addEvent(state, issue.id, "runner", `Turn ${turnIndex}/${maxTurns} finished with status ${directive.status}.${directiveSummary}`.trim());
|
|
3242
3508
|
if (!turnResult.success || directive.status === "failed") {
|
|
3509
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier, turn: turnIndex, directiveStatus: directive.status, exitCode: lastCode }, "[Agent] Session turn failed");
|
|
3243
3510
|
session.status = "failed";
|
|
3244
3511
|
await persistAgentSessionState(sessionKey, issue, provider, cycle, session);
|
|
3245
3512
|
return { success: false, blocked: false, continueRequested: false, code: lastCode, output: lastOutput, turns: turnIndex };
|
|
3246
3513
|
}
|
|
3247
3514
|
if (directive.status === "blocked") {
|
|
3515
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier, turn: turnIndex }, "[Agent] Session turn blocked \u2014 manual intervention requested");
|
|
3248
3516
|
session.status = "blocked";
|
|
3249
3517
|
await persistAgentSessionState(sessionKey, issue, provider, cycle, session);
|
|
3250
3518
|
return { success: false, blocked: true, continueRequested: false, code: lastCode, output: lastOutput, turns: turnIndex };
|
|
3251
3519
|
}
|
|
3252
3520
|
if (directive.status === "continue") {
|
|
3521
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier, turn: turnIndex, maxTurns }, "[Agent] Session requests continuation");
|
|
3253
3522
|
session.status = "running";
|
|
3254
3523
|
await persistAgentSessionState(sessionKey, issue, provider, cycle, session);
|
|
3255
3524
|
return { success: false, blocked: false, continueRequested: true, code: lastCode, output: lastOutput, turns: turnIndex };
|
|
3256
3525
|
}
|
|
3526
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier, turn: turnIndex }, "[Agent] Session completed successfully");
|
|
3257
3527
|
session.status = "done";
|
|
3258
3528
|
await persistAgentSessionState(sessionKey, issue, provider, cycle, session);
|
|
3259
3529
|
return { success: true, blocked: false, continueRequested: false, code: lastCode, output: lastOutput, turns: turnIndex };
|
|
@@ -3261,13 +3531,14 @@ Agent requested additional turns beyond configured limit (${maxTurns}).`, state.
|
|
|
3261
3531
|
async function runAgentPipeline(state, issue, workspacePath, basePromptText, basePromptFile, workflowDefinition, workflowConfig) {
|
|
3262
3532
|
const providers = getEffectiveAgentProviders(state, issue, workflowDefinition, workflowConfig);
|
|
3263
3533
|
const attempt = issue.attempts + 1;
|
|
3534
|
+
logger.debug({ issueId: issue.id, identifier: issue.identifier, attempt, providers: providers.map((p) => `${p.role}:${p.provider}`) }, "[Agent] Starting pipeline");
|
|
3264
3535
|
const { pipeline, key: pipelineFile } = await loadAgentPipelineState(issue, attempt, providers);
|
|
3265
3536
|
const activeProvider = providers[clamp(pipeline.activeIndex, 0, Math.max(0, providers.length - 1))];
|
|
3266
3537
|
const executorIndex = providers.findIndex((provider) => provider.role === "executor");
|
|
3267
3538
|
const skills = discoverSkills(workspacePath);
|
|
3268
3539
|
const skillContext = buildSkillContext(skills);
|
|
3269
3540
|
if (skillContext) {
|
|
3270
|
-
|
|
3541
|
+
writeFileSync3(join8(workspacePath, "fifony-skills.md"), skillContext, "utf8");
|
|
3271
3542
|
}
|
|
3272
3543
|
const compiled = await compileExecution(issue, activeProvider, state.config, workspacePath, skillContext);
|
|
3273
3544
|
let providerPrompt;
|
|
@@ -3283,9 +3554,9 @@ async function runAgentPipeline(state, issue, workspacePath, basePromptText, bas
|
|
|
3283
3554
|
`Plan compiled for ${compiled.meta.adapter}: effort=${compiled.meta.reasoningEffort}, skills=[${compiled.meta.skillsActivated.join(",")}], subagents=[${compiled.meta.subagentsRequested.join(",")}].`
|
|
3284
3555
|
);
|
|
3285
3556
|
if (Object.keys(compiled.env).length > 0) {
|
|
3286
|
-
const envFile =
|
|
3557
|
+
const envFile = join8(workspacePath, ".fifony-compiled-env.sh");
|
|
3287
3558
|
const envLines = Object.entries(compiled.env).map(([k, v]) => `export ${k}=${JSON.stringify(v)}`).join("\n");
|
|
3288
|
-
|
|
3559
|
+
writeFileSync3(envFile, envLines, "utf8");
|
|
3289
3560
|
}
|
|
3290
3561
|
} else {
|
|
3291
3562
|
providerPrompt = await buildProviderBasePrompt(activeProvider, issue, basePromptText, workspacePath, skillContext);
|
|
@@ -3333,6 +3604,7 @@ async function runIssueOnce(state, issue, running, workflowDefinition) {
|
|
|
3333
3604
|
const startTs = Date.now();
|
|
3334
3605
|
const isReview = issue.state === "In Review";
|
|
3335
3606
|
const isResuming = issue.state === "Running" || issue.state === "Interrupted";
|
|
3607
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier, state: issue.state, isReview, isResuming, attempt: issue.attempts + 1, maxAttempts: issue.maxAttempts }, "[Agent] Starting issue execution");
|
|
3336
3608
|
running.add(issue.id);
|
|
3337
3609
|
state.metrics.activeWorkers += 1;
|
|
3338
3610
|
issue.startedAt = issue.startedAt ?? now();
|
|
@@ -3393,8 +3665,8 @@ async function runIssueOnce(state, issue, running, workflowDefinition) {
|
|
|
3393
3665
|
}
|
|
3394
3666
|
const compiled = await compileReview(issue, reviewer, workspacePath, diffSummary);
|
|
3395
3667
|
const effectiveReviewer = { ...reviewer, command: compiled.command || reviewer.command };
|
|
3396
|
-
const reviewPromptFile =
|
|
3397
|
-
|
|
3668
|
+
const reviewPromptFile = join8(workspacePath, "fifony-review-prompt.md");
|
|
3669
|
+
writeFileSync3(reviewPromptFile, `${compiled.prompt}
|
|
3398
3670
|
`, "utf8");
|
|
3399
3671
|
state._workflowDefinition = workflowDefinition;
|
|
3400
3672
|
const reviewResult = await runAgentSession(state, issue, effectiveReviewer, 1, workspacePath, compiled.prompt, reviewPromptFile);
|
|
@@ -3510,7 +3782,10 @@ async function runIssueOnce(state, issue, running, workflowDefinition) {
|
|
|
3510
3782
|
addEvent(state, issue.id, "error", `Issue ${issue.identifier} blocked after unexpected failure.`);
|
|
3511
3783
|
}
|
|
3512
3784
|
} finally {
|
|
3785
|
+
const elapsedMs = Date.now() - startTs;
|
|
3786
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier, finalState: issue.state, elapsedMs, attempts: issue.attempts }, "[Agent] Issue execution finished");
|
|
3513
3787
|
issue.updatedAt = now();
|
|
3788
|
+
markIssueDirty(issue.id);
|
|
3514
3789
|
state.metrics.activeWorkers = Math.max(state.metrics.activeWorkers - 1, 0);
|
|
3515
3790
|
running.delete(issue.id);
|
|
3516
3791
|
state.metrics = computeMetrics(state.issues);
|
|
@@ -3520,7 +3795,7 @@ async function runIssueOnce(state, issue, running, workflowDefinition) {
|
|
|
3520
3795
|
}
|
|
3521
3796
|
}
|
|
3522
3797
|
|
|
3523
|
-
// src/
|
|
3798
|
+
// src/agent/resources/issues.resource.ts
|
|
3524
3799
|
function getIssueId(c) {
|
|
3525
3800
|
if (!c || typeof c !== "object" || !("req" in c) || !c.req || typeof c.req !== "object") {
|
|
3526
3801
|
return null;
|
|
@@ -3746,7 +4021,7 @@ var issues_resource_default = {
|
|
|
3746
4021
|
}
|
|
3747
4022
|
};
|
|
3748
4023
|
|
|
3749
|
-
// src/
|
|
4024
|
+
// src/agent/resources/events.resource.ts
|
|
3750
4025
|
var events_resource_default = {
|
|
3751
4026
|
name: S3DB_EVENT_RESOURCE,
|
|
3752
4027
|
attributes: {
|
|
@@ -3772,7 +4047,7 @@ var events_resource_default = {
|
|
|
3772
4047
|
}
|
|
3773
4048
|
};
|
|
3774
4049
|
|
|
3775
|
-
// src/
|
|
4050
|
+
// src/agent/resources/settings.resource.ts
|
|
3776
4051
|
var settings_resource_default = {
|
|
3777
4052
|
name: S3DB_SETTINGS_RESOURCE,
|
|
3778
4053
|
attributes: {
|
|
@@ -3794,7 +4069,7 @@ var settings_resource_default = {
|
|
|
3794
4069
|
}
|
|
3795
4070
|
};
|
|
3796
4071
|
|
|
3797
|
-
// src/
|
|
4072
|
+
// src/agent/resources/agent-sessions.resource.ts
|
|
3798
4073
|
var agent_sessions_resource_default = {
|
|
3799
4074
|
name: S3DB_AGENT_SESSION_RESOURCE,
|
|
3800
4075
|
attributes: {
|
|
@@ -3824,7 +4099,7 @@ var agent_sessions_resource_default = {
|
|
|
3824
4099
|
}
|
|
3825
4100
|
};
|
|
3826
4101
|
|
|
3827
|
-
// src/
|
|
4102
|
+
// src/agent/resources/agent-pipelines.resource.ts
|
|
3828
4103
|
var agent_pipelines_resource_default = {
|
|
3829
4104
|
name: S3DB_AGENT_PIPELINE_RESOURCE,
|
|
3830
4105
|
attributes: {
|
|
@@ -3850,7 +4125,7 @@ var agent_pipelines_resource_default = {
|
|
|
3850
4125
|
}
|
|
3851
4126
|
};
|
|
3852
4127
|
|
|
3853
|
-
// src/
|
|
4128
|
+
// src/agent/resources/index.ts
|
|
3854
4129
|
var NATIVE_RESOURCE_CONFIGS = [
|
|
3855
4130
|
runtime_state_resource_default,
|
|
3856
4131
|
issues_resource_default,
|
|
@@ -3861,45 +4136,45 @@ var NATIVE_RESOURCE_CONFIGS = [
|
|
|
3861
4136
|
];
|
|
3862
4137
|
var NATIVE_RESOURCE_NAMES = NATIVE_RESOURCE_CONFIGS.map((resource) => resource.name);
|
|
3863
4138
|
|
|
3864
|
-
// src/
|
|
4139
|
+
// src/agent/providers-usage.ts
|
|
3865
4140
|
import { execSync as execSync2 } from "child_process";
|
|
3866
|
-
import { existsSync as
|
|
3867
|
-
import { join as
|
|
4141
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "fs";
|
|
4142
|
+
import { join as join9 } from "path";
|
|
3868
4143
|
import { homedir as homedir4 } from "os";
|
|
3869
|
-
import { env as
|
|
4144
|
+
import { env as env7 } from "process";
|
|
3870
4145
|
function resolveCodexHomeCandidates() {
|
|
3871
4146
|
const homePaths = /* @__PURE__ */ new Set([
|
|
3872
4147
|
homedir4(),
|
|
3873
|
-
|
|
3874
|
-
|
|
4148
|
+
env7.XDG_STATE_HOME?.trim() || "",
|
|
4149
|
+
env7.XDG_DATA_HOME?.trim() || ""
|
|
3875
4150
|
]);
|
|
3876
|
-
const sudoUser =
|
|
4151
|
+
const sudoUser = env7.SUDO_USER?.trim();
|
|
3877
4152
|
if (sudoUser && sudoUser !== "root") {
|
|
3878
4153
|
homePaths.add(`/home/${sudoUser}`);
|
|
3879
4154
|
}
|
|
3880
4155
|
const direct = /* @__PURE__ */ new Set([
|
|
3881
|
-
|
|
4156
|
+
env7.CODEX_HOME?.trim() || ""
|
|
3882
4157
|
]);
|
|
3883
4158
|
const candidates = [...homePaths, ...direct].filter(Boolean).flatMap((candidate) => {
|
|
3884
4159
|
if (candidate.endsWith("/.codex") || candidate.endsWith("/codex")) return [candidate];
|
|
3885
|
-
return [
|
|
4160
|
+
return [join9(candidate, ".codex"), join9(candidate, "codex")];
|
|
3886
4161
|
});
|
|
3887
4162
|
return [...new Set(candidates)];
|
|
3888
4163
|
}
|
|
3889
4164
|
function resolveCodexDir() {
|
|
3890
4165
|
for (const candidate of resolveCodexHomeCandidates()) {
|
|
3891
|
-
if (
|
|
4166
|
+
if (existsSync6(candidate)) {
|
|
3892
4167
|
return candidate;
|
|
3893
4168
|
}
|
|
3894
4169
|
}
|
|
3895
4170
|
return null;
|
|
3896
4171
|
}
|
|
3897
4172
|
function findLatestCodexDb(codexDir) {
|
|
3898
|
-
const explicit =
|
|
3899
|
-
if (
|
|
3900
|
-
const candidates =
|
|
4173
|
+
const explicit = join9(codexDir, "state_5.sqlite");
|
|
4174
|
+
if (existsSync6(explicit)) return explicit;
|
|
4175
|
+
const candidates = readdirSync4(codexDir).filter((name) => name.startsWith("state_") && name.endsWith(".sqlite")).sort().reverse();
|
|
3901
4176
|
if (candidates.length === 0) return null;
|
|
3902
|
-
return
|
|
4177
|
+
return join9(codexDir, candidates[0]);
|
|
3903
4178
|
}
|
|
3904
4179
|
function computeNextMonday() {
|
|
3905
4180
|
const now2 = /* @__PURE__ */ new Date();
|
|
@@ -3936,15 +4211,15 @@ var CLAUDE_PLAN_LIMITS = {
|
|
|
3936
4211
|
};
|
|
3937
4212
|
function collectClaudeUsage() {
|
|
3938
4213
|
const home = homedir4();
|
|
3939
|
-
const claudeDir =
|
|
3940
|
-
if (!
|
|
4214
|
+
const claudeDir = join9(home, ".claude");
|
|
4215
|
+
if (!existsSync6(claudeDir)) return null;
|
|
3941
4216
|
let available = false;
|
|
3942
4217
|
try {
|
|
3943
4218
|
execSync2("which claude", { encoding: "utf8", timeout: 3e3 });
|
|
3944
4219
|
available = true;
|
|
3945
4220
|
} catch {
|
|
3946
4221
|
}
|
|
3947
|
-
const projectsDir =
|
|
4222
|
+
const projectsDir = join9(claudeDir, "projects");
|
|
3948
4223
|
let totalInputTokens = 0;
|
|
3949
4224
|
let totalOutputTokens = 0;
|
|
3950
4225
|
let totalSessions = 0;
|
|
@@ -3958,23 +4233,23 @@ function collectClaudeUsage() {
|
|
|
3958
4233
|
const todayMs = todayStart.getTime();
|
|
3959
4234
|
const weekStart = computeWeekStart();
|
|
3960
4235
|
const weekMs = weekStart.getTime();
|
|
3961
|
-
if (
|
|
4236
|
+
if (existsSync6(projectsDir)) {
|
|
3962
4237
|
try {
|
|
3963
|
-
const projectDirs =
|
|
4238
|
+
const projectDirs = readdirSync4(projectsDir, { withFileTypes: true });
|
|
3964
4239
|
for (const dir of projectDirs) {
|
|
3965
4240
|
if (!dir.isDirectory()) continue;
|
|
3966
|
-
const projectPath =
|
|
4241
|
+
const projectPath = join9(projectsDir, dir.name);
|
|
3967
4242
|
let sessionFiles;
|
|
3968
4243
|
try {
|
|
3969
|
-
sessionFiles =
|
|
4244
|
+
sessionFiles = readdirSync4(projectPath).filter((f) => f.endsWith(".jsonl"));
|
|
3970
4245
|
} catch {
|
|
3971
4246
|
continue;
|
|
3972
4247
|
}
|
|
3973
4248
|
for (const file of sessionFiles) {
|
|
3974
|
-
const filePath =
|
|
4249
|
+
const filePath = join9(projectPath, file);
|
|
3975
4250
|
let content;
|
|
3976
4251
|
try {
|
|
3977
|
-
content =
|
|
4252
|
+
content = readFileSync5(filePath, "utf8");
|
|
3978
4253
|
} catch {
|
|
3979
4254
|
continue;
|
|
3980
4255
|
}
|
|
@@ -4028,10 +4303,10 @@ function collectClaudeUsage() {
|
|
|
4028
4303
|
];
|
|
4029
4304
|
let plan = "pro";
|
|
4030
4305
|
let resetInfo = "Weekly reset (every Monday 00:00 UTC)";
|
|
4031
|
-
const settingsPath =
|
|
4032
|
-
if (
|
|
4306
|
+
const settingsPath = join9(claudeDir, "settings.json");
|
|
4307
|
+
if (existsSync6(settingsPath)) {
|
|
4033
4308
|
try {
|
|
4034
|
-
const settings = JSON.parse(
|
|
4309
|
+
const settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
|
|
4035
4310
|
if (settings.plan === "max" || settings.plan === "max5x") {
|
|
4036
4311
|
plan = settings.plan;
|
|
4037
4312
|
resetInfo = `Plan: ${settings.plan.toUpperCase()} \u2014 Weekly token limit resets every Monday 00:00 UTC`;
|
|
@@ -4069,11 +4344,11 @@ function collectCodexUsage() {
|
|
|
4069
4344
|
} catch {
|
|
4070
4345
|
}
|
|
4071
4346
|
const models = [];
|
|
4072
|
-
const modelsCachePath =
|
|
4347
|
+
const modelsCachePath = join9(codexDir, "models_cache.json");
|
|
4073
4348
|
let currentModel = "";
|
|
4074
|
-
if (
|
|
4349
|
+
if (existsSync6(modelsCachePath)) {
|
|
4075
4350
|
try {
|
|
4076
|
-
const cache = JSON.parse(
|
|
4351
|
+
const cache = JSON.parse(readFileSync5(modelsCachePath, "utf8"));
|
|
4077
4352
|
for (const m of cache.models || []) {
|
|
4078
4353
|
models.push({
|
|
4079
4354
|
slug: m.slug,
|
|
@@ -4084,10 +4359,10 @@ function collectCodexUsage() {
|
|
|
4084
4359
|
} catch {
|
|
4085
4360
|
}
|
|
4086
4361
|
}
|
|
4087
|
-
const configPath =
|
|
4088
|
-
if (
|
|
4362
|
+
const configPath = join9(codexDir, "config.toml");
|
|
4363
|
+
if (existsSync6(configPath)) {
|
|
4089
4364
|
try {
|
|
4090
|
-
const configContent =
|
|
4365
|
+
const configContent = readFileSync5(configPath, "utf8");
|
|
4091
4366
|
const modelMatch = configContent.match(/^model\s*=\s*"([^"]+)"/m);
|
|
4092
4367
|
if (modelMatch) currentModel = modelMatch[1];
|
|
4093
4368
|
} catch {
|
|
@@ -4177,8 +4452,16 @@ function collectProvidersUsage() {
|
|
|
4177
4452
|
};
|
|
4178
4453
|
}
|
|
4179
4454
|
|
|
4180
|
-
// src/
|
|
4455
|
+
// src/agent/scheduler.ts
|
|
4181
4456
|
var shuttingDown = false;
|
|
4457
|
+
var lastPersistAt = 0;
|
|
4458
|
+
var PERSIST_DEBOUNCE_MS = 5e3;
|
|
4459
|
+
var schedulerWakeResolve = null;
|
|
4460
|
+
function wakeScheduler() {
|
|
4461
|
+
schedulerWakeResolve?.();
|
|
4462
|
+
}
|
|
4463
|
+
var IDLE_POLL_MS = 5e3;
|
|
4464
|
+
var ACTIVE_POLL_MS = 500;
|
|
4182
4465
|
function installGracefulShutdown(state, running) {
|
|
4183
4466
|
const handler = async (signal) => {
|
|
4184
4467
|
if (shuttingDown) {
|
|
@@ -4190,9 +4473,11 @@ function installGracefulShutdown(state, running) {
|
|
|
4190
4473
|
addEvent(state, void 0, "info", `Graceful shutdown initiated (${signal}).`);
|
|
4191
4474
|
for (const issue of state.issues) {
|
|
4192
4475
|
if (running.has(issue.id) && (issue.state === "Running" || issue.state === "In Review")) {
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4476
|
+
try {
|
|
4477
|
+
await transitionIssueState(issue, "Interrupted", `Interrupted by ${signal} \u2014 will resume on next start.`, { fallbackToLocal: true });
|
|
4478
|
+
} catch {
|
|
4479
|
+
logger.warn(`Could not transition issue ${issue.identifier} to Interrupted during shutdown.`);
|
|
4480
|
+
}
|
|
4196
4481
|
addEvent(state, issue.id, "info", `Issue ${issue.identifier} interrupted by shutdown.`);
|
|
4197
4482
|
}
|
|
4198
4483
|
}
|
|
@@ -4272,10 +4557,14 @@ async function ensureNotStale(state, staleTimeoutMs) {
|
|
|
4272
4557
|
const limit = Date.now() - staleTimeoutMs;
|
|
4273
4558
|
for (const issue of state.issues) {
|
|
4274
4559
|
if (EXECUTING_STATES.has(issue.state) && Date.parse(issue.updatedAt) < limit && !TERMINAL_STATES.has(issue.state) && !issueHasResumableSession(issue)) {
|
|
4560
|
+
const staleMinutes = Math.round((Date.now() - Date.parse(issue.updatedAt)) / 6e4);
|
|
4561
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier, state: issue.state, updatedAt: issue.updatedAt }, "[Scheduler] Recovering stale issue");
|
|
4275
4562
|
issue.attempts += 1;
|
|
4276
4563
|
issue.nextRetryAt = getNextRetryAt(issue, state.config.retryDelayMs);
|
|
4277
4564
|
issue.startedAt = void 0;
|
|
4565
|
+
markIssueDirty(issue.id);
|
|
4278
4566
|
await transitionIssueState(issue, "Blocked", `Issue state auto-recovered from stale execution.`);
|
|
4567
|
+
addEvent(state, issue.id, "info", `Issue ${issue.identifier} was stale for over ${staleMinutes} minute(s) in ${issue.state} state, moved to Blocked for retry.`);
|
|
4279
4568
|
}
|
|
4280
4569
|
}
|
|
4281
4570
|
}
|
|
@@ -4289,7 +4578,11 @@ function isPerStateFull(issue, state, running) {
|
|
|
4289
4578
|
return count >= limit;
|
|
4290
4579
|
}
|
|
4291
4580
|
function pickNextIssues(state, running, workflowDefinition) {
|
|
4292
|
-
|
|
4581
|
+
const candidates = state.issues.filter((issue) => canRunIssue(issue, running, state) && !isPerStateFull(issue, state, running));
|
|
4582
|
+
if (candidates.length > 0) {
|
|
4583
|
+
logger.debug({ candidates: candidates.map((i) => ({ id: i.identifier, state: i.state, priority: i.priority })) }, "[Scheduler] Eligible candidates for dispatch");
|
|
4584
|
+
}
|
|
4585
|
+
return candidates.sort((a, b) => {
|
|
4293
4586
|
const stateWeight = (c) => c.state === "Running" ? 0 : c.state === "Blocked" ? 2 : 1;
|
|
4294
4587
|
const weightDiff = stateWeight(a) - stateWeight(b);
|
|
4295
4588
|
if (weightDiff !== 0) return weightDiff;
|
|
@@ -4341,15 +4634,29 @@ async function scheduler(state, running, runForever, workflowDefinition) {
|
|
|
4341
4634
|
} else {
|
|
4342
4635
|
const ready = pickNextIssues(state, running, workflowDefinition);
|
|
4343
4636
|
const slots = state.config.workerConcurrency - running.size;
|
|
4344
|
-
if (slots > 0) {
|
|
4637
|
+
if (slots > 0 && ready.length > 0) {
|
|
4345
4638
|
const next = ready.slice(0, Math.max(0, slots));
|
|
4639
|
+
logger.debug({ slots, readyCount: ready.length, dispatching: next.map((i) => i.identifier) }, "[Scheduler] Dispatching issues");
|
|
4346
4640
|
await Promise.all(next.map((issue) => runIssueOnce(state, issue, running, workflowDefinition)));
|
|
4641
|
+
} else if (ready.length > 0 && slots <= 0) {
|
|
4642
|
+
logger.debug({ runningCount: running.size, readyCount: ready.length, concurrency: state.config.workerConcurrency }, "[Scheduler] No slots available, waiting");
|
|
4347
4643
|
}
|
|
4348
4644
|
}
|
|
4349
4645
|
state.updatedAt = now();
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4646
|
+
const shouldPersist = hasDirtyState() || Date.now() - lastPersistAt > PERSIST_DEBOUNCE_MS;
|
|
4647
|
+
if (shouldPersist) {
|
|
4648
|
+
await persistState(state);
|
|
4649
|
+
lastPersistAt = Date.now();
|
|
4650
|
+
}
|
|
4651
|
+
logger.debug({ runningCount: running.size, issueCount: state.issues.length, dirty: hasDirtyState() }, "[Scheduler] Tick completed");
|
|
4652
|
+
const effectivePoll = running.size > 0 ? ACTIVE_POLL_MS : IDLE_POLL_MS;
|
|
4653
|
+
await Promise.race([
|
|
4654
|
+
sleep(effectivePoll),
|
|
4655
|
+
new Promise((resolve4) => {
|
|
4656
|
+
schedulerWakeResolve = resolve4;
|
|
4657
|
+
})
|
|
4658
|
+
]);
|
|
4659
|
+
schedulerWakeResolve = null;
|
|
4353
4660
|
}
|
|
4354
4661
|
return;
|
|
4355
4662
|
}
|
|
@@ -4366,11 +4673,16 @@ async function scheduler(state, running, runForever, workflowDefinition) {
|
|
|
4366
4673
|
const next = ready.slice(0, Math.max(0, slots));
|
|
4367
4674
|
if (next.length === 0 && running.size === 0) {
|
|
4368
4675
|
if (state.issues.some((issue) => issue.state === "Blocked" && issue.nextRetryAt && issue.attempts < issue.maxAttempts)) {
|
|
4676
|
+
logger.debug("[Scheduler] Batch mode: waiting for blocked issues to become eligible for retry");
|
|
4369
4677
|
await sleep(state.config.pollIntervalMs);
|
|
4370
4678
|
continue;
|
|
4371
4679
|
}
|
|
4680
|
+
logger.debug("[Scheduler] Batch mode: no more work to do, exiting loop");
|
|
4372
4681
|
break;
|
|
4373
4682
|
}
|
|
4683
|
+
if (next.length > 0) {
|
|
4684
|
+
logger.debug({ slots, dispatching: next.map((i) => i.identifier) }, "[Scheduler] Batch mode: dispatching issues");
|
|
4685
|
+
}
|
|
4374
4686
|
await Promise.all(next.map((issue) => runIssueOnce(state, issue, running, workflowDefinition)));
|
|
4375
4687
|
state.updatedAt = now();
|
|
4376
4688
|
await persistState(state);
|
|
@@ -4380,12 +4692,12 @@ async function scheduler(state, running, runForever, workflowDefinition) {
|
|
|
4380
4692
|
}
|
|
4381
4693
|
}
|
|
4382
4694
|
|
|
4383
|
-
// src/
|
|
4384
|
-
import { env as
|
|
4385
|
-
import { existsSync as
|
|
4695
|
+
// src/agent/issue-enhancer.ts
|
|
4696
|
+
import { env as env8 } from "process";
|
|
4697
|
+
import { existsSync as existsSync7, mkdtempSync, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
4386
4698
|
import { spawn as spawn3 } from "child_process";
|
|
4387
4699
|
import { tmpdir } from "os";
|
|
4388
|
-
import { join as
|
|
4700
|
+
import { join as join10 } from "path";
|
|
4389
4701
|
function getProviderCommand(provider, config, workflowDefinition) {
|
|
4390
4702
|
const workflowConfig = workflowDefinition ? workflowDefinition.config : {};
|
|
4391
4703
|
const codexCommand = getNestedString(getNestedRecord(workflowConfig, "codex"), "command");
|
|
@@ -4455,23 +4767,23 @@ function parseCandidate(raw, expectedField) {
|
|
|
4455
4767
|
return "";
|
|
4456
4768
|
}
|
|
4457
4769
|
function readProviderOutput(resultFile, fallback) {
|
|
4458
|
-
if (
|
|
4770
|
+
if (existsSync7(resultFile)) {
|
|
4459
4771
|
try {
|
|
4460
|
-
return
|
|
4772
|
+
return readFileSync6(resultFile, "utf8").trim();
|
|
4461
4773
|
} catch {
|
|
4462
4774
|
}
|
|
4463
4775
|
}
|
|
4464
4776
|
return fallback;
|
|
4465
4777
|
}
|
|
4466
4778
|
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
|
-
|
|
4779
|
+
const tempDir = mkdtempSync(join10(tmpdir(), "fifony-enhance-"));
|
|
4780
|
+
const promptFile = join10(tempDir, "fifony-enhance-prompt.md");
|
|
4781
|
+
const issuePayloadFile = join10(tempDir, "fifony-issue.json");
|
|
4782
|
+
const resultFile = join10(tempDir, "fifony-result.txt");
|
|
4783
|
+
const envFile = join10(tempDir, "fifony-enhance-env.sh");
|
|
4784
|
+
writeFileSync4(promptFile, `${prompt}
|
|
4473
4785
|
`, "utf8");
|
|
4474
|
-
|
|
4786
|
+
writeFileSync4(issuePayloadFile, JSON.stringify({ title, description, field }, null, 2), "utf8");
|
|
4475
4787
|
const envLines = [
|
|
4476
4788
|
`export FIFONY_ISSUE_TITLE=${JSON.stringify(title)}`,
|
|
4477
4789
|
`export FIFONY_ISSUE_DESCRIPTION=${JSON.stringify(description)}`,
|
|
@@ -4482,11 +4794,11 @@ async function runProviderCommand(command, provider, prompt, title, description,
|
|
|
4482
4794
|
"export FIFONY_AGENT_PROVIDER=" + JSON.stringify(provider),
|
|
4483
4795
|
"export FIFONY_RESULT_FILE=" + JSON.stringify(resultFile)
|
|
4484
4796
|
];
|
|
4485
|
-
const processEnv = Object.entries(
|
|
4797
|
+
const processEnv = Object.entries(env8).map(([key, value]) => {
|
|
4486
4798
|
if (typeof value !== "string") return `export ${key}=${JSON.stringify("")}`;
|
|
4487
4799
|
return `export ${key}=${JSON.stringify(value)}`;
|
|
4488
4800
|
}).join("\n");
|
|
4489
|
-
|
|
4801
|
+
writeFileSync4(envFile, `${processEnv}
|
|
4490
4802
|
${envLines.join("\n")}
|
|
4491
4803
|
`, "utf8");
|
|
4492
4804
|
const wrappedCommand = `. "${envFile}" && ${command}`;
|
|
@@ -4586,10 +4898,10 @@ async function enhanceIssueField(payload, config, workflowDefinition) {
|
|
|
4586
4898
|
throw new Error(`Could not enhance issue field. ${errors.join(" | ")}`);
|
|
4587
4899
|
}
|
|
4588
4900
|
|
|
4589
|
-
// src/
|
|
4590
|
-
import { mkdtempSync as mkdtempSync2, writeFileSync as
|
|
4901
|
+
// src/agent/issue-planner.ts
|
|
4902
|
+
import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync5, rmSync as rmSync3 } from "fs";
|
|
4591
4903
|
import { spawn as spawn4 } from "child_process";
|
|
4592
|
-
import { join as
|
|
4904
|
+
import { join as join11 } from "path";
|
|
4593
4905
|
import { tmpdir as tmpdir2 } from "os";
|
|
4594
4906
|
var PLANNING_SETTING_ID = "planning:active";
|
|
4595
4907
|
function emptySession() {
|
|
@@ -4772,6 +5084,26 @@ function parsePlanOutput(raw) {
|
|
|
4772
5084
|
} catch {
|
|
4773
5085
|
}
|
|
4774
5086
|
}
|
|
5087
|
+
const repaired = repairTruncatedJson(text);
|
|
5088
|
+
if (repaired) {
|
|
5089
|
+
try {
|
|
5090
|
+
const parsed = JSON.parse(repaired);
|
|
5091
|
+
const plan = tryBuildPlan(parsed);
|
|
5092
|
+
if (plan) {
|
|
5093
|
+
logger.warn("[Planner] Plan parsed from repaired truncated JSON output");
|
|
5094
|
+
return plan;
|
|
5095
|
+
}
|
|
5096
|
+
if (parsed?.structured_output && typeof parsed.structured_output === "object") {
|
|
5097
|
+
const innerPlan = tryBuildPlan(parsed.structured_output);
|
|
5098
|
+
if (innerPlan) {
|
|
5099
|
+
logger.warn("[Planner] Plan parsed from repaired truncated JSON envelope");
|
|
5100
|
+
return innerPlan;
|
|
5101
|
+
}
|
|
5102
|
+
}
|
|
5103
|
+
} catch {
|
|
5104
|
+
logger.debug("[Planner] JSON repair attempted but result still not parseable");
|
|
5105
|
+
}
|
|
5106
|
+
}
|
|
4775
5107
|
return null;
|
|
4776
5108
|
}
|
|
4777
5109
|
async function savePlanningInput(title, description) {
|
|
@@ -4839,6 +5171,7 @@ function extractPlanTokenUsage(raw) {
|
|
|
4839
5171
|
}
|
|
4840
5172
|
async function generatePlan(title, description, config, workflowDefinition, options) {
|
|
4841
5173
|
const fast = options?.fast ?? false;
|
|
5174
|
+
logger.info({ title: title.slice(0, 80), fast }, "[Planner] Starting plan generation");
|
|
4842
5175
|
const providers = detectAvailableProviders();
|
|
4843
5176
|
const available = providers.filter((p) => p.available).map((p) => p.name);
|
|
4844
5177
|
let planStageProvider;
|
|
@@ -4859,6 +5192,7 @@ async function generatePlan(title, description, config, workflowDefinition, opti
|
|
|
4859
5192
|
const effectiveEffort = fast ? "low" : planStageEffort || "medium";
|
|
4860
5193
|
const command = getPlanCommand(preferred, planStageModel);
|
|
4861
5194
|
if (!command) throw new Error(`No command configured for provider ${preferred}.`);
|
|
5195
|
+
logger.debug({ provider: preferred, model: planStageModel, effort: effectiveEffort, command: command.slice(0, 120) }, "[Planner] Provider selected for plan generation");
|
|
4862
5196
|
const planStartMs = Date.now();
|
|
4863
5197
|
const session = {
|
|
4864
5198
|
title,
|
|
@@ -4876,25 +5210,24 @@ async function generatePlan(title, description, config, workflowDefinition, opti
|
|
|
4876
5210
|
};
|
|
4877
5211
|
await persistSession(session);
|
|
4878
5212
|
const prompt = await buildPlanPrompt(title, description, fast);
|
|
4879
|
-
const tempDir = mkdtempSync2(
|
|
4880
|
-
const promptFile =
|
|
4881
|
-
|
|
4882
|
-
writeFileSync4(promptFile, `${prompt}
|
|
5213
|
+
const tempDir = mkdtempSync2(join11(tmpdir2(), "fifony-plan-"));
|
|
5214
|
+
const promptFile = join11(tempDir, "fifony-plan-prompt.md");
|
|
5215
|
+
writeFileSync5(promptFile, `${prompt}
|
|
4883
5216
|
`, "utf8");
|
|
4884
|
-
writeFileSync4(envFile, [
|
|
4885
|
-
`export FIFONY_PROMPT_FILE=${JSON.stringify(promptFile)}`,
|
|
4886
|
-
`export FIFONY_AGENT_PROVIDER=${JSON.stringify(preferred)}`
|
|
4887
|
-
].join("\n"), "utf8");
|
|
4888
|
-
const wrappedCommand = `. "${envFile}" && ${command}`;
|
|
4889
5217
|
let lastProgressPersist = 0;
|
|
4890
5218
|
const PROGRESS_INTERVAL_MS = 2e3;
|
|
4891
5219
|
const output = await new Promise((resolve4, reject) => {
|
|
4892
5220
|
let stdout2 = "";
|
|
4893
|
-
const child = spawn4(
|
|
5221
|
+
const child = spawn4(command, {
|
|
4894
5222
|
shell: true,
|
|
4895
5223
|
cwd: tempDir,
|
|
4896
5224
|
detached: true,
|
|
4897
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
5225
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
5226
|
+
env: {
|
|
5227
|
+
...process.env,
|
|
5228
|
+
FIFONY_PROMPT_FILE: promptFile,
|
|
5229
|
+
FIFONY_AGENT_PROVIDER: preferred
|
|
5230
|
+
}
|
|
4898
5231
|
});
|
|
4899
5232
|
child.unref();
|
|
4900
5233
|
child.stdin?.end();
|
|
@@ -4943,13 +5276,17 @@ async function generatePlan(title, description, config, workflowDefinition, opti
|
|
|
4943
5276
|
});
|
|
4944
5277
|
});
|
|
4945
5278
|
logger.info({ rawOutput: output.slice(0, 2e3) }, `Plan raw output from ${preferred}`);
|
|
5279
|
+
logger.debug({ outputLength: output.length }, "[Planner] Plan command completed, parsing output");
|
|
4946
5280
|
const plan = parsePlanOutput(output);
|
|
4947
5281
|
if (!plan) {
|
|
5282
|
+
const firstBrace = output.indexOf("{");
|
|
5283
|
+
const lastBrace = output.lastIndexOf("}");
|
|
5284
|
+
const truncationHint = firstBrace >= 0 && lastBrace < firstBrace ? " (JSON appears truncated \u2014 opening brace found but no matching close)" : firstBrace < 0 ? " (no JSON object found in output)" : "";
|
|
4948
5285
|
session.status = "error";
|
|
4949
|
-
session.error = `Could not parse plan. Output: ${output.slice(
|
|
5286
|
+
session.error = `Could not parse plan${truncationHint}. Output length: ${output.length} chars. Tail: ${output.slice(-200)}`;
|
|
4950
5287
|
session.pid = null;
|
|
4951
5288
|
await persistSession(session);
|
|
4952
|
-
logger.error({ rawOutput: output.slice(0, 2e3) }, "Could not parse plan from AI output");
|
|
5289
|
+
logger.error({ rawOutput: output.slice(0, 2e3), outputLength: output.length, firstBrace, lastBrace }, "[Planner] Could not parse plan from AI output");
|
|
4953
5290
|
throw new Error(session.error);
|
|
4954
5291
|
}
|
|
4955
5292
|
plan.provider = planStageModel ? `${preferred}/${planStageModel}` : preferred;
|
|
@@ -5013,23 +5350,22 @@ async function refinePlan(issue, feedback, config, workflowDefinition) {
|
|
|
5013
5350
|
if (!command) throw new Error(`No command configured for provider ${preferred}.`);
|
|
5014
5351
|
const refineStartMs = Date.now();
|
|
5015
5352
|
const prompt = await buildRefinePrompt(issue.title, issue.description, issue.plan, feedback);
|
|
5016
|
-
const tempDir = mkdtempSync2(
|
|
5017
|
-
const promptFile =
|
|
5018
|
-
|
|
5019
|
-
writeFileSync4(promptFile, `${prompt}
|
|
5353
|
+
const tempDir = mkdtempSync2(join11(tmpdir2(), "fifony-refine-"));
|
|
5354
|
+
const promptFile = join11(tempDir, "fifony-refine-prompt.md");
|
|
5355
|
+
writeFileSync5(promptFile, `${prompt}
|
|
5020
5356
|
`, "utf8");
|
|
5021
|
-
writeFileSync4(envFile, [
|
|
5022
|
-
`export FIFONY_PROMPT_FILE=${JSON.stringify(promptFile)}`,
|
|
5023
|
-
`export FIFONY_AGENT_PROVIDER=${JSON.stringify(preferred)}`
|
|
5024
|
-
].join("\n"), "utf8");
|
|
5025
|
-
const wrappedCommand = `. "${envFile}" && ${command}`;
|
|
5026
5357
|
const output = await new Promise((resolve4, reject) => {
|
|
5027
5358
|
let stdout2 = "";
|
|
5028
|
-
const child = spawn4(
|
|
5359
|
+
const child = spawn4(command, {
|
|
5029
5360
|
shell: true,
|
|
5030
5361
|
cwd: tempDir,
|
|
5031
5362
|
detached: true,
|
|
5032
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
5363
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
5364
|
+
env: {
|
|
5365
|
+
...process.env,
|
|
5366
|
+
FIFONY_PROMPT_FILE: promptFile,
|
|
5367
|
+
FIFONY_AGENT_PROVIDER: preferred
|
|
5368
|
+
}
|
|
5033
5369
|
});
|
|
5034
5370
|
child.unref();
|
|
5035
5371
|
child.stdin?.end();
|
|
@@ -5067,8 +5403,11 @@ async function refinePlan(issue, feedback, config, workflowDefinition) {
|
|
|
5067
5403
|
logger.info({ rawOutput: output.slice(0, 2e3) }, `Refine raw output from ${preferred}`);
|
|
5068
5404
|
const plan = parsePlanOutput(output);
|
|
5069
5405
|
if (!plan) {
|
|
5070
|
-
|
|
5071
|
-
|
|
5406
|
+
const firstBrace = output.indexOf("{");
|
|
5407
|
+
const lastBrace = output.lastIndexOf("}");
|
|
5408
|
+
const truncationHint = firstBrace >= 0 && lastBrace < firstBrace ? " (JSON appears truncated \u2014 opening brace found but no matching close)" : firstBrace < 0 ? " (no JSON object found in output)" : "";
|
|
5409
|
+
logger.error({ rawOutput: output.slice(0, 2e3), outputLength: output.length, firstBrace, lastBrace }, "Could not parse refined plan from AI output");
|
|
5410
|
+
throw new Error(`Could not parse refined plan${truncationHint}. Output length: ${output.length} chars. Tail: ${output.slice(-200)}`);
|
|
5072
5411
|
}
|
|
5073
5412
|
plan.provider = planStageModel ? `${preferred}/${planStageModel}` : preferred;
|
|
5074
5413
|
const existingRefinements = issue.plan.refinements ?? [];
|
|
@@ -5105,11 +5444,14 @@ function generatePlanInBackground(issue, config, workflowDefinition, callbacks,
|
|
|
5105
5444
|
const { addEvent: addEvent2, persistState: persistState2, applyUsage, applySuggestions } = callbacks;
|
|
5106
5445
|
const fast = options?.fast ?? false;
|
|
5107
5446
|
issue.planningStatus = "planning";
|
|
5447
|
+
issue.planningStartedAt = now();
|
|
5108
5448
|
issue.planningError = void 0;
|
|
5109
5449
|
issue.updatedAt = now();
|
|
5450
|
+
addEvent2(issue.id, "info", `${fast ? "Fast plan" : "Plan"} generation starting for ${issue.identifier} (provider detection in progress).`);
|
|
5110
5451
|
generatePlan(issue.title, issue.description, config, workflowDefinition, { fast }).then(async ({ plan, usage: usage2 }) => {
|
|
5111
5452
|
issue.plan = plan;
|
|
5112
5453
|
issue.planningStatus = "idle";
|
|
5454
|
+
issue.planningStartedAt = void 0;
|
|
5113
5455
|
issue.planningError = void 0;
|
|
5114
5456
|
issue.updatedAt = now();
|
|
5115
5457
|
applyUsage(issue, usage2);
|
|
@@ -5121,6 +5463,7 @@ function generatePlanInBackground(issue, config, workflowDefinition, callbacks,
|
|
|
5121
5463
|
await persistState2();
|
|
5122
5464
|
}).catch(async (err) => {
|
|
5123
5465
|
issue.planningStatus = "idle";
|
|
5466
|
+
issue.planningStartedAt = void 0;
|
|
5124
5467
|
issue.planningError = err instanceof Error ? err.message : String(err);
|
|
5125
5468
|
issue.updatedAt = now();
|
|
5126
5469
|
addEvent2(issue.id, "error", `Plan generation failed for ${issue.identifier}: ${issue.planningError}`);
|
|
@@ -5131,11 +5474,15 @@ function generatePlanInBackground(issue, config, workflowDefinition, callbacks,
|
|
|
5131
5474
|
function refinePlanInBackground(issue, feedback, config, workflowDefinition, callbacks) {
|
|
5132
5475
|
const { addEvent: addEvent2, persistState: persistState2, applyUsage, applySuggestions } = callbacks;
|
|
5133
5476
|
issue.planningStatus = "refining";
|
|
5477
|
+
issue.planningStartedAt = now();
|
|
5134
5478
|
issue.planningError = void 0;
|
|
5135
5479
|
issue.updatedAt = now();
|
|
5480
|
+
const feedbackSnippet = feedback.length > 60 ? `${feedback.slice(0, 57)}...` : feedback;
|
|
5481
|
+
addEvent2(issue.id, "info", `Plan refinement starting for ${issue.identifier}: "${feedbackSnippet}".`);
|
|
5136
5482
|
refinePlan(issue, feedback, config, workflowDefinition).then(async ({ plan, usage: usage2 }) => {
|
|
5137
5483
|
issue.plan = plan;
|
|
5138
5484
|
issue.planningStatus = "idle";
|
|
5485
|
+
issue.planningStartedAt = void 0;
|
|
5139
5486
|
issue.planningError = void 0;
|
|
5140
5487
|
issue.updatedAt = now();
|
|
5141
5488
|
applyUsage(issue, usage2);
|
|
@@ -5148,6 +5495,7 @@ function refinePlanInBackground(issue, feedback, config, workflowDefinition, cal
|
|
|
5148
5495
|
await persistState2();
|
|
5149
5496
|
}).catch(async (err) => {
|
|
5150
5497
|
issue.planningStatus = "idle";
|
|
5498
|
+
issue.planningStartedAt = void 0;
|
|
5151
5499
|
issue.planningError = err instanceof Error ? err.message : String(err);
|
|
5152
5500
|
issue.updatedAt = now();
|
|
5153
5501
|
addEvent2(issue.id, "error", `Plan refinement failed for ${issue.identifier}: ${issue.planningError}`);
|
|
@@ -5156,21 +5504,22 @@ function refinePlanInBackground(issue, feedback, config, workflowDefinition, cal
|
|
|
5156
5504
|
});
|
|
5157
5505
|
}
|
|
5158
5506
|
|
|
5159
|
-
// src/
|
|
5507
|
+
// src/agent/project-scanner.ts
|
|
5160
5508
|
import {
|
|
5161
|
-
existsSync as
|
|
5509
|
+
existsSync as existsSync8,
|
|
5162
5510
|
mkdtempSync as mkdtempSync3,
|
|
5163
|
-
readdirSync as
|
|
5164
|
-
readFileSync as
|
|
5511
|
+
readdirSync as readdirSync5,
|
|
5512
|
+
readFileSync as readFileSync7,
|
|
5165
5513
|
rmSync as rmSync4,
|
|
5166
|
-
writeFileSync as
|
|
5514
|
+
writeFileSync as writeFileSync6
|
|
5167
5515
|
} from "fs";
|
|
5168
|
-
import { join as
|
|
5516
|
+
import { join as join12, basename as basename2 } from "path";
|
|
5169
5517
|
import { spawn as spawn5 } from "child_process";
|
|
5170
5518
|
import { tmpdir as tmpdir3 } from "os";
|
|
5171
|
-
import { env as
|
|
5519
|
+
import { env as env9 } from "process";
|
|
5520
|
+
import { createHash } from "crypto";
|
|
5172
5521
|
function scanProjectFiles(targetRoot) {
|
|
5173
|
-
const check = (rel) =>
|
|
5522
|
+
const check = (rel) => existsSync8(join12(targetRoot, rel));
|
|
5174
5523
|
const files = {
|
|
5175
5524
|
claudeMd: check("CLAUDE.md"),
|
|
5176
5525
|
claudeDir: check(".claude"),
|
|
@@ -5192,10 +5541,10 @@ function scanProjectFiles(targetRoot) {
|
|
|
5192
5541
|
};
|
|
5193
5542
|
const existingAgents = [];
|
|
5194
5543
|
for (const agentDir of [".claude/agents", ".codex/agents"]) {
|
|
5195
|
-
const fullPath =
|
|
5196
|
-
if (!
|
|
5544
|
+
const fullPath = join12(targetRoot, agentDir);
|
|
5545
|
+
if (!existsSync8(fullPath)) continue;
|
|
5197
5546
|
try {
|
|
5198
|
-
const entries =
|
|
5547
|
+
const entries = readdirSync5(fullPath);
|
|
5199
5548
|
for (const entry of entries) {
|
|
5200
5549
|
if (entry.endsWith(".md")) {
|
|
5201
5550
|
existingAgents.push(basename2(entry, ".md"));
|
|
@@ -5206,13 +5555,13 @@ function scanProjectFiles(targetRoot) {
|
|
|
5206
5555
|
}
|
|
5207
5556
|
const existingSkills = [];
|
|
5208
5557
|
for (const skillDir of [".claude/skills", ".codex/skills"]) {
|
|
5209
|
-
const fullPath =
|
|
5210
|
-
if (!
|
|
5558
|
+
const fullPath = join12(targetRoot, skillDir);
|
|
5559
|
+
if (!existsSync8(fullPath)) continue;
|
|
5211
5560
|
try {
|
|
5212
|
-
const entries =
|
|
5561
|
+
const entries = readdirSync5(fullPath);
|
|
5213
5562
|
for (const entry of entries) {
|
|
5214
|
-
const skillFile =
|
|
5215
|
-
if (
|
|
5563
|
+
const skillFile = join12(fullPath, entry, "SKILL.md");
|
|
5564
|
+
if (existsSync8(skillFile)) {
|
|
5216
5565
|
existingSkills.push(entry);
|
|
5217
5566
|
}
|
|
5218
5567
|
}
|
|
@@ -5220,20 +5569,20 @@ function scanProjectFiles(targetRoot) {
|
|
|
5220
5569
|
}
|
|
5221
5570
|
}
|
|
5222
5571
|
let readmeExcerpt = "";
|
|
5223
|
-
const readmePath =
|
|
5224
|
-
if (
|
|
5572
|
+
const readmePath = join12(targetRoot, "README.md");
|
|
5573
|
+
if (existsSync8(readmePath)) {
|
|
5225
5574
|
try {
|
|
5226
|
-
const content =
|
|
5575
|
+
const content = readFileSync7(readmePath, "utf8");
|
|
5227
5576
|
readmeExcerpt = content.slice(0, 200).trim();
|
|
5228
5577
|
} catch {
|
|
5229
5578
|
}
|
|
5230
5579
|
}
|
|
5231
5580
|
let packageName = "";
|
|
5232
5581
|
let packageDescription = "";
|
|
5233
|
-
const pkgPath =
|
|
5234
|
-
if (
|
|
5582
|
+
const pkgPath = join12(targetRoot, "package.json");
|
|
5583
|
+
if (existsSync8(pkgPath)) {
|
|
5235
5584
|
try {
|
|
5236
|
-
const pkg = JSON.parse(
|
|
5585
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
|
|
5237
5586
|
packageName = typeof pkg.name === "string" ? pkg.name : "";
|
|
5238
5587
|
packageDescription = typeof pkg.description === "string" ? pkg.description : "";
|
|
5239
5588
|
} catch {
|
|
@@ -5275,39 +5624,39 @@ function buildFallbackAnalysis(targetRoot) {
|
|
|
5275
5624
|
let description = "";
|
|
5276
5625
|
let readmeExcerpt = "";
|
|
5277
5626
|
for (const readmeFile of ["README.md", "README.rst", "README.txt", "README"]) {
|
|
5278
|
-
const p =
|
|
5279
|
-
if (
|
|
5627
|
+
const p = join12(targetRoot, readmeFile);
|
|
5628
|
+
if (existsSync8(p)) {
|
|
5280
5629
|
try {
|
|
5281
|
-
readmeExcerpt =
|
|
5630
|
+
readmeExcerpt = readFileSync7(p, "utf8").slice(0, 300).trim();
|
|
5282
5631
|
break;
|
|
5283
5632
|
} catch {
|
|
5284
5633
|
}
|
|
5285
5634
|
}
|
|
5286
5635
|
}
|
|
5287
|
-
const pkgPath =
|
|
5288
|
-
if (
|
|
5636
|
+
const pkgPath = join12(targetRoot, "package.json");
|
|
5637
|
+
if (existsSync8(pkgPath)) {
|
|
5289
5638
|
try {
|
|
5290
|
-
const pkg = JSON.parse(
|
|
5639
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
|
|
5291
5640
|
const name = typeof pkg.name === "string" ? pkg.name : "";
|
|
5292
5641
|
const desc = typeof pkg.description === "string" ? pkg.description : "";
|
|
5293
5642
|
if (desc) description = name ? `${name}: ${desc}` : desc;
|
|
5294
5643
|
} catch {
|
|
5295
5644
|
}
|
|
5296
5645
|
}
|
|
5297
|
-
const cargoPath =
|
|
5298
|
-
if (!description &&
|
|
5646
|
+
const cargoPath = join12(targetRoot, "Cargo.toml");
|
|
5647
|
+
if (!description && existsSync8(cargoPath)) {
|
|
5299
5648
|
try {
|
|
5300
|
-
const content =
|
|
5649
|
+
const content = readFileSync7(cargoPath, "utf8");
|
|
5301
5650
|
const descMatch = content.match(/^description\s*=\s*"([^"]+)"/m);
|
|
5302
5651
|
const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m);
|
|
5303
5652
|
if (descMatch) description = nameMatch ? `${nameMatch[1]}: ${descMatch[1]}` : descMatch[1];
|
|
5304
5653
|
} catch {
|
|
5305
5654
|
}
|
|
5306
5655
|
}
|
|
5307
|
-
const pyprojectPath =
|
|
5308
|
-
if (!description &&
|
|
5656
|
+
const pyprojectPath = join12(targetRoot, "pyproject.toml");
|
|
5657
|
+
if (!description && existsSync8(pyprojectPath)) {
|
|
5309
5658
|
try {
|
|
5310
|
-
const content =
|
|
5659
|
+
const content = readFileSync7(pyprojectPath, "utf8");
|
|
5311
5660
|
const descMatch = content.match(/^description\s*=\s*"([^"]+)"/m);
|
|
5312
5661
|
const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m);
|
|
5313
5662
|
if (descMatch) description = nameMatch ? `${nameMatch[1]}: ${descMatch[1]}` : descMatch[1];
|
|
@@ -5320,7 +5669,7 @@ function buildFallbackAnalysis(targetRoot) {
|
|
|
5320
5669
|
let language = "unknown";
|
|
5321
5670
|
const stack = [];
|
|
5322
5671
|
for (const [file, signal] of Object.entries(BUILD_FILE_SIGNALS)) {
|
|
5323
|
-
if (
|
|
5672
|
+
if (existsSync8(join12(targetRoot, file))) {
|
|
5324
5673
|
if (language === "unknown" && signal.language !== "unknown") {
|
|
5325
5674
|
language = signal.language;
|
|
5326
5675
|
}
|
|
@@ -5388,7 +5737,51 @@ function validateAnalysis(parsed) {
|
|
|
5388
5737
|
source: "cli"
|
|
5389
5738
|
};
|
|
5390
5739
|
}
|
|
5391
|
-
|
|
5740
|
+
var ANALYSIS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
5741
|
+
function computeProjectHash(targetRoot) {
|
|
5742
|
+
const buildFiles = Object.keys(BUILD_FILE_SIGNALS);
|
|
5743
|
+
const found = buildFiles.filter((f) => existsSync8(join12(targetRoot, f))).sort();
|
|
5744
|
+
return createHash("sha256").update(found.join(",")).digest("hex").slice(0, 16);
|
|
5745
|
+
}
|
|
5746
|
+
async function loadCachedAnalysis(targetRoot) {
|
|
5747
|
+
const resource = getSettingStateResource();
|
|
5748
|
+
if (!resource) return null;
|
|
5749
|
+
const hash = computeProjectHash(targetRoot);
|
|
5750
|
+
const key = `project-analysis:${hash}`;
|
|
5751
|
+
try {
|
|
5752
|
+
const record2 = await resource.get(key);
|
|
5753
|
+
if (!record2?.value) return null;
|
|
5754
|
+
const cached = record2.value;
|
|
5755
|
+
if (!cached.analysis || !cached.updatedAt) return null;
|
|
5756
|
+
if (Date.now() - Date.parse(cached.updatedAt) > ANALYSIS_CACHE_TTL_MS) return null;
|
|
5757
|
+
return cached.analysis;
|
|
5758
|
+
} catch {
|
|
5759
|
+
return null;
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5762
|
+
async function saveCachedAnalysis(targetRoot, analysis) {
|
|
5763
|
+
const resource = getSettingStateResource();
|
|
5764
|
+
if (!resource) return;
|
|
5765
|
+
const hash = computeProjectHash(targetRoot);
|
|
5766
|
+
const key = `project-analysis:${hash}`;
|
|
5767
|
+
try {
|
|
5768
|
+
await resource.replace(key, {
|
|
5769
|
+
id: key,
|
|
5770
|
+
scope: "system",
|
|
5771
|
+
source: "detected",
|
|
5772
|
+
value: { analysis, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
5773
|
+
});
|
|
5774
|
+
} catch {
|
|
5775
|
+
}
|
|
5776
|
+
}
|
|
5777
|
+
async function analyzeProjectWithCli(provider, targetRoot, options) {
|
|
5778
|
+
if (!options?.forceRefresh) {
|
|
5779
|
+
const cached = await loadCachedAnalysis(targetRoot);
|
|
5780
|
+
if (cached) {
|
|
5781
|
+
logger.info("Using cached project analysis.");
|
|
5782
|
+
return cached;
|
|
5783
|
+
}
|
|
5784
|
+
}
|
|
5392
5785
|
const normalizedProvider = provider.trim().toLowerCase();
|
|
5393
5786
|
const providers = detectAvailableProviders();
|
|
5394
5787
|
const providerInfo = providers.find((p) => p.name === normalizedProvider && p.available);
|
|
@@ -5399,12 +5792,12 @@ async function analyzeProjectWithCli(provider, targetRoot) {
|
|
|
5399
5792
|
);
|
|
5400
5793
|
return buildFallbackAnalysis(targetRoot);
|
|
5401
5794
|
}
|
|
5402
|
-
const tempDir = mkdtempSync3(
|
|
5403
|
-
const promptFile =
|
|
5795
|
+
const tempDir = mkdtempSync3(join12(tmpdir3(), "fifony-scan-"));
|
|
5796
|
+
const promptFile = join12(tempDir, "fifony-scan-prompt.txt");
|
|
5404
5797
|
const analysisPrompt = await renderPrompt("project-analysis");
|
|
5405
|
-
|
|
5798
|
+
writeFileSync6(promptFile, analysisPrompt, "utf8");
|
|
5406
5799
|
const processEnv = {};
|
|
5407
|
-
for (const [key, value] of Object.entries(
|
|
5800
|
+
for (const [key, value] of Object.entries(env9)) {
|
|
5408
5801
|
if (typeof value === "string") processEnv[key] = value;
|
|
5409
5802
|
}
|
|
5410
5803
|
processEnv.FIFONY_PROMPT_FILE = promptFile;
|
|
@@ -5473,6 +5866,7 @@ async function analyzeProjectWithCli(provider, targetRoot) {
|
|
|
5473
5866
|
{ provider: normalizedProvider, domains: analysis.domains, stack: analysis.stack },
|
|
5474
5867
|
"CLI project analysis completed"
|
|
5475
5868
|
);
|
|
5869
|
+
await saveCachedAnalysis(targetRoot, analysis);
|
|
5476
5870
|
return analysis;
|
|
5477
5871
|
}
|
|
5478
5872
|
logger.warn(
|
|
@@ -5494,27 +5888,180 @@ async function analyzeProjectWithCli(provider, targetRoot) {
|
|
|
5494
5888
|
}
|
|
5495
5889
|
}
|
|
5496
5890
|
|
|
5497
|
-
// src/
|
|
5498
|
-
import {
|
|
5499
|
-
|
|
5891
|
+
// src/agent/issue-scanner.ts
|
|
5892
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
5893
|
+
var SCAN_PATTERN = /\b(TODO|FIXME|HACK|XXX)\b[:\s]*(.*)/i;
|
|
5894
|
+
var EXCLUDE_DIRS = [
|
|
5895
|
+
"node_modules",
|
|
5896
|
+
".git",
|
|
5897
|
+
".fifony",
|
|
5898
|
+
"dist",
|
|
5899
|
+
"build",
|
|
5900
|
+
".turbo",
|
|
5901
|
+
".next",
|
|
5902
|
+
".nuxt",
|
|
5903
|
+
"coverage",
|
|
5904
|
+
".venv",
|
|
5905
|
+
"vendor",
|
|
5906
|
+
"tmp",
|
|
5907
|
+
"temp",
|
|
5908
|
+
"artifacts"
|
|
5909
|
+
];
|
|
5910
|
+
function scanForTodos(targetRoot) {
|
|
5911
|
+
const excludeArgs = EXCLUDE_DIRS.flatMap((dir) => ["--exclude-dir", dir]);
|
|
5912
|
+
let output;
|
|
5913
|
+
try {
|
|
5914
|
+
output = execFileSync2("grep", [
|
|
5915
|
+
"-rn",
|
|
5916
|
+
"-E",
|
|
5917
|
+
"\\b(TODO|FIXME|HACK|XXX)\\b",
|
|
5918
|
+
...excludeArgs,
|
|
5919
|
+
"--include=*.ts",
|
|
5920
|
+
"--include=*.tsx",
|
|
5921
|
+
"--include=*.js",
|
|
5922
|
+
"--include=*.jsx",
|
|
5923
|
+
"--include=*.py",
|
|
5924
|
+
"--include=*.rs",
|
|
5925
|
+
"--include=*.go",
|
|
5926
|
+
"--include=*.java",
|
|
5927
|
+
"--include=*.rb",
|
|
5928
|
+
"--include=*.php",
|
|
5929
|
+
"--include=*.cs",
|
|
5930
|
+
"--include=*.swift",
|
|
5931
|
+
"--include=*.kt",
|
|
5932
|
+
"--include=*.vue",
|
|
5933
|
+
"--include=*.svelte",
|
|
5934
|
+
targetRoot
|
|
5935
|
+
], {
|
|
5936
|
+
encoding: "utf8",
|
|
5937
|
+
timeout: 15e3,
|
|
5938
|
+
maxBuffer: 5e6
|
|
5939
|
+
});
|
|
5940
|
+
} catch (error) {
|
|
5941
|
+
if (error.status === 1) return [];
|
|
5942
|
+
if (error.stdout) output = error.stdout;
|
|
5943
|
+
else {
|
|
5944
|
+
logger.warn(`TODO scan failed: ${String(error)}`);
|
|
5945
|
+
return [];
|
|
5946
|
+
}
|
|
5947
|
+
}
|
|
5948
|
+
const results = [];
|
|
5949
|
+
const lines = output.split("\n").filter(Boolean);
|
|
5950
|
+
for (const line of lines) {
|
|
5951
|
+
const match = line.match(/^(.+?):(\d+):(.+)$/);
|
|
5952
|
+
if (!match) continue;
|
|
5953
|
+
const [, file, lineNo, content] = match;
|
|
5954
|
+
const todoMatch = content.match(SCAN_PATTERN);
|
|
5955
|
+
if (!todoMatch) continue;
|
|
5956
|
+
const [, tag, text] = todoMatch;
|
|
5957
|
+
const source = tag.toLowerCase();
|
|
5958
|
+
const trimmedText = text.trim();
|
|
5959
|
+
if (!trimmedText || trimmedText.length < 5) continue;
|
|
5960
|
+
const relativePath = file.startsWith(targetRoot) ? file.slice(targetRoot.length + 1) : file;
|
|
5961
|
+
results.push({
|
|
5962
|
+
source: source === "xxx" ? "hack" : source,
|
|
5963
|
+
title: trimmedText.length > 120 ? `${trimmedText.slice(0, 117)}...` : trimmedText,
|
|
5964
|
+
file: relativePath,
|
|
5965
|
+
line: parseInt(lineNo, 10),
|
|
5966
|
+
context: content.trim()
|
|
5967
|
+
});
|
|
5968
|
+
}
|
|
5969
|
+
return results;
|
|
5970
|
+
}
|
|
5971
|
+
function categorizeScannedIssues(issues, workflowDefinition) {
|
|
5972
|
+
const options = getCapabilityRoutingOptions(workflowDefinition);
|
|
5973
|
+
return issues.map((issue) => {
|
|
5974
|
+
const resolution = resolveTaskCapabilities({
|
|
5975
|
+
id: `scan-${issue.file}:${issue.line}`,
|
|
5976
|
+
identifier: `${issue.source}:${issue.file}:${issue.line}`,
|
|
5977
|
+
title: issue.title,
|
|
5978
|
+
description: issue.context,
|
|
5979
|
+
labels: [issue.source],
|
|
5980
|
+
paths: [issue.file]
|
|
5981
|
+
}, options);
|
|
5982
|
+
return {
|
|
5983
|
+
...issue,
|
|
5984
|
+
category: resolution.category,
|
|
5985
|
+
overlays: resolution.overlays,
|
|
5986
|
+
rationale: resolution.rationale,
|
|
5987
|
+
suggestedLabels: [
|
|
5988
|
+
issue.source,
|
|
5989
|
+
resolution.category ? `capability:${resolution.category}` : ""
|
|
5990
|
+
].filter(Boolean),
|
|
5991
|
+
suggestedPaths: [issue.file]
|
|
5992
|
+
};
|
|
5993
|
+
});
|
|
5994
|
+
}
|
|
5995
|
+
|
|
5996
|
+
// src/agent/github-sync.ts
|
|
5997
|
+
import { execFile } from "child_process";
|
|
5998
|
+
async function fetchGitHubIssues(targetRoot) {
|
|
5999
|
+
return new Promise((resolve4) => {
|
|
6000
|
+
execFile(
|
|
6001
|
+
"gh",
|
|
6002
|
+
[
|
|
6003
|
+
"issue",
|
|
6004
|
+
"list",
|
|
6005
|
+
"--json",
|
|
6006
|
+
"number,title,body,labels,state,url",
|
|
6007
|
+
"--state",
|
|
6008
|
+
"open",
|
|
6009
|
+
"--limit",
|
|
6010
|
+
"50"
|
|
6011
|
+
],
|
|
6012
|
+
{
|
|
6013
|
+
cwd: targetRoot,
|
|
6014
|
+
timeout: 15e3,
|
|
6015
|
+
maxBuffer: 2e6
|
|
6016
|
+
},
|
|
6017
|
+
(error, stdout2) => {
|
|
6018
|
+
if (error) {
|
|
6019
|
+
logger.warn(`Failed to fetch GitHub issues: ${String(error)}`);
|
|
6020
|
+
resolve4([]);
|
|
6021
|
+
return;
|
|
6022
|
+
}
|
|
6023
|
+
try {
|
|
6024
|
+
const issues = JSON.parse(stdout2.trim());
|
|
6025
|
+
const results = issues.map((issue) => ({
|
|
6026
|
+
source: "github",
|
|
6027
|
+
title: issue.title,
|
|
6028
|
+
file: "",
|
|
6029
|
+
line: 0,
|
|
6030
|
+
context: (issue.body || "").slice(0, 500),
|
|
6031
|
+
suggestedLabels: issue.labels.map((l) => l.name),
|
|
6032
|
+
suggestedPaths: []
|
|
6033
|
+
}));
|
|
6034
|
+
resolve4(results);
|
|
6035
|
+
} catch (parseError) {
|
|
6036
|
+
logger.warn(`Failed to parse GitHub issues: ${String(parseError)}`);
|
|
6037
|
+
resolve4([]);
|
|
6038
|
+
}
|
|
6039
|
+
}
|
|
6040
|
+
);
|
|
6041
|
+
});
|
|
6042
|
+
}
|
|
6043
|
+
|
|
6044
|
+
// src/agent/catalog.ts
|
|
6045
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
6046
|
+
import { join as join13, dirname as dirname2 } from "path";
|
|
5500
6047
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5501
6048
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
5502
6049
|
var __dirname2 = dirname2(__filename2);
|
|
5503
6050
|
function resolveFixturePath(filename) {
|
|
5504
6051
|
const candidates = [
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
6052
|
+
join13(__dirname2, "..", "fixtures", filename),
|
|
6053
|
+
join13(__dirname2, "../..", "src", "fixtures", filename),
|
|
6054
|
+
join13(__dirname2, "../../..", "src", "fixtures", filename)
|
|
5508
6055
|
];
|
|
5509
6056
|
for (const candidate of candidates) {
|
|
5510
|
-
if (
|
|
6057
|
+
if (existsSync9(candidate)) return candidate;
|
|
5511
6058
|
}
|
|
5512
6059
|
return candidates[0];
|
|
5513
6060
|
}
|
|
5514
6061
|
function loadAgentCatalog() {
|
|
5515
6062
|
try {
|
|
5516
6063
|
const filePath = resolveFixturePath("agent-catalog.json");
|
|
5517
|
-
const raw =
|
|
6064
|
+
const raw = readFileSync8(filePath, "utf8");
|
|
5518
6065
|
return JSON.parse(raw);
|
|
5519
6066
|
} catch (error) {
|
|
5520
6067
|
logger.error({ err: error }, "Failed to load agent catalog");
|
|
@@ -5524,7 +6071,7 @@ function loadAgentCatalog() {
|
|
|
5524
6071
|
function loadSkillCatalog() {
|
|
5525
6072
|
try {
|
|
5526
6073
|
const filePath = resolveFixturePath("skill-catalog.json");
|
|
5527
|
-
const raw =
|
|
6074
|
+
const raw = readFileSync8(filePath, "utf8");
|
|
5528
6075
|
return JSON.parse(raw);
|
|
5529
6076
|
} catch (error) {
|
|
5530
6077
|
logger.error({ err: error }, "Failed to load skill catalog");
|
|
@@ -5543,9 +6090,9 @@ function filterByDomains(catalog, domains) {
|
|
|
5543
6090
|
function installAgents(targetRoot, agentNames, catalog) {
|
|
5544
6091
|
const result = { installed: [], skipped: [], errors: [] };
|
|
5545
6092
|
const catalogMap = new Map(catalog.map((entry) => [entry.name, entry]));
|
|
5546
|
-
const agentsDir =
|
|
6093
|
+
const agentsDir = join13(targetRoot, ".claude", "agents");
|
|
5547
6094
|
try {
|
|
5548
|
-
|
|
6095
|
+
mkdirSync3(agentsDir, { recursive: true });
|
|
5549
6096
|
} catch (error) {
|
|
5550
6097
|
logger.error({ err: error, path: agentsDir }, "Failed to create agents directory");
|
|
5551
6098
|
result.errors.push({ name: "_directory", error: `Failed to create ${agentsDir}` });
|
|
@@ -5557,13 +6104,13 @@ function installAgents(targetRoot, agentNames, catalog) {
|
|
|
5557
6104
|
result.errors.push({ name, error: "Agent not found in catalog" });
|
|
5558
6105
|
continue;
|
|
5559
6106
|
}
|
|
5560
|
-
const filePath =
|
|
5561
|
-
if (
|
|
6107
|
+
const filePath = join13(agentsDir, `${name}.md`);
|
|
6108
|
+
if (existsSync9(filePath)) {
|
|
5562
6109
|
result.skipped.push(name);
|
|
5563
6110
|
continue;
|
|
5564
6111
|
}
|
|
5565
6112
|
try {
|
|
5566
|
-
|
|
6113
|
+
writeFileSync7(filePath, entry.content, "utf8");
|
|
5567
6114
|
result.installed.push(name);
|
|
5568
6115
|
logger.info({ agent: name, path: filePath }, "Agent installed");
|
|
5569
6116
|
} catch (error) {
|
|
@@ -5578,9 +6125,9 @@ function installAgents(targetRoot, agentNames, catalog) {
|
|
|
5578
6125
|
function installSkills(targetRoot, skillNames, catalog) {
|
|
5579
6126
|
const result = { installed: [], skipped: [], errors: [] };
|
|
5580
6127
|
const catalogMap = new Map(catalog.map((entry) => [entry.name, entry]));
|
|
5581
|
-
const skillsDir =
|
|
6128
|
+
const skillsDir = join13(targetRoot, ".claude", "skills");
|
|
5582
6129
|
try {
|
|
5583
|
-
|
|
6130
|
+
mkdirSync3(skillsDir, { recursive: true });
|
|
5584
6131
|
} catch (error) {
|
|
5585
6132
|
logger.error({ err: error, path: skillsDir }, "Failed to create skills directory");
|
|
5586
6133
|
result.errors.push({ name: "_directory", error: `Failed to create ${skillsDir}` });
|
|
@@ -5592,16 +6139,16 @@ function installSkills(targetRoot, skillNames, catalog) {
|
|
|
5592
6139
|
result.errors.push({ name, error: "Skill not found in catalog" });
|
|
5593
6140
|
continue;
|
|
5594
6141
|
}
|
|
5595
|
-
const skillDir =
|
|
5596
|
-
const filePath =
|
|
5597
|
-
if (
|
|
6142
|
+
const skillDir = join13(skillsDir, name);
|
|
6143
|
+
const filePath = join13(skillDir, "SKILL.md");
|
|
6144
|
+
if (existsSync9(filePath)) {
|
|
5598
6145
|
result.skipped.push(name);
|
|
5599
6146
|
continue;
|
|
5600
6147
|
}
|
|
5601
6148
|
try {
|
|
5602
|
-
|
|
6149
|
+
mkdirSync3(skillDir, { recursive: true });
|
|
5603
6150
|
if (entry.installType === "bundled" && entry.content) {
|
|
5604
|
-
|
|
6151
|
+
writeFileSync7(filePath, entry.content, "utf8");
|
|
5605
6152
|
} else {
|
|
5606
6153
|
const referenceContent = [
|
|
5607
6154
|
`# ${entry.displayName}`,
|
|
@@ -5613,7 +6160,7 @@ function installSkills(targetRoot, skillNames, catalog) {
|
|
|
5613
6160
|
"",
|
|
5614
6161
|
`> This skill references an external resource. Install it from the source above.`
|
|
5615
6162
|
].filter(Boolean).join("\n");
|
|
5616
|
-
|
|
6163
|
+
writeFileSync7(filePath, referenceContent, "utf8");
|
|
5617
6164
|
}
|
|
5618
6165
|
result.installed.push(name);
|
|
5619
6166
|
logger.info({ skill: name, path: filePath, type: entry.installType }, "Skill installed");
|
|
@@ -5627,7 +6174,8 @@ function installSkills(targetRoot, skillNames, catalog) {
|
|
|
5627
6174
|
return result;
|
|
5628
6175
|
}
|
|
5629
6176
|
|
|
5630
|
-
// src/
|
|
6177
|
+
// src/agent/api-server.ts
|
|
6178
|
+
import { join as join14 } from "path";
|
|
5631
6179
|
var wsClients = /* @__PURE__ */ new Map();
|
|
5632
6180
|
var broadcastSeq = 0;
|
|
5633
6181
|
var lastBroadcastIssueSnapshot = /* @__PURE__ */ new Map();
|
|
@@ -5646,6 +6194,7 @@ function sendToAllClients(data) {
|
|
|
5646
6194
|
function broadcastToWebSocketClients(message) {
|
|
5647
6195
|
if (wsClients.size === 0) return;
|
|
5648
6196
|
broadcastSeq++;
|
|
6197
|
+
logger.debug({ seq: broadcastSeq, type: message.type, clientCount: wsClients.size }, "[WebSocket] Broadcasting state update");
|
|
5649
6198
|
const issues = message.issues;
|
|
5650
6199
|
if (issues && lastBroadcastIssueSnapshot.size > 0) {
|
|
5651
6200
|
const currentIds = /* @__PURE__ */ new Set();
|
|
@@ -5693,6 +6242,7 @@ function broadcastToWebSocketClients(message) {
|
|
|
5693
6242
|
}));
|
|
5694
6243
|
}
|
|
5695
6244
|
async function startApiServer(state, port, workflowDefinition) {
|
|
6245
|
+
logger.info({ port }, "[API] Starting API server");
|
|
5696
6246
|
const stateDb2 = getStateDb();
|
|
5697
6247
|
if (!stateDb2) {
|
|
5698
6248
|
throw new Error("Cannot start API plugin before the database is initialized.");
|
|
@@ -5765,9 +6315,15 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
5765
6315
|
if (!issue) {
|
|
5766
6316
|
return c.json({ ok: false, error: "Issue not found" }, 404);
|
|
5767
6317
|
}
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
6318
|
+
try {
|
|
6319
|
+
await updater(issue);
|
|
6320
|
+
await persistState(state);
|
|
6321
|
+
wakeScheduler();
|
|
6322
|
+
return c.json({ ok: true, issue });
|
|
6323
|
+
} catch (error) {
|
|
6324
|
+
logger.error({ err: error, issueId }, "[API] mutateIssueState failed");
|
|
6325
|
+
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
6326
|
+
}
|
|
5771
6327
|
};
|
|
5772
6328
|
const resourceConfigs = Object.fromEntries(
|
|
5773
6329
|
NATIVE_RESOURCE_CONFIGS.map((resourceConfig) => [
|
|
@@ -5787,10 +6343,10 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
5787
6343
|
}
|
|
5788
6344
|
setApiRuntimeContext(state, workflowDefinition);
|
|
5789
6345
|
const serveTextFile = (filePath, contentType, cacheControl = "no-cache") => {
|
|
5790
|
-
if (!
|
|
6346
|
+
if (!existsSync10(filePath)) {
|
|
5791
6347
|
return new Response("Not found", { status: 404 });
|
|
5792
6348
|
}
|
|
5793
|
-
return new Response(
|
|
6349
|
+
return new Response(readFileSync9(filePath), {
|
|
5794
6350
|
headers: {
|
|
5795
6351
|
"content-type": contentType,
|
|
5796
6352
|
"cache-control": cacheControl
|
|
@@ -5798,10 +6354,10 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
5798
6354
|
});
|
|
5799
6355
|
};
|
|
5800
6356
|
const serveAppShell = () => {
|
|
5801
|
-
if (!
|
|
6357
|
+
if (!existsSync10(FRONTEND_INDEX)) {
|
|
5802
6358
|
return new Response("Not found", { status: 404 });
|
|
5803
6359
|
}
|
|
5804
|
-
const html =
|
|
6360
|
+
const html = readFileSync9(FRONTEND_INDEX, "utf8").replace('href="/assets/manifest.webmanifest"', 'href="/manifest.webmanifest"').replaceAll('href="/assets/icon.svg"', 'href="/icon.svg"');
|
|
5805
6361
|
return new Response(html, {
|
|
5806
6362
|
headers: {
|
|
5807
6363
|
"content-type": "text/html; charset=utf-8",
|
|
@@ -5881,14 +6437,17 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
5881
6437
|
"GET /offline.html": () => serveTextFile(FRONTEND_OFFLINE_HTML, "text/html; charset=utf-8"),
|
|
5882
6438
|
"GET /icon.svg": () => serveTextFile(FRONTEND_ICON_SVG, "image/svg+xml", "public, max-age=604800, immutable"),
|
|
5883
6439
|
"GET /icon-maskable.svg": () => serveTextFile(FRONTEND_MASKABLE_ICON_SVG, "image/svg+xml", "public, max-age=604800, immutable"),
|
|
6440
|
+
"GET /onboarding": () => serveAppShell(),
|
|
5884
6441
|
"GET /kanban": () => serveAppShell(),
|
|
5885
6442
|
"GET /issues": () => serveAppShell(),
|
|
6443
|
+
"GET /discover": () => serveAppShell(),
|
|
5886
6444
|
"GET /agents": () => serveAppShell(),
|
|
5887
6445
|
"GET /settings": () => serveAppShell(),
|
|
5888
6446
|
"GET /settings/general": () => serveAppShell(),
|
|
5889
6447
|
"GET /settings/notifications": () => serveAppShell(),
|
|
5890
6448
|
"GET /settings/workflow": () => serveAppShell(),
|
|
5891
6449
|
"GET /settings/providers": () => serveAppShell(),
|
|
6450
|
+
"GET /api/health": (c) => c.json({ status: state.booting ? "booting" : "ready" }),
|
|
5892
6451
|
"GET /api/state": async (c) => {
|
|
5893
6452
|
const showAll = c.req.query("all") === "1";
|
|
5894
6453
|
let issues = state.issues;
|
|
@@ -6037,6 +6596,7 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6037
6596
|
const title = toStringValue(payload.title);
|
|
6038
6597
|
const description = toStringValue(payload.description);
|
|
6039
6598
|
if (!title) return c.json({ ok: false, error: "Title is required." }, 400);
|
|
6599
|
+
logger.info({ title: title.slice(0, 80) }, "[API] POST /api/planning/generate");
|
|
6040
6600
|
const result = await generatePlan(title, description, state.config, workflowDefinition);
|
|
6041
6601
|
return c.json({ ok: true, plan: result.plan, usage: result.usage });
|
|
6042
6602
|
} catch (error) {
|
|
@@ -6064,13 +6624,16 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6064
6624
|
"POST /api/issues/create": async (c) => {
|
|
6065
6625
|
try {
|
|
6066
6626
|
const payload = await c.req.json();
|
|
6627
|
+
logger.info({ title: toStringValue(payload.title, "").slice(0, 80) }, "[API] POST /api/issues/create");
|
|
6067
6628
|
const issue = createIssueFromPayload(payload, state.issues, workflowDefinition);
|
|
6068
6629
|
state.issues.push(issue);
|
|
6630
|
+
markIssueDirty(issue.id);
|
|
6069
6631
|
addEvent(state, issue.id, "info", `Issue ${issue.identifier} created via API.`);
|
|
6070
6632
|
if (issue.plan) {
|
|
6071
6633
|
addEvent(state, issue.id, "info", `Plan: ${issue.plan.steps.length} steps, complexity: ${issue.plan.estimatedComplexity}.`);
|
|
6072
6634
|
}
|
|
6073
6635
|
await persistState(state);
|
|
6636
|
+
wakeScheduler();
|
|
6074
6637
|
return c.json({ ok: true, issue }, 201);
|
|
6075
6638
|
} catch (error) {
|
|
6076
6639
|
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
@@ -6111,14 +6674,17 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6111
6674
|
}
|
|
6112
6675
|
try {
|
|
6113
6676
|
const payload = await c.req.json();
|
|
6677
|
+
logger.info({ issueId, identifier: issue.identifier, targetState: payload.state }, "[API] POST /api/issues/:id/state");
|
|
6114
6678
|
await handleStatePatch(state, issue, payload);
|
|
6115
6679
|
await persistState(state);
|
|
6680
|
+
wakeScheduler();
|
|
6116
6681
|
return c.json({ ok: true, issue });
|
|
6117
6682
|
} catch (error) {
|
|
6118
6683
|
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
6119
6684
|
}
|
|
6120
6685
|
},
|
|
6121
6686
|
"POST /api/issues/:id/retry": async (c) => {
|
|
6687
|
+
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/retry");
|
|
6122
6688
|
return mutateIssueState(c, async (issue) => {
|
|
6123
6689
|
if (TERMINAL_STATES.has(issue.state)) {
|
|
6124
6690
|
await transitionIssueState(issue, "Todo", "Manual retry requested.");
|
|
@@ -6131,6 +6697,7 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6131
6697
|
});
|
|
6132
6698
|
},
|
|
6133
6699
|
"POST /api/issues/:id/cancel": async (c) => {
|
|
6700
|
+
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/cancel");
|
|
6134
6701
|
return mutateIssueState(c, async (issue) => {
|
|
6135
6702
|
await transitionIssueState(issue, "Cancelled", "Manual cancel requested.");
|
|
6136
6703
|
addEvent(state, issue.id, "manual", `Manual cancel requested for ${issue.id}.`);
|
|
@@ -6156,6 +6723,7 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6156
6723
|
});
|
|
6157
6724
|
},
|
|
6158
6725
|
"POST /api/issues/:id/approve": async (c) => {
|
|
6726
|
+
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/approve");
|
|
6159
6727
|
return mutateIssueState(c, async (issue) => {
|
|
6160
6728
|
if (issue.state !== "Planning") {
|
|
6161
6729
|
throw new Error(`Cannot approve issue in state ${issue.state}. Must be in Planning.`);
|
|
@@ -6165,13 +6733,14 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6165
6733
|
});
|
|
6166
6734
|
},
|
|
6167
6735
|
"POST /api/issues/:id/merge": async (c) => {
|
|
6736
|
+
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/merge");
|
|
6168
6737
|
try {
|
|
6169
6738
|
const issueId = parseIssue(c);
|
|
6170
6739
|
if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
6171
6740
|
const issue = findIssue2(issueId);
|
|
6172
6741
|
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
6173
6742
|
const wp = issue.workspacePath;
|
|
6174
|
-
if (!wp || !
|
|
6743
|
+
if (!wp || !existsSync10(wp)) {
|
|
6175
6744
|
return c.json({ ok: false, error: "No workspace found for this issue." }, 400);
|
|
6176
6745
|
}
|
|
6177
6746
|
const result = mergeWorkspace(wp);
|
|
@@ -6190,6 +6759,9 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6190
6759
|
},
|
|
6191
6760
|
"POST /api/issues/:id/plan/refine": async (c) => {
|
|
6192
6761
|
return mutateIssueState(c, async (issue) => {
|
|
6762
|
+
if (issue.state !== "Planning") {
|
|
6763
|
+
throw new Error(`Cannot refine plan for issue in state ${issue.state}. Must be in Planning.`);
|
|
6764
|
+
}
|
|
6193
6765
|
if (!issue.plan) {
|
|
6194
6766
|
throw new Error("Issue has no plan to refine. Generate a plan first.");
|
|
6195
6767
|
}
|
|
@@ -6239,10 +6811,10 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6239
6811
|
const liveLog = wp ? `${wp}/fifony-live-output.log` : null;
|
|
6240
6812
|
let logTail = "";
|
|
6241
6813
|
let logSize = 0;
|
|
6242
|
-
if (liveLog &&
|
|
6814
|
+
if (liveLog && existsSync10(liveLog)) {
|
|
6243
6815
|
try {
|
|
6244
|
-
const
|
|
6245
|
-
logSize =
|
|
6816
|
+
const stat2 = statSync3(liveLog);
|
|
6817
|
+
logSize = stat2.size;
|
|
6246
6818
|
const fd = openSync(liveLog, "r");
|
|
6247
6819
|
const readSize = Math.min(logSize, 8192);
|
|
6248
6820
|
const buf = Buffer.alloc(readSize);
|
|
@@ -6279,10 +6851,10 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6279
6851
|
const issue = findIssue2(issueId);
|
|
6280
6852
|
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
6281
6853
|
const wp = issue.workspacePath;
|
|
6282
|
-
if (!wp || !
|
|
6854
|
+
if (!wp || !existsSync10(wp)) {
|
|
6283
6855
|
return c.json({ ok: true, files: [], diff: "", message: "No workspace found." });
|
|
6284
6856
|
}
|
|
6285
|
-
if (!
|
|
6857
|
+
if (!existsSync10(SOURCE_ROOT)) {
|
|
6286
6858
|
return c.json({ ok: true, files: [], diff: "", message: "Source root not found." });
|
|
6287
6859
|
}
|
|
6288
6860
|
let raw = "";
|
|
@@ -6348,6 +6920,46 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6348
6920
|
});
|
|
6349
6921
|
return c.json({ events: events.slice(0, 200) });
|
|
6350
6922
|
},
|
|
6923
|
+
// ── Onboarding: gitignore check ────────────────────────────────────
|
|
6924
|
+
"GET /api/gitignore/status": async (c) => {
|
|
6925
|
+
try {
|
|
6926
|
+
const gitignorePath = join14(TARGET_ROOT, ".gitignore");
|
|
6927
|
+
if (!existsSync10(gitignorePath)) {
|
|
6928
|
+
return c.json({ exists: false, hasFifony: false });
|
|
6929
|
+
}
|
|
6930
|
+
const content = readFileSync9(gitignorePath, "utf-8");
|
|
6931
|
+
const lines = content.split("\n").map((l) => l.trim());
|
|
6932
|
+
const hasFifony = lines.some((l) => l === ".fifony" || l === ".fifony/" || l === "/.fifony" || l === "/.fifony/");
|
|
6933
|
+
return c.json({ exists: true, hasFifony });
|
|
6934
|
+
} catch (error) {
|
|
6935
|
+
logger.error({ err: error }, "Failed to check .gitignore");
|
|
6936
|
+
return c.json({ exists: false, hasFifony: false, error: "Failed to check .gitignore" }, 500);
|
|
6937
|
+
}
|
|
6938
|
+
},
|
|
6939
|
+
"POST /api/gitignore/add": async (c) => {
|
|
6940
|
+
try {
|
|
6941
|
+
const gitignorePath = join14(TARGET_ROOT, ".gitignore");
|
|
6942
|
+
if (!existsSync10(gitignorePath)) {
|
|
6943
|
+
writeFileSync8(gitignorePath, "# Fifony state directory\n.fifony/\n", "utf-8");
|
|
6944
|
+
return c.json({ ok: true, created: true });
|
|
6945
|
+
}
|
|
6946
|
+
const content = readFileSync9(gitignorePath, "utf-8");
|
|
6947
|
+
const lines = content.split("\n").map((l) => l.trim());
|
|
6948
|
+
const hasFifony = lines.some((l) => l === ".fifony" || l === ".fifony/" || l === "/.fifony" || l === "/.fifony/");
|
|
6949
|
+
if (hasFifony) {
|
|
6950
|
+
return c.json({ ok: true, alreadyPresent: true });
|
|
6951
|
+
}
|
|
6952
|
+
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
6953
|
+
appendFileSync2(gitignorePath, `${suffix}
|
|
6954
|
+
# Fifony state directory
|
|
6955
|
+
.fifony/
|
|
6956
|
+
`, "utf-8");
|
|
6957
|
+
return c.json({ ok: true, added: true });
|
|
6958
|
+
} catch (error) {
|
|
6959
|
+
logger.error({ err: error }, "Failed to update .gitignore");
|
|
6960
|
+
return c.json({ ok: false, error: "Failed to update .gitignore" }, 500);
|
|
6961
|
+
}
|
|
6962
|
+
},
|
|
6351
6963
|
// ── Onboarding: project scanning & catalog ─────────────────────────
|
|
6352
6964
|
"GET /api/scan/project": async (c) => {
|
|
6353
6965
|
try {
|
|
@@ -6369,6 +6981,30 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6369
6981
|
return c.json({ ok: false, error: "Failed to analyze project." }, 500);
|
|
6370
6982
|
}
|
|
6371
6983
|
},
|
|
6984
|
+
"GET /api/scan/issues": async (c) => {
|
|
6985
|
+
try {
|
|
6986
|
+
const todos = scanForTodos(TARGET_ROOT);
|
|
6987
|
+
const categorized = categorizeScannedIssues(todos, workflowDefinition);
|
|
6988
|
+
return c.json({ ok: true, issues: categorized, total: categorized.length });
|
|
6989
|
+
} catch (error) {
|
|
6990
|
+
logger.error({ err: error }, "Failed to scan for TODOs");
|
|
6991
|
+
return c.json({ ok: false, error: "Failed to scan for issues." }, 500);
|
|
6992
|
+
}
|
|
6993
|
+
},
|
|
6994
|
+
"POST /api/boot/skip-scan": async (c) => {
|
|
6995
|
+
broadcastToWebSocketClients({ type: "boot:scan:skipped" });
|
|
6996
|
+
return c.json({ ok: true, message: "Scan skipped." });
|
|
6997
|
+
},
|
|
6998
|
+
"GET /api/scan/github-issues": async (c) => {
|
|
6999
|
+
try {
|
|
7000
|
+
const issues = await fetchGitHubIssues(TARGET_ROOT);
|
|
7001
|
+
const categorized = categorizeScannedIssues(issues, workflowDefinition);
|
|
7002
|
+
return c.json({ ok: true, issues: categorized, total: categorized.length });
|
|
7003
|
+
} catch (error) {
|
|
7004
|
+
logger.error({ err: error }, "Failed to fetch GitHub issues");
|
|
7005
|
+
return c.json({ ok: false, error: "Failed to fetch GitHub issues." }, 500);
|
|
7006
|
+
}
|
|
7007
|
+
},
|
|
6372
7008
|
"GET /api/catalog/agents": async (c) => {
|
|
6373
7009
|
const domainsParam = c.req.query("domains");
|
|
6374
7010
|
const domains = typeof domainsParam === "string" ? domainsParam.split(",").map((d) => d.trim()).filter(Boolean) : [];
|
|
@@ -6419,7 +7055,7 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6419
7055
|
logger.info(`OpenAPI docs available at http://localhost:${port}/docs`);
|
|
6420
7056
|
}
|
|
6421
7057
|
|
|
6422
|
-
// src/
|
|
7058
|
+
// src/agent/store.ts
|
|
6423
7059
|
var loadedS3dbModule = null;
|
|
6424
7060
|
var stateDb = null;
|
|
6425
7061
|
var runtimeStateResource = null;
|
|
@@ -6495,7 +7131,7 @@ async function initStateStore() {
|
|
|
6495
7131
|
debugBoot("initStateStore:start");
|
|
6496
7132
|
const { S3db, FileSystemClient, StateMachinePlugin } = await loadS3dbModule();
|
|
6497
7133
|
debugBoot("initStateStore:module-loaded");
|
|
6498
|
-
|
|
7134
|
+
mkdirSync4(S3DB_DATABASE_PATH, { recursive: true });
|
|
6499
7135
|
stateDb = new S3db({
|
|
6500
7136
|
client: new FileSystemClient({
|
|
6501
7137
|
basePath: S3DB_DATABASE_PATH,
|
|
@@ -6589,7 +7225,11 @@ function isStateNotFoundError(error) {
|
|
|
6589
7225
|
return false;
|
|
6590
7226
|
}
|
|
6591
7227
|
async function loadPersistedState() {
|
|
6592
|
-
if (!runtimeStateResource)
|
|
7228
|
+
if (!runtimeStateResource) {
|
|
7229
|
+
logger.debug("[Store] No runtime state resource available, skipping load");
|
|
7230
|
+
return null;
|
|
7231
|
+
}
|
|
7232
|
+
logger.debug("[Store] Loading persisted state from s3db");
|
|
6593
7233
|
try {
|
|
6594
7234
|
const record2 = await runtimeStateResource.get(S3DB_RUNTIME_RECORD_ID);
|
|
6595
7235
|
if (record2?.state && typeof record2.state === "object") {
|
|
@@ -6624,7 +7264,7 @@ async function recoverStateFromIssueResource() {
|
|
|
6624
7264
|
config: {},
|
|
6625
7265
|
issues,
|
|
6626
7266
|
events: [],
|
|
6627
|
-
metrics:
|
|
7267
|
+
metrics: getMetrics(issues),
|
|
6628
7268
|
notes: ["State recovered from individual issue records after corruption."]
|
|
6629
7269
|
};
|
|
6630
7270
|
} catch (error) {
|
|
@@ -6634,20 +7274,30 @@ async function recoverStateFromIssueResource() {
|
|
|
6634
7274
|
}
|
|
6635
7275
|
async function persistState(state) {
|
|
6636
7276
|
state.metrics = {
|
|
6637
|
-
...
|
|
7277
|
+
...getMetrics(state.issues),
|
|
6638
7278
|
activeWorkers: state.metrics.activeWorkers
|
|
6639
7279
|
};
|
|
6640
7280
|
if (!runtimeStateResource) return;
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
7281
|
+
const dirty = hasDirtyState();
|
|
7282
|
+
const dirtyIssueCount = getDirtyIssueIds().size;
|
|
7283
|
+
const dirtyEventCount = getDirtyEventIds().size;
|
|
7284
|
+
if (dirty || dirtyIssueCount > 0 || dirtyEventCount > 0) {
|
|
7285
|
+
logger.debug({ dirty, dirtyIssues: dirtyIssueCount, dirtyEvents: dirtyEventCount }, "[Store] Persisting state");
|
|
7286
|
+
}
|
|
7287
|
+
if (dirty) {
|
|
7288
|
+
await runtimeStateResource.replace(S3DB_RUNTIME_RECORD_ID, {
|
|
7289
|
+
id: S3DB_RUNTIME_RECORD_ID,
|
|
7290
|
+
schemaVersion: S3DB_RUNTIME_SCHEMA_VERSION,
|
|
7291
|
+
trackerKind: "filesystem",
|
|
7292
|
+
runtimeTag: "local-only",
|
|
7293
|
+
updatedAt: now(),
|
|
7294
|
+
state
|
|
7295
|
+
});
|
|
7296
|
+
}
|
|
7297
|
+
const dirtyIssues = getDirtyIssueIds();
|
|
7298
|
+
if (issueStateResource && dirtyIssues.size > 0) {
|
|
6650
7299
|
for (const issue of state.issues) {
|
|
7300
|
+
if (!dirtyIssues.has(issue.id)) continue;
|
|
6651
7301
|
const clean = {
|
|
6652
7302
|
...issue,
|
|
6653
7303
|
nextRetryAt: issue.nextRetryAt || void 0,
|
|
@@ -6662,11 +7312,15 @@ async function persistState(state) {
|
|
|
6662
7312
|
logger.warn(`Failed to persist issue ${issue.id}: ${String(error)}`);
|
|
6663
7313
|
}
|
|
6664
7314
|
}
|
|
7315
|
+
clearDirtyIssueIds();
|
|
6665
7316
|
}
|
|
6666
|
-
|
|
7317
|
+
const dirtyEvents = getDirtyEventIds();
|
|
7318
|
+
if (eventStateResource && dirtyEvents.size > 0) {
|
|
6667
7319
|
for (const event of state.events) {
|
|
7320
|
+
if (!dirtyEvents.has(event.id)) continue;
|
|
6668
7321
|
await eventStateResource.replace(event.id, event);
|
|
6669
7322
|
}
|
|
7323
|
+
clearDirtyEventIds();
|
|
6670
7324
|
}
|
|
6671
7325
|
broadcastToWebSocketClients({
|
|
6672
7326
|
type: "state:update",
|
|
@@ -6677,6 +7331,11 @@ async function persistState(state) {
|
|
|
6677
7331
|
updatedAt: state.updatedAt
|
|
6678
7332
|
});
|
|
6679
7333
|
}
|
|
7334
|
+
async function persistStateFull(state) {
|
|
7335
|
+
markAllIssuesDirty(state.issues.map((i) => i.id));
|
|
7336
|
+
markAllEventsDirty(state.events.map((e) => e.id));
|
|
7337
|
+
await persistState(state);
|
|
7338
|
+
}
|
|
6680
7339
|
async function loadPersistedSettings() {
|
|
6681
7340
|
if (!settingStateResource?.list) return [];
|
|
6682
7341
|
try {
|
|
@@ -6696,6 +7355,7 @@ async function replacePersistedSetting(setting) {
|
|
|
6696
7355
|
await settingStateResource.replace(setting.id, setting);
|
|
6697
7356
|
}
|
|
6698
7357
|
async function closeStateStore() {
|
|
7358
|
+
logger.info("[Store] Closing state store and plugins");
|
|
6699
7359
|
clearApiRuntimeContext();
|
|
6700
7360
|
if (activeEcPlugin?.stop) {
|
|
6701
7361
|
try {
|
|
@@ -6750,120 +7410,7 @@ async function closeStateStore() {
|
|
|
6750
7410
|
}
|
|
6751
7411
|
}
|
|
6752
7412
|
|
|
6753
|
-
// src/
|
|
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
|
-
// src/runtime/dev-server.ts
|
|
7413
|
+
// src/agent/dev-server.ts
|
|
6867
7414
|
import { resolve as resolve3 } from "path";
|
|
6868
7415
|
var VITE_CONFIG_PATH = resolve3(PACKAGE_ROOT, "app/vite.config.js");
|
|
6869
7416
|
async function startDevFrontend(apiPort, devPort) {
|
|
@@ -6901,7 +7448,7 @@ async function startDevFrontend(apiPort, devPort) {
|
|
|
6901
7448
|
}
|
|
6902
7449
|
}
|
|
6903
7450
|
|
|
6904
|
-
// src/
|
|
7451
|
+
// src/agent/run-local.ts
|
|
6905
7452
|
function usage() {
|
|
6906
7453
|
console.log(
|
|
6907
7454
|
`Usage: ${argv3[1]} [options]
|
|
@@ -6915,6 +7462,10 @@ Options:
|
|
|
6915
7462
|
--timeout <ms> Agent command timeout in ms (default: 1800000)
|
|
6916
7463
|
--dev Start Vite dev server alongside API (HMR on port+1)
|
|
6917
7464
|
--once Process once and exit
|
|
7465
|
+
--skip-source Skip source snapshot copy
|
|
7466
|
+
--skip-scan Skip project analysis
|
|
7467
|
+
--skip-recovery Skip orphaned agent recovery
|
|
7468
|
+
--fast-boot Equivalent to --skip-source --skip-scan --skip-recovery
|
|
6918
7469
|
`
|
|
6919
7470
|
);
|
|
6920
7471
|
}
|
|
@@ -6930,6 +7481,8 @@ async function main() {
|
|
|
6930
7481
|
}
|
|
6931
7482
|
mkdirSync5(STATE_ROOT, { recursive: true });
|
|
6932
7483
|
initLogger(STATE_ROOT);
|
|
7484
|
+
logger.info("[Boot] Fifony runtime starting");
|
|
7485
|
+
logger.info({ stateRoot: STATE_ROOT, cwd: process.cwd() }, "[Boot] State root initialized");
|
|
6933
7486
|
const detectedProviders = detectAvailableProviders();
|
|
6934
7487
|
for (const p of detectedProviders) {
|
|
6935
7488
|
logger.info(`Provider ${p.name}: ${p.available ? `available at ${p.path}` : "not found"}`);
|
|
@@ -6937,8 +7490,13 @@ async function main() {
|
|
|
6937
7490
|
const interfaceMode = (env10.FIFONY_INTERFACE ?? "cli").trim().toLowerCase();
|
|
6938
7491
|
const runOnce = args.includes("--once");
|
|
6939
7492
|
const devMode = args.includes("--dev") || env10.NODE_ENV === "development";
|
|
7493
|
+
const fastBoot = args.includes("--fast-boot");
|
|
7494
|
+
const skipSource = fastBoot || args.includes("--skip-source");
|
|
7495
|
+
if (skipSource) setSkipSource(true);
|
|
6940
7496
|
debugBoot("main:state-root-ready");
|
|
7497
|
+
logger.debug("[Boot] Loading workflow definition");
|
|
6941
7498
|
const workflowDefinition = loadWorkflowDefinition();
|
|
7499
|
+
logger.info({ workflowPath: workflowDefinition.workflowPath }, "[Boot] Workflow definition loaded");
|
|
6942
7500
|
debugBoot("main:workflow-loaded");
|
|
6943
7501
|
const port = parsePort(args);
|
|
6944
7502
|
let config = applyWorkflowConfig(deriveConfig(args), workflowDefinition, port);
|
|
@@ -6953,14 +7511,44 @@ async function main() {
|
|
|
6953
7511
|
}
|
|
6954
7512
|
}
|
|
6955
7513
|
const dashboardPort = port ?? (config.dashboardPort ? Number.parseInt(config.dashboardPort, 10) : void 0);
|
|
6956
|
-
|
|
6957
|
-
debugBoot("main:
|
|
7514
|
+
const skipRecovery = args.includes("--skip-recovery") || args.includes("--fast-boot");
|
|
7515
|
+
debugBoot("main:phase-b-start");
|
|
7516
|
+
logger.debug("[Boot] Initializing state store (s3db)");
|
|
6958
7517
|
await initStateStore();
|
|
7518
|
+
logger.info("[Boot] State store initialized");
|
|
6959
7519
|
debugBoot("main:store-initialized");
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
7520
|
+
const earlyState = {
|
|
7521
|
+
startedAt: now(),
|
|
7522
|
+
updatedAt: now(),
|
|
7523
|
+
trackerKind: "filesystem",
|
|
7524
|
+
sourceRepoUrl: "",
|
|
7525
|
+
sourceRef: "workspace",
|
|
7526
|
+
workflowPath: "",
|
|
7527
|
+
config,
|
|
7528
|
+
issues: [],
|
|
7529
|
+
events: [],
|
|
7530
|
+
metrics: { total: 0, queued: 0, inProgress: 0, blocked: 0, done: 0, cancelled: 0, activeWorkers: 0 },
|
|
7531
|
+
notes: [],
|
|
7532
|
+
booting: true
|
|
7533
|
+
};
|
|
7534
|
+
let apiState = earlyState;
|
|
7535
|
+
if (dashboardPort) {
|
|
7536
|
+
await startApiServer(apiState, dashboardPort, workflowDefinition);
|
|
7537
|
+
debugBoot("main:api-server-early-start");
|
|
7538
|
+
if (devMode) {
|
|
7539
|
+
const devPort = dashboardPort + 1;
|
|
7540
|
+
await startDevFrontend(dashboardPort, devPort);
|
|
7541
|
+
}
|
|
7542
|
+
}
|
|
7543
|
+
debugBoot("main:phase-c-start");
|
|
7544
|
+
logger.debug("[Boot] Loading persisted state, settings, and recovering sessions");
|
|
7545
|
+
const [previous, persistedSettings] = await Promise.all([
|
|
7546
|
+
loadPersistedState(),
|
|
7547
|
+
loadRuntimeSettings(),
|
|
7548
|
+
persistDetectedProvidersSetting(detectedProviders),
|
|
7549
|
+
recoverPlanningSession()
|
|
7550
|
+
]);
|
|
7551
|
+
logger.info({ hadPreviousState: previous !== null, issueCount: previous?.issues?.length ?? 0, settingsCount: persistedSettings.length }, "[Boot] State loaded from persistence");
|
|
6964
7552
|
debugBoot("main:state-loaded");
|
|
6965
7553
|
config = applyPersistedSettings(config, persistedSettings);
|
|
6966
7554
|
await syncRuntimeConfigSettings(config, persistedSettings);
|
|
@@ -6969,6 +7557,7 @@ async function main() {
|
|
|
6969
7557
|
state.config.dashboardPort = dashboardPort ? String(dashboardPort) : void 0;
|
|
6970
7558
|
state.workflowPath = WORKFLOW_RENDERED;
|
|
6971
7559
|
state.updatedAt = now();
|
|
7560
|
+
state.booting = false;
|
|
6972
7561
|
if (state.config.agentCommand) {
|
|
6973
7562
|
state.notes.push(`Using agent command: ${state.config.agentCommand}`);
|
|
6974
7563
|
}
|
|
@@ -6998,25 +7587,32 @@ async function main() {
|
|
|
6998
7587
|
logger.info("Background workspace cleanup complete.");
|
|
6999
7588
|
});
|
|
7000
7589
|
}
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
if (
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
issue.
|
|
7012
|
-
issue.
|
|
7013
|
-
|
|
7590
|
+
if (!skipRecovery) {
|
|
7591
|
+
logger.debug({ issueCount: state.issues.filter((i) => i.state === "Running" || i.state === "Interrupted" || i.state === "Queued").length }, "[Boot] Checking for orphaned agent processes");
|
|
7592
|
+
for (const issue of state.issues) {
|
|
7593
|
+
if (issue.state === "Running" || issue.state === "Interrupted" || issue.state === "Queued") {
|
|
7594
|
+
const { alive, pid } = isAgentStillRunning(issue);
|
|
7595
|
+
if (alive && pid) {
|
|
7596
|
+
logger.info(`Agent for ${issue.identifier} still alive (PID ${pid.pid}), keeping state as Running.`);
|
|
7597
|
+
issue.state = "Running";
|
|
7598
|
+
addEvent(state, issue.id, "info", `Orphaned agent detected (PID ${pid.pid}), still alive \u2014 tracking resumed.`);
|
|
7599
|
+
} else {
|
|
7600
|
+
if (issue.workspacePath) cleanStalePidFile(issue.workspacePath);
|
|
7601
|
+
if (issue.state === "Running") {
|
|
7602
|
+
issue.state = "Interrupted";
|
|
7603
|
+
issue.history.push(`[${now()}] Agent process not found on boot \u2014 marked Interrupted.`);
|
|
7604
|
+
addEvent(state, issue.id, "info", `Agent for ${issue.identifier} not found, marked Interrupted.`);
|
|
7605
|
+
}
|
|
7014
7606
|
}
|
|
7015
7607
|
}
|
|
7016
7608
|
}
|
|
7017
7609
|
}
|
|
7018
7610
|
state.metrics = computeMetrics(state.issues);
|
|
7019
|
-
await
|
|
7611
|
+
await persistStateFull(state);
|
|
7612
|
+
if (dashboardPort) {
|
|
7613
|
+
Object.assign(apiState, state);
|
|
7614
|
+
debugBoot("main:api-state-swapped");
|
|
7615
|
+
}
|
|
7020
7616
|
const running = /* @__PURE__ */ new Set();
|
|
7021
7617
|
installGracefulShutdown(state, running);
|
|
7022
7618
|
logger.info(`Rendered local workflow: ${WORKFLOW_RENDERED}`);
|
|
@@ -7027,16 +7623,10 @@ async function main() {
|
|
|
7027
7623
|
logger.info(`Max turns: ${state.config.maxTurns}`);
|
|
7028
7624
|
logger.info(`Agent provider: ${state.config.agentProvider}`);
|
|
7029
7625
|
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
7626
|
try {
|
|
7038
7627
|
addEvent(state, void 0, "info", `Runtime started in local-only mode (filesystem tracker).`);
|
|
7039
7628
|
const runForever = !runOnce && (Boolean(dashboardPort) || interfaceMode === "mcp");
|
|
7629
|
+
logger.info({ runForever, runOnce, dashboardPort, interfaceMode }, "[Boot] Entering scheduler loop");
|
|
7040
7630
|
await scheduler(state, running, runForever, workflowDefinition);
|
|
7041
7631
|
} catch (error) {
|
|
7042
7632
|
console.error("FATAL STACK TRACE:", error);
|
|
@@ -7046,7 +7636,7 @@ async function main() {
|
|
|
7046
7636
|
} finally {
|
|
7047
7637
|
state.updatedAt = now();
|
|
7048
7638
|
state.metrics = computeMetrics(state.issues);
|
|
7049
|
-
await
|
|
7639
|
+
await persistStateFull(state);
|
|
7050
7640
|
await closeStateStore();
|
|
7051
7641
|
}
|
|
7052
7642
|
}
|