fifony 0.1.26 → 0.1.27-next.84df008
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -129
- package/app/dist/assets/{KeyboardShortcutsHelp-lTNj9GiT.js → KeyboardShortcutsHelp-By0KcDhZ.js} +1 -1
- package/app/dist/assets/OnboardingWizard-5cz7Onsu.js +1 -0
- package/app/dist/assets/{analytics.lazy-C42PFRzr.js → analytics.lazy-ArFwnOEn.js} +1 -1
- package/app/dist/assets/{createLucideIcon-BWC-guQt.js → createLucideIcon-DgMTp0yx.js} +1 -1
- package/app/dist/assets/index-59O8esMr.js +45 -0
- package/app/dist/assets/{index-Qr2OPvRO.css → index-DuBwUsuf.css} +1 -1
- package/app/dist/assets/vendor-D-IqxHHu.js +9 -0
- package/app/dist/dinofffaur.webp +0 -0
- package/app/dist/index.html +4 -4
- package/app/dist/service-worker.js +1 -1
- package/dist/agent/run-local.js +57 -144
- package/dist/agent-KMXNVDRO.js +74 -0
- package/dist/chunk-FJR4ALEN.js +847 -0
- package/dist/{chunk-2F3Q2MAG.js → chunk-HSGUPFTV.js} +1224 -611
- package/dist/chunk-O5AEQXUV.js +311 -0
- package/dist/chunk-OONOOWNC.js +123 -0
- package/dist/{chunk-NFHVAIPW.js → chunk-UYCDOH6S.js} +380 -795
- package/dist/chunk-XENKNHFS.js +295 -0
- package/dist/cli.js +6 -4
- package/dist/issue-runner-JJAFMHKV.js +15 -0
- package/dist/{issue-state-machine-OWABY5S2.js → issue-state-machine-ACMUJSXC.js} +5 -3
- package/dist/issues-VDFXBK3N.js +40 -0
- package/dist/mcp/server.js +23 -121
- package/dist/queue-workers-U47CVPTO.js +23 -0
- package/dist/scheduler-MEXEDV4M.js +21 -0
- package/dist/{store-WN47MDT5.js → store-AG6LLYJ7.js} +7 -5
- package/dist/workspace-474CCKTW.js +44 -0
- package/package.json +6 -6
- package/app/dist/assets/OnboardingWizard-B6LlJR9B.js +0 -1
- package/app/dist/assets/index-fVSxs9d5.js +0 -43
- package/app/dist/assets/vendor-BTlTWMUF.js +0 -9
- package/dist/chunk-AMOGDOM7.js +0 -796
- package/dist/chunk-IA7IMQ5F.js +0 -91
- package/dist/issue-runner-DA4IDLKX.js +0 -13
- package/dist/queue-workers-JIH5ZMNQ.js +0 -20
|
@@ -1,36 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
areQueueWorkersActive,
|
|
3
|
-
enqueueForExecution,
|
|
4
|
-
enqueueForPlanning,
|
|
5
|
-
enqueueForReview
|
|
6
|
-
} from "./chunk-IA7IMQ5F.js";
|
|
7
|
-
import {
|
|
8
|
-
ADAPTERS,
|
|
9
2
|
ISSUE_STATE_MACHINE_ID,
|
|
10
|
-
applyCapabilityMetadata,
|
|
11
|
-
buildExecutionPayload,
|
|
12
|
-
buildFullPlanPrompt,
|
|
13
|
-
buildProviderBasePrompt,
|
|
14
|
-
buildTurnPrompt,
|
|
15
|
-
cleanWorkspace,
|
|
16
|
-
computeCapabilityCounts,
|
|
17
|
-
computeDiffStats,
|
|
18
3
|
computeMetrics,
|
|
19
|
-
describeRoutingSignals,
|
|
20
|
-
detectAvailableProviders,
|
|
21
|
-
discoverModels,
|
|
22
|
-
ensureWorktreeCommitted,
|
|
23
4
|
executeTransition,
|
|
24
5
|
findIssueStateMachineTransitionPath,
|
|
25
|
-
getCapabilityRoutingOptions,
|
|
26
6
|
getDirtyEventIds,
|
|
27
7
|
getDirtyIssueIds,
|
|
28
|
-
getEffectiveAgentProviders,
|
|
29
8
|
getIssueStateMachinePlugin,
|
|
30
9
|
getMetrics,
|
|
31
|
-
getProviderDefaultCommand,
|
|
32
10
|
hasDirtyState,
|
|
33
|
-
hydrateIssuePathsFromWorkspace,
|
|
34
11
|
issueStateMachineConfig,
|
|
35
12
|
markAllEventsDirty,
|
|
36
13
|
markAllIssuePlansDirty,
|
|
@@ -38,22 +15,65 @@ import {
|
|
|
38
15
|
markEventDirty,
|
|
39
16
|
markIssueDirty,
|
|
40
17
|
markIssuePlanDirty,
|
|
18
|
+
setFsmEventEmitter,
|
|
19
|
+
setIssueResourceStateApi,
|
|
20
|
+
setIssueStateMachinePlugin,
|
|
21
|
+
snapshotAndClearDirtyEventIds,
|
|
22
|
+
snapshotAndClearDirtyIssueIds,
|
|
23
|
+
snapshotAndClearDirtyIssuePlanIds
|
|
24
|
+
} from "./chunk-FJR4ALEN.js";
|
|
25
|
+
import {
|
|
26
|
+
ADAPTERS,
|
|
27
|
+
buildExecutionPayload,
|
|
28
|
+
buildFullPlanPrompt,
|
|
29
|
+
buildProviderBasePrompt,
|
|
30
|
+
buildRetryContext,
|
|
31
|
+
buildTurnPrompt,
|
|
32
|
+
cleanWorkspace,
|
|
33
|
+
computeDiffStats,
|
|
34
|
+
detectAvailableProviders,
|
|
35
|
+
discoverModels,
|
|
36
|
+
ensureWorktreeCommitted,
|
|
37
|
+
getEffectiveAgentProviders,
|
|
38
|
+
getProviderDefaultCommand,
|
|
39
|
+
hydrateIssuePathsFromWorkspace,
|
|
41
40
|
mergeWorkspace,
|
|
42
41
|
normalizeAgentProvider,
|
|
43
42
|
parseDiffStats,
|
|
44
43
|
prepareWorkspace,
|
|
45
|
-
pushWorktreeBranch,
|
|
46
44
|
readCodexConfig,
|
|
47
45
|
resolveAgentCommand,
|
|
48
46
|
runCommandWithTimeout,
|
|
49
|
-
runHook
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
runHook
|
|
48
|
+
} from "./chunk-UYCDOH6S.js";
|
|
49
|
+
import {
|
|
50
|
+
appendFileTail,
|
|
51
|
+
clamp,
|
|
52
|
+
debugBoot,
|
|
53
|
+
extractJsonObjects,
|
|
54
|
+
fail,
|
|
55
|
+
idToSafePath,
|
|
56
|
+
isoWeek,
|
|
57
|
+
normalizeState,
|
|
58
|
+
now,
|
|
59
|
+
parseEnvNumber,
|
|
60
|
+
parseIntArg,
|
|
61
|
+
parseIssueState,
|
|
62
|
+
parsePositiveIntEnv,
|
|
63
|
+
renderPrompt,
|
|
64
|
+
repairTruncatedJson,
|
|
65
|
+
toBooleanValue,
|
|
66
|
+
toNumberValue,
|
|
67
|
+
toStringArray,
|
|
68
|
+
toStringValue,
|
|
69
|
+
withRetryBackoff
|
|
70
|
+
} from "./chunk-O5AEQXUV.js";
|
|
71
|
+
import {
|
|
72
|
+
enqueue
|
|
73
|
+
} from "./chunk-XENKNHFS.js";
|
|
74
|
+
import {
|
|
75
|
+
logger
|
|
76
|
+
} from "./chunk-DVU3CXWA.js";
|
|
57
77
|
import {
|
|
58
78
|
ALLOWED_STATES,
|
|
59
79
|
ATTACHMENTS_ROOT,
|
|
@@ -81,42 +101,18 @@ import {
|
|
|
81
101
|
STATE_ROOT,
|
|
82
102
|
TARGET_ROOT,
|
|
83
103
|
TERMINAL_STATES,
|
|
84
|
-
WORKSPACE_ROOT
|
|
85
|
-
|
|
86
|
-
clamp,
|
|
87
|
-
debugBoot,
|
|
88
|
-
extractJsonObjects,
|
|
89
|
-
fail,
|
|
90
|
-
idToSafePath,
|
|
91
|
-
inferCapabilityPaths,
|
|
92
|
-
isoWeek,
|
|
93
|
-
normalizeState,
|
|
94
|
-
now,
|
|
95
|
-
parseEnvNumber,
|
|
96
|
-
parseIntArg,
|
|
97
|
-
parseIssueState,
|
|
98
|
-
parsePositiveIntEnv,
|
|
99
|
-
renderPrompt,
|
|
100
|
-
repairTruncatedJson,
|
|
101
|
-
resolveTaskCapabilities,
|
|
102
|
-
toNumberValue,
|
|
103
|
-
toStringArray,
|
|
104
|
-
toStringValue,
|
|
105
|
-
withRetryBackoff
|
|
106
|
-
} from "./chunk-AMOGDOM7.js";
|
|
107
|
-
import {
|
|
108
|
-
logger
|
|
109
|
-
} from "./chunk-DVU3CXWA.js";
|
|
104
|
+
WORKSPACE_ROOT
|
|
105
|
+
} from "./chunk-OONOOWNC.js";
|
|
110
106
|
|
|
111
107
|
// src/agents/issue-runner.ts
|
|
112
108
|
import {
|
|
113
|
-
existsSync as
|
|
114
|
-
mkdirSync as
|
|
115
|
-
readFileSync as
|
|
116
|
-
writeFileSync as
|
|
109
|
+
existsSync as existsSync14,
|
|
110
|
+
mkdirSync as mkdirSync8,
|
|
111
|
+
readFileSync as readFileSync11,
|
|
112
|
+
writeFileSync as writeFileSync12
|
|
117
113
|
} from "fs";
|
|
118
|
-
import { join as
|
|
119
|
-
import { execSync as
|
|
114
|
+
import { join as join17 } from "path";
|
|
115
|
+
import { execSync as execSync5 } from "child_process";
|
|
120
116
|
|
|
121
117
|
// src/domains/tokens.ts
|
|
122
118
|
var EMPTY = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
@@ -290,19 +286,19 @@ function getAnalytics(topN = 20) {
|
|
|
290
286
|
}
|
|
291
287
|
|
|
292
288
|
// src/domains/project.ts
|
|
293
|
-
import { execFileSync, spawn as spawn3 } from "child_process";
|
|
289
|
+
import { execFileSync as execFileSync2, spawn as spawn3 } from "child_process";
|
|
294
290
|
import { createHash } from "crypto";
|
|
295
291
|
import {
|
|
296
|
-
existsSync as
|
|
297
|
-
mkdirSync as
|
|
292
|
+
existsSync as existsSync13,
|
|
293
|
+
mkdirSync as mkdirSync7,
|
|
298
294
|
mkdtempSync as mkdtempSync4,
|
|
299
|
-
readdirSync as
|
|
300
|
-
readFileSync as
|
|
295
|
+
readdirSync as readdirSync4,
|
|
296
|
+
readFileSync as readFileSync10,
|
|
301
297
|
rmSync as rmSync5,
|
|
302
|
-
writeFileSync as
|
|
298
|
+
writeFileSync as writeFileSync11
|
|
303
299
|
} from "fs";
|
|
304
300
|
import { homedir as homedir3, tmpdir as tmpdir4 } from "os";
|
|
305
|
-
import { basename as basename4, dirname as dirname2, join as
|
|
301
|
+
import { basename as basename4, dirname as dirname2, join as join16, relative as relativePath, resolve as resolve2 } from "path";
|
|
306
302
|
import { env as env3 } from "process";
|
|
307
303
|
|
|
308
304
|
// src/persistence/plugins/api-runtime-context.ts
|
|
@@ -322,8 +318,8 @@ function getApiRuntimeContextOrThrow() {
|
|
|
322
318
|
|
|
323
319
|
// src/persistence/plugins/api-server.ts
|
|
324
320
|
import {
|
|
325
|
-
existsSync as
|
|
326
|
-
readFileSync as
|
|
321
|
+
existsSync as existsSync12,
|
|
322
|
+
readFileSync as readFileSync9
|
|
327
323
|
} from "fs";
|
|
328
324
|
|
|
329
325
|
// src/persistence/resources/runtime-state.resource.ts
|
|
@@ -417,6 +413,31 @@ function extractTokenUsage(output, jsonObj) {
|
|
|
417
413
|
};
|
|
418
414
|
}
|
|
419
415
|
}
|
|
416
|
+
const stats = jsonObj.stats;
|
|
417
|
+
const geminiModels = stats?.models ?? null;
|
|
418
|
+
if (geminiModels && typeof geminiModels === "object") {
|
|
419
|
+
let totalInput = 0, totalOutput = 0, primaryModel = "", maxTokens = 0;
|
|
420
|
+
for (const [model, data] of Object.entries(geminiModels)) {
|
|
421
|
+
const tokens = data?.tokens;
|
|
422
|
+
if (!tokens) continue;
|
|
423
|
+
const inp = Number(tokens.input || 0) + Number(tokens.cached || 0);
|
|
424
|
+
const out = Number(tokens.candidates || 0);
|
|
425
|
+
totalInput += inp;
|
|
426
|
+
totalOutput += out;
|
|
427
|
+
if (inp + out > maxTokens) {
|
|
428
|
+
maxTokens = inp + out;
|
|
429
|
+
primaryModel = model;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (totalInput > 0 || totalOutput > 0) {
|
|
433
|
+
return {
|
|
434
|
+
inputTokens: totalInput,
|
|
435
|
+
outputTokens: totalOutput,
|
|
436
|
+
totalTokens: totalInput + totalOutput,
|
|
437
|
+
model: primaryModel || void 0
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
420
441
|
const usage = jsonObj.usage;
|
|
421
442
|
if (usage && typeof usage === "object") {
|
|
422
443
|
const inp = Number(usage.input_tokens) || 0;
|
|
@@ -466,6 +487,15 @@ function tryParseJsonOutput(output) {
|
|
|
466
487
|
}
|
|
467
488
|
}
|
|
468
489
|
if (obj.status) return obj;
|
|
490
|
+
if (typeof obj.response === "string") {
|
|
491
|
+
try {
|
|
492
|
+
const inner = JSON.parse(obj.response);
|
|
493
|
+
if (inner && typeof inner === "object" && !Array.isArray(inner)) {
|
|
494
|
+
return inner;
|
|
495
|
+
}
|
|
496
|
+
} catch {
|
|
497
|
+
}
|
|
498
|
+
}
|
|
469
499
|
}
|
|
470
500
|
} catch {
|
|
471
501
|
}
|
|
@@ -764,24 +794,25 @@ async function loadAgentSessionSnapshotsForIssue(issue, providers, pipeline, _wo
|
|
|
764
794
|
|
|
765
795
|
// src/agents/agent-pipeline.ts
|
|
766
796
|
import {
|
|
767
|
-
|
|
797
|
+
mkdirSync as mkdirSync2,
|
|
798
|
+
writeFileSync as writeFileSync3
|
|
768
799
|
} from "fs";
|
|
769
|
-
import { join as
|
|
800
|
+
import { join as join6 } from "path";
|
|
770
801
|
|
|
771
802
|
// src/agents/adapters/index.ts
|
|
772
803
|
import { writeFileSync } from "fs";
|
|
773
804
|
import { join as join3 } from "path";
|
|
774
|
-
async function compileExecution(issue, provider, config, workspacePath, skillContext) {
|
|
805
|
+
async function compileExecution(issue, provider, config, workspacePath, skillContext, capabilitiesManifest) {
|
|
775
806
|
const plan = issue.plan;
|
|
776
807
|
if (!plan?.steps?.length) return null;
|
|
777
808
|
const adapter = ADAPTERS[provider.provider];
|
|
778
809
|
if (!adapter) return null;
|
|
779
810
|
const payload = buildExecutionPayload(issue, provider, plan, workspacePath);
|
|
780
|
-
const compiled = await adapter.compile(issue, provider, plan, config, workspacePath, skillContext);
|
|
811
|
+
const compiled = await adapter.compile(issue, provider, plan, config, workspacePath, skillContext, capabilitiesManifest);
|
|
781
812
|
compiled.payload = payload;
|
|
782
813
|
return compiled;
|
|
783
814
|
}
|
|
784
|
-
async function compileReview(issue, reviewer, workspacePath, diffSummary) {
|
|
815
|
+
async function compileReview(issue, reviewer, workspacePath, diffSummary, config) {
|
|
785
816
|
const plan = issue.plan;
|
|
786
817
|
const prompt = await renderPrompt("compile-review", {
|
|
787
818
|
issueIdentifier: issue.identifier,
|
|
@@ -794,7 +825,7 @@ async function compileReview(issue, reviewer, workspacePath, diffSummary) {
|
|
|
794
825
|
diffSummary
|
|
795
826
|
});
|
|
796
827
|
const adapter = ADAPTERS[reviewer.provider];
|
|
797
|
-
const command = adapter ? adapter.buildReviewCommand(reviewer) : reviewer.command;
|
|
828
|
+
const command = adapter ? adapter.buildReviewCommand(reviewer, config) : reviewer.command;
|
|
798
829
|
return { prompt, command };
|
|
799
830
|
}
|
|
800
831
|
function buildExecutionAudit(provider, compiled, issue, durationMs, result) {
|
|
@@ -863,32 +894,186 @@ function persistExecutionAudit(workspacePath, audit) {
|
|
|
863
894
|
}
|
|
864
895
|
|
|
865
896
|
// src/agents/skills.ts
|
|
866
|
-
import { existsSync as
|
|
897
|
+
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4 } from "fs";
|
|
867
898
|
import { homedir } from "os";
|
|
868
|
-
import { join as
|
|
899
|
+
import { join as join5, resolve } from "path";
|
|
900
|
+
|
|
901
|
+
// src/agents/catalog.ts
|
|
902
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
903
|
+
import { join as join4 } from "path";
|
|
904
|
+
function parseFrontmatter(content) {
|
|
905
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
906
|
+
if (!match) return {};
|
|
907
|
+
const result = {};
|
|
908
|
+
for (const line of match[1].split("\n")) {
|
|
909
|
+
const idx = line.indexOf(":");
|
|
910
|
+
if (idx === -1) continue;
|
|
911
|
+
const key = line.slice(0, idx).trim();
|
|
912
|
+
const value = line.slice(idx + 1).trim().replace(/^["']|["']$/g, "");
|
|
913
|
+
if (key) result[key] = value;
|
|
914
|
+
}
|
|
915
|
+
return result;
|
|
916
|
+
}
|
|
917
|
+
function loadAgentCatalog() {
|
|
918
|
+
const entries = [];
|
|
919
|
+
try {
|
|
920
|
+
const repos = listReferenceRepositories();
|
|
921
|
+
for (const repo of repos) {
|
|
922
|
+
if (!repo.present || !repo.synced) continue;
|
|
923
|
+
const artifacts = collectArtifacts(repo.path, repo.id).filter((a) => a.kind === "agent");
|
|
924
|
+
for (const artifact of artifacts) {
|
|
925
|
+
try {
|
|
926
|
+
const content = readFileSync3(artifact.sourcePath, "utf8");
|
|
927
|
+
const fm = parseFrontmatter(content);
|
|
928
|
+
entries.push({
|
|
929
|
+
name: artifact.targetName,
|
|
930
|
+
displayName: fm.name || artifact.targetName,
|
|
931
|
+
description: fm.description || "",
|
|
932
|
+
emoji: fm.emoji || "\u{1F916}",
|
|
933
|
+
domains: fm.domains ? fm.domains.split(",").map((d) => d.trim()).filter(Boolean) : [],
|
|
934
|
+
source: repo.id,
|
|
935
|
+
content
|
|
936
|
+
});
|
|
937
|
+
} catch (err) {
|
|
938
|
+
logger.warn({ err, path: artifact.sourcePath }, "Failed to read agent file");
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
} catch (error) {
|
|
943
|
+
logger.error({ err: error }, "Failed to load agent catalog from repositories");
|
|
944
|
+
}
|
|
945
|
+
return entries;
|
|
946
|
+
}
|
|
947
|
+
function loadSkillCatalog() {
|
|
948
|
+
return [];
|
|
949
|
+
}
|
|
950
|
+
function filterByDomains(catalog, domains) {
|
|
951
|
+
const domainSet = new Set(domains.map((d) => d.toLowerCase().trim()));
|
|
952
|
+
if (domainSet.size === 0) return catalog;
|
|
953
|
+
const scored = catalog.map((entry) => {
|
|
954
|
+
const matchCount = entry.domains.filter((d) => domainSet.has(d.toLowerCase())).length;
|
|
955
|
+
return { entry, matchCount };
|
|
956
|
+
});
|
|
957
|
+
return scored.filter((item) => item.matchCount > 0).sort((a, b) => b.matchCount - a.matchCount).map((item) => item.entry);
|
|
958
|
+
}
|
|
959
|
+
function installAgents(targetRoot, agentNames, catalog) {
|
|
960
|
+
const result = { installed: [], skipped: [], errors: [] };
|
|
961
|
+
const catalogMap = new Map(catalog.map((entry) => [entry.name, entry]));
|
|
962
|
+
const agentsDir = join4(targetRoot, ".claude", "agents");
|
|
963
|
+
try {
|
|
964
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
965
|
+
} catch (error) {
|
|
966
|
+
logger.error({ err: error, path: agentsDir }, "Failed to create agents directory");
|
|
967
|
+
result.errors.push({ name: "_directory", error: `Failed to create ${agentsDir}` });
|
|
968
|
+
return result;
|
|
969
|
+
}
|
|
970
|
+
for (const name of agentNames) {
|
|
971
|
+
const entry = catalogMap.get(name);
|
|
972
|
+
if (!entry) {
|
|
973
|
+
result.errors.push({ name, error: "Agent not found in catalog" });
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
const filePath = join4(agentsDir, `${name}.md`);
|
|
977
|
+
if (existsSync3(filePath)) {
|
|
978
|
+
result.skipped.push(name);
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
try {
|
|
982
|
+
writeFileSync2(filePath, entry.content, "utf8");
|
|
983
|
+
result.installed.push(name);
|
|
984
|
+
logger.info({ agent: name, path: filePath }, "Agent installed");
|
|
985
|
+
} catch (error) {
|
|
986
|
+
result.errors.push({
|
|
987
|
+
name,
|
|
988
|
+
error: error instanceof Error ? error.message : String(error)
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return result;
|
|
993
|
+
}
|
|
994
|
+
function installSkills(targetRoot, skillNames, catalog) {
|
|
995
|
+
const result = { installed: [], skipped: [], errors: [] };
|
|
996
|
+
const catalogMap = new Map(catalog.map((entry) => [entry.name, entry]));
|
|
997
|
+
const skillsDir = join4(targetRoot, ".claude", "skills");
|
|
998
|
+
try {
|
|
999
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
logger.error({ err: error, path: skillsDir }, "Failed to create skills directory");
|
|
1002
|
+
result.errors.push({ name: "_directory", error: `Failed to create ${skillsDir}` });
|
|
1003
|
+
return result;
|
|
1004
|
+
}
|
|
1005
|
+
for (const name of skillNames) {
|
|
1006
|
+
const entry = catalogMap.get(name);
|
|
1007
|
+
if (!entry) {
|
|
1008
|
+
result.errors.push({ name, error: "Skill not found in catalog" });
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
const skillDir = join4(skillsDir, name);
|
|
1012
|
+
const filePath = join4(skillDir, "SKILL.md");
|
|
1013
|
+
if (existsSync3(filePath)) {
|
|
1014
|
+
result.skipped.push(name);
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
try {
|
|
1018
|
+
mkdirSync(skillDir, { recursive: true });
|
|
1019
|
+
if (entry.installType === "bundled" && entry.content) {
|
|
1020
|
+
writeFileSync2(filePath, entry.content, "utf8");
|
|
1021
|
+
} else {
|
|
1022
|
+
const referenceContent = [
|
|
1023
|
+
`# ${entry.displayName}`,
|
|
1024
|
+
"",
|
|
1025
|
+
entry.description,
|
|
1026
|
+
"",
|
|
1027
|
+
`**Source**: ${entry.source}`,
|
|
1028
|
+
entry.url ? `**URL**: ${entry.url}` : "",
|
|
1029
|
+
"",
|
|
1030
|
+
`> This skill references an external resource. Install it from the source above.`
|
|
1031
|
+
].filter(Boolean).join("\n");
|
|
1032
|
+
writeFileSync2(filePath, referenceContent, "utf8");
|
|
1033
|
+
}
|
|
1034
|
+
result.installed.push(name);
|
|
1035
|
+
logger.info({ skill: name, path: filePath, type: entry.installType }, "Skill installed");
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
result.errors.push({
|
|
1038
|
+
name,
|
|
1039
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return result;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// src/agents/skills.ts
|
|
869
1047
|
function discoverSkills(workspacePath) {
|
|
870
1048
|
const home = homedir();
|
|
871
|
-
const codePath =
|
|
1049
|
+
const codePath = existsSync4(join5(workspacePath, "worktree")) ? join5(workspacePath, "worktree") : workspacePath;
|
|
872
1050
|
const searchPaths = [
|
|
873
1051
|
resolve(codePath, ".codex", "skills"),
|
|
874
1052
|
resolve(codePath, ".claude", "skills"),
|
|
875
|
-
|
|
876
|
-
|
|
1053
|
+
join5(home, ".codex", "skills"),
|
|
1054
|
+
join5(home, ".claude", "skills")
|
|
877
1055
|
];
|
|
878
1056
|
const seen = /* @__PURE__ */ new Set();
|
|
879
1057
|
const skills = [];
|
|
880
1058
|
for (const basePath of searchPaths) {
|
|
881
|
-
if (!
|
|
1059
|
+
if (!existsSync4(basePath)) continue;
|
|
882
1060
|
for (const entry of readdirSync(basePath, { withFileTypes: true })) {
|
|
883
1061
|
if (!entry.isDirectory()) continue;
|
|
884
1062
|
if (seen.has(entry.name)) continue;
|
|
885
|
-
const skillFile =
|
|
886
|
-
if (!
|
|
1063
|
+
const skillFile = join5(basePath, entry.name, "SKILL.md");
|
|
1064
|
+
if (!existsSync4(skillFile)) continue;
|
|
887
1065
|
try {
|
|
888
|
-
const content =
|
|
1066
|
+
const content = readFileSync4(skillFile, "utf8").trim();
|
|
889
1067
|
if (content) {
|
|
890
1068
|
seen.add(entry.name);
|
|
891
|
-
|
|
1069
|
+
const fm = parseFrontmatter(content);
|
|
1070
|
+
skills.push({
|
|
1071
|
+
name: entry.name,
|
|
1072
|
+
content,
|
|
1073
|
+
description: fm.description || void 0,
|
|
1074
|
+
whenToUse: fm.when_to_use || fm.whenToUse || void 0,
|
|
1075
|
+
avoidIf: fm.avoid_if || fm.avoidIf || void 0
|
|
1076
|
+
});
|
|
892
1077
|
}
|
|
893
1078
|
} catch {
|
|
894
1079
|
}
|
|
@@ -906,8 +1091,141 @@ ${skill.content}`
|
|
|
906
1091
|
|
|
907
1092
|
${sections.join("\n\n")}`;
|
|
908
1093
|
}
|
|
1094
|
+
function extractFirstLine(content) {
|
|
1095
|
+
for (const line of content.split("\n")) {
|
|
1096
|
+
const trimmed = line.replace(/^#+\s*/, "").trim();
|
|
1097
|
+
if (trimmed && !trimmed.startsWith("---")) return trimmed;
|
|
1098
|
+
}
|
|
1099
|
+
return "";
|
|
1100
|
+
}
|
|
1101
|
+
function discoverAgents(workspacePath) {
|
|
1102
|
+
const home = homedir();
|
|
1103
|
+
const codePath = existsSync4(join5(workspacePath, "worktree")) ? join5(workspacePath, "worktree") : workspacePath;
|
|
1104
|
+
const searchPaths = [
|
|
1105
|
+
resolve(codePath, ".claude", "agents"),
|
|
1106
|
+
resolve(codePath, ".codex", "agents"),
|
|
1107
|
+
join5(home, ".claude", "agents"),
|
|
1108
|
+
join5(home, ".codex", "agents")
|
|
1109
|
+
];
|
|
1110
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1111
|
+
const agents = [];
|
|
1112
|
+
for (const basePath of searchPaths) {
|
|
1113
|
+
if (!existsSync4(basePath)) continue;
|
|
1114
|
+
for (const entry of readdirSync(basePath, { withFileTypes: true })) {
|
|
1115
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1116
|
+
const name = entry.name.replace(/\.md$/, "");
|
|
1117
|
+
if (seen.has(name)) continue;
|
|
1118
|
+
try {
|
|
1119
|
+
const content = readFileSync4(join5(basePath, entry.name), "utf8").trim();
|
|
1120
|
+
if (content) {
|
|
1121
|
+
seen.add(name);
|
|
1122
|
+
const fm = parseFrontmatter(content);
|
|
1123
|
+
agents.push({
|
|
1124
|
+
name,
|
|
1125
|
+
description: fm.description || extractFirstLine(content),
|
|
1126
|
+
whenToUse: fm.when_to_use || fm.whenToUse || void 0,
|
|
1127
|
+
avoidIf: fm.avoid_if || fm.avoidIf || void 0
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
} catch {
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
return agents;
|
|
1135
|
+
}
|
|
1136
|
+
function discoverCommands(workspacePath) {
|
|
1137
|
+
const home = homedir();
|
|
1138
|
+
const codePath = existsSync4(join5(workspacePath, "worktree")) ? join5(workspacePath, "worktree") : workspacePath;
|
|
1139
|
+
const searchPaths = [
|
|
1140
|
+
resolve(codePath, ".claude", "commands"),
|
|
1141
|
+
resolve(codePath, ".codex", "commands"),
|
|
1142
|
+
join5(home, ".claude", "commands"),
|
|
1143
|
+
join5(home, ".codex", "commands")
|
|
1144
|
+
];
|
|
1145
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1146
|
+
const commands = [];
|
|
1147
|
+
for (const basePath of searchPaths) {
|
|
1148
|
+
if (!existsSync4(basePath)) continue;
|
|
1149
|
+
for (const entry of readdirSync(basePath, { withFileTypes: true })) {
|
|
1150
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1151
|
+
const name = entry.name.replace(/\.md$/, "");
|
|
1152
|
+
if (seen.has(name)) continue;
|
|
1153
|
+
try {
|
|
1154
|
+
const content = readFileSync4(join5(basePath, entry.name), "utf8").trim();
|
|
1155
|
+
if (content) {
|
|
1156
|
+
seen.add(name);
|
|
1157
|
+
commands.push({ name, description: extractFirstLine(content) });
|
|
1158
|
+
}
|
|
1159
|
+
} catch {
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return commands;
|
|
1164
|
+
}
|
|
1165
|
+
var MAX_CAPABILITIES_ITEMS = 40;
|
|
1166
|
+
function buildCapabilitiesManifest(skills, agents, commands) {
|
|
1167
|
+
if (skills.length === 0 && agents.length === 0 && commands.length === 0) return "";
|
|
1168
|
+
const sections = ["## Your Capabilities"];
|
|
1169
|
+
let itemCount = 0;
|
|
1170
|
+
if (commands.length > 0) {
|
|
1171
|
+
sections.push("");
|
|
1172
|
+
sections.push("### Slash Commands");
|
|
1173
|
+
sections.push("You have these commands available. Invoke with `/command-name`:");
|
|
1174
|
+
const show = commands.slice(0, MAX_CAPABILITIES_ITEMS);
|
|
1175
|
+
for (const cmd of show) {
|
|
1176
|
+
sections.push(`- \`/${cmd.name}\`${cmd.description ? ` \u2014 ${cmd.description}` : ""}`);
|
|
1177
|
+
itemCount++;
|
|
1178
|
+
}
|
|
1179
|
+
if (commands.length > show.length) {
|
|
1180
|
+
sections.push(`- ...and ${commands.length - show.length} more available`);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
if (skills.length > 0) {
|
|
1184
|
+
const remaining = Math.max(5, MAX_CAPABILITIES_ITEMS - itemCount);
|
|
1185
|
+
sections.push("");
|
|
1186
|
+
sections.push("### Skills");
|
|
1187
|
+
sections.push("Specialized procedures available for this workspace:");
|
|
1188
|
+
const show = skills.slice(0, remaining);
|
|
1189
|
+
for (const skill of show) {
|
|
1190
|
+
let line = `- **${skill.name}**`;
|
|
1191
|
+
if (skill.description) line += ` \u2014 ${skill.description}`;
|
|
1192
|
+
if (skill.whenToUse) line += ` (Use when: ${skill.whenToUse})`;
|
|
1193
|
+
sections.push(line);
|
|
1194
|
+
itemCount++;
|
|
1195
|
+
}
|
|
1196
|
+
if (skills.length > show.length) {
|
|
1197
|
+
sections.push(`- ...and ${skills.length - show.length} more available`);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
if (agents.length > 0) {
|
|
1201
|
+
const remaining = Math.max(5, MAX_CAPABILITIES_ITEMS - itemCount);
|
|
1202
|
+
sections.push("");
|
|
1203
|
+
sections.push("### Subagents");
|
|
1204
|
+
sections.push("You can delegate to these specialist agents via the Agent tool:");
|
|
1205
|
+
const show = agents.slice(0, remaining);
|
|
1206
|
+
for (const agent of show) {
|
|
1207
|
+
let line = `- **${agent.name}**`;
|
|
1208
|
+
if (agent.description) line += ` \u2014 ${agent.description}`;
|
|
1209
|
+
if (agent.whenToUse) line += ` (Use when: ${agent.whenToUse})`;
|
|
1210
|
+
if (agent.avoidIf) line += ` (Avoid if: ${agent.avoidIf})`;
|
|
1211
|
+
sections.push(line);
|
|
1212
|
+
}
|
|
1213
|
+
if (agents.length > show.length) {
|
|
1214
|
+
sections.push(`- ...and ${agents.length - show.length} more available`);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
sections.push("");
|
|
1218
|
+
sections.push("When a task matches a capability above, USE IT instead of doing everything manually.");
|
|
1219
|
+
return sections.join("\n");
|
|
1220
|
+
}
|
|
909
1221
|
|
|
910
1222
|
// src/agents/agent-pipeline.ts
|
|
1223
|
+
function resolveOutputFileName(role, planVersion, attempt, turn) {
|
|
1224
|
+
if (role === "planner") {
|
|
1225
|
+
return `plan.v${planVersion}.t${turn}.stdout.log`;
|
|
1226
|
+
}
|
|
1227
|
+
return `${role === "reviewer" ? "review" : "execute"}.v${planVersion}a${attempt}.t${turn}.stdout.log`;
|
|
1228
|
+
}
|
|
911
1229
|
async function runAgentSession(state, issue, provider, cycle, workspacePath, basePromptText, basePromptFile) {
|
|
912
1230
|
const maxTurns = clamp(state.config.maxTurns, 1, 16);
|
|
913
1231
|
const attempt = issue.attempts + 1;
|
|
@@ -919,7 +1237,7 @@ async function runAgentSession(state, issue, provider, cycle, workspacePath, bas
|
|
|
919
1237
|
let nextPrompt = session.nextPrompt;
|
|
920
1238
|
let lastCode = session.lastCode;
|
|
921
1239
|
let lastOutput = session.lastOutput;
|
|
922
|
-
const resultFile =
|
|
1240
|
+
const resultFile = join6(workspacePath, `result-${provider.role}-${provider.provider}.json`);
|
|
923
1241
|
if (session.status === "done" && session.turns.length > 0) {
|
|
924
1242
|
logger.debug({ issueId: issue.id, identifier: issue.identifier, provider: provider.provider, role: provider.role }, "[Agent] Session already completed, returning cached result");
|
|
925
1243
|
return { success: true, blocked: false, continueRequested: false, code: session.lastCode, output: session.lastOutput, turns: session.turns.length };
|
|
@@ -936,14 +1254,23 @@ Agent requested additional turns beyond configured limit (${maxTurns}).`;
|
|
|
936
1254
|
const compactedOutput = previousOutput.length > maxOutputChars ? `[...${previousOutput.length - maxOutputChars} chars truncated...]
|
|
937
1255
|
${previousOutput.slice(-maxOutputChars)}` : previousOutput;
|
|
938
1256
|
const turnPrompt = await buildTurnPrompt(issue, basePromptText, compactedOutput, turnIndex, maxTurns, nextPrompt);
|
|
939
|
-
const turnPromptFile = turnIndex === 1 ? basePromptFile :
|
|
940
|
-
if (turnIndex > 1)
|
|
1257
|
+
const turnPromptFile = turnIndex === 1 ? basePromptFile : join6(workspacePath, `turn-${String(turnIndex).padStart(2, "0")}.md`);
|
|
1258
|
+
if (turnIndex > 1) writeFileSync3(turnPromptFile, `${turnPrompt}
|
|
941
1259
|
`, "utf8");
|
|
942
1260
|
session.status = "running";
|
|
943
1261
|
session.lastPrompt = turnPrompt;
|
|
944
1262
|
session.lastPromptFile = turnPromptFile;
|
|
945
1263
|
session.maxTurns = maxTurns;
|
|
946
1264
|
await persistAgentSessionState(sessionKey, issue, provider, cycle, session);
|
|
1265
|
+
const outputsDir = join6(workspacePath, "outputs");
|
|
1266
|
+
mkdirSync2(outputsDir, { recursive: true });
|
|
1267
|
+
const outputFileName = resolveOutputFileName(
|
|
1268
|
+
provider.role,
|
|
1269
|
+
issue.planVersion ?? 1,
|
|
1270
|
+
provider.role === "planner" ? 0 : issue.executeAttempt ?? 1,
|
|
1271
|
+
turnIndex
|
|
1272
|
+
);
|
|
1273
|
+
const outputFilePath = join6(outputsDir, outputFileName);
|
|
947
1274
|
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");
|
|
948
1275
|
const turnStartedAt = now();
|
|
949
1276
|
const turnEnv = {
|
|
@@ -967,7 +1294,7 @@ ${previousOutput.slice(-maxOutputChars)}` : previousOutput;
|
|
|
967
1294
|
await runHook(state.config.beforeRunHook, workspacePath, issue, "before_run", turnEnv);
|
|
968
1295
|
}
|
|
969
1296
|
addEvent(state, issue.id, "runner", `Turn ${turnIndex}/${maxTurns} started for ${issue.identifier}.`);
|
|
970
|
-
const turnResult = await runCommandWithTimeout(provider.command, workspacePath, issue, state.config, turnPrompt, turnPromptFile, turnEnv);
|
|
1297
|
+
const turnResult = await runCommandWithTimeout(provider.command, workspacePath, issue, state.config, turnPrompt, turnPromptFile, turnEnv, outputFilePath);
|
|
971
1298
|
if (state.config.afterRunHook) {
|
|
972
1299
|
await runHook(state.config.afterRunHook, workspacePath, issue, "after_run", {
|
|
973
1300
|
...turnEnv,
|
|
@@ -1056,10 +1383,13 @@ async function runAgentPipeline(state, issue, workspacePath, basePromptText, bas
|
|
|
1056
1383
|
const executorIndex = providers.findIndex((provider) => provider.role === "executor");
|
|
1057
1384
|
const skills = discoverSkills(workspacePath);
|
|
1058
1385
|
const skillContext = buildSkillContext(skills);
|
|
1386
|
+
const agents = discoverAgents(workspacePath);
|
|
1387
|
+
const commands = discoverCommands(workspacePath);
|
|
1388
|
+
const capabilitiesManifest = buildCapabilitiesManifest(skills, agents, commands);
|
|
1059
1389
|
if (skillContext) {
|
|
1060
|
-
|
|
1390
|
+
writeFileSync3(join6(workspacePath, "skills.md"), skillContext, "utf8");
|
|
1061
1391
|
}
|
|
1062
|
-
const compiled = await compileExecution(issue, activeProvider, state.config, workspacePath, skillContext);
|
|
1392
|
+
const compiled = await compileExecution(issue, activeProvider, state.config, workspacePath, skillContext, capabilitiesManifest);
|
|
1063
1393
|
let providerPrompt;
|
|
1064
1394
|
let effectiveProvider = activeProvider;
|
|
1065
1395
|
if (compiled) {
|
|
@@ -1073,16 +1403,24 @@ async function runAgentPipeline(state, issue, workspacePath, basePromptText, bas
|
|
|
1073
1403
|
`Plan compiled for ${compiled.meta.adapter}: effort=${compiled.meta.reasoningEffort}, skills=[${compiled.meta.skillsActivated.join(",")}], subagents=[${compiled.meta.subagentsRequested.join(",")}].`
|
|
1074
1404
|
);
|
|
1075
1405
|
if (Object.keys(compiled.env).length > 0) {
|
|
1076
|
-
const envFile =
|
|
1406
|
+
const envFile = join6(workspacePath, ".compiled-env.sh");
|
|
1077
1407
|
const envLines = Object.entries(compiled.env).map(([k, v]) => `export ${k}=${JSON.stringify(v)}`).join("\n");
|
|
1078
|
-
|
|
1408
|
+
writeFileSync3(envFile, envLines, "utf8");
|
|
1079
1409
|
}
|
|
1080
1410
|
} else {
|
|
1081
|
-
providerPrompt = await buildProviderBasePrompt(activeProvider, issue, basePromptText, workspacePath, skillContext);
|
|
1411
|
+
providerPrompt = await buildProviderBasePrompt(activeProvider, issue, basePromptText, workspacePath, skillContext, capabilitiesManifest);
|
|
1082
1412
|
}
|
|
1083
1413
|
if (!effectiveProvider.command.trim()) {
|
|
1084
1414
|
throw new Error(`No command configured for provider ${effectiveProvider.provider} (${effectiveProvider.role}).`);
|
|
1085
1415
|
}
|
|
1416
|
+
if (issue.attempts > 0) {
|
|
1417
|
+
const retryCtx = buildRetryContext(issue);
|
|
1418
|
+
if (retryCtx) {
|
|
1419
|
+
providerPrompt = `${providerPrompt}
|
|
1420
|
+
|
|
1421
|
+
${retryCtx}`;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1086
1424
|
pipeline.history.push(`[${now()}] Running ${effectiveProvider.role}:${effectiveProvider.provider} in cycle ${pipeline.cycle}${compiled ? ` [${compiled.meta.adapter} adapter]` : ""}.`);
|
|
1087
1425
|
await persistAgentPipelineState(pipelineFile, pipeline);
|
|
1088
1426
|
const result = await runAgentSession(state, issue, effectiveProvider, pipeline.cycle, workspacePath, providerPrompt, basePromptFile);
|
|
@@ -1206,7 +1544,6 @@ function applyPlanUsage(issue, usage) {
|
|
|
1206
1544
|
}
|
|
1207
1545
|
function applyPlanSuggestions(issue, plan) {
|
|
1208
1546
|
if (plan.suggestedPaths?.length && !issue.paths?.length) issue.paths = plan.suggestedPaths;
|
|
1209
|
-
if (plan.suggestedLabels?.length && !issue.labels?.length) issue.labels = plan.suggestedLabels;
|
|
1210
1547
|
if (plan.suggestedEffort && !issue.effort) issue.effort = plan.suggestedEffort;
|
|
1211
1548
|
}
|
|
1212
1549
|
async function mutateIssueState(state, c, updater) {
|
|
@@ -1240,30 +1577,11 @@ function createS3dbEventStore(state) {
|
|
|
1240
1577
|
};
|
|
1241
1578
|
}
|
|
1242
1579
|
|
|
1243
|
-
// src/persistence/s3queue-adapter.ts
|
|
1244
|
-
function createS3QueueAdapter() {
|
|
1245
|
-
return {
|
|
1246
|
-
async enqueueForPlanning(issue) {
|
|
1247
|
-
return enqueueForPlanning(issue);
|
|
1248
|
-
},
|
|
1249
|
-
async enqueueForExecution(issue) {
|
|
1250
|
-
return enqueueForExecution(issue);
|
|
1251
|
-
},
|
|
1252
|
-
async enqueueForReview(issue) {
|
|
1253
|
-
return enqueueForReview(issue);
|
|
1254
|
-
},
|
|
1255
|
-
isActive() {
|
|
1256
|
-
return areQueueWorkersActive();
|
|
1257
|
-
}
|
|
1258
|
-
};
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
1580
|
// src/persistence/container.ts
|
|
1262
1581
|
var _container = null;
|
|
1263
1582
|
function createContainer(state) {
|
|
1264
1583
|
const issueRepository = createS3dbIssueRepository(state);
|
|
1265
1584
|
const eventStore = createS3dbEventStore(state);
|
|
1266
|
-
const queuePort = createS3QueueAdapter();
|
|
1267
1585
|
const persistencePort = {
|
|
1268
1586
|
persistState: (s) => persistState(s),
|
|
1269
1587
|
loadState: async () => null
|
|
@@ -1271,7 +1589,6 @@ function createContainer(state) {
|
|
|
1271
1589
|
const container = {
|
|
1272
1590
|
issueRepository,
|
|
1273
1591
|
eventStore,
|
|
1274
|
-
queuePort,
|
|
1275
1592
|
persistencePort
|
|
1276
1593
|
};
|
|
1277
1594
|
_container = container;
|
|
@@ -1286,19 +1603,19 @@ function getContainer() {
|
|
|
1286
1603
|
}
|
|
1287
1604
|
|
|
1288
1605
|
// src/commands/create-issue.command.ts
|
|
1289
|
-
import { existsSync as
|
|
1290
|
-
import { basename, join as
|
|
1606
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, renameSync } from "fs";
|
|
1607
|
+
import { basename, join as join7 } from "path";
|
|
1291
1608
|
async function createIssueCommand(input, deps) {
|
|
1292
1609
|
const { payload, state } = input;
|
|
1293
1610
|
const issue = createIssueFromPayload(payload, state.issues, state.config.defaultBranch);
|
|
1294
1611
|
const tempImages = Array.isArray(payload.images) ? payload.images : [];
|
|
1295
1612
|
if (tempImages.length) {
|
|
1296
|
-
const issueAttachDir =
|
|
1297
|
-
|
|
1613
|
+
const issueAttachDir = join7(ATTACHMENTS_ROOT, issue.id);
|
|
1614
|
+
mkdirSync3(issueAttachDir, { recursive: true });
|
|
1298
1615
|
const finalPaths = [];
|
|
1299
1616
|
for (const tempPath of tempImages) {
|
|
1300
|
-
if (typeof tempPath === "string" &&
|
|
1301
|
-
const dest =
|
|
1617
|
+
if (typeof tempPath === "string" && existsSync5(tempPath)) {
|
|
1618
|
+
const dest = join7(issueAttachDir, basename(tempPath));
|
|
1302
1619
|
try {
|
|
1303
1620
|
renameSync(tempPath, dest);
|
|
1304
1621
|
finalPaths.push(dest);
|
|
@@ -1317,13 +1634,13 @@ async function createIssueCommand(input, deps) {
|
|
|
1317
1634
|
}
|
|
1318
1635
|
await deps.persistencePort.persistState(state);
|
|
1319
1636
|
if (issue.state === "Planning") {
|
|
1320
|
-
|
|
1637
|
+
enqueue(issue, "plan").catch(() => {
|
|
1321
1638
|
});
|
|
1322
1639
|
} else if (issue.state === "Queued" || issue.state === "Running") {
|
|
1323
|
-
|
|
1640
|
+
enqueue(issue, "execute").catch(() => {
|
|
1324
1641
|
});
|
|
1325
1642
|
} else if (issue.state === "Reviewing") {
|
|
1326
|
-
|
|
1643
|
+
enqueue(issue, "review").catch(() => {
|
|
1327
1644
|
});
|
|
1328
1645
|
}
|
|
1329
1646
|
return { issue };
|
|
@@ -1503,17 +1820,12 @@ var issues_resource_default = {
|
|
|
1503
1820
|
identifier: "string|required",
|
|
1504
1821
|
title: "string|required",
|
|
1505
1822
|
description: "string|optional",
|
|
1506
|
-
priority: "number|required",
|
|
1507
1823
|
state: "string|required",
|
|
1508
1824
|
branchName: "string|optional",
|
|
1509
1825
|
url: "string|optional",
|
|
1510
1826
|
assigneeId: "string|optional",
|
|
1511
1827
|
labels: "json|required",
|
|
1512
1828
|
paths: "json|optional",
|
|
1513
|
-
inferredPaths: "json|optional",
|
|
1514
|
-
capabilityCategory: "string|optional",
|
|
1515
|
-
capabilityOverlays: "json|optional",
|
|
1516
|
-
capabilityRationale: "json|optional",
|
|
1517
1829
|
blockedBy: "json|required",
|
|
1518
1830
|
assignedToWorker: "boolean|required",
|
|
1519
1831
|
createdAt: "datetime|required",
|
|
@@ -1561,10 +1873,6 @@ var issues_resource_default = {
|
|
|
1561
1873
|
},
|
|
1562
1874
|
partitions: {
|
|
1563
1875
|
byState: { fields: { state: "string" } },
|
|
1564
|
-
byCapabilityCategory: { fields: { capabilityCategory: "string" } },
|
|
1565
|
-
byStateAndCapability: {
|
|
1566
|
-
fields: { state: "string", capabilityCategory: "string" }
|
|
1567
|
-
},
|
|
1568
1876
|
byTerminalWeek: { fields: { terminalWeek: "string" } }
|
|
1569
1877
|
},
|
|
1570
1878
|
asyncPartitions: true,
|
|
@@ -1801,7 +2109,6 @@ function broadcastToWebSocketClients(message) {
|
|
|
1801
2109
|
type: "state:delta",
|
|
1802
2110
|
seq: broadcastSeq,
|
|
1803
2111
|
metrics: message.metrics,
|
|
1804
|
-
capabilities: message.capabilities,
|
|
1805
2112
|
updatedAt: message.updatedAt,
|
|
1806
2113
|
issuesDelta: changedIssues,
|
|
1807
2114
|
issuesRemoved: removedIds,
|
|
@@ -1835,7 +2142,6 @@ function makeWebSocketConfig(state) {
|
|
|
1835
2142
|
seq: broadcastSeq,
|
|
1836
2143
|
timestamp: now(),
|
|
1837
2144
|
metrics: computeMetrics(state.issues),
|
|
1838
|
-
capabilities: computeCapabilityCounts(state.issues),
|
|
1839
2145
|
issues: state.issues,
|
|
1840
2146
|
events: state.events.slice(0, 50)
|
|
1841
2147
|
}));
|
|
@@ -1864,7 +2170,7 @@ var shuttingDown = false;
|
|
|
1864
2170
|
function isShuttingDown() {
|
|
1865
2171
|
return shuttingDown;
|
|
1866
2172
|
}
|
|
1867
|
-
function installGracefulShutdown(state
|
|
2173
|
+
function installGracefulShutdown(state) {
|
|
1868
2174
|
const handler = async (signal) => {
|
|
1869
2175
|
if (shuttingDown) {
|
|
1870
2176
|
logger.warn(`Received ${signal} again, forcing exit.`);
|
|
@@ -1875,7 +2181,7 @@ function installGracefulShutdown(state, running) {
|
|
|
1875
2181
|
const container = getContainer();
|
|
1876
2182
|
container.eventStore.addEvent(void 0, "info", `Graceful shutdown initiated (${signal}).`);
|
|
1877
2183
|
for (const issue of state.issues) {
|
|
1878
|
-
if (
|
|
2184
|
+
if (issue.state === "Running" || issue.state === "Reviewing") {
|
|
1879
2185
|
try {
|
|
1880
2186
|
await transitionIssueCommand({ issue, target: "Queued", note: `Interrupted by ${signal} \u2014 queued for resume on next start.`, fallbackToLocal: true }, container);
|
|
1881
2187
|
} catch {
|
|
@@ -1907,7 +2213,7 @@ function installGracefulShutdown(state, running) {
|
|
|
1907
2213
|
}
|
|
1908
2214
|
function analyzeParallelizability(issues) {
|
|
1909
2215
|
const todo = issues.filter(
|
|
1910
|
-
(issue) => issue.state === "
|
|
2216
|
+
(issue) => issue.state === "PendingApproval" && issue.assignedToWorker && issue.blockedBy.length === 0
|
|
1911
2217
|
);
|
|
1912
2218
|
if (todo.length === 0) {
|
|
1913
2219
|
return {
|
|
@@ -1917,7 +2223,7 @@ function analyzeParallelizability(issues) {
|
|
|
1917
2223
|
groups: []
|
|
1918
2224
|
};
|
|
1919
2225
|
}
|
|
1920
|
-
const getIssuePaths = (issue) => /* @__PURE__ */ new Set([...issue.paths ?? []
|
|
2226
|
+
const getIssuePaths = (issue) => /* @__PURE__ */ new Set([...issue.paths ?? []]);
|
|
1921
2227
|
const hasPathOverlap = (a, b) => {
|
|
1922
2228
|
const pathsA = getIssuePaths(a);
|
|
1923
2229
|
const pathsB = getIssuePaths(b);
|
|
@@ -1986,6 +2292,9 @@ async function ensureNotStale(state, staleTimeoutMs) {
|
|
|
1986
2292
|
if (pidDead) {
|
|
1987
2293
|
logger.info({ issueId: issue.id, identifier: issue.identifier, state: issue.state, pid: agentStatus.pid?.pid }, "[Scheduler] PID dead \u2014 silently recovering to Queued");
|
|
1988
2294
|
issue.startedAt = void 0;
|
|
2295
|
+
issue.lastError = `Agent process died unexpectedly (PID ${agentStatus.pid.pid}).`;
|
|
2296
|
+
issue.lastFailedPhase = "crash";
|
|
2297
|
+
issue.attempts = (issue.attempts ?? 0) + 1;
|
|
1989
2298
|
container.issueRepository.markDirty(issue.id);
|
|
1990
2299
|
await transitionIssueCommand({ issue, target: "Queued", note: `Agent process died (PID ${agentStatus.pid.pid}) \u2014 auto-recovering.` }, container);
|
|
1991
2300
|
container.eventStore.addEvent(issue.id, "info", `Issue ${issue.identifier} agent process died (PID ${agentStatus.pid.pid}), silently recovered to Queued.`);
|
|
@@ -2011,8 +2320,8 @@ function hasTerminalQueue(state) {
|
|
|
2011
2320
|
// src/agents/providers-usage.ts
|
|
2012
2321
|
import { execFile } from "child_process";
|
|
2013
2322
|
import { promisify } from "util";
|
|
2014
|
-
import { existsSync as
|
|
2015
|
-
import { join as
|
|
2323
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync2, realpathSync } from "fs";
|
|
2324
|
+
import { join as join8, dirname } from "path";
|
|
2016
2325
|
import { homedir as homedir2 } from "os";
|
|
2017
2326
|
import { env } from "process";
|
|
2018
2327
|
var execFileAsync = promisify(execFile);
|
|
@@ -2039,24 +2348,24 @@ function resolveCodexHomeCandidates() {
|
|
|
2039
2348
|
]);
|
|
2040
2349
|
const candidates = [...homePaths, ...direct].filter(Boolean).flatMap((candidate) => {
|
|
2041
2350
|
if (candidate.endsWith("/.codex") || candidate.endsWith("/codex")) return [candidate];
|
|
2042
|
-
return [
|
|
2351
|
+
return [join8(candidate, ".codex"), join8(candidate, "codex")];
|
|
2043
2352
|
});
|
|
2044
2353
|
return [...new Set(candidates)];
|
|
2045
2354
|
}
|
|
2046
2355
|
function resolveCodexDir() {
|
|
2047
2356
|
for (const candidate of resolveCodexHomeCandidates()) {
|
|
2048
|
-
if (
|
|
2357
|
+
if (existsSync6(candidate)) {
|
|
2049
2358
|
return candidate;
|
|
2050
2359
|
}
|
|
2051
2360
|
}
|
|
2052
2361
|
return null;
|
|
2053
2362
|
}
|
|
2054
2363
|
function findLatestCodexDb(codexDir) {
|
|
2055
|
-
const explicit =
|
|
2056
|
-
if (
|
|
2364
|
+
const explicit = join8(codexDir, "state_5.sqlite");
|
|
2365
|
+
if (existsSync6(explicit)) return explicit;
|
|
2057
2366
|
const candidates = readdirSync2(codexDir).filter((name) => name.startsWith("state_") && name.endsWith(".sqlite")).sort().reverse();
|
|
2058
2367
|
if (candidates.length === 0) return null;
|
|
2059
|
-
return
|
|
2368
|
+
return join8(codexDir, candidates[0]);
|
|
2060
2369
|
}
|
|
2061
2370
|
function computeNextMonday() {
|
|
2062
2371
|
const now2 = /* @__PURE__ */ new Date();
|
|
@@ -2093,10 +2402,10 @@ var CLAUDE_PLAN_LIMITS = {
|
|
|
2093
2402
|
};
|
|
2094
2403
|
async function collectClaudeUsage() {
|
|
2095
2404
|
const home = homedir2();
|
|
2096
|
-
const claudeDir =
|
|
2097
|
-
if (!
|
|
2405
|
+
const claudeDir = join8(home, ".claude");
|
|
2406
|
+
if (!existsSync6(claudeDir)) return null;
|
|
2098
2407
|
const available = await whichExists("claude");
|
|
2099
|
-
const projectsDir =
|
|
2408
|
+
const projectsDir = join8(claudeDir, "projects");
|
|
2100
2409
|
let totalInputTokens = 0;
|
|
2101
2410
|
let totalOutputTokens = 0;
|
|
2102
2411
|
let totalSessions = 0;
|
|
@@ -2110,12 +2419,12 @@ async function collectClaudeUsage() {
|
|
|
2110
2419
|
const todayMs = todayStart.getTime();
|
|
2111
2420
|
const weekStart = computeWeekStart();
|
|
2112
2421
|
const weekMs = weekStart.getTime();
|
|
2113
|
-
if (
|
|
2422
|
+
if (existsSync6(projectsDir)) {
|
|
2114
2423
|
try {
|
|
2115
2424
|
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true });
|
|
2116
2425
|
for (const dir of projectDirs) {
|
|
2117
2426
|
if (!dir.isDirectory()) continue;
|
|
2118
|
-
const projectPath =
|
|
2427
|
+
const projectPath = join8(projectsDir, dir.name);
|
|
2119
2428
|
let sessionFiles;
|
|
2120
2429
|
try {
|
|
2121
2430
|
sessionFiles = readdirSync2(projectPath).filter((f) => f.endsWith(".jsonl"));
|
|
@@ -2123,10 +2432,10 @@ async function collectClaudeUsage() {
|
|
|
2123
2432
|
continue;
|
|
2124
2433
|
}
|
|
2125
2434
|
for (const file of sessionFiles) {
|
|
2126
|
-
const filePath =
|
|
2435
|
+
const filePath = join8(projectPath, file);
|
|
2127
2436
|
let content;
|
|
2128
2437
|
try {
|
|
2129
|
-
content =
|
|
2438
|
+
content = readFileSync5(filePath, "utf8");
|
|
2130
2439
|
} catch {
|
|
2131
2440
|
continue;
|
|
2132
2441
|
}
|
|
@@ -2181,10 +2490,10 @@ async function collectClaudeUsage() {
|
|
|
2181
2490
|
let plan = "pro";
|
|
2182
2491
|
let resetInfo = "Weekly reset (every Monday 00:00 UTC)";
|
|
2183
2492
|
let currentModel = "";
|
|
2184
|
-
const settingsPath =
|
|
2185
|
-
if (
|
|
2493
|
+
const settingsPath = join8(claudeDir, "settings.json");
|
|
2494
|
+
if (existsSync6(settingsPath)) {
|
|
2186
2495
|
try {
|
|
2187
|
-
const settings = JSON.parse(
|
|
2496
|
+
const settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
|
|
2188
2497
|
if (settings.plan === "max" || settings.plan === "max5x") {
|
|
2189
2498
|
plan = settings.plan;
|
|
2190
2499
|
resetInfo = `Plan: ${settings.plan.toUpperCase()} \u2014 Weekly token limit resets every Monday 00:00 UTC`;
|
|
@@ -2220,11 +2529,11 @@ async function collectCodexUsage() {
|
|
|
2220
2529
|
if (!codexDir) return null;
|
|
2221
2530
|
const available = await whichExists("codex");
|
|
2222
2531
|
const models = [];
|
|
2223
|
-
const modelsCachePath =
|
|
2532
|
+
const modelsCachePath = join8(codexDir, "models_cache.json");
|
|
2224
2533
|
let currentModel = "";
|
|
2225
|
-
if (
|
|
2534
|
+
if (existsSync6(modelsCachePath)) {
|
|
2226
2535
|
try {
|
|
2227
|
-
const cache = JSON.parse(
|
|
2536
|
+
const cache = JSON.parse(readFileSync5(modelsCachePath, "utf8"));
|
|
2228
2537
|
for (const m of cache.models || []) {
|
|
2229
2538
|
models.push({
|
|
2230
2539
|
slug: m.slug,
|
|
@@ -2235,10 +2544,10 @@ async function collectCodexUsage() {
|
|
|
2235
2544
|
} catch {
|
|
2236
2545
|
}
|
|
2237
2546
|
}
|
|
2238
|
-
const configPath =
|
|
2239
|
-
if (
|
|
2547
|
+
const configPath = join8(codexDir, "config.toml");
|
|
2548
|
+
if (existsSync6(configPath)) {
|
|
2240
2549
|
try {
|
|
2241
|
-
const configContent =
|
|
2550
|
+
const configContent = readFileSync5(configPath, "utf8");
|
|
2242
2551
|
const modelMatch = configContent.match(/^model\s*=\s*"([^"]+)"/m);
|
|
2243
2552
|
if (modelMatch) currentModel = modelMatch[1];
|
|
2244
2553
|
} catch {
|
|
@@ -2327,9 +2636,9 @@ async function collectGeminiUsage() {
|
|
|
2327
2636
|
try {
|
|
2328
2637
|
const { stdout: binPath } = await execFileAsync("which", ["gemini"], { encoding: "utf8", timeout: 3e3 });
|
|
2329
2638
|
const realBin = realpathSync(binPath.trim());
|
|
2330
|
-
const modelsPath =
|
|
2331
|
-
if (
|
|
2332
|
-
const content =
|
|
2639
|
+
const modelsPath = join8(dirname(dirname(realBin)), "node_modules", "@google", "gemini-cli-core", "dist", "src", "config", "models.js");
|
|
2640
|
+
if (existsSync6(modelsPath)) {
|
|
2641
|
+
const content = readFileSync5(modelsPath, "utf8");
|
|
2333
2642
|
const regex = /export const ([A-Z0-9_]+)\s*=\s*'(gemini-[^']+)';/g;
|
|
2334
2643
|
const seen = /* @__PURE__ */ new Set();
|
|
2335
2644
|
let m;
|
|
@@ -2347,10 +2656,10 @@ async function collectGeminiUsage() {
|
|
|
2347
2656
|
} catch {
|
|
2348
2657
|
}
|
|
2349
2658
|
let currentModel = "";
|
|
2350
|
-
const settingsPath =
|
|
2351
|
-
if (
|
|
2659
|
+
const settingsPath = join8(homedir2(), ".gemini", "settings.json");
|
|
2660
|
+
if (existsSync6(settingsPath)) {
|
|
2352
2661
|
try {
|
|
2353
|
-
const settings = JSON.parse(
|
|
2662
|
+
const settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
|
|
2354
2663
|
if (typeof settings.model === "string" && settings.model.trim()) {
|
|
2355
2664
|
currentModel = settings.model.trim();
|
|
2356
2665
|
}
|
|
@@ -2394,10 +2703,10 @@ async function collectProvidersUsage() {
|
|
|
2394
2703
|
}
|
|
2395
2704
|
|
|
2396
2705
|
// src/routes/state.ts
|
|
2397
|
-
import { existsSync as
|
|
2706
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
2398
2707
|
import { randomUUID } from "crypto";
|
|
2399
|
-
import { execSync as
|
|
2400
|
-
import { basename as basename2, extname, join as
|
|
2708
|
+
import { execSync as execSync3 } from "child_process";
|
|
2709
|
+
import { basename as basename2, extname, join as join9 } from "path";
|
|
2401
2710
|
|
|
2402
2711
|
// src/commands/approve-plan.command.ts
|
|
2403
2712
|
async function approvePlanCommand(input, deps) {
|
|
@@ -2406,7 +2715,7 @@ async function approvePlanCommand(input, deps) {
|
|
|
2406
2715
|
throw new Error(`Cannot approve issue in state ${issue.state}. Must be in Planning.`);
|
|
2407
2716
|
}
|
|
2408
2717
|
await transitionIssueCommand(
|
|
2409
|
-
{ issue, target: "
|
|
2718
|
+
{ issue, target: "PendingApproval", note: `Plan approved for ${issue.identifier}. Ready for execution.` },
|
|
2410
2719
|
deps
|
|
2411
2720
|
);
|
|
2412
2721
|
await transitionIssueCommand(
|
|
@@ -2418,8 +2727,8 @@ async function approvePlanCommand(input, deps) {
|
|
|
2418
2727
|
// src/commands/execute-issue.command.ts
|
|
2419
2728
|
async function executeIssueCommand(input, deps) {
|
|
2420
2729
|
const { issue } = input;
|
|
2421
|
-
if (issue.state !== "
|
|
2422
|
-
throw new Error(`Cannot execute issue in state ${issue.state}. Must be in
|
|
2730
|
+
if (issue.state !== "PendingApproval") {
|
|
2731
|
+
throw new Error(`Cannot execute issue in state ${issue.state}. Must be in PendingApproval.`);
|
|
2423
2732
|
}
|
|
2424
2733
|
await transitionIssueCommand(
|
|
2425
2734
|
{ issue, target: "Queued", note: `Execution requested for ${issue.identifier}.` },
|
|
@@ -2460,21 +2769,63 @@ async function replanIssueCommand(input, deps) {
|
|
|
2460
2769
|
}
|
|
2461
2770
|
|
|
2462
2771
|
// src/commands/merge-workspace.command.ts
|
|
2463
|
-
import { existsSync as
|
|
2772
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2464
2773
|
import { execSync } from "child_process";
|
|
2774
|
+
|
|
2775
|
+
// src/domains/validation.ts
|
|
2776
|
+
import { execFile as execFile2 } from "child_process";
|
|
2777
|
+
async function runValidationGate(issue, config) {
|
|
2778
|
+
if (!config.testCommand) return null;
|
|
2779
|
+
const cwd = issue.worktreePath ?? issue.workspacePath;
|
|
2780
|
+
if (!cwd) {
|
|
2781
|
+
logger.warn({ issueId: issue.id }, "[Validation] No workspace path \u2014 skipping gate");
|
|
2782
|
+
return null;
|
|
2783
|
+
}
|
|
2784
|
+
const command = config.testCommand;
|
|
2785
|
+
logger.info({ issueId: issue.id, command, cwd }, "[Validation] Running validation gate");
|
|
2786
|
+
return new Promise((resolve3) => {
|
|
2787
|
+
const child = execFile2("sh", ["-c", command], {
|
|
2788
|
+
cwd,
|
|
2789
|
+
encoding: "utf8",
|
|
2790
|
+
timeout: 3e5,
|
|
2791
|
+
maxBuffer: 2 * 1024 * 1024
|
|
2792
|
+
}, (err, stdout, stderr) => {
|
|
2793
|
+
const combined = (stdout || "") + (stderr || "");
|
|
2794
|
+
if (!err) {
|
|
2795
|
+
logger.info({ issueId: issue.id }, "[Validation] Gate passed");
|
|
2796
|
+
resolve3({
|
|
2797
|
+
passed: true,
|
|
2798
|
+
output: combined.slice(-2048),
|
|
2799
|
+
command,
|
|
2800
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2801
|
+
});
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
logger.warn({ issueId: issue.id, exitCode: err.code }, "[Validation] Gate failed");
|
|
2805
|
+
resolve3({
|
|
2806
|
+
passed: false,
|
|
2807
|
+
output: combined.slice(-2048) || String(err).slice(0, 2048),
|
|
2808
|
+
command,
|
|
2809
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2810
|
+
});
|
|
2811
|
+
});
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
// src/commands/merge-workspace.command.ts
|
|
2465
2816
|
async function mergeWorkspaceCommand(input, deps) {
|
|
2466
2817
|
const { issue, state } = input;
|
|
2467
|
-
if (!["
|
|
2468
|
-
throw new Error(`Issue ${issue.identifier} is in state ${issue.state}. Merge is only allowed in Reviewing,
|
|
2818
|
+
if (!["Approved", "Reviewing", "PendingDecision"].includes(issue.state)) {
|
|
2819
|
+
throw new Error(`Issue ${issue.identifier} is in state ${issue.state}. Merge is only allowed in Reviewing, PendingDecision, or Approved state.`);
|
|
2469
2820
|
}
|
|
2470
|
-
if (issue.state === "Reviewing" || issue.state === "
|
|
2821
|
+
if (issue.state === "Reviewing" || issue.state === "PendingDecision") {
|
|
2471
2822
|
await transitionIssueCommand(
|
|
2472
|
-
{ issue, target: "
|
|
2823
|
+
{ issue, target: "Approved", note: "Approved and merged by user." },
|
|
2473
2824
|
deps
|
|
2474
2825
|
);
|
|
2475
2826
|
}
|
|
2476
2827
|
const wp = issue.worktreePath ?? issue.workspacePath;
|
|
2477
|
-
if (!wp || !
|
|
2828
|
+
if (!wp || !existsSync7(wp)) {
|
|
2478
2829
|
throw new Error("No workspace found for this issue.");
|
|
2479
2830
|
}
|
|
2480
2831
|
if (issue.branchName && issue.baseBranch) {
|
|
@@ -2498,12 +2849,20 @@ async function mergeWorkspaceCommand(input, deps) {
|
|
|
2498
2849
|
}
|
|
2499
2850
|
} catch {
|
|
2500
2851
|
}
|
|
2852
|
+
const validation = await runValidationGate(issue, state.config);
|
|
2853
|
+
if (validation) {
|
|
2854
|
+
issue.validationResult = validation;
|
|
2855
|
+
if (!validation.passed) {
|
|
2856
|
+
throw new Error(`Validation gate failed (${validation.command}): ${validation.output.slice(0, 500)}`);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2501
2859
|
const result = mergeWorkspace(issue);
|
|
2502
2860
|
issue.mergeResult = {
|
|
2503
2861
|
copied: result.copied.length,
|
|
2504
2862
|
deleted: result.deleted.length,
|
|
2505
2863
|
skipped: result.skipped.length,
|
|
2506
|
-
conflicts: result.conflicts.length
|
|
2864
|
+
conflicts: result.conflicts.length,
|
|
2865
|
+
conflictFiles: result.conflicts.length > 0 ? result.conflicts : void 0
|
|
2507
2866
|
};
|
|
2508
2867
|
if (result.conflicts.length > 0) {
|
|
2509
2868
|
deps.eventStore.addEvent(issue.id, "error", `Merge conflicts: ${result.conflicts.join(", ")}`);
|
|
@@ -2527,6 +2886,139 @@ async function mergeWorkspaceCommand(input, deps) {
|
|
|
2527
2886
|
return result;
|
|
2528
2887
|
}
|
|
2529
2888
|
|
|
2889
|
+
// src/commands/push-workspace.command.ts
|
|
2890
|
+
import { execFileSync, execSync as execSync2 } from "child_process";
|
|
2891
|
+
function isGhAvailable() {
|
|
2892
|
+
try {
|
|
2893
|
+
execFileSync("gh", ["--version"], { stdio: "pipe", timeout: 5e3 });
|
|
2894
|
+
return true;
|
|
2895
|
+
} catch {
|
|
2896
|
+
return false;
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
function getCompareUrl(branchName, baseBranch) {
|
|
2900
|
+
try {
|
|
2901
|
+
const remote = execSync2("git remote get-url origin", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
|
|
2902
|
+
const cleanRemote = remote.replace(/\.git$/, "");
|
|
2903
|
+
return `${cleanRemote}/compare/${baseBranch}...${branchName}`;
|
|
2904
|
+
} catch {
|
|
2905
|
+
return `(branch pushed: ${branchName})`;
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
function findExistingPr(branchName) {
|
|
2909
|
+
try {
|
|
2910
|
+
const result = execFileSync(
|
|
2911
|
+
"gh",
|
|
2912
|
+
["pr", "view", branchName, "--json", "url,state", "--jq", 'select(.state == "OPEN") | .url'],
|
|
2913
|
+
{ cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe", timeout: 15e3 }
|
|
2914
|
+
).trim();
|
|
2915
|
+
return result || null;
|
|
2916
|
+
} catch {
|
|
2917
|
+
return null;
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
function createPr(branchName, baseBranch, title, body) {
|
|
2921
|
+
return execFileSync(
|
|
2922
|
+
"gh",
|
|
2923
|
+
["pr", "create", "--head", branchName, "--base", baseBranch, "--title", title, "--body", body],
|
|
2924
|
+
{ cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe", timeout: 3e4 }
|
|
2925
|
+
).trim();
|
|
2926
|
+
}
|
|
2927
|
+
async function pushWorkspaceCommand(input, deps) {
|
|
2928
|
+
const { issue, state } = input;
|
|
2929
|
+
if (!["Approved", "Reviewing", "PendingDecision"].includes(issue.state)) {
|
|
2930
|
+
throw new Error(`Issue ${issue.identifier} is in state ${issue.state}. Push is only allowed in Reviewing, PendingDecision, or Approved state.`);
|
|
2931
|
+
}
|
|
2932
|
+
if (!issue.branchName || !issue.baseBranch || !issue.worktreePath) {
|
|
2933
|
+
throw new Error(`Issue ${issue.identifier} has no git worktree \u2014 cannot push.`);
|
|
2934
|
+
}
|
|
2935
|
+
if (issue.state === "Reviewing" || issue.state === "PendingDecision") {
|
|
2936
|
+
await transitionIssueCommand(
|
|
2937
|
+
{ issue, target: "Approved", note: "Approved and pushed by user." },
|
|
2938
|
+
deps
|
|
2939
|
+
);
|
|
2940
|
+
}
|
|
2941
|
+
ensureWorktreeCommitted(issue);
|
|
2942
|
+
const validation = await runValidationGate(issue, state.config);
|
|
2943
|
+
if (validation) {
|
|
2944
|
+
issue.validationResult = validation;
|
|
2945
|
+
if (!validation.passed) {
|
|
2946
|
+
throw new Error(`Validation gate failed (${validation.command}): ${validation.output.slice(0, 500)}`);
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
computeDiffStats(issue);
|
|
2950
|
+
const planSummary = issue.plan?.summary ?? issue.title;
|
|
2951
|
+
let diffStat = "";
|
|
2952
|
+
try {
|
|
2953
|
+
diffStat = execSync2(
|
|
2954
|
+
`git diff --stat "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
2955
|
+
{ cwd: TARGET_ROOT, encoding: "utf8", maxBuffer: 512e3, timeout: 1e4, stdio: "pipe" }
|
|
2956
|
+
).trim();
|
|
2957
|
+
} catch {
|
|
2958
|
+
}
|
|
2959
|
+
const body = `## Summary
|
|
2960
|
+
${planSummary}
|
|
2961
|
+
|
|
2962
|
+
## Diff Stats
|
|
2963
|
+
\`\`\`
|
|
2964
|
+
${diffStat || "No diff stats available"}
|
|
2965
|
+
\`\`\`
|
|
2966
|
+
|
|
2967
|
+
*Automated by fifony*`;
|
|
2968
|
+
execSync2(`git push -u origin "${issue.branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
2969
|
+
const prBase = state.config.prBaseBranch || issue.baseBranch;
|
|
2970
|
+
const ghAvailable = isGhAvailable();
|
|
2971
|
+
let prUrl;
|
|
2972
|
+
if (!ghAvailable) {
|
|
2973
|
+
prUrl = getCompareUrl(issue.branchName, prBase);
|
|
2974
|
+
logger.info({ issueId: issue.id, prUrl }, "[Push] gh CLI not available \u2014 using compare URL");
|
|
2975
|
+
} else {
|
|
2976
|
+
const existingUrl = findExistingPr(issue.branchName);
|
|
2977
|
+
if (existingUrl) {
|
|
2978
|
+
prUrl = existingUrl;
|
|
2979
|
+
logger.info({ issueId: issue.id, prUrl }, "[Push] Existing open PR found");
|
|
2980
|
+
} else {
|
|
2981
|
+
try {
|
|
2982
|
+
prUrl = createPr(issue.branchName, prBase, issue.title, body);
|
|
2983
|
+
logger.info({ issueId: issue.id, prUrl }, "[Push] PR created");
|
|
2984
|
+
} catch (err) {
|
|
2985
|
+
const ghError = (err.stderr || err.stdout || String(err)).toString().slice(0, 500);
|
|
2986
|
+
logger.error({ issueId: issue.id, ghError }, "[Push] gh pr create failed");
|
|
2987
|
+
prUrl = getCompareUrl(issue.branchName, prBase);
|
|
2988
|
+
deps.eventStore.addEvent(issue.id, "error", `gh pr create failed: ${ghError}. Branch was pushed \u2014 use the compare URL to create the PR manually.`);
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
issue.prUrl = prUrl;
|
|
2993
|
+
if (!issue.mergedReason) issue.mergedReason = "Pushed to origin and PR created.";
|
|
2994
|
+
await transitionIssueCommand(
|
|
2995
|
+
{ issue, target: "Merged", note: `Branch ${issue.branchName} pushed. PR: ${prUrl}` },
|
|
2996
|
+
deps
|
|
2997
|
+
);
|
|
2998
|
+
deps.eventStore.addEvent(issue.id, "merge", `PR created: ${prUrl}`);
|
|
2999
|
+
await deps.persistencePort.persistState(state);
|
|
3000
|
+
return { prUrl, ghAvailable };
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
// src/commands/retry-execution.command.ts
|
|
3004
|
+
async function retryExecutionCommand(input, deps) {
|
|
3005
|
+
const { issue, note } = input;
|
|
3006
|
+
if (issue.state !== "Blocked") {
|
|
3007
|
+
throw new Error(
|
|
3008
|
+
`retryExecutionCommand requires Blocked state, got ${issue.state}. Use replanIssueCommand for re-planning or the generic /retry endpoint for other states.`
|
|
3009
|
+
);
|
|
3010
|
+
}
|
|
3011
|
+
await transitionIssueCommand(
|
|
3012
|
+
{ issue, target: "Queued", note: note ?? `Retry execution for ${issue.identifier} (attempt ${issue.attempts + 1}).` },
|
|
3013
|
+
deps
|
|
3014
|
+
);
|
|
3015
|
+
deps.eventStore.addEvent(
|
|
3016
|
+
issue.id,
|
|
3017
|
+
"manual",
|
|
3018
|
+
`Execution retry requested for ${issue.identifier} \u2014 re-queued from Blocked.`
|
|
3019
|
+
);
|
|
3020
|
+
}
|
|
3021
|
+
|
|
2530
3022
|
// src/routes/state.ts
|
|
2531
3023
|
function getStateQuery(state, showAll = false) {
|
|
2532
3024
|
let issues = state.issues;
|
|
@@ -2544,7 +3036,6 @@ function getStateQuery(state, showAll = false) {
|
|
|
2544
3036
|
return {
|
|
2545
3037
|
...state,
|
|
2546
3038
|
issues,
|
|
2547
|
-
capabilities: computeCapabilityCounts(issues),
|
|
2548
3039
|
metrics: computeMetrics(issues),
|
|
2549
3040
|
_filter: showAll ? "all" : "recent",
|
|
2550
3041
|
_totalIssues: state.issues.length
|
|
@@ -2629,7 +3120,7 @@ function registerStateRoutes(app, state) {
|
|
|
2629
3120
|
);
|
|
2630
3121
|
if (issue.plan?.steps?.length) {
|
|
2631
3122
|
await transitionIssueCommand(
|
|
2632
|
-
{ issue, target: "
|
|
3123
|
+
{ issue, target: "PendingApproval", note: "Existing plan found." },
|
|
2633
3124
|
container
|
|
2634
3125
|
);
|
|
2635
3126
|
await transitionIssueCommand(
|
|
@@ -2638,11 +3129,26 @@ function registerStateRoutes(app, state) {
|
|
|
2638
3129
|
);
|
|
2639
3130
|
}
|
|
2640
3131
|
} else if (issue.state === "Blocked") {
|
|
3132
|
+
await retryExecutionCommand(
|
|
3133
|
+
{ issue, note: "Manual retry from Blocked." },
|
|
3134
|
+
container
|
|
3135
|
+
);
|
|
3136
|
+
} else if (issue.state === "Approved") {
|
|
2641
3137
|
await transitionIssueCommand(
|
|
2642
|
-
{ issue, target: "
|
|
3138
|
+
{ issue, target: "Planning", note: "Requeued for rework after merge conflicts." },
|
|
2643
3139
|
container
|
|
2644
3140
|
);
|
|
2645
|
-
|
|
3141
|
+
if (issue.plan?.steps?.length) {
|
|
3142
|
+
await transitionIssueCommand(
|
|
3143
|
+
{ issue, target: "PendingApproval", note: "Existing plan found." },
|
|
3144
|
+
container
|
|
3145
|
+
);
|
|
3146
|
+
await transitionIssueCommand(
|
|
3147
|
+
{ issue, target: "Queued", note: "Auto-queued for rework." },
|
|
3148
|
+
container
|
|
3149
|
+
);
|
|
3150
|
+
}
|
|
3151
|
+
} else if (issue.state === "PendingApproval") {
|
|
2646
3152
|
await transitionIssueCommand(
|
|
2647
3153
|
{ issue, target: "Queued", note: "Manual retry \u2014 queued for execution." },
|
|
2648
3154
|
container
|
|
@@ -2691,6 +3197,10 @@ function registerStateRoutes(app, state) {
|
|
|
2691
3197
|
const issue = findIssue(state, issueId);
|
|
2692
3198
|
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
2693
3199
|
const container = getContainer();
|
|
3200
|
+
if (state.config.mergeMode === "push-pr") {
|
|
3201
|
+
const result2 = await pushWorkspaceCommand({ issue, state }, container);
|
|
3202
|
+
return c.json({ ok: true, prUrl: result2.prUrl, ghAvailable: result2.ghAvailable });
|
|
3203
|
+
}
|
|
2694
3204
|
const result = await mergeWorkspaceCommand({ issue, state }, container);
|
|
2695
3205
|
return c.json({ ok: true, ...result });
|
|
2696
3206
|
} catch (error) {
|
|
@@ -2699,17 +3209,51 @@ function registerStateRoutes(app, state) {
|
|
|
2699
3209
|
return c.json({ ok: false, error: String(error) }, 500);
|
|
2700
3210
|
}
|
|
2701
3211
|
});
|
|
3212
|
+
app.get("/api/issues/:id/merge-preview", async (c) => {
|
|
3213
|
+
logger.info({ issueId: parseIssue(c) }, "[API] GET /api/issues/:id/merge-preview");
|
|
3214
|
+
try {
|
|
3215
|
+
const issueId = parseIssue(c);
|
|
3216
|
+
if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
3217
|
+
const issue = findIssue(state, issueId);
|
|
3218
|
+
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
3219
|
+
const { dryMerge } = await import("./workspace-474CCKTW.js");
|
|
3220
|
+
const result = dryMerge(issue);
|
|
3221
|
+
return c.json({ ok: true, ...result });
|
|
3222
|
+
} catch (error) {
|
|
3223
|
+
logger.error(`Failed to preview merge for ${parseIssue(c) || "<unknown>"}: ${String(error)}`);
|
|
3224
|
+
return c.json({ ok: false, error: String(error) }, 500);
|
|
3225
|
+
}
|
|
3226
|
+
});
|
|
3227
|
+
app.post("/api/issues/:id/rebase", async (c) => {
|
|
3228
|
+
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/rebase");
|
|
3229
|
+
try {
|
|
3230
|
+
const issueId = parseIssue(c);
|
|
3231
|
+
if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
3232
|
+
const issue = findIssue(state, issueId);
|
|
3233
|
+
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
3234
|
+
const { rebaseWorktree } = await import("./workspace-474CCKTW.js");
|
|
3235
|
+
const result = rebaseWorktree(issue);
|
|
3236
|
+
if (result.success) {
|
|
3237
|
+
addEvent(state, issue.id, "info", `Branch ${issue.branchName} rebased onto ${issue.baseBranch}.`);
|
|
3238
|
+
}
|
|
3239
|
+
await persistState(state);
|
|
3240
|
+
return c.json({ ok: true, ...result });
|
|
3241
|
+
} catch (error) {
|
|
3242
|
+
logger.error(`Failed to rebase for ${parseIssue(c) || "<unknown>"}: ${String(error)}`);
|
|
3243
|
+
return c.json({ ok: false, error: String(error) }, 500);
|
|
3244
|
+
}
|
|
3245
|
+
});
|
|
2702
3246
|
app.post("/api/issues/:id/try", async (c) => {
|
|
2703
3247
|
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/try");
|
|
2704
3248
|
return mutateIssueState(state, c, async (issue) => {
|
|
2705
|
-
if (!["Reviewing", "
|
|
3249
|
+
if (!["Reviewing", "PendingDecision"].includes(issue.state)) {
|
|
2706
3250
|
throw new Error(`Cannot apply test for issue in state ${issue.state}.`);
|
|
2707
3251
|
}
|
|
2708
3252
|
if (!issue.branchName) {
|
|
2709
3253
|
throw new Error("No branch name found for this issue.");
|
|
2710
3254
|
}
|
|
2711
3255
|
try {
|
|
2712
|
-
|
|
3256
|
+
execSync3(
|
|
2713
3257
|
`git merge --squash "${issue.branchName}"`,
|
|
2714
3258
|
{ encoding: "utf8", cwd: TARGET_ROOT, stdio: "pipe", timeout: 3e4 }
|
|
2715
3259
|
);
|
|
@@ -2724,8 +3268,8 @@ function registerStateRoutes(app, state) {
|
|
|
2724
3268
|
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/revert-try");
|
|
2725
3269
|
return mutateIssueState(state, c, async (issue) => {
|
|
2726
3270
|
try {
|
|
2727
|
-
|
|
2728
|
-
|
|
3271
|
+
execSync3("git reset --hard HEAD", { cwd: TARGET_ROOT, stdio: "pipe", timeout: 15e3 });
|
|
3272
|
+
execSync3("git clean -fd", { cwd: TARGET_ROOT, stdio: "pipe", timeout: 15e3 });
|
|
2729
3273
|
} catch (err) {
|
|
2730
3274
|
const msg = err.stderr || err.stdout || String(err);
|
|
2731
3275
|
throw new Error(`git reset/clean failed: ${msg}`);
|
|
@@ -2736,7 +3280,7 @@ function registerStateRoutes(app, state) {
|
|
|
2736
3280
|
app.post("/api/issues/:id/rollback", async (c) => {
|
|
2737
3281
|
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/rollback");
|
|
2738
3282
|
return mutateIssueState(state, c, async (issue) => {
|
|
2739
|
-
if (!["Reviewing", "
|
|
3283
|
+
if (!["Reviewing", "PendingDecision", "Approved"].includes(issue.state)) {
|
|
2740
3284
|
throw new Error(`Cannot rollback issue in state ${issue.state}. Must be in Reviewing, Reviewed, or Done.`);
|
|
2741
3285
|
}
|
|
2742
3286
|
if (issue.workspacePath) {
|
|
@@ -2766,15 +3310,15 @@ function registerStateRoutes(app, state) {
|
|
|
2766
3310
|
if (!Array.isArray(payload.files) || payload.files.length === 0) {
|
|
2767
3311
|
return c.json({ ok: false, error: "No files provided." }, 400);
|
|
2768
3312
|
}
|
|
2769
|
-
const issueAttachDir =
|
|
2770
|
-
|
|
3313
|
+
const issueAttachDir = join9(ATTACHMENTS_ROOT, issue.id);
|
|
3314
|
+
mkdirSync4(issueAttachDir, { recursive: true });
|
|
2771
3315
|
const newPaths = [];
|
|
2772
3316
|
for (const file of payload.files) {
|
|
2773
3317
|
if (typeof file.data !== "string" || !file.name) continue;
|
|
2774
3318
|
const safeExt = extname(file.name).replace(/[^a-z0-9.]/gi, "").slice(0, 10) || ".bin";
|
|
2775
3319
|
const safeName = `${randomUUID()}${safeExt}`;
|
|
2776
|
-
const dest =
|
|
2777
|
-
|
|
3320
|
+
const dest = join9(issueAttachDir, safeName);
|
|
3321
|
+
writeFileSync4(dest, Buffer.from(file.data, "base64"));
|
|
2778
3322
|
newPaths.push(dest);
|
|
2779
3323
|
}
|
|
2780
3324
|
issue.images = [...issue.images ?? [], ...newPaths];
|
|
@@ -2794,8 +3338,8 @@ function registerStateRoutes(app, state) {
|
|
|
2794
3338
|
const filename = c.req.param?.("filename") ?? c.req.params?.filename ?? "";
|
|
2795
3339
|
if (!filename) return c.json({ ok: false, error: "Filename is required." }, 400);
|
|
2796
3340
|
const safeName = basename2(filename);
|
|
2797
|
-
const filePath =
|
|
2798
|
-
if (!
|
|
3341
|
+
const filePath = join9(ATTACHMENTS_ROOT, issueId, safeName);
|
|
3342
|
+
if (!existsSync8(filePath)) return c.json({ ok: false, error: "Image not found." }, 404);
|
|
2799
3343
|
const ext = extname(safeName).toLowerCase();
|
|
2800
3344
|
const mimeMap = {
|
|
2801
3345
|
".png": "image/png",
|
|
@@ -2806,8 +3350,8 @@ function registerStateRoutes(app, state) {
|
|
|
2806
3350
|
".svg": "image/svg+xml"
|
|
2807
3351
|
};
|
|
2808
3352
|
const mime = mimeMap[ext] ?? "application/octet-stream";
|
|
2809
|
-
const { readFileSync:
|
|
2810
|
-
const data =
|
|
3353
|
+
const { readFileSync: readFileSync12 } = await import("fs");
|
|
3354
|
+
const data = readFileSync12(filePath);
|
|
2811
3355
|
return new Response(data, { headers: { "Content-Type": mime, "Cache-Control": "private, max-age=86400" } });
|
|
2812
3356
|
} catch (error) {
|
|
2813
3357
|
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
@@ -2819,7 +3363,7 @@ function registerStateRoutes(app, state) {
|
|
|
2819
3363
|
const issue = findIssue(state, issueId);
|
|
2820
3364
|
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
2821
3365
|
try {
|
|
2822
|
-
const { getIssueTransitionHistory } = await import("./issue-state-machine-
|
|
3366
|
+
const { getIssueTransitionHistory } = await import("./issue-state-machine-ACMUJSXC.js");
|
|
2823
3367
|
const limit = parseInt(c.req.query("limit") ?? "50", 10);
|
|
2824
3368
|
const offset = parseInt(c.req.query("offset") ?? "0", 10);
|
|
2825
3369
|
const transitions = await getIssueTransitionHistory(issue.id, { limit, offset });
|
|
@@ -2830,7 +3374,7 @@ function registerStateRoutes(app, state) {
|
|
|
2830
3374
|
});
|
|
2831
3375
|
app.get("/api/state-machine/transitions", async (c) => {
|
|
2832
3376
|
try {
|
|
2833
|
-
const { getStateMachineTransitions } = await import("./issue-state-machine-
|
|
3377
|
+
const { getStateMachineTransitions } = await import("./issue-state-machine-ACMUJSXC.js");
|
|
2834
3378
|
return c.json({ ok: true, transitions: getStateMachineTransitions() });
|
|
2835
3379
|
} catch (error) {
|
|
2836
3380
|
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
@@ -2838,7 +3382,7 @@ function registerStateRoutes(app, state) {
|
|
|
2838
3382
|
});
|
|
2839
3383
|
app.get("/api/state-machine/visualize", async (c) => {
|
|
2840
3384
|
try {
|
|
2841
|
-
const { visualizeStateMachine } = await import("./issue-state-machine-
|
|
3385
|
+
const { visualizeStateMachine } = await import("./issue-state-machine-ACMUJSXC.js");
|
|
2842
3386
|
const dot = visualizeStateMachine();
|
|
2843
3387
|
if (!dot) return c.json({ ok: false, error: "Visualization not available." }, 404);
|
|
2844
3388
|
return c.json({ ok: true, dot });
|
|
@@ -2923,8 +3467,8 @@ async function recoverPlanningSession() {
|
|
|
2923
3467
|
}
|
|
2924
3468
|
|
|
2925
3469
|
// src/agents/planning/plan-generator.ts
|
|
2926
|
-
import { writeFileSync as
|
|
2927
|
-
import { join as
|
|
3470
|
+
import { writeFileSync as writeFileSync6 } from "fs";
|
|
3471
|
+
import { join as join11 } from "path";
|
|
2928
3472
|
import { mkdtempSync, rmSync as rmSync2 } from "fs";
|
|
2929
3473
|
import { tmpdir } from "os";
|
|
2930
3474
|
|
|
@@ -2982,10 +3526,9 @@ function tryBuildPlan(parsed) {
|
|
|
2982
3526
|
})) : void 0,
|
|
2983
3527
|
validation: toStringArray(parsed.validation),
|
|
2984
3528
|
deliverables: toStringArray(parsed.deliverables),
|
|
2985
|
-
executionStrategy: parsed.executionStrategy || parsed.execution_strategy || void 0,
|
|
2986
|
-
toolingDecision: parsed.toolingDecision || parsed.tooling_decision || void 0,
|
|
2987
3529
|
suggestedPaths: toStringArray(parsed.suggestedPaths || parsed.suggested_paths || parsed.suggestedFilePaths || parsed.filePaths || parsed.file_paths || parsed.paths),
|
|
2988
|
-
|
|
3530
|
+
suggestedSkills: toStringArray(parsed.suggestedSkills || parsed.suggested_skills),
|
|
3531
|
+
suggestedAgents: toStringArray(parsed.suggestedAgents || parsed.suggested_agents),
|
|
2989
3532
|
suggestedEffort: parsed.suggestedEffort || parsed.suggested_effort || parsed.effortSuggestion || parsed.effort_suggestion || parsed.effort || { default: "medium" },
|
|
2990
3533
|
provider: "",
|
|
2991
3534
|
createdAt: now()
|
|
@@ -3096,28 +3639,27 @@ function extractPlanTokenUsage(raw) {
|
|
|
3096
3639
|
}
|
|
3097
3640
|
|
|
3098
3641
|
// src/agents/planning/planning-prompts.ts
|
|
3099
|
-
import { mkdirSync as
|
|
3642
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
3100
3643
|
import { spawn } from "child_process";
|
|
3101
|
-
import { join as
|
|
3644
|
+
import { join as join10 } from "path";
|
|
3102
3645
|
|
|
3103
3646
|
// src/agents/planning/planning-schema.ts
|
|
3104
3647
|
var STEP_SCHEMA = {
|
|
3105
3648
|
type: "object",
|
|
3106
3649
|
additionalProperties: false,
|
|
3107
|
-
required: ["step", "action", "files", "details", "
|
|
3650
|
+
required: ["step", "action", "files", "details", "doneWhen"],
|
|
3108
3651
|
properties: {
|
|
3109
3652
|
step: { type: "number" },
|
|
3110
3653
|
action: { type: "string" },
|
|
3111
3654
|
files: { type: "array", items: { type: "string" } },
|
|
3112
3655
|
details: { type: "string" },
|
|
3113
|
-
ownerType: { type: "string", enum: ["human", "agent", "skill", "subagent", "tool"] },
|
|
3114
3656
|
doneWhen: { type: "string" }
|
|
3115
3657
|
}
|
|
3116
3658
|
};
|
|
3117
3659
|
var PLAN_JSON_SCHEMA = JSON.stringify({
|
|
3118
3660
|
type: "object",
|
|
3119
3661
|
additionalProperties: false,
|
|
3120
|
-
required: ["summary", "steps", "
|
|
3662
|
+
required: ["summary", "steps", "estimatedComplexity", "suggestedPaths", "suggestedEffort"],
|
|
3121
3663
|
properties: {
|
|
3122
3664
|
summary: { type: "string" },
|
|
3123
3665
|
estimatedComplexity: { type: "string", enum: ["trivial", "low", "medium", "high"] },
|
|
@@ -3125,16 +3667,8 @@ var PLAN_JSON_SCHEMA = JSON.stringify({
|
|
|
3125
3667
|
constraints: { type: "array", items: { type: "string" } },
|
|
3126
3668
|
unknowns: { type: "array", items: { type: "object", additionalProperties: false, properties: { question: { type: "string" }, whyItMatters: { type: "string" }, howToResolve: { type: "string" } }, required: ["question", "whyItMatters", "howToResolve"] } },
|
|
3127
3669
|
successCriteria: { type: "array", items: { type: "string" } },
|
|
3128
|
-
executionStrategy: { type: "object", additionalProperties: false, required: ["approach", "whyThisApproach", "alternativesConsidered"], properties: { approach: { type: "string" }, whyThisApproach: { type: "string" }, alternativesConsidered: { type: "array", items: { type: "string" } } } },
|
|
3129
|
-
toolingDecision: { type: "object", additionalProperties: false, required: ["shouldUseSkills", "skillsToUse", "shouldUseSubagents", "subagentsToUse", "decisionSummary"], properties: {
|
|
3130
|
-
shouldUseSkills: { type: "boolean" },
|
|
3131
|
-
skillsToUse: { type: "array", items: { type: "object", additionalProperties: false, properties: { name: { type: "string" }, why: { type: "string" } }, required: ["name", "why"] } },
|
|
3132
|
-
shouldUseSubagents: { type: "boolean" },
|
|
3133
|
-
subagentsToUse: { type: "array", items: { type: "object", additionalProperties: false, properties: { name: { type: "string" }, role: { type: "string" }, why: { type: "string" } }, required: ["name", "role", "why"] } },
|
|
3134
|
-
decisionSummary: { type: "string" }
|
|
3135
|
-
} },
|
|
3136
3670
|
steps: { type: "array", items: STEP_SCHEMA },
|
|
3137
|
-
phases: { type: "array", items: { type: "object", additionalProperties: false, required: ["phaseName", "goal", "tasks"
|
|
3671
|
+
phases: { type: "array", items: { type: "object", additionalProperties: false, required: ["phaseName", "goal", "tasks"], properties: {
|
|
3138
3672
|
phaseName: { type: "string" },
|
|
3139
3673
|
goal: { type: "string" },
|
|
3140
3674
|
tasks: { type: "array", items: STEP_SCHEMA },
|
|
@@ -3145,8 +3679,9 @@ var PLAN_JSON_SCHEMA = JSON.stringify({
|
|
|
3145
3679
|
validation: { type: "array", items: { type: "string" } },
|
|
3146
3680
|
deliverables: { type: "array", items: { type: "string" } },
|
|
3147
3681
|
suggestedPaths: { type: "array", items: { type: "string" } },
|
|
3148
|
-
|
|
3149
|
-
|
|
3682
|
+
suggestedSkills: { type: "array", items: { type: "string" } },
|
|
3683
|
+
suggestedAgents: { type: "array", items: { type: "string" } },
|
|
3684
|
+
suggestedEffort: { type: "object", additionalProperties: false, required: ["default"], properties: { default: { type: "string" }, planner: { type: "string" }, executor: { type: "string" }, reviewer: { type: "string" } } }
|
|
3150
3685
|
}
|
|
3151
3686
|
});
|
|
3152
3687
|
var PLAN_SCHEMA_OBJECT = JSON.parse(PLAN_JSON_SCHEMA);
|
|
@@ -3166,6 +3701,9 @@ var SETTING_ID_AGENT_COMMAND = "runtime.agentCommand";
|
|
|
3166
3701
|
var SETTING_ID_DEFAULT_EFFORT = "runtime.defaultEffort";
|
|
3167
3702
|
var SETTING_ID_DETECTED_PROVIDERS = "providers.detected";
|
|
3168
3703
|
var SETTING_ID_WORKFLOW_CONFIG = "runtime.workflowConfig";
|
|
3704
|
+
var SETTING_ID_TEST_COMMAND = "runtime.testCommand";
|
|
3705
|
+
var SETTING_ID_MERGE_MODE = "runtime.mergeMode";
|
|
3706
|
+
var SETTING_ID_PR_BASE_BRANCH = "runtime.prBaseBranch";
|
|
3169
3707
|
async function loadRuntimeSettings() {
|
|
3170
3708
|
return loadPersistedSettings();
|
|
3171
3709
|
}
|
|
@@ -3181,7 +3719,10 @@ var RUNTIME_CONFIG_SETTING_IDS = /* @__PURE__ */ new Set([
|
|
|
3181
3719
|
SETTING_ID_MAX_CONCURRENT_BY_STATE,
|
|
3182
3720
|
SETTING_ID_AGENT_PROVIDER,
|
|
3183
3721
|
SETTING_ID_AGENT_COMMAND,
|
|
3184
|
-
SETTING_ID_DEFAULT_EFFORT
|
|
3722
|
+
SETTING_ID_DEFAULT_EFFORT,
|
|
3723
|
+
SETTING_ID_TEST_COMMAND,
|
|
3724
|
+
SETTING_ID_MERGE_MODE,
|
|
3725
|
+
SETTING_ID_PR_BASE_BRANCH
|
|
3185
3726
|
]);
|
|
3186
3727
|
var VALID_REASONING_EFFORTS = /* @__PURE__ */ new Set(["low", "medium", "high", "extra-high"]);
|
|
3187
3728
|
function parseIntegerSetting(value) {
|
|
@@ -3240,7 +3781,10 @@ function buildRuntimeConfigSettings(config, source) {
|
|
|
3240
3781
|
{ id: SETTING_ID_MAX_CONCURRENT_BY_STATE, scope: "runtime", value: config.maxConcurrentByState, source, updatedAt },
|
|
3241
3782
|
{ id: SETTING_ID_AGENT_PROVIDER, scope: "runtime", value: config.agentProvider, source, updatedAt },
|
|
3242
3783
|
{ id: SETTING_ID_AGENT_COMMAND, scope: "runtime", value: config.agentCommand, source, updatedAt },
|
|
3243
|
-
{ id: SETTING_ID_DEFAULT_EFFORT, scope: "runtime", value: config.defaultEffort, source, updatedAt }
|
|
3784
|
+
{ id: SETTING_ID_DEFAULT_EFFORT, scope: "runtime", value: config.defaultEffort, source, updatedAt },
|
|
3785
|
+
{ id: SETTING_ID_TEST_COMMAND, scope: "runtime", value: config.testCommand ?? "", source, updatedAt },
|
|
3786
|
+
{ id: SETTING_ID_MERGE_MODE, scope: "runtime", value: config.mergeMode ?? "local", source, updatedAt },
|
|
3787
|
+
{ id: SETTING_ID_PR_BASE_BRANCH, scope: "runtime", value: config.prBaseBranch ?? "", source, updatedAt }
|
|
3244
3788
|
];
|
|
3245
3789
|
}
|
|
3246
3790
|
function applyPersistedSettings(config, settings) {
|
|
@@ -3324,10 +3868,28 @@ function applyPersistedSettings(config, settings) {
|
|
|
3324
3868
|
agentCommandOverridden = true;
|
|
3325
3869
|
break;
|
|
3326
3870
|
}
|
|
3327
|
-
case SETTING_ID_DEFAULT_EFFORT: {
|
|
3328
|
-
const parsed = sanitizeDefaultEffort(setting.value);
|
|
3329
|
-
if (parsed) {
|
|
3330
|
-
nextConfig.defaultEffort = parsed;
|
|
3871
|
+
case SETTING_ID_DEFAULT_EFFORT: {
|
|
3872
|
+
const parsed = sanitizeDefaultEffort(setting.value);
|
|
3873
|
+
if (parsed) {
|
|
3874
|
+
nextConfig.defaultEffort = parsed;
|
|
3875
|
+
}
|
|
3876
|
+
break;
|
|
3877
|
+
}
|
|
3878
|
+
case SETTING_ID_TEST_COMMAND: {
|
|
3879
|
+
if (typeof setting.value === "string") {
|
|
3880
|
+
nextConfig.testCommand = setting.value.trim() || void 0;
|
|
3881
|
+
}
|
|
3882
|
+
break;
|
|
3883
|
+
}
|
|
3884
|
+
case SETTING_ID_MERGE_MODE: {
|
|
3885
|
+
if (setting.value === "local" || setting.value === "push-pr") {
|
|
3886
|
+
nextConfig.mergeMode = setting.value;
|
|
3887
|
+
}
|
|
3888
|
+
break;
|
|
3889
|
+
}
|
|
3890
|
+
case SETTING_ID_PR_BASE_BRANCH: {
|
|
3891
|
+
if (typeof setting.value === "string" && setting.value.trim()) {
|
|
3892
|
+
nextConfig.prBaseBranch = setting.value.trim();
|
|
3331
3893
|
}
|
|
3332
3894
|
break;
|
|
3333
3895
|
}
|
|
@@ -3435,11 +3997,19 @@ async function persistWorkflowConfig(config) {
|
|
|
3435
3997
|
|
|
3436
3998
|
// src/agents/planning/planning-prompts.ts
|
|
3437
3999
|
async function buildPlanPrompt(title, description, fast = false, images) {
|
|
4000
|
+
const skills = discoverSkills(TARGET_ROOT);
|
|
4001
|
+
const agents = discoverAgents(TARGET_ROOT);
|
|
4002
|
+
const commands = discoverCommands(TARGET_ROOT);
|
|
4003
|
+
const hasCapabilities = skills.length > 0 || agents.length > 0 || commands.length > 0;
|
|
3438
4004
|
return renderPrompt("issue-planner", {
|
|
3439
4005
|
title,
|
|
3440
4006
|
description: description || "(none provided)",
|
|
3441
4007
|
fast,
|
|
3442
|
-
images: images?.length ? images : void 0
|
|
4008
|
+
images: images?.length ? images : void 0,
|
|
4009
|
+
availableCapabilities: hasCapabilities,
|
|
4010
|
+
availableSkills: skills.map((s) => ({ name: s.name, description: s.description || "", whenToUse: s.whenToUse || "" })),
|
|
4011
|
+
availableAgents: agents.map((a) => ({ name: a.name, description: a.description || "", whenToUse: a.whenToUse || "", avoidIf: a.avoidIf || "" })),
|
|
4012
|
+
availableCommands: commands.map((c) => ({ name: c.name }))
|
|
3443
4013
|
});
|
|
3444
4014
|
}
|
|
3445
4015
|
async function buildRefinePrompt(title, description, currentPlan, feedback) {
|
|
@@ -3458,11 +4028,11 @@ function getPlanCommand(provider, model, imagePaths) {
|
|
|
3458
4028
|
}
|
|
3459
4029
|
function savePlanDebugFiles(slug, prompt, output) {
|
|
3460
4030
|
try {
|
|
3461
|
-
const debugDir =
|
|
3462
|
-
|
|
4031
|
+
const debugDir = join10(STATE_ROOT, "debug");
|
|
4032
|
+
mkdirSync5(debugDir, { recursive: true });
|
|
3463
4033
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
3464
|
-
|
|
3465
|
-
if (output)
|
|
4034
|
+
writeFileSync5(join10(debugDir, `plan-${slug}-${ts}-prompt.md`), prompt, "utf8");
|
|
4035
|
+
if (output) writeFileSync5(join10(debugDir, `plan-${slug}-${ts}-output.txt`), output, "utf8");
|
|
3466
4036
|
} catch {
|
|
3467
4037
|
}
|
|
3468
4038
|
}
|
|
@@ -3620,9 +4190,9 @@ async function generatePlan(title, description, config, _workflowDefinition, opt
|
|
|
3620
4190
|
const command = getPlanCommand(preferred, planStageModel, images);
|
|
3621
4191
|
if (!command) throw new Error(`No command configured for provider ${preferred}.`);
|
|
3622
4192
|
logger.debug({ provider: preferred, model: planStageModel, command: command.slice(0, 120) }, "[Planner] Provider selected for plan generation");
|
|
3623
|
-
const tempDir = mkdtempSync(
|
|
3624
|
-
const promptFile =
|
|
3625
|
-
|
|
4193
|
+
const tempDir = mkdtempSync(join11(tmpdir(), "fifony-plan-"));
|
|
4194
|
+
const promptFile = join11(tempDir, "fifony-plan-prompt.md");
|
|
4195
|
+
writeFileSync6(promptFile, `${prompt}
|
|
3626
4196
|
`, "utf8");
|
|
3627
4197
|
let lastProgressPersist = 0;
|
|
3628
4198
|
const PROGRESS_INTERVAL_MS = 2e3;
|
|
@@ -3699,8 +4269,8 @@ async function generatePlan(title, description, config, _workflowDefinition, opt
|
|
|
3699
4269
|
}
|
|
3700
4270
|
|
|
3701
4271
|
// src/agents/planning/plan-refiner.ts
|
|
3702
|
-
import { writeFileSync as
|
|
3703
|
-
import { join as
|
|
4272
|
+
import { writeFileSync as writeFileSync7 } from "fs";
|
|
4273
|
+
import { join as join12 } from "path";
|
|
3704
4274
|
import { mkdtempSync as mkdtempSync2, rmSync as rmSync3 } from "fs";
|
|
3705
4275
|
import { tmpdir as tmpdir2 } from "os";
|
|
3706
4276
|
async function refinePlan(issue, feedback, config, _workflowDefinition) {
|
|
@@ -3713,9 +4283,9 @@ async function refinePlan(issue, feedback, config, _workflowDefinition) {
|
|
|
3713
4283
|
{
|
|
3714
4284
|
const command = getPlanCommand(preferred, planStageModel);
|
|
3715
4285
|
if (!command) throw new Error(`No command configured for provider ${preferred}.`);
|
|
3716
|
-
const tempDir = mkdtempSync2(
|
|
3717
|
-
const promptFile =
|
|
3718
|
-
|
|
4286
|
+
const tempDir = mkdtempSync2(join12(tmpdir2(), "fifony-refine-"));
|
|
4287
|
+
const promptFile = join12(tempDir, "fifony-refine-prompt.md");
|
|
4288
|
+
writeFileSync7(promptFile, `${prompt}
|
|
3719
4289
|
`, "utf8");
|
|
3720
4290
|
const output = await runPlanningProcess({
|
|
3721
4291
|
command,
|
|
@@ -3836,10 +4406,10 @@ function refinePlanInBackground(issue, feedback, config, _workflowDefinition, ca
|
|
|
3836
4406
|
|
|
3837
4407
|
// src/agents/planning/issue-enhancer.ts
|
|
3838
4408
|
import { env as env2 } from "process";
|
|
3839
|
-
import { existsSync as
|
|
4409
|
+
import { existsSync as existsSync9, mkdtempSync as mkdtempSync3, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "fs";
|
|
3840
4410
|
import { spawn as spawn2 } from "child_process";
|
|
3841
4411
|
import { tmpdir as tmpdir3 } from "os";
|
|
3842
|
-
import { join as
|
|
4412
|
+
import { join as join13 } from "path";
|
|
3843
4413
|
function getProviderCommand(provider, config) {
|
|
3844
4414
|
return resolveAgentCommand(provider, config.agentCommand || "", "", "");
|
|
3845
4415
|
}
|
|
@@ -3908,22 +4478,22 @@ function parseCandidate(raw, expectedField) {
|
|
|
3908
4478
|
return "";
|
|
3909
4479
|
}
|
|
3910
4480
|
function readProviderOutput(resultFile, fallback) {
|
|
3911
|
-
if (
|
|
4481
|
+
if (existsSync9(resultFile)) {
|
|
3912
4482
|
try {
|
|
3913
|
-
return
|
|
4483
|
+
return readFileSync6(resultFile, "utf8").trim();
|
|
3914
4484
|
} catch {
|
|
3915
4485
|
}
|
|
3916
4486
|
}
|
|
3917
4487
|
return fallback;
|
|
3918
4488
|
}
|
|
3919
4489
|
async function runProviderCommand(command, provider, prompt, title, description, field, timeoutMs, images) {
|
|
3920
|
-
const tempDir = mkdtempSync3(
|
|
3921
|
-
const promptFile =
|
|
3922
|
-
const issuePayloadFile =
|
|
3923
|
-
const resultFile =
|
|
3924
|
-
|
|
4490
|
+
const tempDir = mkdtempSync3(join13(tmpdir3(), "fifony-enhance-"));
|
|
4491
|
+
const promptFile = join13(tempDir, "fifony-enhance-prompt.md");
|
|
4492
|
+
const issuePayloadFile = join13(tempDir, "fifony-issue.json");
|
|
4493
|
+
const resultFile = join13(tempDir, "fifony-result.txt");
|
|
4494
|
+
writeFileSync8(promptFile, `${prompt}
|
|
3925
4495
|
`, "utf8");
|
|
3926
|
-
|
|
4496
|
+
writeFileSync8(issuePayloadFile, JSON.stringify({ title, description, field }, null, 2), "utf8");
|
|
3927
4497
|
let effectiveCommand = command;
|
|
3928
4498
|
if (provider === "codex" && images?.length) {
|
|
3929
4499
|
const imageFlags = images.map((p) => `--image "${p}"`).join(" ");
|
|
@@ -4140,7 +4710,6 @@ function registerPlanRoutes(app, state) {
|
|
|
4140
4710
|
applyUsage: (iss, usage) => applyPlanUsage(iss, usage),
|
|
4141
4711
|
applySuggestions: (iss, plan) => {
|
|
4142
4712
|
if (plan.suggestedPaths?.length) iss.paths = plan.suggestedPaths;
|
|
4143
|
-
if (plan.suggestedLabels?.length) iss.labels = plan.suggestedLabels;
|
|
4144
4713
|
if (plan.suggestedEffort) iss.effort = plan.suggestedEffort;
|
|
4145
4714
|
}
|
|
4146
4715
|
});
|
|
@@ -4322,7 +4891,7 @@ function registerAnalyticsRoutes(app) {
|
|
|
4322
4891
|
try {
|
|
4323
4892
|
const context2 = getApiRuntimeContextOrThrow();
|
|
4324
4893
|
const doneIssues = context2.state.issues.filter(
|
|
4325
|
-
(i) => (i.state === "
|
|
4894
|
+
(i) => (i.state === "Approved" || i.state === "Merged") && i.completedAt
|
|
4326
4895
|
);
|
|
4327
4896
|
const msToDay = (ms) => ms / (1e3 * 60 * 60 * 24);
|
|
4328
4897
|
const avg = (arr) => arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : null;
|
|
@@ -4379,149 +4948,59 @@ function registerScanningRoutes(app, state) {
|
|
|
4379
4948
|
});
|
|
4380
4949
|
}
|
|
4381
4950
|
|
|
4382
|
-
// src/agents/
|
|
4383
|
-
import { existsSync as
|
|
4384
|
-
import { join as
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
}
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
}
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
}
|
|
4431
|
-
|
|
4432
|
-
const domainSet = new Set(domains.map((d) => d.toLowerCase().trim()));
|
|
4433
|
-
if (domainSet.size === 0) return catalog;
|
|
4434
|
-
const scored = catalog.map((entry) => {
|
|
4435
|
-
const matchCount = entry.domains.filter((d) => domainSet.has(d.toLowerCase())).length;
|
|
4436
|
-
return { entry, matchCount };
|
|
4437
|
-
});
|
|
4438
|
-
return scored.filter((item) => item.matchCount > 0).sort((a, b) => b.matchCount - a.matchCount).map((item) => item.entry);
|
|
4439
|
-
}
|
|
4440
|
-
function installAgents(targetRoot, agentNames, catalog) {
|
|
4441
|
-
const result = { installed: [], skipped: [], errors: [] };
|
|
4442
|
-
const catalogMap = new Map(catalog.map((entry) => [entry.name, entry]));
|
|
4443
|
-
const agentsDir = join13(targetRoot, ".claude", "agents");
|
|
4444
|
-
try {
|
|
4445
|
-
mkdirSync4(agentsDir, { recursive: true });
|
|
4446
|
-
} catch (error) {
|
|
4447
|
-
logger.error({ err: error, path: agentsDir }, "Failed to create agents directory");
|
|
4448
|
-
result.errors.push({ name: "_directory", error: `Failed to create ${agentsDir}` });
|
|
4449
|
-
return result;
|
|
4450
|
-
}
|
|
4451
|
-
for (const name of agentNames) {
|
|
4452
|
-
const entry = catalogMap.get(name);
|
|
4453
|
-
if (!entry) {
|
|
4454
|
-
result.errors.push({ name, error: "Agent not found in catalog" });
|
|
4455
|
-
continue;
|
|
4456
|
-
}
|
|
4457
|
-
const filePath = join13(agentsDir, `${name}.md`);
|
|
4458
|
-
if (existsSync9(filePath)) {
|
|
4459
|
-
result.skipped.push(name);
|
|
4460
|
-
continue;
|
|
4461
|
-
}
|
|
4462
|
-
try {
|
|
4463
|
-
writeFileSync8(filePath, entry.content, "utf8");
|
|
4464
|
-
result.installed.push(name);
|
|
4465
|
-
logger.info({ agent: name, path: filePath }, "Agent installed");
|
|
4466
|
-
} catch (error) {
|
|
4467
|
-
result.errors.push({
|
|
4468
|
-
name,
|
|
4469
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4470
|
-
});
|
|
4471
|
-
}
|
|
4472
|
-
}
|
|
4473
|
-
return result;
|
|
4474
|
-
}
|
|
4475
|
-
function installSkills(targetRoot, skillNames, catalog) {
|
|
4476
|
-
const result = { installed: [], skipped: [], errors: [] };
|
|
4477
|
-
const catalogMap = new Map(catalog.map((entry) => [entry.name, entry]));
|
|
4478
|
-
const skillsDir = join13(targetRoot, ".claude", "skills");
|
|
4479
|
-
try {
|
|
4480
|
-
mkdirSync4(skillsDir, { recursive: true });
|
|
4481
|
-
} catch (error) {
|
|
4482
|
-
logger.error({ err: error, path: skillsDir }, "Failed to create skills directory");
|
|
4483
|
-
result.errors.push({ name: "_directory", error: `Failed to create ${skillsDir}` });
|
|
4484
|
-
return result;
|
|
4485
|
-
}
|
|
4486
|
-
for (const name of skillNames) {
|
|
4487
|
-
const entry = catalogMap.get(name);
|
|
4488
|
-
if (!entry) {
|
|
4489
|
-
result.errors.push({ name, error: "Skill not found in catalog" });
|
|
4490
|
-
continue;
|
|
4491
|
-
}
|
|
4492
|
-
const skillDir = join13(skillsDir, name);
|
|
4493
|
-
const filePath = join13(skillDir, "SKILL.md");
|
|
4494
|
-
if (existsSync9(filePath)) {
|
|
4495
|
-
result.skipped.push(name);
|
|
4496
|
-
continue;
|
|
4497
|
-
}
|
|
4498
|
-
try {
|
|
4499
|
-
mkdirSync4(skillDir, { recursive: true });
|
|
4500
|
-
if (entry.installType === "bundled" && entry.content) {
|
|
4501
|
-
writeFileSync8(filePath, entry.content, "utf8");
|
|
4502
|
-
} else {
|
|
4503
|
-
const referenceContent = [
|
|
4504
|
-
`# ${entry.displayName}`,
|
|
4505
|
-
"",
|
|
4506
|
-
entry.description,
|
|
4507
|
-
"",
|
|
4508
|
-
`**Source**: ${entry.source}`,
|
|
4509
|
-
entry.url ? `**URL**: ${entry.url}` : "",
|
|
4510
|
-
"",
|
|
4511
|
-
`> This skill references an external resource. Install it from the source above.`
|
|
4512
|
-
].filter(Boolean).join("\n");
|
|
4513
|
-
writeFileSync8(filePath, referenceContent, "utf8");
|
|
4514
|
-
}
|
|
4515
|
-
result.installed.push(name);
|
|
4516
|
-
logger.info({ skill: name, path: filePath, type: entry.installType }, "Skill installed");
|
|
4517
|
-
} catch (error) {
|
|
4518
|
-
result.errors.push({
|
|
4519
|
-
name,
|
|
4520
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4521
|
-
});
|
|
4522
|
-
}
|
|
4951
|
+
// src/agents/claude-md-manager.ts
|
|
4952
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
4953
|
+
import { join as join14 } from "path";
|
|
4954
|
+
var BLOCK_START = "<!-- FIFONY:START \u2014 managed by fifony, do not edit manually -->";
|
|
4955
|
+
var BLOCK_END = "<!-- FIFONY:END -->";
|
|
4956
|
+
var BLOCK_PATTERN = /<!-- FIFONY:START[^>]*-->[\s\S]*?<!-- FIFONY:END -->/;
|
|
4957
|
+
function buildManagedBlock(skills, agents, commands) {
|
|
4958
|
+
const lines = [
|
|
4959
|
+
BLOCK_START,
|
|
4960
|
+
"## Fifony \u2014 Installed Capabilities",
|
|
4961
|
+
"",
|
|
4962
|
+
"This workspace has fifony-managed agents and skills installed.",
|
|
4963
|
+
""
|
|
4964
|
+
];
|
|
4965
|
+
if (commands.length > 0) {
|
|
4966
|
+
lines.push(`**Commands**: ${commands.map((c) => `/${c.name}`).join(", ")}`);
|
|
4967
|
+
}
|
|
4968
|
+
if (skills.length > 0) {
|
|
4969
|
+
lines.push(`**Skills**: ${skills.map((s) => s.name).join(", ")}`);
|
|
4970
|
+
}
|
|
4971
|
+
if (agents.length > 0) {
|
|
4972
|
+
lines.push(`**Agents**: ${agents.map((a) => a.name).join(", ")}`);
|
|
4973
|
+
}
|
|
4974
|
+
lines.push("");
|
|
4975
|
+
lines.push("Use these capabilities when working on tasks. For details:");
|
|
4976
|
+
lines.push("- Skills: `.claude/skills/*/SKILL.md`");
|
|
4977
|
+
lines.push("- Agents: `.claude/agents/*.md`");
|
|
4978
|
+
lines.push("- Commands: `.claude/commands/*.md`");
|
|
4979
|
+
lines.push(BLOCK_END);
|
|
4980
|
+
return lines.join("\n");
|
|
4981
|
+
}
|
|
4982
|
+
function updateClaudeMdManagedBlock(targetRoot, skills, agents, commands) {
|
|
4983
|
+
if (skills.length === 0 && agents.length === 0 && commands.length === 0) return;
|
|
4984
|
+
const claudeMdPath = join14(targetRoot, "CLAUDE.md");
|
|
4985
|
+
const newBlock = buildManagedBlock(skills, agents, commands);
|
|
4986
|
+
let existing = "";
|
|
4987
|
+
if (existsSync10(claudeMdPath)) {
|
|
4988
|
+
existing = readFileSync7(claudeMdPath, "utf8");
|
|
4989
|
+
}
|
|
4990
|
+
let updated;
|
|
4991
|
+
if (BLOCK_PATTERN.test(existing)) {
|
|
4992
|
+
updated = existing.replace(BLOCK_PATTERN, newBlock);
|
|
4993
|
+
} else if (existing) {
|
|
4994
|
+
updated = `${existing.trimEnd()}
|
|
4995
|
+
|
|
4996
|
+
${newBlock}
|
|
4997
|
+
`;
|
|
4998
|
+
} else {
|
|
4999
|
+
updated = `${newBlock}
|
|
5000
|
+
`;
|
|
4523
5001
|
}
|
|
4524
|
-
return
|
|
5002
|
+
if (updated === existing) return;
|
|
5003
|
+
writeFileSync9(claudeMdPath, updated, "utf8");
|
|
4525
5004
|
}
|
|
4526
5005
|
|
|
4527
5006
|
// src/routes/catalog.ts
|
|
@@ -4545,6 +5024,10 @@ function registerCatalogRoutes(app) {
|
|
|
4545
5024
|
}
|
|
4546
5025
|
const catalog = loadAgentCatalog();
|
|
4547
5026
|
const result = installAgents(TARGET_ROOT, agentNames, catalog);
|
|
5027
|
+
try {
|
|
5028
|
+
updateClaudeMdManagedBlock(TARGET_ROOT, discoverSkills(TARGET_ROOT), discoverAgents(TARGET_ROOT), discoverCommands(TARGET_ROOT));
|
|
5029
|
+
} catch {
|
|
5030
|
+
}
|
|
4548
5031
|
return c.json({ ok: true, ...result });
|
|
4549
5032
|
} catch (error) {
|
|
4550
5033
|
logger.error({ err: error }, "Failed to install agents");
|
|
@@ -4560,6 +5043,10 @@ function registerCatalogRoutes(app) {
|
|
|
4560
5043
|
}
|
|
4561
5044
|
const catalog = loadSkillCatalog();
|
|
4562
5045
|
const result = installSkills(TARGET_ROOT, skillNames, catalog);
|
|
5046
|
+
try {
|
|
5047
|
+
updateClaudeMdManagedBlock(TARGET_ROOT, discoverSkills(TARGET_ROOT), discoverAgents(TARGET_ROOT), discoverCommands(TARGET_ROOT));
|
|
5048
|
+
} catch {
|
|
5049
|
+
}
|
|
4563
5050
|
return c.json({ ok: true, ...result });
|
|
4564
5051
|
} catch (error) {
|
|
4565
5052
|
logger.error({ err: error }, "Failed to install skills");
|
|
@@ -4622,6 +5109,12 @@ function registerReferenceRepositoryRoutes(app) {
|
|
|
4622
5109
|
dryRun: payload?.dryRun === true,
|
|
4623
5110
|
importToGlobal: payload?.global === true
|
|
4624
5111
|
});
|
|
5112
|
+
if (!payload?.dryRun) {
|
|
5113
|
+
try {
|
|
5114
|
+
updateClaudeMdManagedBlock(TARGET_ROOT, discoverSkills(TARGET_ROOT), discoverAgents(TARGET_ROOT), discoverCommands(TARGET_ROOT));
|
|
5115
|
+
} catch {
|
|
5116
|
+
}
|
|
5117
|
+
}
|
|
4625
5118
|
return c.json({
|
|
4626
5119
|
ok: true,
|
|
4627
5120
|
...summary
|
|
@@ -4636,35 +5129,34 @@ function registerReferenceRepositoryRoutes(app) {
|
|
|
4636
5129
|
}
|
|
4637
5130
|
|
|
4638
5131
|
// src/routes/misc.ts
|
|
4639
|
-
import { execSync as
|
|
5132
|
+
import { execSync as execSync4 } from "child_process";
|
|
4640
5133
|
import {
|
|
4641
5134
|
appendFileSync,
|
|
4642
5135
|
closeSync,
|
|
4643
|
-
existsSync as
|
|
4644
|
-
mkdirSync as
|
|
5136
|
+
existsSync as existsSync11,
|
|
5137
|
+
mkdirSync as mkdirSync6,
|
|
4645
5138
|
openSync,
|
|
4646
|
-
|
|
5139
|
+
readdirSync as readdirSync3,
|
|
5140
|
+
readFileSync as readFileSync8,
|
|
4647
5141
|
readSync,
|
|
4648
5142
|
statSync,
|
|
4649
|
-
writeFileSync as
|
|
5143
|
+
writeFileSync as writeFileSync10
|
|
4650
5144
|
} from "fs";
|
|
4651
5145
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
4652
|
-
import { extname as extname2, join as
|
|
5146
|
+
import { basename as basename3, extname as extname2, join as join15 } from "path";
|
|
4653
5147
|
function registerMiscRoutes(app, state) {
|
|
4654
5148
|
app.post("/api/issues/:id/push", async (c) => {
|
|
4655
5149
|
const issueId = parseIssue(c);
|
|
4656
5150
|
if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
4657
5151
|
const issue = findIssue(state, issueId);
|
|
4658
5152
|
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
4659
|
-
if (
|
|
4660
|
-
return c.json({ ok: false, error: `Issue ${issue.identifier} must be in
|
|
5153
|
+
if (!["Approved", "Reviewing", "PendingDecision"].includes(issue.state)) {
|
|
5154
|
+
return c.json({ ok: false, error: `Issue ${issue.identifier} must be in Approved, Reviewing, or PendingDecision state to push. Current state: ${issue.state}.` }, 409);
|
|
4661
5155
|
}
|
|
4662
5156
|
try {
|
|
4663
|
-
const
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
await persistState(state);
|
|
4667
|
-
return c.json({ ok: true, prUrl });
|
|
5157
|
+
const container = getContainer();
|
|
5158
|
+
const result = await pushWorkspaceCommand({ issue, state }, container);
|
|
5159
|
+
return c.json({ ok: true, prUrl: result.prUrl, ghAvailable: result.ghAvailable });
|
|
4668
5160
|
} catch (error) {
|
|
4669
5161
|
logger.error({ err: error }, `[API] Failed to push branch for ${issueId}`);
|
|
4670
5162
|
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
@@ -4687,7 +5179,7 @@ function registerMiscRoutes(app, state) {
|
|
|
4687
5179
|
const wp = issue.workspacePath;
|
|
4688
5180
|
const liveLog = wp ? `${wp}/live-output.log` : null;
|
|
4689
5181
|
let lastSize = 0;
|
|
4690
|
-
if (liveLog &&
|
|
5182
|
+
if (liveLog && existsSync11(liveLog)) {
|
|
4691
5183
|
try {
|
|
4692
5184
|
const stat = statSync(liveLog);
|
|
4693
5185
|
lastSize = stat.size;
|
|
@@ -4715,7 +5207,7 @@ function registerMiscRoutes(app, state) {
|
|
|
4715
5207
|
return;
|
|
4716
5208
|
}
|
|
4717
5209
|
const logPath = currentIssue.workspacePath ? `${currentIssue.workspacePath}/live-output.log` : null;
|
|
4718
|
-
if (logPath &&
|
|
5210
|
+
if (logPath && existsSync11(logPath)) {
|
|
4719
5211
|
try {
|
|
4720
5212
|
const stat = statSync(logPath);
|
|
4721
5213
|
if (stat.size > lastSize) {
|
|
@@ -4770,7 +5262,7 @@ function registerMiscRoutes(app, state) {
|
|
|
4770
5262
|
const liveLog = wp ? `${wp}/live-output.log` : null;
|
|
4771
5263
|
let logTail = "";
|
|
4772
5264
|
let logSize = 0;
|
|
4773
|
-
if (liveLog &&
|
|
5265
|
+
if (liveLog && existsSync11(liveLog)) {
|
|
4774
5266
|
try {
|
|
4775
5267
|
const stat = statSync(liveLog);
|
|
4776
5268
|
logSize = stat.size;
|
|
@@ -4810,13 +5302,13 @@ function registerMiscRoutes(app, state) {
|
|
|
4810
5302
|
const issue = findIssue(state, issueId);
|
|
4811
5303
|
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
4812
5304
|
const wp = issue.workspacePath;
|
|
4813
|
-
if (!wp || !
|
|
5305
|
+
if (!wp || !existsSync11(wp)) {
|
|
4814
5306
|
return c.json({ ok: true, files: [], diff: "", message: "No workspace found." });
|
|
4815
5307
|
}
|
|
4816
5308
|
let raw = "";
|
|
4817
5309
|
if (issue.branchName && issue.baseBranch) {
|
|
4818
5310
|
try {
|
|
4819
|
-
raw =
|
|
5311
|
+
raw = execSync4(
|
|
4820
5312
|
`git diff --no-color "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
4821
5313
|
{ encoding: "utf8", maxBuffer: 4 * 1024 * 1024, timeout: 15e3, cwd: TARGET_ROOT, stdio: "pipe" }
|
|
4822
5314
|
);
|
|
@@ -4824,11 +5316,11 @@ function registerMiscRoutes(app, state) {
|
|
|
4824
5316
|
raw = err.stdout || "";
|
|
4825
5317
|
}
|
|
4826
5318
|
} else {
|
|
4827
|
-
if (!
|
|
5319
|
+
if (!existsSync11(SOURCE_ROOT)) {
|
|
4828
5320
|
return c.json({ ok: true, files: [], diff: "", message: "Source root not found." });
|
|
4829
5321
|
}
|
|
4830
5322
|
try {
|
|
4831
|
-
raw =
|
|
5323
|
+
raw = execSync4(
|
|
4832
5324
|
`git diff --no-index --no-color -- "${SOURCE_ROOT}" "${wp}"`,
|
|
4833
5325
|
{ encoding: "utf8", maxBuffer: 4 * 1024 * 1024, timeout: 15e3 }
|
|
4834
5326
|
);
|
|
@@ -4878,7 +5370,7 @@ function registerMiscRoutes(app, state) {
|
|
|
4878
5370
|
try {
|
|
4879
5371
|
const isGit = (() => {
|
|
4880
5372
|
try {
|
|
4881
|
-
|
|
5373
|
+
execSync4("git rev-parse --git-dir", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
4882
5374
|
return true;
|
|
4883
5375
|
} catch {
|
|
4884
5376
|
return false;
|
|
@@ -4887,14 +5379,14 @@ function registerMiscRoutes(app, state) {
|
|
|
4887
5379
|
if (!isGit) return c.json({ isGit: false, branch: null, hasCommits: false });
|
|
4888
5380
|
const branch = (() => {
|
|
4889
5381
|
try {
|
|
4890
|
-
return
|
|
5382
|
+
return execSync4("git rev-parse --abbrev-ref HEAD", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
|
|
4891
5383
|
} catch {
|
|
4892
5384
|
return null;
|
|
4893
5385
|
}
|
|
4894
5386
|
})();
|
|
4895
5387
|
const hasCommits = (() => {
|
|
4896
5388
|
try {
|
|
4897
|
-
|
|
5389
|
+
execSync4("git rev-parse HEAD", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
4898
5390
|
return true;
|
|
4899
5391
|
} catch {
|
|
4900
5392
|
return false;
|
|
@@ -4907,9 +5399,9 @@ function registerMiscRoutes(app, state) {
|
|
|
4907
5399
|
});
|
|
4908
5400
|
app.post("/api/git/init", async (c) => {
|
|
4909
5401
|
try {
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
const branch =
|
|
5402
|
+
execSync4("git init", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
5403
|
+
execSync4('git commit --allow-empty -m "Initial commit"', { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
5404
|
+
const branch = execSync4("git rev-parse --abbrev-ref HEAD", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
|
|
4913
5405
|
state.config.defaultBranch = branch;
|
|
4914
5406
|
await persistState(state);
|
|
4915
5407
|
return c.json({ ok: true, branch });
|
|
@@ -4923,7 +5415,7 @@ function registerMiscRoutes(app, state) {
|
|
|
4923
5415
|
if (!branchName || !/^[a-zA-Z0-9/_.-]+$/.test(branchName)) {
|
|
4924
5416
|
return c.json({ ok: false, error: "Invalid branch name." }, 400);
|
|
4925
5417
|
}
|
|
4926
|
-
|
|
5418
|
+
execSync4(`git checkout -b "${branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
4927
5419
|
state.config.defaultBranch = branchName;
|
|
4928
5420
|
await persistState(state);
|
|
4929
5421
|
return c.json({ ok: true, defaultBranch: branchName });
|
|
@@ -4931,6 +5423,26 @@ function registerMiscRoutes(app, state) {
|
|
|
4931
5423
|
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
4932
5424
|
}
|
|
4933
5425
|
});
|
|
5426
|
+
app.post("/api/git/switch", async (c) => {
|
|
5427
|
+
try {
|
|
5428
|
+
const { branchName } = await c.req.json();
|
|
5429
|
+
if (!branchName || !/^[a-zA-Z0-9/_.-]+$/.test(branchName)) {
|
|
5430
|
+
return c.json({ ok: false, error: "Invalid branch name." }, 400);
|
|
5431
|
+
}
|
|
5432
|
+
let created = false;
|
|
5433
|
+
try {
|
|
5434
|
+
execSync4(`git checkout "${branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
5435
|
+
} catch {
|
|
5436
|
+
execSync4(`git checkout -b "${branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
5437
|
+
created = true;
|
|
5438
|
+
}
|
|
5439
|
+
state.config.defaultBranch = branchName;
|
|
5440
|
+
await persistState(state);
|
|
5441
|
+
return c.json({ ok: true, defaultBranch: branchName, created });
|
|
5442
|
+
} catch (error) {
|
|
5443
|
+
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
5444
|
+
}
|
|
5445
|
+
});
|
|
4934
5446
|
app.get("/api/events/feed", async (c) => {
|
|
4935
5447
|
const since = c.req.query("since");
|
|
4936
5448
|
const issueId = c.req.query("issueId");
|
|
@@ -4944,11 +5456,11 @@ function registerMiscRoutes(app, state) {
|
|
|
4944
5456
|
});
|
|
4945
5457
|
app.get("/api/gitignore/status", async (c) => {
|
|
4946
5458
|
try {
|
|
4947
|
-
const gitignorePath =
|
|
4948
|
-
if (!
|
|
5459
|
+
const gitignorePath = join15(TARGET_ROOT, ".gitignore");
|
|
5460
|
+
if (!existsSync11(gitignorePath)) {
|
|
4949
5461
|
return c.json({ exists: false, hasFifony: false });
|
|
4950
5462
|
}
|
|
4951
|
-
const content =
|
|
5463
|
+
const content = readFileSync8(gitignorePath, "utf-8");
|
|
4952
5464
|
const lines = content.split("\n").map((l) => l.trim());
|
|
4953
5465
|
const hasFifony = lines.some((l) => l === ".fifony" || l === ".fifony/" || l === "/.fifony" || l === "/.fifony/");
|
|
4954
5466
|
return c.json({ exists: true, hasFifony });
|
|
@@ -4959,12 +5471,12 @@ function registerMiscRoutes(app, state) {
|
|
|
4959
5471
|
});
|
|
4960
5472
|
app.post("/api/gitignore/add", async (c) => {
|
|
4961
5473
|
try {
|
|
4962
|
-
const gitignorePath =
|
|
4963
|
-
if (!
|
|
4964
|
-
|
|
5474
|
+
const gitignorePath = join15(TARGET_ROOT, ".gitignore");
|
|
5475
|
+
if (!existsSync11(gitignorePath)) {
|
|
5476
|
+
writeFileSync10(gitignorePath, "# Fifony state directory\n.fifony/\n", "utf-8");
|
|
4965
5477
|
return c.json({ ok: true, created: true });
|
|
4966
5478
|
}
|
|
4967
|
-
const content =
|
|
5479
|
+
const content = readFileSync8(gitignorePath, "utf-8");
|
|
4968
5480
|
const lines = content.split("\n").map((l) => l.trim());
|
|
4969
5481
|
const hasFifony = lines.some((l) => l === ".fifony" || l === ".fifony/" || l === "/.fifony" || l === "/.fifony/");
|
|
4970
5482
|
if (hasFifony) {
|
|
@@ -4981,6 +5493,51 @@ function registerMiscRoutes(app, state) {
|
|
|
4981
5493
|
return c.json({ ok: false, error: "Failed to update .gitignore" }, 500);
|
|
4982
5494
|
}
|
|
4983
5495
|
});
|
|
5496
|
+
app.get("/api/issues/:id/outputs", async (c) => {
|
|
5497
|
+
try {
|
|
5498
|
+
const issueId = parseIssue(c);
|
|
5499
|
+
if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
5500
|
+
const issue = findIssue(state, issueId);
|
|
5501
|
+
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
5502
|
+
const wp = issue.workspacePath;
|
|
5503
|
+
if (!wp) return c.json({ ok: true, files: [] });
|
|
5504
|
+
const outputsDir = join15(wp, "outputs");
|
|
5505
|
+
if (!existsSync11(outputsDir)) return c.json({ ok: true, files: [] });
|
|
5506
|
+
const entries = readdirSync3(outputsDir).filter((f) => f.endsWith(".stdout.log")).map((f) => {
|
|
5507
|
+
try {
|
|
5508
|
+
const s = statSync(join15(outputsDir, f));
|
|
5509
|
+
return { name: f, size: s.size };
|
|
5510
|
+
} catch {
|
|
5511
|
+
return { name: f, size: 0 };
|
|
5512
|
+
}
|
|
5513
|
+
});
|
|
5514
|
+
return c.json({ ok: true, files: entries });
|
|
5515
|
+
} catch (error) {
|
|
5516
|
+
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
5517
|
+
}
|
|
5518
|
+
});
|
|
5519
|
+
app.get("/api/issues/:id/outputs/:filename", async (c) => {
|
|
5520
|
+
try {
|
|
5521
|
+
const issueId = parseIssue(c);
|
|
5522
|
+
if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
5523
|
+
const issue = findIssue(state, issueId);
|
|
5524
|
+
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
5525
|
+
const filename = c.req.param?.("filename") ?? c.req.params?.filename ?? "";
|
|
5526
|
+
if (!filename) return c.json({ ok: false, error: "Filename is required." }, 400);
|
|
5527
|
+
const safeName = basename3(filename);
|
|
5528
|
+
if (safeName !== filename || !safeName.endsWith(".stdout.log")) {
|
|
5529
|
+
return c.json({ ok: false, error: "Invalid filename." }, 400);
|
|
5530
|
+
}
|
|
5531
|
+
const wp = issue.workspacePath;
|
|
5532
|
+
if (!wp) return c.json({ ok: false, error: "No workspace found." }, 404);
|
|
5533
|
+
const filePath = join15(wp, "outputs", safeName);
|
|
5534
|
+
if (!existsSync11(filePath)) return c.json({ ok: false, error: "Output file not found." }, 404);
|
|
5535
|
+
const content = readFileSync8(filePath, "utf8");
|
|
5536
|
+
return new Response(content, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
|
|
5537
|
+
} catch (error) {
|
|
5538
|
+
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
5539
|
+
}
|
|
5540
|
+
});
|
|
4984
5541
|
app.post("/api/attachments/upload", async (c) => {
|
|
4985
5542
|
try {
|
|
4986
5543
|
const payload = await c.req.json();
|
|
@@ -4988,15 +5545,15 @@ function registerMiscRoutes(app, state) {
|
|
|
4988
5545
|
return c.json({ ok: false, error: "No files provided." }, 400);
|
|
4989
5546
|
}
|
|
4990
5547
|
const uploadId = randomUUID2();
|
|
4991
|
-
const uploadDir =
|
|
4992
|
-
|
|
5548
|
+
const uploadDir = join15(ATTACHMENTS_ROOT, "temp", uploadId);
|
|
5549
|
+
mkdirSync6(uploadDir, { recursive: true });
|
|
4993
5550
|
const paths = [];
|
|
4994
5551
|
for (const file of payload.files) {
|
|
4995
5552
|
if (typeof file.data !== "string" || !file.name) continue;
|
|
4996
5553
|
const safeExt = extname2(file.name).replace(/[^a-z0-9.]/gi, "").slice(0, 10) || ".bin";
|
|
4997
5554
|
const safeName = `${randomUUID2()}${safeExt}`;
|
|
4998
|
-
const dest =
|
|
4999
|
-
|
|
5555
|
+
const dest = join15(uploadDir, safeName);
|
|
5556
|
+
writeFileSync10(dest, Buffer.from(file.data, "base64"));
|
|
5000
5557
|
paths.push(dest);
|
|
5001
5558
|
}
|
|
5002
5559
|
return c.json({ ok: true, paths, uploadId });
|
|
@@ -5051,10 +5608,10 @@ async function startApiServer(state, port) {
|
|
|
5051
5608
|
}
|
|
5052
5609
|
setApiRuntimeContext(state);
|
|
5053
5610
|
const serveTextFile = (filePath, contentType, cacheControl = "no-cache") => {
|
|
5054
|
-
if (!
|
|
5611
|
+
if (!existsSync12(filePath)) {
|
|
5055
5612
|
return new Response("Not found", { status: 404 });
|
|
5056
5613
|
}
|
|
5057
|
-
return new Response(
|
|
5614
|
+
return new Response(readFileSync9(filePath), {
|
|
5058
5615
|
headers: {
|
|
5059
5616
|
"content-type": contentType,
|
|
5060
5617
|
"cache-control": cacheControl
|
|
@@ -5062,10 +5619,10 @@ async function startApiServer(state, port) {
|
|
|
5062
5619
|
});
|
|
5063
5620
|
};
|
|
5064
5621
|
const serveAppShell = () => {
|
|
5065
|
-
if (!
|
|
5622
|
+
if (!existsSync12(FRONTEND_INDEX)) {
|
|
5066
5623
|
return new Response("Not found", { status: 404 });
|
|
5067
5624
|
}
|
|
5068
|
-
const html =
|
|
5625
|
+
const html = readFileSync9(FRONTEND_INDEX, "utf8").replace('href="/assets/manifest.webmanifest"', 'href="/manifest.webmanifest"').replaceAll('href="/assets/icon.svg"', 'href="/icon.svg"');
|
|
5069
5626
|
return new Response(html, {
|
|
5070
5627
|
headers: {
|
|
5071
5628
|
"content-type": "text/html; charset=utf-8",
|
|
@@ -5086,6 +5643,9 @@ async function startApiServer(state, port) {
|
|
|
5086
5643
|
port,
|
|
5087
5644
|
host: "0.0.0.0",
|
|
5088
5645
|
versionPrefix: false,
|
|
5646
|
+
metrics: {
|
|
5647
|
+
logLevel: "info"
|
|
5648
|
+
},
|
|
5089
5649
|
// HTTP + WebSocket on the same port via listeners
|
|
5090
5650
|
listeners: [{
|
|
5091
5651
|
bind: { host: "0.0.0.0", port },
|
|
@@ -5124,6 +5684,7 @@ async function startApiServer(state, port) {
|
|
|
5124
5684
|
"GET /onboarding": () => serveAppShell(),
|
|
5125
5685
|
"GET /kanban": () => serveAppShell(),
|
|
5126
5686
|
"GET /issues": () => serveAppShell(),
|
|
5687
|
+
"GET /analytics": () => serveAppShell(),
|
|
5127
5688
|
"GET /agents": () => serveAppShell(),
|
|
5128
5689
|
"GET /settings": () => serveAppShell(),
|
|
5129
5690
|
"GET /settings/general": () => serveAppShell(),
|
|
@@ -5433,7 +5994,6 @@ async function persistState(state) {
|
|
|
5433
5994
|
broadcastToWebSocketClients({
|
|
5434
5995
|
type: "state:update",
|
|
5435
5996
|
metrics: state.metrics,
|
|
5436
|
-
capabilities: computeCapabilityCounts(state.issues),
|
|
5437
5997
|
issues: state.issues,
|
|
5438
5998
|
events: state.events.slice(0, 50),
|
|
5439
5999
|
updatedAt: state.updatedAt
|
|
@@ -5577,12 +6137,6 @@ async function closeStateStore() {
|
|
|
5577
6137
|
|
|
5578
6138
|
// src/domains/project.ts
|
|
5579
6139
|
var SETTING_ID_PROJECT_NAME = "system.projectName";
|
|
5580
|
-
var LEGACY_PROJECT_SETTING_IDS = [
|
|
5581
|
-
"runtime.projectName",
|
|
5582
|
-
"ui.projectName",
|
|
5583
|
-
"projectName",
|
|
5584
|
-
"project.name"
|
|
5585
|
-
];
|
|
5586
6140
|
function normalizeProjectName(value) {
|
|
5587
6141
|
return typeof value === "string" ? value.trim().replace(/\s+/g, " ") : "";
|
|
5588
6142
|
}
|
|
@@ -5592,14 +6146,7 @@ function detectProjectName(targetRoot) {
|
|
|
5592
6146
|
return normalizeProjectName(basename4(normalizedPath));
|
|
5593
6147
|
}
|
|
5594
6148
|
function readSavedProjectName(settings) {
|
|
5595
|
-
|
|
5596
|
-
for (const id of settingIds) {
|
|
5597
|
-
const value = normalizeProjectName(settings.find((setting) => setting.id === id)?.value);
|
|
5598
|
-
if (value) {
|
|
5599
|
-
return value;
|
|
5600
|
-
}
|
|
5601
|
-
}
|
|
5602
|
-
return "";
|
|
6149
|
+
return normalizeProjectName(settings.find((s) => s.id === SETTING_ID_PROJECT_NAME)?.value);
|
|
5603
6150
|
}
|
|
5604
6151
|
function buildQueueTitle(projectName) {
|
|
5605
6152
|
const normalizedProjectName = normalizeProjectName(projectName);
|
|
@@ -5617,7 +6164,7 @@ function resolveProjectMetadata(settings, targetRoot) {
|
|
|
5617
6164
|
};
|
|
5618
6165
|
}
|
|
5619
6166
|
function scanProjectFiles(targetRoot) {
|
|
5620
|
-
const check = (rel) =>
|
|
6167
|
+
const check = (rel) => existsSync13(join16(targetRoot, rel));
|
|
5621
6168
|
const files = {
|
|
5622
6169
|
claudeMd: check("CLAUDE.md"),
|
|
5623
6170
|
claudeDir: check(".claude"),
|
|
@@ -5638,10 +6185,10 @@ function scanProjectFiles(targetRoot) {
|
|
|
5638
6185
|
};
|
|
5639
6186
|
const existingAgents = [];
|
|
5640
6187
|
for (const agentDir of [".claude/agents", ".codex/agents"]) {
|
|
5641
|
-
const fullPath =
|
|
5642
|
-
if (!
|
|
6188
|
+
const fullPath = join16(targetRoot, agentDir);
|
|
6189
|
+
if (!existsSync13(fullPath)) continue;
|
|
5643
6190
|
try {
|
|
5644
|
-
const entries =
|
|
6191
|
+
const entries = readdirSync4(fullPath);
|
|
5645
6192
|
for (const entry of entries) {
|
|
5646
6193
|
if (entry.endsWith(".md")) {
|
|
5647
6194
|
existingAgents.push(basename4(entry, ".md"));
|
|
@@ -5652,13 +6199,13 @@ function scanProjectFiles(targetRoot) {
|
|
|
5652
6199
|
}
|
|
5653
6200
|
const existingSkills = [];
|
|
5654
6201
|
for (const skillDir of [".claude/skills", ".codex/skills"]) {
|
|
5655
|
-
const fullPath =
|
|
5656
|
-
if (!
|
|
6202
|
+
const fullPath = join16(targetRoot, skillDir);
|
|
6203
|
+
if (!existsSync13(fullPath)) continue;
|
|
5657
6204
|
try {
|
|
5658
|
-
const entries =
|
|
6205
|
+
const entries = readdirSync4(fullPath);
|
|
5659
6206
|
for (const entry of entries) {
|
|
5660
|
-
const skillFile =
|
|
5661
|
-
if (
|
|
6207
|
+
const skillFile = join16(fullPath, entry, "SKILL.md");
|
|
6208
|
+
if (existsSync13(skillFile)) {
|
|
5662
6209
|
existingSkills.push(entry);
|
|
5663
6210
|
}
|
|
5664
6211
|
}
|
|
@@ -5666,20 +6213,20 @@ function scanProjectFiles(targetRoot) {
|
|
|
5666
6213
|
}
|
|
5667
6214
|
}
|
|
5668
6215
|
let readmeExcerpt = "";
|
|
5669
|
-
const readmePath =
|
|
5670
|
-
if (
|
|
6216
|
+
const readmePath = join16(targetRoot, "README.md");
|
|
6217
|
+
if (existsSync13(readmePath)) {
|
|
5671
6218
|
try {
|
|
5672
|
-
const content =
|
|
6219
|
+
const content = readFileSync10(readmePath, "utf8");
|
|
5673
6220
|
readmeExcerpt = content.slice(0, 200).trim();
|
|
5674
6221
|
} catch {
|
|
5675
6222
|
}
|
|
5676
6223
|
}
|
|
5677
6224
|
let packageName = "";
|
|
5678
6225
|
let packageDescription = "";
|
|
5679
|
-
const pkgPath =
|
|
5680
|
-
if (
|
|
6226
|
+
const pkgPath = join16(targetRoot, "package.json");
|
|
6227
|
+
if (existsSync13(pkgPath)) {
|
|
5681
6228
|
try {
|
|
5682
|
-
const pkg = JSON.parse(
|
|
6229
|
+
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
5683
6230
|
packageName = typeof pkg.name === "string" ? pkg.name : "";
|
|
5684
6231
|
packageDescription = typeof pkg.description === "string" ? pkg.description : "";
|
|
5685
6232
|
} catch {
|
|
@@ -5721,39 +6268,39 @@ function buildFallbackAnalysis(targetRoot) {
|
|
|
5721
6268
|
let description = "";
|
|
5722
6269
|
let readmeExcerpt = "";
|
|
5723
6270
|
for (const readmeFile of ["README.md", "README.rst", "README.txt", "README"]) {
|
|
5724
|
-
const p =
|
|
5725
|
-
if (
|
|
6271
|
+
const p = join16(targetRoot, readmeFile);
|
|
6272
|
+
if (existsSync13(p)) {
|
|
5726
6273
|
try {
|
|
5727
|
-
readmeExcerpt =
|
|
6274
|
+
readmeExcerpt = readFileSync10(p, "utf8").slice(0, 300).trim();
|
|
5728
6275
|
break;
|
|
5729
6276
|
} catch {
|
|
5730
6277
|
}
|
|
5731
6278
|
}
|
|
5732
6279
|
}
|
|
5733
|
-
const pkgPath =
|
|
5734
|
-
if (
|
|
6280
|
+
const pkgPath = join16(targetRoot, "package.json");
|
|
6281
|
+
if (existsSync13(pkgPath)) {
|
|
5735
6282
|
try {
|
|
5736
|
-
const pkg = JSON.parse(
|
|
6283
|
+
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
5737
6284
|
const name = typeof pkg.name === "string" ? pkg.name : "";
|
|
5738
6285
|
const desc = typeof pkg.description === "string" ? pkg.description : "";
|
|
5739
6286
|
if (desc) description = name ? `${name}: ${desc}` : desc;
|
|
5740
6287
|
} catch {
|
|
5741
6288
|
}
|
|
5742
6289
|
}
|
|
5743
|
-
const cargoPath =
|
|
5744
|
-
if (!description &&
|
|
6290
|
+
const cargoPath = join16(targetRoot, "Cargo.toml");
|
|
6291
|
+
if (!description && existsSync13(cargoPath)) {
|
|
5745
6292
|
try {
|
|
5746
|
-
const content =
|
|
6293
|
+
const content = readFileSync10(cargoPath, "utf8");
|
|
5747
6294
|
const descMatch = content.match(/^description\s*=\s*"([^"]+)"/m);
|
|
5748
6295
|
const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m);
|
|
5749
6296
|
if (descMatch) description = nameMatch ? `${nameMatch[1]}: ${descMatch[1]}` : descMatch[1];
|
|
5750
6297
|
} catch {
|
|
5751
6298
|
}
|
|
5752
6299
|
}
|
|
5753
|
-
const pyprojectPath =
|
|
5754
|
-
if (!description &&
|
|
6300
|
+
const pyprojectPath = join16(targetRoot, "pyproject.toml");
|
|
6301
|
+
if (!description && existsSync13(pyprojectPath)) {
|
|
5755
6302
|
try {
|
|
5756
|
-
const content =
|
|
6303
|
+
const content = readFileSync10(pyprojectPath, "utf8");
|
|
5757
6304
|
const descMatch = content.match(/^description\s*=\s*"([^"]+)"/m);
|
|
5758
6305
|
const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m);
|
|
5759
6306
|
if (descMatch) description = nameMatch ? `${nameMatch[1]}: ${descMatch[1]}` : descMatch[1];
|
|
@@ -5766,7 +6313,7 @@ function buildFallbackAnalysis(targetRoot) {
|
|
|
5766
6313
|
let language = "unknown";
|
|
5767
6314
|
const stack = [];
|
|
5768
6315
|
for (const [file, signal] of Object.entries(BUILD_FILE_SIGNALS)) {
|
|
5769
|
-
if (
|
|
6316
|
+
if (existsSync13(join16(targetRoot, file))) {
|
|
5770
6317
|
if (language === "unknown" && signal.language !== "unknown") {
|
|
5771
6318
|
language = signal.language;
|
|
5772
6319
|
}
|
|
@@ -5849,7 +6396,7 @@ function isBlockedProjectAnalysisResponse(analysis) {
|
|
|
5849
6396
|
var ANALYSIS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
5850
6397
|
function computeProjectHash(targetRoot) {
|
|
5851
6398
|
const buildFiles = Object.keys(BUILD_FILE_SIGNALS);
|
|
5852
|
-
const found = buildFiles.filter((f) =>
|
|
6399
|
+
const found = buildFiles.filter((f) => existsSync13(join16(targetRoot, f))).sort();
|
|
5853
6400
|
return createHash("sha256").update(found.join(",")).digest("hex").slice(0, 16);
|
|
5854
6401
|
}
|
|
5855
6402
|
async function loadCachedAnalysis(targetRoot) {
|
|
@@ -5901,10 +6448,10 @@ async function analyzeProjectWithCli(provider, targetRoot, options) {
|
|
|
5901
6448
|
);
|
|
5902
6449
|
return buildFallbackAnalysis(targetRoot);
|
|
5903
6450
|
}
|
|
5904
|
-
const tempDir = mkdtempSync4(
|
|
5905
|
-
const promptFile =
|
|
6451
|
+
const tempDir = mkdtempSync4(join16(tmpdir4(), "fifony-scan-"));
|
|
6452
|
+
const promptFile = join16(tempDir, "fifony-scan-prompt.txt");
|
|
5906
6453
|
const analysisPrompt = await renderPrompt("project-analysis");
|
|
5907
|
-
|
|
6454
|
+
writeFileSync11(promptFile, analysisPrompt, "utf8");
|
|
5908
6455
|
const processEnv = {};
|
|
5909
6456
|
for (const [key, value] of Object.entries(env3)) {
|
|
5910
6457
|
if (typeof value === "string") processEnv[key] = value;
|
|
@@ -6022,6 +6569,12 @@ var DEFAULT_REFERENCE_REPOSITORIES = [
|
|
|
6022
6569
|
name: "pbakaus/impeccable",
|
|
6023
6570
|
url: "https://github.com/pbakaus/impeccable.git",
|
|
6024
6571
|
description: "Frontend polish and impeccable-style quality workflows."
|
|
6572
|
+
},
|
|
6573
|
+
{
|
|
6574
|
+
id: "everything-claude-code",
|
|
6575
|
+
name: "affaan-m/everything-claude-code",
|
|
6576
|
+
url: "https://github.com/affaan-m/everything-claude-code.git",
|
|
6577
|
+
description: "28 specialized agents, 116 skills, and 59 commands \u2014 agent harness performance system."
|
|
6025
6578
|
}
|
|
6026
6579
|
];
|
|
6027
6580
|
var REPOSITORY_ROOT = resolve2(homedir3(), ".fifony", "repositories");
|
|
@@ -6046,7 +6599,7 @@ var REFERENCE_REPOSITORY_PARSERS = {
|
|
|
6046
6599
|
impeccable: collectImpeccableArtifacts
|
|
6047
6600
|
};
|
|
6048
6601
|
function runGit(args, cwd) {
|
|
6049
|
-
return
|
|
6602
|
+
return execFileSync2("git", args, {
|
|
6050
6603
|
cwd,
|
|
6051
6604
|
encoding: "utf8",
|
|
6052
6605
|
stdio: "pipe",
|
|
@@ -6073,7 +6626,7 @@ function uniqueSuffix(base, used) {
|
|
|
6073
6626
|
}
|
|
6074
6627
|
function collectDirectoryEntries(path) {
|
|
6075
6628
|
try {
|
|
6076
|
-
return
|
|
6629
|
+
return readdirSync4(path, { withFileTypes: true });
|
|
6077
6630
|
} catch {
|
|
6078
6631
|
return [];
|
|
6079
6632
|
}
|
|
@@ -6099,7 +6652,7 @@ function isMarkdownFile(value, expectedName) {
|
|
|
6099
6652
|
function isReferenceFrontMatterFile(filePath) {
|
|
6100
6653
|
let source;
|
|
6101
6654
|
try {
|
|
6102
|
-
source =
|
|
6655
|
+
source = readFileSync10(filePath, "utf8");
|
|
6103
6656
|
} catch {
|
|
6104
6657
|
return false;
|
|
6105
6658
|
}
|
|
@@ -6121,10 +6674,10 @@ function collectAgentArtifacts(agentsDir, usedNames, out) {
|
|
|
6121
6674
|
const parent = slugify(basename4(dirname2(agentsDir)));
|
|
6122
6675
|
const entries = collectDirectoryEntries(agentsDir);
|
|
6123
6676
|
for (const entry of entries) {
|
|
6124
|
-
const itemPath =
|
|
6677
|
+
const itemPath = join16(agentsDir, entry.name);
|
|
6125
6678
|
if (entry.isDirectory()) {
|
|
6126
|
-
const nestedAgentSpec =
|
|
6127
|
-
if (
|
|
6679
|
+
const nestedAgentSpec = join16(itemPath, "AGENT.md");
|
|
6680
|
+
if (existsSync13(nestedAgentSpec)) {
|
|
6128
6681
|
const name2 = uniqueSuffix(`${parent}__${slugify(entry.name)}`, usedNames);
|
|
6129
6682
|
out.push({ kind: "agent", sourcePath: nestedAgentSpec, targetName: name2 });
|
|
6130
6683
|
}
|
|
@@ -6145,10 +6698,10 @@ function collectSkillArtifacts(skillsDir, usedNames, out) {
|
|
|
6145
6698
|
const parent = slugify(basename4(dirname2(skillsDir)));
|
|
6146
6699
|
const entries = collectDirectoryEntries(skillsDir);
|
|
6147
6700
|
for (const entry of entries) {
|
|
6148
|
-
const itemPath =
|
|
6701
|
+
const itemPath = join16(skillsDir, entry.name);
|
|
6149
6702
|
if (entry.isDirectory()) {
|
|
6150
|
-
const skillFile =
|
|
6151
|
-
if (
|
|
6703
|
+
const skillFile = join16(itemPath, "SKILL.md");
|
|
6704
|
+
if (existsSync13(skillFile)) {
|
|
6152
6705
|
const name = uniqueSuffix(`${parent}__${slugify(entry.name)}`, usedNames);
|
|
6153
6706
|
out.push({ kind: "skill", sourcePath: skillFile, targetName: name });
|
|
6154
6707
|
}
|
|
@@ -6175,7 +6728,7 @@ function collectStandardArtifacts(repoPath) {
|
|
|
6175
6728
|
for (const entry of entries) {
|
|
6176
6729
|
if (!entry.isDirectory()) continue;
|
|
6177
6730
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
6178
|
-
const childPath =
|
|
6731
|
+
const childPath = join16(state.path, entry.name);
|
|
6179
6732
|
if (entry.name === "agents") {
|
|
6180
6733
|
collectAgentArtifacts(childPath, agentsUsed, artifacts);
|
|
6181
6734
|
}
|
|
@@ -6203,13 +6756,13 @@ function collectAgencyArtifacts(repoPath) {
|
|
|
6203
6756
|
if (SKIP_DIRS.has(entry.name) || AGENCY_AGENTS_EXCLUDED_DIRS.has(entry.name)) {
|
|
6204
6757
|
continue;
|
|
6205
6758
|
}
|
|
6206
|
-
queue.push({ path:
|
|
6759
|
+
queue.push({ path: join16(state.path, entry.name), depth: state.depth + 1 });
|
|
6207
6760
|
continue;
|
|
6208
6761
|
}
|
|
6209
|
-
if (!isMarkdownFile(entry.name, "readme.md") || !isReferenceFrontMatterFile(
|
|
6762
|
+
if (!isMarkdownFile(entry.name, "readme.md") || !isReferenceFrontMatterFile(join16(state.path, entry.name))) {
|
|
6210
6763
|
continue;
|
|
6211
6764
|
}
|
|
6212
|
-
const itemPath =
|
|
6765
|
+
const itemPath = join16(state.path, entry.name);
|
|
6213
6766
|
const targetName = uniqueSuffix(buildRelativeArtifactName(repoPath, itemPath), agentsUsed);
|
|
6214
6767
|
artifacts.push({
|
|
6215
6768
|
kind: "agent",
|
|
@@ -6223,13 +6776,13 @@ function collectAgencyArtifacts(repoPath) {
|
|
|
6223
6776
|
function collectImpeccableArtifacts(repoPath) {
|
|
6224
6777
|
const skillsUsed = /* @__PURE__ */ new Set();
|
|
6225
6778
|
const artifacts = [];
|
|
6226
|
-
const sourceSkills =
|
|
6227
|
-
if (
|
|
6779
|
+
const sourceSkills = join16(repoPath, "source", "skills");
|
|
6780
|
+
if (existsSync13(sourceSkills)) {
|
|
6228
6781
|
collectSkillArtifacts(sourceSkills, skillsUsed, artifacts);
|
|
6229
6782
|
return artifacts;
|
|
6230
6783
|
}
|
|
6231
|
-
const claudeSkills =
|
|
6232
|
-
if (
|
|
6784
|
+
const claudeSkills = join16(repoPath, ".claude", "skills");
|
|
6785
|
+
if (existsSync13(claudeSkills)) {
|
|
6233
6786
|
collectSkillArtifacts(claudeSkills, skillsUsed, artifacts);
|
|
6234
6787
|
}
|
|
6235
6788
|
return artifacts;
|
|
@@ -6255,19 +6808,19 @@ function getReferenceRepositoriesRoot() {
|
|
|
6255
6808
|
}
|
|
6256
6809
|
function listReferenceRepositories() {
|
|
6257
6810
|
return DEFAULT_REFERENCE_REPOSITORIES.map((repo) => {
|
|
6258
|
-
const path =
|
|
6811
|
+
const path = join16(REPOSITORY_ROOT, repo.id);
|
|
6259
6812
|
const status = {
|
|
6260
6813
|
id: repo.id,
|
|
6261
6814
|
name: repo.name,
|
|
6262
6815
|
url: repo.url,
|
|
6263
6816
|
path,
|
|
6264
|
-
present:
|
|
6817
|
+
present: existsSync13(path),
|
|
6265
6818
|
synced: false
|
|
6266
6819
|
};
|
|
6267
6820
|
if (!status.present) {
|
|
6268
6821
|
return status;
|
|
6269
6822
|
}
|
|
6270
|
-
if (!
|
|
6823
|
+
if (!existsSync13(join16(path, ".git"))) {
|
|
6271
6824
|
status.error = "Path exists but is not a git repo";
|
|
6272
6825
|
return status;
|
|
6273
6826
|
}
|
|
@@ -6291,7 +6844,7 @@ function resolveReferenceRepository(query) {
|
|
|
6291
6844
|
}
|
|
6292
6845
|
function syncReferenceRepositories(repositoryId) {
|
|
6293
6846
|
const root = REPOSITORY_ROOT;
|
|
6294
|
-
|
|
6847
|
+
mkdirSync7(root, { recursive: true });
|
|
6295
6848
|
const repos = repositoryId ? [resolveReferenceRepository(repositoryId)] : DEFAULT_REFERENCE_REPOSITORIES;
|
|
6296
6849
|
const selected = repos.filter((repo) => Boolean(repo));
|
|
6297
6850
|
if (repositoryId && selected.length === 0) {
|
|
@@ -6299,9 +6852,9 @@ function syncReferenceRepositories(repositoryId) {
|
|
|
6299
6852
|
}
|
|
6300
6853
|
const results = [];
|
|
6301
6854
|
for (const repo of selected) {
|
|
6302
|
-
const target =
|
|
6855
|
+
const target = join16(root, repo.id);
|
|
6303
6856
|
const candidates = [repo.url, ...repo.fallbackUrls ?? []];
|
|
6304
|
-
if (!
|
|
6857
|
+
if (!existsSync13(target)) {
|
|
6305
6858
|
let cloneError;
|
|
6306
6859
|
for (const candidate of candidates) {
|
|
6307
6860
|
try {
|
|
@@ -6328,7 +6881,7 @@ function syncReferenceRepositories(repositoryId) {
|
|
|
6328
6881
|
}
|
|
6329
6882
|
continue;
|
|
6330
6883
|
}
|
|
6331
|
-
if (!
|
|
6884
|
+
if (!existsSync13(join16(target, ".git"))) {
|
|
6332
6885
|
results.push({
|
|
6333
6886
|
id: repo.id,
|
|
6334
6887
|
path: target,
|
|
@@ -6363,14 +6916,14 @@ function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
|
|
|
6363
6916
|
if (!repository) {
|
|
6364
6917
|
throw new Error(`Unknown reference repository: ${repositoryId}`);
|
|
6365
6918
|
}
|
|
6366
|
-
const localPath =
|
|
6367
|
-
if (!
|
|
6919
|
+
const localPath = join16(REPOSITORY_ROOT, repository.id);
|
|
6920
|
+
if (!existsSync13(localPath)) {
|
|
6368
6921
|
throw new Error(`Repository not synced yet: ${repository.id}. Run 'fifony onboarding sync --repository ${repository.id}' first.`);
|
|
6369
6922
|
}
|
|
6370
6923
|
const basePath = resolve2(workspaceRoot);
|
|
6371
|
-
const targetBase = options.importToGlobal ?
|
|
6372
|
-
const agentsDir =
|
|
6373
|
-
const skillsDir =
|
|
6924
|
+
const targetBase = options.importToGlobal ? join16(homedir3(), ".codex") : join16(basePath, ".codex");
|
|
6925
|
+
const agentsDir = join16(targetBase, "agents");
|
|
6926
|
+
const skillsDir = join16(targetBase, "skills");
|
|
6374
6927
|
const artifacts = collectArtifacts(localPath, repository.id);
|
|
6375
6928
|
const filtered = options.kind === "all" ? artifacts : artifacts.filter((artifact) => artifact.kind === options.kind.slice(0, -1));
|
|
6376
6929
|
const summary = {
|
|
@@ -6388,16 +6941,16 @@ function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
|
|
|
6388
6941
|
return summary;
|
|
6389
6942
|
}
|
|
6390
6943
|
if (!options.dryRun) {
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6944
|
+
mkdirSync7(targetBase, { recursive: true });
|
|
6945
|
+
mkdirSync7(agentsDir, { recursive: true });
|
|
6946
|
+
mkdirSync7(skillsDir, { recursive: true });
|
|
6394
6947
|
}
|
|
6395
6948
|
for (const artifact of filtered) {
|
|
6396
6949
|
try {
|
|
6397
|
-
const source =
|
|
6950
|
+
const source = readFileSync10(artifact.sourcePath, "utf8");
|
|
6398
6951
|
if (artifact.kind === "agent") {
|
|
6399
|
-
const target =
|
|
6400
|
-
if (!options.overwrite &&
|
|
6952
|
+
const target = join16(agentsDir, `${artifact.targetName}.md`);
|
|
6953
|
+
if (!options.overwrite && existsSync13(target)) {
|
|
6401
6954
|
summary.skippedAgents.push(artifact.targetName);
|
|
6402
6955
|
continue;
|
|
6403
6956
|
}
|
|
@@ -6405,12 +6958,12 @@ function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
|
|
|
6405
6958
|
summary.importedAgents.push(artifact.targetName);
|
|
6406
6959
|
continue;
|
|
6407
6960
|
}
|
|
6408
|
-
|
|
6961
|
+
writeFileSync11(target, source, "utf8");
|
|
6409
6962
|
summary.importedAgents.push(artifact.targetName);
|
|
6410
6963
|
} else {
|
|
6411
|
-
const targetDir =
|
|
6412
|
-
const target =
|
|
6413
|
-
if (!options.overwrite &&
|
|
6964
|
+
const targetDir = join16(skillsDir, artifact.targetName);
|
|
6965
|
+
const target = join16(targetDir, "SKILL.md");
|
|
6966
|
+
if (!options.overwrite && existsSync13(target)) {
|
|
6414
6967
|
summary.skippedSkills.push(artifact.targetName);
|
|
6415
6968
|
continue;
|
|
6416
6969
|
}
|
|
@@ -6418,8 +6971,8 @@ function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
|
|
|
6418
6971
|
summary.importedSkills.push(artifact.targetName);
|
|
6419
6972
|
continue;
|
|
6420
6973
|
}
|
|
6421
|
-
|
|
6422
|
-
|
|
6974
|
+
mkdirSync7(targetDir, { recursive: true });
|
|
6975
|
+
writeFileSync11(target, source, "utf8");
|
|
6423
6976
|
summary.importedSkills.push(artifact.targetName);
|
|
6424
6977
|
}
|
|
6425
6978
|
} catch (error) {
|
|
@@ -6533,6 +7086,37 @@ function validateConfig(config) {
|
|
|
6533
7086
|
}
|
|
6534
7087
|
|
|
6535
7088
|
// src/domains/issues.ts
|
|
7089
|
+
function normalizeIssue(raw) {
|
|
7090
|
+
const id = toStringValue(raw.id, "");
|
|
7091
|
+
if (!id) return null;
|
|
7092
|
+
const createdAt = toStringValue(raw.createdAt, now());
|
|
7093
|
+
const updatedAt = toStringValue(raw.updatedAt, createdAt);
|
|
7094
|
+
const issue = {
|
|
7095
|
+
id,
|
|
7096
|
+
identifier: toStringValue(raw.identifier, id),
|
|
7097
|
+
title: toStringValue(raw.title, `Issue ${id}`),
|
|
7098
|
+
description: toStringValue(raw.description, ""),
|
|
7099
|
+
state: normalizeState(raw.state, raw.plan && typeof raw.plan === "object" ? "PendingApproval" : "Planning"),
|
|
7100
|
+
branchName: toStringValue(raw.branchName),
|
|
7101
|
+
url: toStringValue(raw.url),
|
|
7102
|
+
assigneeId: toStringValue(raw.assigneeId),
|
|
7103
|
+
labels: toStringArray(raw.labels),
|
|
7104
|
+
paths: toStringArray(raw.paths),
|
|
7105
|
+
blockedBy: toStringArray(raw.blockedBy),
|
|
7106
|
+
assignedToWorker: toBooleanValue(raw.assignedToWorker, true),
|
|
7107
|
+
createdAt,
|
|
7108
|
+
updatedAt,
|
|
7109
|
+
history: [],
|
|
7110
|
+
attempts: toNumberValue(raw.attempts, 0),
|
|
7111
|
+
maxAttempts: toNumberValue(raw.maxAttempts, 3),
|
|
7112
|
+
nextRetryAt: toStringValue(raw.nextRetryAt),
|
|
7113
|
+
planVersion: 0,
|
|
7114
|
+
executeAttempt: 0,
|
|
7115
|
+
reviewAttempt: 0,
|
|
7116
|
+
planHistory: []
|
|
7117
|
+
};
|
|
7118
|
+
return issue;
|
|
7119
|
+
}
|
|
6536
7120
|
function nextLocalIssueId(issues) {
|
|
6537
7121
|
const maxId = issues.reduce((current, issue) => {
|
|
6538
7122
|
const match = issue.identifier.match(/^#(\d+)$/);
|
|
@@ -6550,13 +7134,12 @@ function createIssueFromPayload(payload, issues, defaultBranch) {
|
|
|
6550
7134
|
const blockedBy = toStringArray(payload.blockedBy);
|
|
6551
7135
|
const paths = toStringArray(payload.paths);
|
|
6552
7136
|
const images = toStringArray(payload.images);
|
|
6553
|
-
const initialState = parseIssueState(payload.state) ?? (payload.plan ? "
|
|
7137
|
+
const initialState = parseIssueState(payload.state) ?? (payload.plan ? "PendingApproval" : "Planning");
|
|
6554
7138
|
const issue = {
|
|
6555
7139
|
id,
|
|
6556
7140
|
identifier,
|
|
6557
7141
|
title: toStringValue(payload.title, `Issue ${identifier}`),
|
|
6558
7142
|
description: toStringValue(payload.description, ""),
|
|
6559
|
-
priority: clamp(toNumberValue(payload.priority, 1), 1, 10),
|
|
6560
7143
|
state: initialState,
|
|
6561
7144
|
branchName: toStringValue(payload.branchName),
|
|
6562
7145
|
baseBranch: toStringValue(payload.baseBranch) || defaultBranch,
|
|
@@ -6564,10 +7147,6 @@ function createIssueFromPayload(payload, issues, defaultBranch) {
|
|
|
6564
7147
|
assigneeId: toStringValue(payload.assigneeId),
|
|
6565
7148
|
labels: toStringArray(payload.labels),
|
|
6566
7149
|
paths,
|
|
6567
|
-
inferredPaths: [],
|
|
6568
|
-
capabilityCategory: "",
|
|
6569
|
-
capabilityOverlays: [],
|
|
6570
|
-
capabilityRationale: [],
|
|
6571
7150
|
blockedBy,
|
|
6572
7151
|
assignedToWorker: true,
|
|
6573
7152
|
createdAt,
|
|
@@ -6589,21 +7168,10 @@ function createIssueFromPayload(payload, issues, defaultBranch) {
|
|
|
6589
7168
|
if (issue.plan.suggestedPaths?.length && !issue.paths?.length) {
|
|
6590
7169
|
issue.paths = issue.plan.suggestedPaths;
|
|
6591
7170
|
}
|
|
6592
|
-
if (issue.plan.suggestedLabels?.length && !issue.labels?.length) {
|
|
6593
|
-
issue.labels = issue.plan.suggestedLabels;
|
|
6594
|
-
}
|
|
6595
7171
|
if (issue.plan.suggestedEffort && !issue.effort) {
|
|
6596
7172
|
issue.effort = issue.plan.suggestedEffort;
|
|
6597
7173
|
}
|
|
6598
7174
|
}
|
|
6599
|
-
applyCapabilityMetadata(issue, resolveTaskCapabilities({
|
|
6600
|
-
id: issue.id,
|
|
6601
|
-
identifier: issue.identifier,
|
|
6602
|
-
title: issue.title,
|
|
6603
|
-
description: issue.description,
|
|
6604
|
-
labels: issue.labels,
|
|
6605
|
-
paths: issue.paths
|
|
6606
|
-
}, getCapabilityRoutingOptions()));
|
|
6607
7175
|
return issue;
|
|
6608
7176
|
}
|
|
6609
7177
|
function dedupHistoryEntries(issues) {
|
|
@@ -6627,13 +7195,10 @@ function buildRuntimeState(previous, config, projectMetadata = resolveProjectMet
|
|
|
6627
7195
|
identifier: toStringValue(existing.identifier, existing.id),
|
|
6628
7196
|
title: toStringValue(existing.title, `Issue ${toStringValue(existing.identifier, existing.id)}`),
|
|
6629
7197
|
description: toStringValue(existing.description, ""),
|
|
6630
|
-
state: normalizeState(existing.state, existing.plan ? "
|
|
7198
|
+
state: normalizeState(existing.state, existing.plan ? "PendingApproval" : "Planning"),
|
|
6631
7199
|
paths: toStringArray(existing.paths),
|
|
6632
|
-
inferredPaths: toStringArray(existing.inferredPaths),
|
|
6633
7200
|
labels: toStringArray(existing.labels),
|
|
6634
|
-
|
|
6635
|
-
capabilityRationale: toStringArray(existing.capabilityRationale),
|
|
6636
|
-
blockedBy: toStringArray(existing.blockedBy).length > 0 ? toStringArray(existing.blockedBy) : toStringArray(existing.blocked_by),
|
|
7201
|
+
blockedBy: toStringArray(existing.blockedBy),
|
|
6637
7202
|
history: Array.isArray(existing.history) ? existing.history : [],
|
|
6638
7203
|
attempts: clamp(toNumberValue(existing.attempts, 0), 0, config.maxAttemptsDefault),
|
|
6639
7204
|
maxAttempts: clamp(toNumberValue(existing.maxAttempts, config.maxAttemptsDefault), 1, config.maxAttemptsDefault),
|
|
@@ -6706,6 +7271,14 @@ async function transitionIssue(issue, event, context2 = {}) {
|
|
|
6706
7271
|
logger.debug({ issueId: issue.id, identifier: issue.identifier, from: issue.state, event, context: context2 }, "[State] Issue transition");
|
|
6707
7272
|
await executeTransition(issue, event, { ...context2, issue });
|
|
6708
7273
|
}
|
|
7274
|
+
function issueDependenciesResolved(issue, allIssues) {
|
|
7275
|
+
if (issue.blockedBy.length === 0) return true;
|
|
7276
|
+
const map = new Map(allIssues.map((entry) => [entry.id, entry]));
|
|
7277
|
+
return issue.blockedBy.every((dependencyId) => {
|
|
7278
|
+
const dep = map.get(dependencyId);
|
|
7279
|
+
return dep?.state === "Approved" || dep?.state === "Merged";
|
|
7280
|
+
});
|
|
7281
|
+
}
|
|
6709
7282
|
function getNextRetryAt(issue, baseMs) {
|
|
6710
7283
|
const nextAttempt = issue.attempts + 1;
|
|
6711
7284
|
const nextDelay = withRetryBackoff(nextAttempt, baseMs);
|
|
@@ -6723,7 +7296,7 @@ async function handleStatePatch(state, issue, payload) {
|
|
|
6723
7296
|
for (const event of path) {
|
|
6724
7297
|
await transitionIssue(issue, event, { note: `Manual state update: ${nextState}`, reason: toStringValue(payload.reason) });
|
|
6725
7298
|
}
|
|
6726
|
-
if (nextState === "
|
|
7299
|
+
if (nextState === "PendingApproval") {
|
|
6727
7300
|
issue.nextRetryAt = void 0;
|
|
6728
7301
|
issue.lastError = void 0;
|
|
6729
7302
|
}
|
|
@@ -6733,6 +7306,34 @@ async function handleStatePatch(state, issue, payload) {
|
|
|
6733
7306
|
addEvent(state, issue.id, "manual", `Manual state transition to ${nextState}`);
|
|
6734
7307
|
}
|
|
6735
7308
|
|
|
7309
|
+
// src/commands/request-rework.command.ts
|
|
7310
|
+
async function requestReworkCommand(input, deps) {
|
|
7311
|
+
const { issue, reviewerFeedback, note } = input;
|
|
7312
|
+
if (issue.state !== "Reviewing" && issue.state !== "PendingDecision") {
|
|
7313
|
+
throw new Error(
|
|
7314
|
+
`requestReworkCommand requires Reviewing or PendingDecision state, got ${issue.state}.`
|
|
7315
|
+
);
|
|
7316
|
+
}
|
|
7317
|
+
issue.lastError = reviewerFeedback;
|
|
7318
|
+
issue.lastFailedPhase = "review";
|
|
7319
|
+
issue.attempts += 1;
|
|
7320
|
+
if (issue.state === "Reviewing") {
|
|
7321
|
+
await transitionIssueCommand(
|
|
7322
|
+
{ issue, target: "PendingDecision", note: `Reviewer completed for ${issue.identifier}.` },
|
|
7323
|
+
deps
|
|
7324
|
+
);
|
|
7325
|
+
}
|
|
7326
|
+
await transitionIssueCommand(
|
|
7327
|
+
{ issue, target: "Queued", note: note ?? `Reviewer requested rework for ${issue.identifier}.` },
|
|
7328
|
+
deps
|
|
7329
|
+
);
|
|
7330
|
+
deps.eventStore.addEvent(
|
|
7331
|
+
issue.id,
|
|
7332
|
+
"runner",
|
|
7333
|
+
`Issue ${issue.identifier} sent back for rework by reviewer.`
|
|
7334
|
+
);
|
|
7335
|
+
}
|
|
7336
|
+
|
|
6736
7337
|
// src/agents/issue-runner.ts
|
|
6737
7338
|
async function runPlanningJob(state, issue) {
|
|
6738
7339
|
issue.planningStatus = "planning";
|
|
@@ -6741,8 +7342,8 @@ async function runPlanningJob(state, issue) {
|
|
|
6741
7342
|
issue.updatedAt = now();
|
|
6742
7343
|
markIssueDirty(issue.id);
|
|
6743
7344
|
const safeId = idToSafePath(issue.id);
|
|
6744
|
-
const workspaceDir =
|
|
6745
|
-
|
|
7345
|
+
const workspaceDir = join17(WORKSPACE_ROOT, safeId);
|
|
7346
|
+
mkdirSync8(workspaceDir, { recursive: true });
|
|
6746
7347
|
addEvent(state, issue.id, "info", `Plan generation started for ${issue.identifier} (v${(issue.planVersion ?? 0) + 1}).`);
|
|
6747
7348
|
try {
|
|
6748
7349
|
const { plan, usage, prompt } = await generatePlan(
|
|
@@ -6756,7 +7357,6 @@ async function runPlanningJob(state, issue) {
|
|
|
6756
7357
|
markIssuePlanDirty(issue.id);
|
|
6757
7358
|
issue.planVersion = Math.max(issue.planVersion ?? 0, 1);
|
|
6758
7359
|
if (plan.suggestedPaths?.length && !issue.paths?.length) issue.paths = plan.suggestedPaths;
|
|
6759
|
-
if (plan.suggestedLabels?.length && !issue.labels?.length) issue.labels = plan.suggestedLabels;
|
|
6760
7360
|
if (plan.suggestedEffort && !issue.effort) issue.effort = plan.suggestedEffort;
|
|
6761
7361
|
if (usage.totalTokens > 0) {
|
|
6762
7362
|
addTokenUsage(issue, {
|
|
@@ -6768,8 +7368,8 @@ async function runPlanningJob(state, issue) {
|
|
|
6768
7368
|
}
|
|
6769
7369
|
const pv = issue.planVersion;
|
|
6770
7370
|
try {
|
|
6771
|
-
|
|
6772
|
-
|
|
7371
|
+
writeFileSync12(join17(workspaceDir, `plan.v${pv}.json`), JSON.stringify(plan, null, 2), "utf8");
|
|
7372
|
+
writeFileSync12(join17(workspaceDir, `plan.v${pv}.prompt.md`), prompt, "utf8");
|
|
6773
7373
|
} catch (artifactErr) {
|
|
6774
7374
|
logger.warn({ err: String(artifactErr) }, "[Agent] Failed to write versioned plan artifacts");
|
|
6775
7375
|
}
|
|
@@ -6798,21 +7398,21 @@ async function handleReviewStage(state, issue, workspacePath, startTs, routedPro
|
|
|
6798
7398
|
const reviewer = routedProviders.find((p) => p.role === "reviewer");
|
|
6799
7399
|
if (!reviewer) {
|
|
6800
7400
|
issue.mergedReason = "Auto-approved: no reviewer configured.";
|
|
6801
|
-
await transitionIssueCommand({ issue, target: "
|
|
7401
|
+
await transitionIssueCommand({ issue, target: "Approved", note: `No reviewer configured; auto-approved for ${issue.identifier}.` }, container);
|
|
6802
7402
|
return;
|
|
6803
7403
|
}
|
|
6804
7404
|
addEvent(state, issue.id, "info", `Review provider: ${reviewer.role}:${reviewer.provider}${reviewer.model ? `/${reviewer.model}` : ""}${reviewer.profile ? `:${reviewer.profile}` : ""}.`);
|
|
6805
7405
|
let diffSummary = "";
|
|
6806
7406
|
try {
|
|
6807
7407
|
if (issue.baseBranch && issue.branchName) {
|
|
6808
|
-
const diffResult =
|
|
7408
|
+
const diffResult = execSync5(
|
|
6809
7409
|
`git diff --stat "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
6810
7410
|
{ cwd: TARGET_ROOT, encoding: "utf8", maxBuffer: 512e3, timeout: 1e4 }
|
|
6811
7411
|
);
|
|
6812
7412
|
diffSummary = diffResult.trim();
|
|
6813
7413
|
} else {
|
|
6814
7414
|
const diffTarget = issue.worktreePath ?? workspacePath;
|
|
6815
|
-
const diffResult =
|
|
7415
|
+
const diffResult = execSync5(
|
|
6816
7416
|
`git diff --no-index --stat -- "${SOURCE_ROOT}" "${diffTarget}" 2>/dev/null`,
|
|
6817
7417
|
{ encoding: "utf8", maxBuffer: 512e3, timeout: 1e4 }
|
|
6818
7418
|
);
|
|
@@ -6821,10 +7421,10 @@ async function handleReviewStage(state, issue, workspacePath, startTs, routedPro
|
|
|
6821
7421
|
} catch (err) {
|
|
6822
7422
|
diffSummary = (err.stdout || "").trim();
|
|
6823
7423
|
}
|
|
6824
|
-
const compiled = await compileReview(issue, reviewer, workspacePath, diffSummary);
|
|
7424
|
+
const compiled = await compileReview(issue, reviewer, workspacePath, diffSummary, state.config);
|
|
6825
7425
|
const effectiveReviewer = { ...reviewer, command: compiled.command || reviewer.command };
|
|
6826
|
-
const reviewPromptFile =
|
|
6827
|
-
|
|
7426
|
+
const reviewPromptFile = join17(workspacePath, "review-prompt.md");
|
|
7427
|
+
writeFileSync12(reviewPromptFile, `${compiled.prompt}
|
|
6828
7428
|
`, "utf8");
|
|
6829
7429
|
const reviewResult = await runAgentSession(state, issue, effectiveReviewer, 1, workspacePath, compiled.prompt, reviewPromptFile);
|
|
6830
7430
|
issue.durationMs = (issue.durationMs ?? 0) + (Date.now() - startTs);
|
|
@@ -6835,27 +7435,40 @@ async function handleReviewStage(state, issue, workspacePath, startTs, routedPro
|
|
|
6835
7435
|
try {
|
|
6836
7436
|
const rpv = issue.planVersion ?? 1;
|
|
6837
7437
|
const rra = issue.reviewAttempt ?? 1;
|
|
6838
|
-
const vReviewPromptSrc =
|
|
6839
|
-
const vReviewAuditSrc =
|
|
6840
|
-
if (
|
|
6841
|
-
|
|
7438
|
+
const vReviewPromptSrc = join17(workspacePath, "review-prompt.md");
|
|
7439
|
+
const vReviewAuditSrc = join17(workspacePath, "execution-audit.json");
|
|
7440
|
+
if (existsSync14(vReviewPromptSrc)) {
|
|
7441
|
+
writeFileSync12(join17(workspacePath, `review.v${rpv}a${rra}.prompt.md`), readFileSync11(vReviewPromptSrc, "utf8"), "utf8");
|
|
6842
7442
|
}
|
|
6843
|
-
if (
|
|
6844
|
-
|
|
7443
|
+
if (existsSync14(vReviewAuditSrc)) {
|
|
7444
|
+
writeFileSync12(join17(workspacePath, `review.v${rpv}a${rra}.audit.json`), readFileSync11(vReviewAuditSrc, "utf8"), "utf8");
|
|
6845
7445
|
}
|
|
6846
7446
|
} catch (vErr) {
|
|
6847
7447
|
logger.warn({ err: String(vErr) }, "[Agent] Failed to write versioned review artifacts");
|
|
6848
7448
|
}
|
|
6849
7449
|
if (reviewResult.success) {
|
|
6850
7450
|
issue.mergedReason = `Auto-approved by reviewer in ${reviewResult.turns} turn(s).`;
|
|
6851
|
-
await transitionIssueCommand({ issue, target: "
|
|
6852
|
-
await
|
|
7451
|
+
await transitionIssueCommand({ issue, target: "PendingDecision", note: `Reviewer completed for ${issue.identifier}.` }, container);
|
|
7452
|
+
const validation = await runValidationGate(issue, state.config);
|
|
7453
|
+
if (validation) {
|
|
7454
|
+
issue.validationResult = validation;
|
|
7455
|
+
markIssueDirty(issue.id);
|
|
7456
|
+
if (!validation.passed) {
|
|
7457
|
+
addEvent(state, issue.id, "error", `Validation gate failed for ${issue.identifier}: ${validation.command}`);
|
|
7458
|
+
logger.warn({ issueId: issue.id, command: validation.command }, "[Agent] Validation gate failed \u2014 staying in Reviewed");
|
|
7459
|
+
return;
|
|
7460
|
+
}
|
|
7461
|
+
addEvent(state, issue.id, "info", `Validation gate passed for ${issue.identifier}.`);
|
|
7462
|
+
}
|
|
7463
|
+
await transitionIssueCommand({ issue, target: "Approved", note: `Reviewer approved ${issue.identifier} in ${reviewResult.turns} turn(s).` }, container);
|
|
6853
7464
|
} else if (reviewResult.continueRequested) {
|
|
6854
|
-
await
|
|
6855
|
-
|
|
6856
|
-
|
|
7465
|
+
await requestReworkCommand(
|
|
7466
|
+
{ issue, reviewerFeedback: reviewResult.output, note: `Reviewer requested rework for ${issue.identifier}.` },
|
|
7467
|
+
container
|
|
7468
|
+
);
|
|
6857
7469
|
} else {
|
|
6858
7470
|
issue.lastError = reviewResult.output;
|
|
7471
|
+
issue.lastFailedPhase = "review";
|
|
6859
7472
|
issue.attempts += 1;
|
|
6860
7473
|
if (issue.attempts >= issue.maxAttempts) {
|
|
6861
7474
|
issue.cancelledReason = `Max attempts reached (${issue.attempts}/${issue.maxAttempts}): reviewer failed or blocked.`;
|
|
@@ -6873,12 +7486,8 @@ async function handleExecutionStage(state, issue, workspacePath, promptText, pro
|
|
|
6873
7486
|
container.eventStore.addEvent(
|
|
6874
7487
|
issue.id,
|
|
6875
7488
|
"info",
|
|
6876
|
-
`
|
|
7489
|
+
`Agent providers: ${routedProviders.map((p) => `${p.role}:${p.provider}${p.model ? `/${p.model}` : ""}${p.reasoningEffort ? ` [${p.reasoningEffort}]` : ""}`).join(", ")}.`
|
|
6877
7490
|
);
|
|
6878
|
-
const routingSignals = describeRoutingSignals(issue, workspaceDerivedPaths);
|
|
6879
|
-
if (routingSignals) {
|
|
6880
|
-
container.eventStore.addEvent(issue.id, "info", `Capability routing signals: ${routingSignals}.`);
|
|
6881
|
-
}
|
|
6882
7491
|
const runResult = await runAgentPipeline(state, issue, workspacePath, promptText, promptFile, workflowConfig);
|
|
6883
7492
|
issue.durationMs = Date.now() - startTs;
|
|
6884
7493
|
issue.commandExitCode = runResult.code;
|
|
@@ -6898,13 +7507,13 @@ async function handleExecutionStage(state, issue, workspacePath, promptText, pro
|
|
|
6898
7507
|
try {
|
|
6899
7508
|
const epv = issue.planVersion ?? 1;
|
|
6900
7509
|
const eea = issue.executeAttempt ?? 1;
|
|
6901
|
-
const vExecPromptSrc =
|
|
6902
|
-
const vExecAuditSrc =
|
|
6903
|
-
if (
|
|
6904
|
-
|
|
7510
|
+
const vExecPromptSrc = join17(workspacePath, "prompt.md");
|
|
7511
|
+
const vExecAuditSrc = join17(workspacePath, "execution-audit.json");
|
|
7512
|
+
if (existsSync14(vExecPromptSrc)) {
|
|
7513
|
+
writeFileSync12(join17(workspacePath, `execute.v${epv}a${eea}.prompt.md`), readFileSync11(vExecPromptSrc, "utf8"), "utf8");
|
|
6905
7514
|
}
|
|
6906
|
-
if (
|
|
6907
|
-
|
|
7515
|
+
if (existsSync14(vExecAuditSrc)) {
|
|
7516
|
+
writeFileSync12(join17(workspacePath, `execute.v${epv}a${eea}.audit.json`), readFileSync11(vExecAuditSrc, "utf8"), "utf8");
|
|
6908
7517
|
}
|
|
6909
7518
|
} catch (vErr) {
|
|
6910
7519
|
logger.warn({ err: String(vErr) }, "[Agent] Failed to write versioned execute artifacts");
|
|
@@ -6921,6 +7530,7 @@ async function handleExecutionStage(state, issue, workspacePath, promptText, pro
|
|
|
6921
7530
|
container.eventStore.addEvent(issue.id, "runner", `Issue ${issue.identifier} queued for next turn.`);
|
|
6922
7531
|
} else {
|
|
6923
7532
|
issue.lastError = runResult.output;
|
|
7533
|
+
issue.lastFailedPhase = "execute";
|
|
6924
7534
|
issue.attempts += 1;
|
|
6925
7535
|
if (issue.attempts >= issue.maxAttempts) {
|
|
6926
7536
|
issue.commandExitCode = runResult.code;
|
|
@@ -6938,17 +7548,10 @@ async function runIssueOnce(state, issue, running) {
|
|
|
6938
7548
|
const isResuming = issue.state === "Running";
|
|
6939
7549
|
logger.info({ issueId: issue.id, identifier: issue.identifier, state: issue.state, isReviewing, isResuming, attempt: issue.attempts + 1, maxAttempts: issue.maxAttempts }, "[Agent] Starting issue execution");
|
|
6940
7550
|
if (issue.state === "Planning") {
|
|
6941
|
-
|
|
6942
|
-
runPlanningJob(state, issue).catch((err) => logger.error({ err, issueId: issue.id, identifier: issue.identifier }, "[Agent] Unexpected error in background planning job")).finally(() => {
|
|
6943
|
-
state.metrics = computeMetrics(state.issues);
|
|
6944
|
-
state.updatedAt = now();
|
|
6945
|
-
persistState(state).catch(() => {
|
|
6946
|
-
});
|
|
6947
|
-
});
|
|
7551
|
+
logger.warn({ issueId: issue.id }, "[Agent] runIssueOnce called for Planning state \u2014 skipping (queue handles planning)");
|
|
6948
7552
|
return;
|
|
6949
7553
|
}
|
|
6950
7554
|
running.add(issue.id);
|
|
6951
|
-
state.metrics.activeWorkers += 1;
|
|
6952
7555
|
issue.startedAt = issue.startedAt ?? now();
|
|
6953
7556
|
let workflowConfig = null;
|
|
6954
7557
|
try {
|
|
@@ -6973,20 +7576,10 @@ async function runIssueOnce(state, issue, running) {
|
|
|
6973
7576
|
}
|
|
6974
7577
|
try {
|
|
6975
7578
|
const workspaceDerivedPaths = hydrateIssuePathsFromWorkspace(issue);
|
|
6976
|
-
if ((issue.paths ?? []).length > 0) {
|
|
6977
|
-
issue.inferredPaths = [.../* @__PURE__ */ new Set([...issue.inferredPaths ?? [], ...inferCapabilityPaths({
|
|
6978
|
-
id: issue.id,
|
|
6979
|
-
identifier: issue.identifier,
|
|
6980
|
-
title: issue.title,
|
|
6981
|
-
description: issue.description,
|
|
6982
|
-
labels: issue.labels,
|
|
6983
|
-
paths: issue.paths
|
|
6984
|
-
})])];
|
|
6985
|
-
}
|
|
6986
7579
|
const { workspacePath, promptText, promptFile } = await prepareWorkspace(issue, state, state.config.defaultBranch);
|
|
6987
7580
|
container.issueRepository.markDirty(issue.id);
|
|
6988
7581
|
try {
|
|
6989
|
-
const { getIssueStateResource: getIssueStateResource2 } = await import("./store-
|
|
7582
|
+
const { getIssueStateResource: getIssueStateResource2 } = await import("./store-AG6LLYJ7.js");
|
|
6990
7583
|
const res = getIssueStateResource2();
|
|
6991
7584
|
if (res) {
|
|
6992
7585
|
await res.patch(issue.id, {
|
|
@@ -7008,6 +7601,7 @@ async function runIssueOnce(state, issue, running) {
|
|
|
7008
7601
|
} catch (error) {
|
|
7009
7602
|
issue.attempts += 1;
|
|
7010
7603
|
issue.lastError = String(error);
|
|
7604
|
+
issue.lastFailedPhase = issue.lastFailedPhase ?? "execute";
|
|
7011
7605
|
if (issue.attempts >= issue.maxAttempts) {
|
|
7012
7606
|
issue.cancelledReason = `Max attempts reached (${issue.attempts}/${issue.maxAttempts}): unexpected failure \u2014 ${issue.lastError?.slice(0, 120) ?? "unknown error"}.`;
|
|
7013
7607
|
await transitionIssueCommand({ issue, target: "Cancelled", note: `Issue failed unexpectedly: ${issue.lastError}` }, container);
|
|
@@ -7020,18 +7614,25 @@ async function runIssueOnce(state, issue, running) {
|
|
|
7020
7614
|
logger.info({ issueId: issue.id, identifier: issue.identifier, finalState: issue.state, elapsedMs, attempts: issue.attempts }, "[Agent] Issue execution finished");
|
|
7021
7615
|
issue.updatedAt = now();
|
|
7022
7616
|
container.issueRepository.markDirty(issue.id);
|
|
7023
|
-
state.metrics.activeWorkers = Math.max(state.metrics.activeWorkers - 1, 0);
|
|
7024
7617
|
running.delete(issue.id);
|
|
7025
7618
|
state.metrics = computeMetrics(state.issues);
|
|
7026
|
-
state.metrics.activeWorkers = Math.max(state.metrics.activeWorkers, 0);
|
|
7027
7619
|
state.updatedAt = now();
|
|
7028
7620
|
await container.persistencePort.persistState(state);
|
|
7029
7621
|
}
|
|
7030
7622
|
}
|
|
7031
7623
|
|
|
7032
7624
|
export {
|
|
7625
|
+
addTokenUsage,
|
|
7626
|
+
extractTokenUsage,
|
|
7627
|
+
tryParseJsonOutput,
|
|
7628
|
+
readAgentDirective,
|
|
7629
|
+
readAgentPid,
|
|
7630
|
+
isProcessAlive,
|
|
7033
7631
|
isAgentStillRunning,
|
|
7034
7632
|
cleanStalePidFile,
|
|
7633
|
+
loadAgentPipelineState,
|
|
7634
|
+
loadAgentPipelineSnapshotForIssue,
|
|
7635
|
+
loadAgentSessionSnapshotsForIssue,
|
|
7035
7636
|
hydrate,
|
|
7036
7637
|
recoverPlanningSession,
|
|
7037
7638
|
loadRuntimeSettings,
|
|
@@ -7041,15 +7642,27 @@ export {
|
|
|
7041
7642
|
createContainer,
|
|
7042
7643
|
runPlanningJob,
|
|
7043
7644
|
runIssueOnce,
|
|
7645
|
+
isShuttingDown,
|
|
7646
|
+
installGracefulShutdown,
|
|
7647
|
+
analyzeParallelizability,
|
|
7648
|
+
ensureNotStale,
|
|
7649
|
+
hasTerminalQueue,
|
|
7044
7650
|
deriveConfig,
|
|
7045
7651
|
applyWorkflowConfig,
|
|
7046
7652
|
validateConfig,
|
|
7653
|
+
normalizeIssue,
|
|
7654
|
+
nextLocalIssueId,
|
|
7655
|
+
createIssueFromPayload,
|
|
7656
|
+
dedupHistoryEntries,
|
|
7047
7657
|
buildRuntimeState,
|
|
7048
7658
|
addEvent,
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7659
|
+
transitionIssue,
|
|
7660
|
+
issueDependenciesResolved,
|
|
7661
|
+
getNextRetryAt,
|
|
7662
|
+
handleStatePatch,
|
|
7663
|
+
runAgentSession,
|
|
7664
|
+
runAgentPipeline,
|
|
7665
|
+
issueHasResumableSession,
|
|
7053
7666
|
startApiServer,
|
|
7054
7667
|
getStateDb,
|
|
7055
7668
|
getIssueStateResource,
|
|
@@ -7078,4 +7691,4 @@ export {
|
|
|
7078
7691
|
syncReferenceRepositories,
|
|
7079
7692
|
importReferenceArtifacts
|
|
7080
7693
|
};
|
|
7081
|
-
//# sourceMappingURL=chunk-
|
|
7694
|
+
//# sourceMappingURL=chunk-HSGUPFTV.js.map
|