fifony 0.1.27 → 0.1.28
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 +51 -29
- package/app/dist/assets/{KeyboardShortcutsHelp-NmaeCZMn.js → KeyboardShortcutsHelp-BrI56bfa.js} +1 -1
- package/app/dist/assets/OnboardingWizard-7MvouAkN.js +1 -0
- package/app/dist/assets/{analytics.lazy-BpH26eA2.js → analytics.lazy-D99c8M-T.js} +1 -1
- package/app/dist/assets/{createLucideIcon-BWC-guQt.js → createLucideIcon-DgMTp0yx.js} +1 -1
- package/app/dist/assets/index-DHHTOl-9.js +45 -0
- package/app/dist/assets/{index-DntTEHv8.css → index-ZlyvZ7KI.css} +1 -1
- package/app/dist/assets/vendor-D-IqxHHu.js +9 -0
- package/app/dist/index.html +4 -4
- package/app/dist/service-worker.js +1 -1
- package/dist/agent/run-local.js +64 -144
- package/dist/agent-FPUYBJZD.js +74 -0
- package/dist/chunk-2G6SRDOC.js +847 -0
- package/dist/{chunk-G7W4NEOA.js → chunk-3FCJI2GK.js} +1232 -633
- package/dist/chunk-O5AEQXUV.js +311 -0
- package/dist/chunk-OONOOWNC.js +123 -0
- package/dist/chunk-VOQT7RVT.js +295 -0
- package/dist/{chunk-XN2QKKMY.js → chunk-XVF6GOVS.js} +456 -814
- package/dist/cli.js +6 -4
- package/dist/issue-runner-MRHO5ZAB.js +15 -0
- package/dist/{issue-state-machine-SKODQ6MG.js → issue-state-machine-V2KPUYPW.js} +5 -3
- package/dist/issues-3PUMY63N.js +40 -0
- package/dist/mcp/server.js +23 -121
- package/dist/queue-workers-EGHCDDLB.js +23 -0
- package/dist/scheduler-V4GMCBTE.js +21 -0
- package/dist/{store-366NGWR4.js → store-RVKQ6UEY.js} +7 -5
- package/dist/workspace-KEHFITYR.js +52 -0
- package/package.json +6 -6
- package/app/dist/assets/OnboardingWizard-CwW6b_X4.js +0 -1
- package/app/dist/assets/index-D6jtlB7h.js +0 -43
- package/app/dist/assets/vendor-BTlTWMUF.js +0 -9
- package/dist/chunk-AMOGDOM7.js +0 -796
- package/dist/chunk-MT3S55TM.js +0 -91
- package/dist/issue-runner-MTAIYNVN.js +0 -13
- package/dist/queue-workers-Q3IWRFLI.js +0 -20
|
@@ -1,36 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
areQueueWorkersActive,
|
|
3
|
-
enqueueForExecution,
|
|
4
|
-
enqueueForPlanning,
|
|
5
|
-
enqueueForReview
|
|
6
|
-
} from "./chunk-MT3S55TM.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,69 @@ 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-2G6SRDOC.js";
|
|
25
|
+
import {
|
|
26
|
+
ADAPTERS,
|
|
27
|
+
assertIssueHasGitWorktree,
|
|
28
|
+
buildExecutionPayload,
|
|
29
|
+
buildFullPlanPrompt,
|
|
30
|
+
buildProviderBasePrompt,
|
|
31
|
+
buildRetryContext,
|
|
32
|
+
buildTurnPrompt,
|
|
33
|
+
cleanWorkspace,
|
|
34
|
+
computeDiffStats,
|
|
35
|
+
detectAvailableProviders,
|
|
36
|
+
discoverModels,
|
|
37
|
+
ensureGitRepoReadyForWorktrees,
|
|
38
|
+
ensureWorktreeCommitted,
|
|
39
|
+
getEffectiveAgentProviders,
|
|
40
|
+
getGitRepoStatus,
|
|
41
|
+
getProviderDefaultCommand,
|
|
42
|
+
hydrateIssuePathsFromWorkspace,
|
|
43
|
+
initializeGitRepoForWorktrees,
|
|
41
44
|
mergeWorkspace,
|
|
42
45
|
normalizeAgentProvider,
|
|
43
46
|
parseDiffStats,
|
|
44
47
|
prepareWorkspace,
|
|
45
|
-
pushWorktreeBranch,
|
|
46
48
|
readCodexConfig,
|
|
47
49
|
resolveAgentCommand,
|
|
48
50
|
runCommandWithTimeout,
|
|
49
|
-
runHook
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
runHook
|
|
52
|
+
} from "./chunk-XVF6GOVS.js";
|
|
53
|
+
import {
|
|
54
|
+
appendFileTail,
|
|
55
|
+
clamp,
|
|
56
|
+
debugBoot,
|
|
57
|
+
extractJsonObjects,
|
|
58
|
+
fail,
|
|
59
|
+
idToSafePath,
|
|
60
|
+
isoWeek,
|
|
61
|
+
normalizeState,
|
|
62
|
+
now,
|
|
63
|
+
parseEnvNumber,
|
|
64
|
+
parseIntArg,
|
|
65
|
+
parseIssueState,
|
|
66
|
+
parsePositiveIntEnv,
|
|
67
|
+
renderPrompt,
|
|
68
|
+
repairTruncatedJson,
|
|
69
|
+
toBooleanValue,
|
|
70
|
+
toNumberValue,
|
|
71
|
+
toStringArray,
|
|
72
|
+
toStringValue,
|
|
73
|
+
withRetryBackoff
|
|
74
|
+
} from "./chunk-O5AEQXUV.js";
|
|
75
|
+
import {
|
|
76
|
+
enqueue
|
|
77
|
+
} from "./chunk-VOQT7RVT.js";
|
|
78
|
+
import {
|
|
79
|
+
logger
|
|
80
|
+
} from "./chunk-DVU3CXWA.js";
|
|
57
81
|
import {
|
|
58
82
|
ALLOWED_STATES,
|
|
59
83
|
ATTACHMENTS_ROOT,
|
|
@@ -81,42 +105,18 @@ import {
|
|
|
81
105
|
STATE_ROOT,
|
|
82
106
|
TARGET_ROOT,
|
|
83
107
|
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";
|
|
108
|
+
WORKSPACE_ROOT
|
|
109
|
+
} from "./chunk-OONOOWNC.js";
|
|
110
110
|
|
|
111
111
|
// src/agents/issue-runner.ts
|
|
112
112
|
import {
|
|
113
|
-
existsSync as
|
|
114
|
-
mkdirSync as
|
|
115
|
-
readFileSync as
|
|
116
|
-
writeFileSync as
|
|
113
|
+
existsSync as existsSync14,
|
|
114
|
+
mkdirSync as mkdirSync8,
|
|
115
|
+
readFileSync as readFileSync11,
|
|
116
|
+
writeFileSync as writeFileSync12
|
|
117
117
|
} from "fs";
|
|
118
|
-
import { join as
|
|
119
|
-
import { execSync as
|
|
118
|
+
import { join as join17 } from "path";
|
|
119
|
+
import { execSync as execSync5 } from "child_process";
|
|
120
120
|
|
|
121
121
|
// src/domains/tokens.ts
|
|
122
122
|
var EMPTY = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
@@ -290,19 +290,19 @@ function getAnalytics(topN = 20) {
|
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
// src/domains/project.ts
|
|
293
|
-
import { execFileSync, spawn as spawn3 } from "child_process";
|
|
293
|
+
import { execFileSync as execFileSync2, spawn as spawn3 } from "child_process";
|
|
294
294
|
import { createHash } from "crypto";
|
|
295
295
|
import {
|
|
296
|
-
existsSync as
|
|
297
|
-
mkdirSync as
|
|
296
|
+
existsSync as existsSync13,
|
|
297
|
+
mkdirSync as mkdirSync7,
|
|
298
298
|
mkdtempSync as mkdtempSync4,
|
|
299
|
-
readdirSync as
|
|
300
|
-
readFileSync as
|
|
299
|
+
readdirSync as readdirSync4,
|
|
300
|
+
readFileSync as readFileSync10,
|
|
301
301
|
rmSync as rmSync5,
|
|
302
|
-
writeFileSync as
|
|
302
|
+
writeFileSync as writeFileSync11
|
|
303
303
|
} from "fs";
|
|
304
304
|
import { homedir as homedir3, tmpdir as tmpdir4 } from "os";
|
|
305
|
-
import { basename as basename4, dirname as dirname2, join as
|
|
305
|
+
import { basename as basename4, dirname as dirname2, join as join16, relative as relativePath, resolve as resolve2 } from "path";
|
|
306
306
|
import { env as env3 } from "process";
|
|
307
307
|
|
|
308
308
|
// src/persistence/plugins/api-runtime-context.ts
|
|
@@ -322,8 +322,8 @@ function getApiRuntimeContextOrThrow() {
|
|
|
322
322
|
|
|
323
323
|
// src/persistence/plugins/api-server.ts
|
|
324
324
|
import {
|
|
325
|
-
existsSync as
|
|
326
|
-
readFileSync as
|
|
325
|
+
existsSync as existsSync12,
|
|
326
|
+
readFileSync as readFileSync9
|
|
327
327
|
} from "fs";
|
|
328
328
|
|
|
329
329
|
// src/persistence/resources/runtime-state.resource.ts
|
|
@@ -417,6 +417,31 @@ function extractTokenUsage(output, jsonObj) {
|
|
|
417
417
|
};
|
|
418
418
|
}
|
|
419
419
|
}
|
|
420
|
+
const stats = jsonObj.stats;
|
|
421
|
+
const geminiModels = stats?.models ?? null;
|
|
422
|
+
if (geminiModels && typeof geminiModels === "object") {
|
|
423
|
+
let totalInput = 0, totalOutput = 0, primaryModel = "", maxTokens = 0;
|
|
424
|
+
for (const [model, data] of Object.entries(geminiModels)) {
|
|
425
|
+
const tokens = data?.tokens;
|
|
426
|
+
if (!tokens) continue;
|
|
427
|
+
const inp = Number(tokens.input || 0) + Number(tokens.cached || 0);
|
|
428
|
+
const out = Number(tokens.candidates || 0);
|
|
429
|
+
totalInput += inp;
|
|
430
|
+
totalOutput += out;
|
|
431
|
+
if (inp + out > maxTokens) {
|
|
432
|
+
maxTokens = inp + out;
|
|
433
|
+
primaryModel = model;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (totalInput > 0 || totalOutput > 0) {
|
|
437
|
+
return {
|
|
438
|
+
inputTokens: totalInput,
|
|
439
|
+
outputTokens: totalOutput,
|
|
440
|
+
totalTokens: totalInput + totalOutput,
|
|
441
|
+
model: primaryModel || void 0
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
420
445
|
const usage = jsonObj.usage;
|
|
421
446
|
if (usage && typeof usage === "object") {
|
|
422
447
|
const inp = Number(usage.input_tokens) || 0;
|
|
@@ -466,6 +491,15 @@ function tryParseJsonOutput(output) {
|
|
|
466
491
|
}
|
|
467
492
|
}
|
|
468
493
|
if (obj.status) return obj;
|
|
494
|
+
if (typeof obj.response === "string") {
|
|
495
|
+
try {
|
|
496
|
+
const inner = JSON.parse(obj.response);
|
|
497
|
+
if (inner && typeof inner === "object" && !Array.isArray(inner)) {
|
|
498
|
+
return inner;
|
|
499
|
+
}
|
|
500
|
+
} catch {
|
|
501
|
+
}
|
|
502
|
+
}
|
|
469
503
|
}
|
|
470
504
|
} catch {
|
|
471
505
|
}
|
|
@@ -764,24 +798,25 @@ async function loadAgentSessionSnapshotsForIssue(issue, providers, pipeline, _wo
|
|
|
764
798
|
|
|
765
799
|
// src/agents/agent-pipeline.ts
|
|
766
800
|
import {
|
|
767
|
-
|
|
801
|
+
mkdirSync as mkdirSync2,
|
|
802
|
+
writeFileSync as writeFileSync3
|
|
768
803
|
} from "fs";
|
|
769
|
-
import { join as
|
|
804
|
+
import { join as join6 } from "path";
|
|
770
805
|
|
|
771
806
|
// src/agents/adapters/index.ts
|
|
772
807
|
import { writeFileSync } from "fs";
|
|
773
808
|
import { join as join3 } from "path";
|
|
774
|
-
async function compileExecution(issue, provider, config, workspacePath, skillContext) {
|
|
809
|
+
async function compileExecution(issue, provider, config, workspacePath, skillContext, capabilitiesManifest) {
|
|
775
810
|
const plan = issue.plan;
|
|
776
811
|
if (!plan?.steps?.length) return null;
|
|
777
812
|
const adapter = ADAPTERS[provider.provider];
|
|
778
813
|
if (!adapter) return null;
|
|
779
814
|
const payload = buildExecutionPayload(issue, provider, plan, workspacePath);
|
|
780
|
-
const compiled = await adapter.compile(issue, provider, plan, config, workspacePath, skillContext);
|
|
815
|
+
const compiled = await adapter.compile(issue, provider, plan, config, workspacePath, skillContext, capabilitiesManifest);
|
|
781
816
|
compiled.payload = payload;
|
|
782
817
|
return compiled;
|
|
783
818
|
}
|
|
784
|
-
async function compileReview(issue, reviewer, workspacePath, diffSummary) {
|
|
819
|
+
async function compileReview(issue, reviewer, workspacePath, diffSummary, config) {
|
|
785
820
|
const plan = issue.plan;
|
|
786
821
|
const prompt = await renderPrompt("compile-review", {
|
|
787
822
|
issueIdentifier: issue.identifier,
|
|
@@ -794,7 +829,7 @@ async function compileReview(issue, reviewer, workspacePath, diffSummary) {
|
|
|
794
829
|
diffSummary
|
|
795
830
|
});
|
|
796
831
|
const adapter = ADAPTERS[reviewer.provider];
|
|
797
|
-
const command = adapter ? adapter.buildReviewCommand(reviewer) : reviewer.command;
|
|
832
|
+
const command = adapter ? adapter.buildReviewCommand(reviewer, config) : reviewer.command;
|
|
798
833
|
return { prompt, command };
|
|
799
834
|
}
|
|
800
835
|
function buildExecutionAudit(provider, compiled, issue, durationMs, result) {
|
|
@@ -863,32 +898,186 @@ function persistExecutionAudit(workspacePath, audit) {
|
|
|
863
898
|
}
|
|
864
899
|
|
|
865
900
|
// src/agents/skills.ts
|
|
866
|
-
import { existsSync as
|
|
901
|
+
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4 } from "fs";
|
|
867
902
|
import { homedir } from "os";
|
|
868
|
-
import { join as
|
|
903
|
+
import { join as join5, resolve } from "path";
|
|
904
|
+
|
|
905
|
+
// src/agents/catalog.ts
|
|
906
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
907
|
+
import { join as join4 } from "path";
|
|
908
|
+
function parseFrontmatter(content) {
|
|
909
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
910
|
+
if (!match) return {};
|
|
911
|
+
const result = {};
|
|
912
|
+
for (const line of match[1].split("\n")) {
|
|
913
|
+
const idx = line.indexOf(":");
|
|
914
|
+
if (idx === -1) continue;
|
|
915
|
+
const key = line.slice(0, idx).trim();
|
|
916
|
+
const value = line.slice(idx + 1).trim().replace(/^["']|["']$/g, "");
|
|
917
|
+
if (key) result[key] = value;
|
|
918
|
+
}
|
|
919
|
+
return result;
|
|
920
|
+
}
|
|
921
|
+
function loadAgentCatalog() {
|
|
922
|
+
const entries = [];
|
|
923
|
+
try {
|
|
924
|
+
const repos = listReferenceRepositories();
|
|
925
|
+
for (const repo of repos) {
|
|
926
|
+
if (!repo.present || !repo.synced) continue;
|
|
927
|
+
const artifacts = collectArtifacts(repo.path, repo.id).filter((a) => a.kind === "agent");
|
|
928
|
+
for (const artifact of artifacts) {
|
|
929
|
+
try {
|
|
930
|
+
const content = readFileSync3(artifact.sourcePath, "utf8");
|
|
931
|
+
const fm = parseFrontmatter(content);
|
|
932
|
+
entries.push({
|
|
933
|
+
name: artifact.targetName,
|
|
934
|
+
displayName: fm.name || artifact.targetName,
|
|
935
|
+
description: fm.description || "",
|
|
936
|
+
emoji: fm.emoji || "\u{1F916}",
|
|
937
|
+
domains: fm.domains ? fm.domains.split(",").map((d) => d.trim()).filter(Boolean) : [],
|
|
938
|
+
source: repo.id,
|
|
939
|
+
content
|
|
940
|
+
});
|
|
941
|
+
} catch (err) {
|
|
942
|
+
logger.warn({ err, path: artifact.sourcePath }, "Failed to read agent file");
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
} catch (error) {
|
|
947
|
+
logger.error({ err: error }, "Failed to load agent catalog from repositories");
|
|
948
|
+
}
|
|
949
|
+
return entries;
|
|
950
|
+
}
|
|
951
|
+
function loadSkillCatalog() {
|
|
952
|
+
return [];
|
|
953
|
+
}
|
|
954
|
+
function filterByDomains(catalog, domains) {
|
|
955
|
+
const domainSet = new Set(domains.map((d) => d.toLowerCase().trim()));
|
|
956
|
+
if (domainSet.size === 0) return catalog;
|
|
957
|
+
const scored = catalog.map((entry) => {
|
|
958
|
+
const matchCount = entry.domains.filter((d) => domainSet.has(d.toLowerCase())).length;
|
|
959
|
+
return { entry, matchCount };
|
|
960
|
+
});
|
|
961
|
+
return scored.filter((item) => item.matchCount > 0).sort((a, b) => b.matchCount - a.matchCount).map((item) => item.entry);
|
|
962
|
+
}
|
|
963
|
+
function installAgents(targetRoot, agentNames, catalog) {
|
|
964
|
+
const result = { installed: [], skipped: [], errors: [] };
|
|
965
|
+
const catalogMap = new Map(catalog.map((entry) => [entry.name, entry]));
|
|
966
|
+
const agentsDir = join4(targetRoot, ".claude", "agents");
|
|
967
|
+
try {
|
|
968
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
969
|
+
} catch (error) {
|
|
970
|
+
logger.error({ err: error, path: agentsDir }, "Failed to create agents directory");
|
|
971
|
+
result.errors.push({ name: "_directory", error: `Failed to create ${agentsDir}` });
|
|
972
|
+
return result;
|
|
973
|
+
}
|
|
974
|
+
for (const name of agentNames) {
|
|
975
|
+
const entry = catalogMap.get(name);
|
|
976
|
+
if (!entry) {
|
|
977
|
+
result.errors.push({ name, error: "Agent not found in catalog" });
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
const filePath = join4(agentsDir, `${name}.md`);
|
|
981
|
+
if (existsSync3(filePath)) {
|
|
982
|
+
result.skipped.push(name);
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
try {
|
|
986
|
+
writeFileSync2(filePath, entry.content, "utf8");
|
|
987
|
+
result.installed.push(name);
|
|
988
|
+
logger.info({ agent: name, path: filePath }, "Agent installed");
|
|
989
|
+
} catch (error) {
|
|
990
|
+
result.errors.push({
|
|
991
|
+
name,
|
|
992
|
+
error: error instanceof Error ? error.message : String(error)
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return result;
|
|
997
|
+
}
|
|
998
|
+
function installSkills(targetRoot, skillNames, catalog) {
|
|
999
|
+
const result = { installed: [], skipped: [], errors: [] };
|
|
1000
|
+
const catalogMap = new Map(catalog.map((entry) => [entry.name, entry]));
|
|
1001
|
+
const skillsDir = join4(targetRoot, ".claude", "skills");
|
|
1002
|
+
try {
|
|
1003
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
logger.error({ err: error, path: skillsDir }, "Failed to create skills directory");
|
|
1006
|
+
result.errors.push({ name: "_directory", error: `Failed to create ${skillsDir}` });
|
|
1007
|
+
return result;
|
|
1008
|
+
}
|
|
1009
|
+
for (const name of skillNames) {
|
|
1010
|
+
const entry = catalogMap.get(name);
|
|
1011
|
+
if (!entry) {
|
|
1012
|
+
result.errors.push({ name, error: "Skill not found in catalog" });
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
const skillDir = join4(skillsDir, name);
|
|
1016
|
+
const filePath = join4(skillDir, "SKILL.md");
|
|
1017
|
+
if (existsSync3(filePath)) {
|
|
1018
|
+
result.skipped.push(name);
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
try {
|
|
1022
|
+
mkdirSync(skillDir, { recursive: true });
|
|
1023
|
+
if (entry.installType === "bundled" && entry.content) {
|
|
1024
|
+
writeFileSync2(filePath, entry.content, "utf8");
|
|
1025
|
+
} else {
|
|
1026
|
+
const referenceContent = [
|
|
1027
|
+
`# ${entry.displayName}`,
|
|
1028
|
+
"",
|
|
1029
|
+
entry.description,
|
|
1030
|
+
"",
|
|
1031
|
+
`**Source**: ${entry.source}`,
|
|
1032
|
+
entry.url ? `**URL**: ${entry.url}` : "",
|
|
1033
|
+
"",
|
|
1034
|
+
`> This skill references an external resource. Install it from the source above.`
|
|
1035
|
+
].filter(Boolean).join("\n");
|
|
1036
|
+
writeFileSync2(filePath, referenceContent, "utf8");
|
|
1037
|
+
}
|
|
1038
|
+
result.installed.push(name);
|
|
1039
|
+
logger.info({ skill: name, path: filePath, type: entry.installType }, "Skill installed");
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
result.errors.push({
|
|
1042
|
+
name,
|
|
1043
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return result;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// src/agents/skills.ts
|
|
869
1051
|
function discoverSkills(workspacePath) {
|
|
870
1052
|
const home = homedir();
|
|
871
|
-
const codePath =
|
|
1053
|
+
const codePath = existsSync4(join5(workspacePath, "worktree")) ? join5(workspacePath, "worktree") : workspacePath;
|
|
872
1054
|
const searchPaths = [
|
|
873
1055
|
resolve(codePath, ".codex", "skills"),
|
|
874
1056
|
resolve(codePath, ".claude", "skills"),
|
|
875
|
-
|
|
876
|
-
|
|
1057
|
+
join5(home, ".codex", "skills"),
|
|
1058
|
+
join5(home, ".claude", "skills")
|
|
877
1059
|
];
|
|
878
1060
|
const seen = /* @__PURE__ */ new Set();
|
|
879
1061
|
const skills = [];
|
|
880
1062
|
for (const basePath of searchPaths) {
|
|
881
|
-
if (!
|
|
1063
|
+
if (!existsSync4(basePath)) continue;
|
|
882
1064
|
for (const entry of readdirSync(basePath, { withFileTypes: true })) {
|
|
883
1065
|
if (!entry.isDirectory()) continue;
|
|
884
1066
|
if (seen.has(entry.name)) continue;
|
|
885
|
-
const skillFile =
|
|
886
|
-
if (!
|
|
1067
|
+
const skillFile = join5(basePath, entry.name, "SKILL.md");
|
|
1068
|
+
if (!existsSync4(skillFile)) continue;
|
|
887
1069
|
try {
|
|
888
|
-
const content =
|
|
1070
|
+
const content = readFileSync4(skillFile, "utf8").trim();
|
|
889
1071
|
if (content) {
|
|
890
1072
|
seen.add(entry.name);
|
|
891
|
-
|
|
1073
|
+
const fm = parseFrontmatter(content);
|
|
1074
|
+
skills.push({
|
|
1075
|
+
name: entry.name,
|
|
1076
|
+
content,
|
|
1077
|
+
description: fm.description || void 0,
|
|
1078
|
+
whenToUse: fm.when_to_use || fm.whenToUse || void 0,
|
|
1079
|
+
avoidIf: fm.avoid_if || fm.avoidIf || void 0
|
|
1080
|
+
});
|
|
892
1081
|
}
|
|
893
1082
|
} catch {
|
|
894
1083
|
}
|
|
@@ -906,8 +1095,141 @@ ${skill.content}`
|
|
|
906
1095
|
|
|
907
1096
|
${sections.join("\n\n")}`;
|
|
908
1097
|
}
|
|
1098
|
+
function extractFirstLine(content) {
|
|
1099
|
+
for (const line of content.split("\n")) {
|
|
1100
|
+
const trimmed = line.replace(/^#+\s*/, "").trim();
|
|
1101
|
+
if (trimmed && !trimmed.startsWith("---")) return trimmed;
|
|
1102
|
+
}
|
|
1103
|
+
return "";
|
|
1104
|
+
}
|
|
1105
|
+
function discoverAgents(workspacePath) {
|
|
1106
|
+
const home = homedir();
|
|
1107
|
+
const codePath = existsSync4(join5(workspacePath, "worktree")) ? join5(workspacePath, "worktree") : workspacePath;
|
|
1108
|
+
const searchPaths = [
|
|
1109
|
+
resolve(codePath, ".claude", "agents"),
|
|
1110
|
+
resolve(codePath, ".codex", "agents"),
|
|
1111
|
+
join5(home, ".claude", "agents"),
|
|
1112
|
+
join5(home, ".codex", "agents")
|
|
1113
|
+
];
|
|
1114
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1115
|
+
const agents = [];
|
|
1116
|
+
for (const basePath of searchPaths) {
|
|
1117
|
+
if (!existsSync4(basePath)) continue;
|
|
1118
|
+
for (const entry of readdirSync(basePath, { withFileTypes: true })) {
|
|
1119
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1120
|
+
const name = entry.name.replace(/\.md$/, "");
|
|
1121
|
+
if (seen.has(name)) continue;
|
|
1122
|
+
try {
|
|
1123
|
+
const content = readFileSync4(join5(basePath, entry.name), "utf8").trim();
|
|
1124
|
+
if (content) {
|
|
1125
|
+
seen.add(name);
|
|
1126
|
+
const fm = parseFrontmatter(content);
|
|
1127
|
+
agents.push({
|
|
1128
|
+
name,
|
|
1129
|
+
description: fm.description || extractFirstLine(content),
|
|
1130
|
+
whenToUse: fm.when_to_use || fm.whenToUse || void 0,
|
|
1131
|
+
avoidIf: fm.avoid_if || fm.avoidIf || void 0
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
} catch {
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return agents;
|
|
1139
|
+
}
|
|
1140
|
+
function discoverCommands(workspacePath) {
|
|
1141
|
+
const home = homedir();
|
|
1142
|
+
const codePath = existsSync4(join5(workspacePath, "worktree")) ? join5(workspacePath, "worktree") : workspacePath;
|
|
1143
|
+
const searchPaths = [
|
|
1144
|
+
resolve(codePath, ".claude", "commands"),
|
|
1145
|
+
resolve(codePath, ".codex", "commands"),
|
|
1146
|
+
join5(home, ".claude", "commands"),
|
|
1147
|
+
join5(home, ".codex", "commands")
|
|
1148
|
+
];
|
|
1149
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1150
|
+
const commands = [];
|
|
1151
|
+
for (const basePath of searchPaths) {
|
|
1152
|
+
if (!existsSync4(basePath)) continue;
|
|
1153
|
+
for (const entry of readdirSync(basePath, { withFileTypes: true })) {
|
|
1154
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1155
|
+
const name = entry.name.replace(/\.md$/, "");
|
|
1156
|
+
if (seen.has(name)) continue;
|
|
1157
|
+
try {
|
|
1158
|
+
const content = readFileSync4(join5(basePath, entry.name), "utf8").trim();
|
|
1159
|
+
if (content) {
|
|
1160
|
+
seen.add(name);
|
|
1161
|
+
commands.push({ name, description: extractFirstLine(content) });
|
|
1162
|
+
}
|
|
1163
|
+
} catch {
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return commands;
|
|
1168
|
+
}
|
|
1169
|
+
var MAX_CAPABILITIES_ITEMS = 40;
|
|
1170
|
+
function buildCapabilitiesManifest(skills, agents, commands) {
|
|
1171
|
+
if (skills.length === 0 && agents.length === 0 && commands.length === 0) return "";
|
|
1172
|
+
const sections = ["## Your Capabilities"];
|
|
1173
|
+
let itemCount = 0;
|
|
1174
|
+
if (commands.length > 0) {
|
|
1175
|
+
sections.push("");
|
|
1176
|
+
sections.push("### Slash Commands");
|
|
1177
|
+
sections.push("You have these commands available. Invoke with `/command-name`:");
|
|
1178
|
+
const show = commands.slice(0, MAX_CAPABILITIES_ITEMS);
|
|
1179
|
+
for (const cmd of show) {
|
|
1180
|
+
sections.push(`- \`/${cmd.name}\`${cmd.description ? ` \u2014 ${cmd.description}` : ""}`);
|
|
1181
|
+
itemCount++;
|
|
1182
|
+
}
|
|
1183
|
+
if (commands.length > show.length) {
|
|
1184
|
+
sections.push(`- ...and ${commands.length - show.length} more available`);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
if (skills.length > 0) {
|
|
1188
|
+
const remaining = Math.max(5, MAX_CAPABILITIES_ITEMS - itemCount);
|
|
1189
|
+
sections.push("");
|
|
1190
|
+
sections.push("### Skills");
|
|
1191
|
+
sections.push("Specialized procedures available for this workspace:");
|
|
1192
|
+
const show = skills.slice(0, remaining);
|
|
1193
|
+
for (const skill of show) {
|
|
1194
|
+
let line = `- **${skill.name}**`;
|
|
1195
|
+
if (skill.description) line += ` \u2014 ${skill.description}`;
|
|
1196
|
+
if (skill.whenToUse) line += ` (Use when: ${skill.whenToUse})`;
|
|
1197
|
+
sections.push(line);
|
|
1198
|
+
itemCount++;
|
|
1199
|
+
}
|
|
1200
|
+
if (skills.length > show.length) {
|
|
1201
|
+
sections.push(`- ...and ${skills.length - show.length} more available`);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
if (agents.length > 0) {
|
|
1205
|
+
const remaining = Math.max(5, MAX_CAPABILITIES_ITEMS - itemCount);
|
|
1206
|
+
sections.push("");
|
|
1207
|
+
sections.push("### Subagents");
|
|
1208
|
+
sections.push("You can delegate to these specialist agents via the Agent tool:");
|
|
1209
|
+
const show = agents.slice(0, remaining);
|
|
1210
|
+
for (const agent of show) {
|
|
1211
|
+
let line = `- **${agent.name}**`;
|
|
1212
|
+
if (agent.description) line += ` \u2014 ${agent.description}`;
|
|
1213
|
+
if (agent.whenToUse) line += ` (Use when: ${agent.whenToUse})`;
|
|
1214
|
+
if (agent.avoidIf) line += ` (Avoid if: ${agent.avoidIf})`;
|
|
1215
|
+
sections.push(line);
|
|
1216
|
+
}
|
|
1217
|
+
if (agents.length > show.length) {
|
|
1218
|
+
sections.push(`- ...and ${agents.length - show.length} more available`);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
sections.push("");
|
|
1222
|
+
sections.push("When a task matches a capability above, USE IT instead of doing everything manually.");
|
|
1223
|
+
return sections.join("\n");
|
|
1224
|
+
}
|
|
909
1225
|
|
|
910
1226
|
// src/agents/agent-pipeline.ts
|
|
1227
|
+
function resolveOutputFileName(role, planVersion, attempt, turn) {
|
|
1228
|
+
if (role === "planner") {
|
|
1229
|
+
return `plan.v${planVersion}.t${turn}.stdout.log`;
|
|
1230
|
+
}
|
|
1231
|
+
return `${role === "reviewer" ? "review" : "execute"}.v${planVersion}a${attempt}.t${turn}.stdout.log`;
|
|
1232
|
+
}
|
|
911
1233
|
async function runAgentSession(state, issue, provider, cycle, workspacePath, basePromptText, basePromptFile) {
|
|
912
1234
|
const maxTurns = clamp(state.config.maxTurns, 1, 16);
|
|
913
1235
|
const attempt = issue.attempts + 1;
|
|
@@ -919,7 +1241,7 @@ async function runAgentSession(state, issue, provider, cycle, workspacePath, bas
|
|
|
919
1241
|
let nextPrompt = session.nextPrompt;
|
|
920
1242
|
let lastCode = session.lastCode;
|
|
921
1243
|
let lastOutput = session.lastOutput;
|
|
922
|
-
const resultFile =
|
|
1244
|
+
const resultFile = join6(workspacePath, `result-${provider.role}-${provider.provider}.json`);
|
|
923
1245
|
if (session.status === "done" && session.turns.length > 0) {
|
|
924
1246
|
logger.debug({ issueId: issue.id, identifier: issue.identifier, provider: provider.provider, role: provider.role }, "[Agent] Session already completed, returning cached result");
|
|
925
1247
|
return { success: true, blocked: false, continueRequested: false, code: session.lastCode, output: session.lastOutput, turns: session.turns.length };
|
|
@@ -936,14 +1258,23 @@ Agent requested additional turns beyond configured limit (${maxTurns}).`;
|
|
|
936
1258
|
const compactedOutput = previousOutput.length > maxOutputChars ? `[...${previousOutput.length - maxOutputChars} chars truncated...]
|
|
937
1259
|
${previousOutput.slice(-maxOutputChars)}` : previousOutput;
|
|
938
1260
|
const turnPrompt = await buildTurnPrompt(issue, basePromptText, compactedOutput, turnIndex, maxTurns, nextPrompt);
|
|
939
|
-
const turnPromptFile = turnIndex === 1 ? basePromptFile :
|
|
940
|
-
if (turnIndex > 1)
|
|
1261
|
+
const turnPromptFile = turnIndex === 1 ? basePromptFile : join6(workspacePath, `turn-${String(turnIndex).padStart(2, "0")}.md`);
|
|
1262
|
+
if (turnIndex > 1) writeFileSync3(turnPromptFile, `${turnPrompt}
|
|
941
1263
|
`, "utf8");
|
|
942
1264
|
session.status = "running";
|
|
943
1265
|
session.lastPrompt = turnPrompt;
|
|
944
1266
|
session.lastPromptFile = turnPromptFile;
|
|
945
1267
|
session.maxTurns = maxTurns;
|
|
946
1268
|
await persistAgentSessionState(sessionKey, issue, provider, cycle, session);
|
|
1269
|
+
const outputsDir = join6(workspacePath, "outputs");
|
|
1270
|
+
mkdirSync2(outputsDir, { recursive: true });
|
|
1271
|
+
const outputFileName = resolveOutputFileName(
|
|
1272
|
+
provider.role,
|
|
1273
|
+
issue.planVersion ?? 1,
|
|
1274
|
+
provider.role === "planner" ? 0 : issue.executeAttempt ?? 1,
|
|
1275
|
+
turnIndex
|
|
1276
|
+
);
|
|
1277
|
+
const outputFilePath = join6(outputsDir, outputFileName);
|
|
947
1278
|
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
1279
|
const turnStartedAt = now();
|
|
949
1280
|
const turnEnv = {
|
|
@@ -967,7 +1298,7 @@ ${previousOutput.slice(-maxOutputChars)}` : previousOutput;
|
|
|
967
1298
|
await runHook(state.config.beforeRunHook, workspacePath, issue, "before_run", turnEnv);
|
|
968
1299
|
}
|
|
969
1300
|
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);
|
|
1301
|
+
const turnResult = await runCommandWithTimeout(provider.command, workspacePath, issue, state.config, turnPrompt, turnPromptFile, turnEnv, outputFilePath);
|
|
971
1302
|
if (state.config.afterRunHook) {
|
|
972
1303
|
await runHook(state.config.afterRunHook, workspacePath, issue, "after_run", {
|
|
973
1304
|
...turnEnv,
|
|
@@ -1056,10 +1387,13 @@ async function runAgentPipeline(state, issue, workspacePath, basePromptText, bas
|
|
|
1056
1387
|
const executorIndex = providers.findIndex((provider) => provider.role === "executor");
|
|
1057
1388
|
const skills = discoverSkills(workspacePath);
|
|
1058
1389
|
const skillContext = buildSkillContext(skills);
|
|
1390
|
+
const agents = discoverAgents(workspacePath);
|
|
1391
|
+
const commands = discoverCommands(workspacePath);
|
|
1392
|
+
const capabilitiesManifest = buildCapabilitiesManifest(skills, agents, commands);
|
|
1059
1393
|
if (skillContext) {
|
|
1060
|
-
|
|
1394
|
+
writeFileSync3(join6(workspacePath, "skills.md"), skillContext, "utf8");
|
|
1061
1395
|
}
|
|
1062
|
-
const compiled = await compileExecution(issue, activeProvider, state.config, workspacePath, skillContext);
|
|
1396
|
+
const compiled = await compileExecution(issue, activeProvider, state.config, workspacePath, skillContext, capabilitiesManifest);
|
|
1063
1397
|
let providerPrompt;
|
|
1064
1398
|
let effectiveProvider = activeProvider;
|
|
1065
1399
|
if (compiled) {
|
|
@@ -1073,16 +1407,24 @@ async function runAgentPipeline(state, issue, workspacePath, basePromptText, bas
|
|
|
1073
1407
|
`Plan compiled for ${compiled.meta.adapter}: effort=${compiled.meta.reasoningEffort}, skills=[${compiled.meta.skillsActivated.join(",")}], subagents=[${compiled.meta.subagentsRequested.join(",")}].`
|
|
1074
1408
|
);
|
|
1075
1409
|
if (Object.keys(compiled.env).length > 0) {
|
|
1076
|
-
const envFile =
|
|
1410
|
+
const envFile = join6(workspacePath, ".compiled-env.sh");
|
|
1077
1411
|
const envLines = Object.entries(compiled.env).map(([k, v]) => `export ${k}=${JSON.stringify(v)}`).join("\n");
|
|
1078
|
-
|
|
1412
|
+
writeFileSync3(envFile, envLines, "utf8");
|
|
1079
1413
|
}
|
|
1080
1414
|
} else {
|
|
1081
|
-
providerPrompt = await buildProviderBasePrompt(activeProvider, issue, basePromptText, workspacePath, skillContext);
|
|
1415
|
+
providerPrompt = await buildProviderBasePrompt(activeProvider, issue, basePromptText, workspacePath, skillContext, capabilitiesManifest);
|
|
1082
1416
|
}
|
|
1083
1417
|
if (!effectiveProvider.command.trim()) {
|
|
1084
1418
|
throw new Error(`No command configured for provider ${effectiveProvider.provider} (${effectiveProvider.role}).`);
|
|
1085
1419
|
}
|
|
1420
|
+
if (issue.attempts > 0) {
|
|
1421
|
+
const retryCtx = buildRetryContext(issue);
|
|
1422
|
+
if (retryCtx) {
|
|
1423
|
+
providerPrompt = `${providerPrompt}
|
|
1424
|
+
|
|
1425
|
+
${retryCtx}`;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1086
1428
|
pipeline.history.push(`[${now()}] Running ${effectiveProvider.role}:${effectiveProvider.provider} in cycle ${pipeline.cycle}${compiled ? ` [${compiled.meta.adapter} adapter]` : ""}.`);
|
|
1087
1429
|
await persistAgentPipelineState(pipelineFile, pipeline);
|
|
1088
1430
|
const result = await runAgentSession(state, issue, effectiveProvider, pipeline.cycle, workspacePath, providerPrompt, basePromptFile);
|
|
@@ -1206,7 +1548,6 @@ function applyPlanUsage(issue, usage) {
|
|
|
1206
1548
|
}
|
|
1207
1549
|
function applyPlanSuggestions(issue, plan) {
|
|
1208
1550
|
if (plan.suggestedPaths?.length && !issue.paths?.length) issue.paths = plan.suggestedPaths;
|
|
1209
|
-
if (plan.suggestedLabels?.length && !issue.labels?.length) issue.labels = plan.suggestedLabels;
|
|
1210
1551
|
if (plan.suggestedEffort && !issue.effort) issue.effort = plan.suggestedEffort;
|
|
1211
1552
|
}
|
|
1212
1553
|
async function mutateIssueState(state, c, updater) {
|
|
@@ -1240,30 +1581,11 @@ function createS3dbEventStore(state) {
|
|
|
1240
1581
|
};
|
|
1241
1582
|
}
|
|
1242
1583
|
|
|
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
1584
|
// src/persistence/container.ts
|
|
1262
1585
|
var _container = null;
|
|
1263
1586
|
function createContainer(state) {
|
|
1264
1587
|
const issueRepository = createS3dbIssueRepository(state);
|
|
1265
1588
|
const eventStore = createS3dbEventStore(state);
|
|
1266
|
-
const queuePort = createS3QueueAdapter();
|
|
1267
1589
|
const persistencePort = {
|
|
1268
1590
|
persistState: (s) => persistState(s),
|
|
1269
1591
|
loadState: async () => null
|
|
@@ -1271,7 +1593,6 @@ function createContainer(state) {
|
|
|
1271
1593
|
const container = {
|
|
1272
1594
|
issueRepository,
|
|
1273
1595
|
eventStore,
|
|
1274
|
-
queuePort,
|
|
1275
1596
|
persistencePort
|
|
1276
1597
|
};
|
|
1277
1598
|
_container = container;
|
|
@@ -1286,19 +1607,19 @@ function getContainer() {
|
|
|
1286
1607
|
}
|
|
1287
1608
|
|
|
1288
1609
|
// src/commands/create-issue.command.ts
|
|
1289
|
-
import { existsSync as
|
|
1290
|
-
import { basename, join as
|
|
1610
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, renameSync } from "fs";
|
|
1611
|
+
import { basename, join as join7 } from "path";
|
|
1291
1612
|
async function createIssueCommand(input, deps) {
|
|
1292
1613
|
const { payload, state } = input;
|
|
1293
1614
|
const issue = createIssueFromPayload(payload, state.issues, state.config.defaultBranch);
|
|
1294
1615
|
const tempImages = Array.isArray(payload.images) ? payload.images : [];
|
|
1295
1616
|
if (tempImages.length) {
|
|
1296
|
-
const issueAttachDir =
|
|
1297
|
-
|
|
1617
|
+
const issueAttachDir = join7(ATTACHMENTS_ROOT, issue.id);
|
|
1618
|
+
mkdirSync3(issueAttachDir, { recursive: true });
|
|
1298
1619
|
const finalPaths = [];
|
|
1299
1620
|
for (const tempPath of tempImages) {
|
|
1300
|
-
if (typeof tempPath === "string" &&
|
|
1301
|
-
const dest =
|
|
1621
|
+
if (typeof tempPath === "string" && existsSync5(tempPath)) {
|
|
1622
|
+
const dest = join7(issueAttachDir, basename(tempPath));
|
|
1302
1623
|
try {
|
|
1303
1624
|
renameSync(tempPath, dest);
|
|
1304
1625
|
finalPaths.push(dest);
|
|
@@ -1317,13 +1638,13 @@ async function createIssueCommand(input, deps) {
|
|
|
1317
1638
|
}
|
|
1318
1639
|
await deps.persistencePort.persistState(state);
|
|
1319
1640
|
if (issue.state === "Planning") {
|
|
1320
|
-
|
|
1641
|
+
enqueue(issue, "plan").catch(() => {
|
|
1321
1642
|
});
|
|
1322
1643
|
} else if (issue.state === "Queued" || issue.state === "Running") {
|
|
1323
|
-
|
|
1644
|
+
enqueue(issue, "execute").catch(() => {
|
|
1324
1645
|
});
|
|
1325
1646
|
} else if (issue.state === "Reviewing") {
|
|
1326
|
-
|
|
1647
|
+
enqueue(issue, "review").catch(() => {
|
|
1327
1648
|
});
|
|
1328
1649
|
}
|
|
1329
1650
|
return { issue };
|
|
@@ -1503,17 +1824,12 @@ var issues_resource_default = {
|
|
|
1503
1824
|
identifier: "string|required",
|
|
1504
1825
|
title: "string|required",
|
|
1505
1826
|
description: "string|optional",
|
|
1506
|
-
priority: "number|required",
|
|
1507
1827
|
state: "string|required",
|
|
1508
1828
|
branchName: "string|optional",
|
|
1509
1829
|
url: "string|optional",
|
|
1510
1830
|
assigneeId: "string|optional",
|
|
1511
1831
|
labels: "json|required",
|
|
1512
1832
|
paths: "json|optional",
|
|
1513
|
-
inferredPaths: "json|optional",
|
|
1514
|
-
capabilityCategory: "string|optional",
|
|
1515
|
-
capabilityOverlays: "json|optional",
|
|
1516
|
-
capabilityRationale: "json|optional",
|
|
1517
1833
|
blockedBy: "json|required",
|
|
1518
1834
|
assignedToWorker: "boolean|required",
|
|
1519
1835
|
createdAt: "datetime|required",
|
|
@@ -1561,10 +1877,6 @@ var issues_resource_default = {
|
|
|
1561
1877
|
},
|
|
1562
1878
|
partitions: {
|
|
1563
1879
|
byState: { fields: { state: "string" } },
|
|
1564
|
-
byCapabilityCategory: { fields: { capabilityCategory: "string" } },
|
|
1565
|
-
byStateAndCapability: {
|
|
1566
|
-
fields: { state: "string", capabilityCategory: "string" }
|
|
1567
|
-
},
|
|
1568
1880
|
byTerminalWeek: { fields: { terminalWeek: "string" } }
|
|
1569
1881
|
},
|
|
1570
1882
|
asyncPartitions: true,
|
|
@@ -1801,7 +2113,6 @@ function broadcastToWebSocketClients(message) {
|
|
|
1801
2113
|
type: "state:delta",
|
|
1802
2114
|
seq: broadcastSeq,
|
|
1803
2115
|
metrics: message.metrics,
|
|
1804
|
-
capabilities: message.capabilities,
|
|
1805
2116
|
updatedAt: message.updatedAt,
|
|
1806
2117
|
issuesDelta: changedIssues,
|
|
1807
2118
|
issuesRemoved: removedIds,
|
|
@@ -1835,7 +2146,6 @@ function makeWebSocketConfig(state) {
|
|
|
1835
2146
|
seq: broadcastSeq,
|
|
1836
2147
|
timestamp: now(),
|
|
1837
2148
|
metrics: computeMetrics(state.issues),
|
|
1838
|
-
capabilities: computeCapabilityCounts(state.issues),
|
|
1839
2149
|
issues: state.issues,
|
|
1840
2150
|
events: state.events.slice(0, 50)
|
|
1841
2151
|
}));
|
|
@@ -1864,7 +2174,7 @@ var shuttingDown = false;
|
|
|
1864
2174
|
function isShuttingDown() {
|
|
1865
2175
|
return shuttingDown;
|
|
1866
2176
|
}
|
|
1867
|
-
function installGracefulShutdown(state
|
|
2177
|
+
function installGracefulShutdown(state) {
|
|
1868
2178
|
const handler = async (signal) => {
|
|
1869
2179
|
if (shuttingDown) {
|
|
1870
2180
|
logger.warn(`Received ${signal} again, forcing exit.`);
|
|
@@ -1875,7 +2185,7 @@ function installGracefulShutdown(state, running) {
|
|
|
1875
2185
|
const container = getContainer();
|
|
1876
2186
|
container.eventStore.addEvent(void 0, "info", `Graceful shutdown initiated (${signal}).`);
|
|
1877
2187
|
for (const issue of state.issues) {
|
|
1878
|
-
if (
|
|
2188
|
+
if (issue.state === "Running" || issue.state === "Reviewing") {
|
|
1879
2189
|
try {
|
|
1880
2190
|
await transitionIssueCommand({ issue, target: "Queued", note: `Interrupted by ${signal} \u2014 queued for resume on next start.`, fallbackToLocal: true }, container);
|
|
1881
2191
|
} catch {
|
|
@@ -1907,7 +2217,7 @@ function installGracefulShutdown(state, running) {
|
|
|
1907
2217
|
}
|
|
1908
2218
|
function analyzeParallelizability(issues) {
|
|
1909
2219
|
const todo = issues.filter(
|
|
1910
|
-
(issue) => issue.state === "
|
|
2220
|
+
(issue) => issue.state === "PendingApproval" && issue.assignedToWorker && issue.blockedBy.length === 0
|
|
1911
2221
|
);
|
|
1912
2222
|
if (todo.length === 0) {
|
|
1913
2223
|
return {
|
|
@@ -1917,7 +2227,7 @@ function analyzeParallelizability(issues) {
|
|
|
1917
2227
|
groups: []
|
|
1918
2228
|
};
|
|
1919
2229
|
}
|
|
1920
|
-
const getIssuePaths = (issue) => /* @__PURE__ */ new Set([...issue.paths ?? []
|
|
2230
|
+
const getIssuePaths = (issue) => /* @__PURE__ */ new Set([...issue.paths ?? []]);
|
|
1921
2231
|
const hasPathOverlap = (a, b) => {
|
|
1922
2232
|
const pathsA = getIssuePaths(a);
|
|
1923
2233
|
const pathsB = getIssuePaths(b);
|
|
@@ -1986,6 +2296,9 @@ async function ensureNotStale(state, staleTimeoutMs) {
|
|
|
1986
2296
|
if (pidDead) {
|
|
1987
2297
|
logger.info({ issueId: issue.id, identifier: issue.identifier, state: issue.state, pid: agentStatus.pid?.pid }, "[Scheduler] PID dead \u2014 silently recovering to Queued");
|
|
1988
2298
|
issue.startedAt = void 0;
|
|
2299
|
+
issue.lastError = `Agent process died unexpectedly (PID ${agentStatus.pid.pid}).`;
|
|
2300
|
+
issue.lastFailedPhase = "crash";
|
|
2301
|
+
issue.attempts = (issue.attempts ?? 0) + 1;
|
|
1989
2302
|
container.issueRepository.markDirty(issue.id);
|
|
1990
2303
|
await transitionIssueCommand({ issue, target: "Queued", note: `Agent process died (PID ${agentStatus.pid.pid}) \u2014 auto-recovering.` }, container);
|
|
1991
2304
|
container.eventStore.addEvent(issue.id, "info", `Issue ${issue.identifier} agent process died (PID ${agentStatus.pid.pid}), silently recovered to Queued.`);
|
|
@@ -2011,8 +2324,8 @@ function hasTerminalQueue(state) {
|
|
|
2011
2324
|
// src/agents/providers-usage.ts
|
|
2012
2325
|
import { execFile } from "child_process";
|
|
2013
2326
|
import { promisify } from "util";
|
|
2014
|
-
import { existsSync as
|
|
2015
|
-
import { join as
|
|
2327
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync2, realpathSync } from "fs";
|
|
2328
|
+
import { join as join8, dirname } from "path";
|
|
2016
2329
|
import { homedir as homedir2 } from "os";
|
|
2017
2330
|
import { env } from "process";
|
|
2018
2331
|
var execFileAsync = promisify(execFile);
|
|
@@ -2039,24 +2352,24 @@ function resolveCodexHomeCandidates() {
|
|
|
2039
2352
|
]);
|
|
2040
2353
|
const candidates = [...homePaths, ...direct].filter(Boolean).flatMap((candidate) => {
|
|
2041
2354
|
if (candidate.endsWith("/.codex") || candidate.endsWith("/codex")) return [candidate];
|
|
2042
|
-
return [
|
|
2355
|
+
return [join8(candidate, ".codex"), join8(candidate, "codex")];
|
|
2043
2356
|
});
|
|
2044
2357
|
return [...new Set(candidates)];
|
|
2045
2358
|
}
|
|
2046
2359
|
function resolveCodexDir() {
|
|
2047
2360
|
for (const candidate of resolveCodexHomeCandidates()) {
|
|
2048
|
-
if (
|
|
2361
|
+
if (existsSync6(candidate)) {
|
|
2049
2362
|
return candidate;
|
|
2050
2363
|
}
|
|
2051
2364
|
}
|
|
2052
2365
|
return null;
|
|
2053
2366
|
}
|
|
2054
2367
|
function findLatestCodexDb(codexDir) {
|
|
2055
|
-
const explicit =
|
|
2056
|
-
if (
|
|
2368
|
+
const explicit = join8(codexDir, "state_5.sqlite");
|
|
2369
|
+
if (existsSync6(explicit)) return explicit;
|
|
2057
2370
|
const candidates = readdirSync2(codexDir).filter((name) => name.startsWith("state_") && name.endsWith(".sqlite")).sort().reverse();
|
|
2058
2371
|
if (candidates.length === 0) return null;
|
|
2059
|
-
return
|
|
2372
|
+
return join8(codexDir, candidates[0]);
|
|
2060
2373
|
}
|
|
2061
2374
|
function computeNextMonday() {
|
|
2062
2375
|
const now2 = /* @__PURE__ */ new Date();
|
|
@@ -2093,10 +2406,10 @@ var CLAUDE_PLAN_LIMITS = {
|
|
|
2093
2406
|
};
|
|
2094
2407
|
async function collectClaudeUsage() {
|
|
2095
2408
|
const home = homedir2();
|
|
2096
|
-
const claudeDir =
|
|
2097
|
-
if (!
|
|
2409
|
+
const claudeDir = join8(home, ".claude");
|
|
2410
|
+
if (!existsSync6(claudeDir)) return null;
|
|
2098
2411
|
const available = await whichExists("claude");
|
|
2099
|
-
const projectsDir =
|
|
2412
|
+
const projectsDir = join8(claudeDir, "projects");
|
|
2100
2413
|
let totalInputTokens = 0;
|
|
2101
2414
|
let totalOutputTokens = 0;
|
|
2102
2415
|
let totalSessions = 0;
|
|
@@ -2110,12 +2423,12 @@ async function collectClaudeUsage() {
|
|
|
2110
2423
|
const todayMs = todayStart.getTime();
|
|
2111
2424
|
const weekStart = computeWeekStart();
|
|
2112
2425
|
const weekMs = weekStart.getTime();
|
|
2113
|
-
if (
|
|
2426
|
+
if (existsSync6(projectsDir)) {
|
|
2114
2427
|
try {
|
|
2115
2428
|
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true });
|
|
2116
2429
|
for (const dir of projectDirs) {
|
|
2117
2430
|
if (!dir.isDirectory()) continue;
|
|
2118
|
-
const projectPath =
|
|
2431
|
+
const projectPath = join8(projectsDir, dir.name);
|
|
2119
2432
|
let sessionFiles;
|
|
2120
2433
|
try {
|
|
2121
2434
|
sessionFiles = readdirSync2(projectPath).filter((f) => f.endsWith(".jsonl"));
|
|
@@ -2123,10 +2436,10 @@ async function collectClaudeUsage() {
|
|
|
2123
2436
|
continue;
|
|
2124
2437
|
}
|
|
2125
2438
|
for (const file of sessionFiles) {
|
|
2126
|
-
const filePath =
|
|
2439
|
+
const filePath = join8(projectPath, file);
|
|
2127
2440
|
let content;
|
|
2128
2441
|
try {
|
|
2129
|
-
content =
|
|
2442
|
+
content = readFileSync5(filePath, "utf8");
|
|
2130
2443
|
} catch {
|
|
2131
2444
|
continue;
|
|
2132
2445
|
}
|
|
@@ -2181,10 +2494,10 @@ async function collectClaudeUsage() {
|
|
|
2181
2494
|
let plan = "pro";
|
|
2182
2495
|
let resetInfo = "Weekly reset (every Monday 00:00 UTC)";
|
|
2183
2496
|
let currentModel = "";
|
|
2184
|
-
const settingsPath =
|
|
2185
|
-
if (
|
|
2497
|
+
const settingsPath = join8(claudeDir, "settings.json");
|
|
2498
|
+
if (existsSync6(settingsPath)) {
|
|
2186
2499
|
try {
|
|
2187
|
-
const settings = JSON.parse(
|
|
2500
|
+
const settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
|
|
2188
2501
|
if (settings.plan === "max" || settings.plan === "max5x") {
|
|
2189
2502
|
plan = settings.plan;
|
|
2190
2503
|
resetInfo = `Plan: ${settings.plan.toUpperCase()} \u2014 Weekly token limit resets every Monday 00:00 UTC`;
|
|
@@ -2220,11 +2533,11 @@ async function collectCodexUsage() {
|
|
|
2220
2533
|
if (!codexDir) return null;
|
|
2221
2534
|
const available = await whichExists("codex");
|
|
2222
2535
|
const models = [];
|
|
2223
|
-
const modelsCachePath =
|
|
2536
|
+
const modelsCachePath = join8(codexDir, "models_cache.json");
|
|
2224
2537
|
let currentModel = "";
|
|
2225
|
-
if (
|
|
2538
|
+
if (existsSync6(modelsCachePath)) {
|
|
2226
2539
|
try {
|
|
2227
|
-
const cache = JSON.parse(
|
|
2540
|
+
const cache = JSON.parse(readFileSync5(modelsCachePath, "utf8"));
|
|
2228
2541
|
for (const m of cache.models || []) {
|
|
2229
2542
|
models.push({
|
|
2230
2543
|
slug: m.slug,
|
|
@@ -2235,10 +2548,10 @@ async function collectCodexUsage() {
|
|
|
2235
2548
|
} catch {
|
|
2236
2549
|
}
|
|
2237
2550
|
}
|
|
2238
|
-
const configPath =
|
|
2239
|
-
if (
|
|
2551
|
+
const configPath = join8(codexDir, "config.toml");
|
|
2552
|
+
if (existsSync6(configPath)) {
|
|
2240
2553
|
try {
|
|
2241
|
-
const configContent =
|
|
2554
|
+
const configContent = readFileSync5(configPath, "utf8");
|
|
2242
2555
|
const modelMatch = configContent.match(/^model\s*=\s*"([^"]+)"/m);
|
|
2243
2556
|
if (modelMatch) currentModel = modelMatch[1];
|
|
2244
2557
|
} catch {
|
|
@@ -2327,9 +2640,9 @@ async function collectGeminiUsage() {
|
|
|
2327
2640
|
try {
|
|
2328
2641
|
const { stdout: binPath } = await execFileAsync("which", ["gemini"], { encoding: "utf8", timeout: 3e3 });
|
|
2329
2642
|
const realBin = realpathSync(binPath.trim());
|
|
2330
|
-
const modelsPath =
|
|
2331
|
-
if (
|
|
2332
|
-
const content =
|
|
2643
|
+
const modelsPath = join8(dirname(dirname(realBin)), "node_modules", "@google", "gemini-cli-core", "dist", "src", "config", "models.js");
|
|
2644
|
+
if (existsSync6(modelsPath)) {
|
|
2645
|
+
const content = readFileSync5(modelsPath, "utf8");
|
|
2333
2646
|
const regex = /export const ([A-Z0-9_]+)\s*=\s*'(gemini-[^']+)';/g;
|
|
2334
2647
|
const seen = /* @__PURE__ */ new Set();
|
|
2335
2648
|
let m;
|
|
@@ -2347,10 +2660,10 @@ async function collectGeminiUsage() {
|
|
|
2347
2660
|
} catch {
|
|
2348
2661
|
}
|
|
2349
2662
|
let currentModel = "";
|
|
2350
|
-
const settingsPath =
|
|
2351
|
-
if (
|
|
2663
|
+
const settingsPath = join8(homedir2(), ".gemini", "settings.json");
|
|
2664
|
+
if (existsSync6(settingsPath)) {
|
|
2352
2665
|
try {
|
|
2353
|
-
const settings = JSON.parse(
|
|
2666
|
+
const settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
|
|
2354
2667
|
if (typeof settings.model === "string" && settings.model.trim()) {
|
|
2355
2668
|
currentModel = settings.model.trim();
|
|
2356
2669
|
}
|
|
@@ -2394,10 +2707,10 @@ async function collectProvidersUsage() {
|
|
|
2394
2707
|
}
|
|
2395
2708
|
|
|
2396
2709
|
// src/routes/state.ts
|
|
2397
|
-
import { existsSync as
|
|
2710
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
2398
2711
|
import { randomUUID } from "crypto";
|
|
2399
|
-
import { execSync as
|
|
2400
|
-
import { basename as basename2, extname, join as
|
|
2712
|
+
import { execSync as execSync3 } from "child_process";
|
|
2713
|
+
import { basename as basename2, extname, join as join9 } from "path";
|
|
2401
2714
|
|
|
2402
2715
|
// src/commands/approve-plan.command.ts
|
|
2403
2716
|
async function approvePlanCommand(input, deps) {
|
|
@@ -2405,8 +2718,9 @@ async function approvePlanCommand(input, deps) {
|
|
|
2405
2718
|
if (issue.state !== "Planning") {
|
|
2406
2719
|
throw new Error(`Cannot approve issue in state ${issue.state}. Must be in Planning.`);
|
|
2407
2720
|
}
|
|
2721
|
+
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "execute approved plans");
|
|
2408
2722
|
await transitionIssueCommand(
|
|
2409
|
-
{ issue, target: "
|
|
2723
|
+
{ issue, target: "PendingApproval", note: `Plan approved for ${issue.identifier}. Ready for execution.` },
|
|
2410
2724
|
deps
|
|
2411
2725
|
);
|
|
2412
2726
|
await transitionIssueCommand(
|
|
@@ -2418,9 +2732,10 @@ async function approvePlanCommand(input, deps) {
|
|
|
2418
2732
|
// src/commands/execute-issue.command.ts
|
|
2419
2733
|
async function executeIssueCommand(input, deps) {
|
|
2420
2734
|
const { issue } = input;
|
|
2421
|
-
if (issue.state !== "
|
|
2422
|
-
throw new Error(`Cannot execute issue in state ${issue.state}. Must be in
|
|
2735
|
+
if (issue.state !== "PendingApproval") {
|
|
2736
|
+
throw new Error(`Cannot execute issue in state ${issue.state}. Must be in PendingApproval.`);
|
|
2423
2737
|
}
|
|
2738
|
+
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "execute issues");
|
|
2424
2739
|
await transitionIssueCommand(
|
|
2425
2740
|
{ issue, target: "Queued", note: `Execution requested for ${issue.identifier}.` },
|
|
2426
2741
|
deps
|
|
@@ -2460,22 +2775,65 @@ async function replanIssueCommand(input, deps) {
|
|
|
2460
2775
|
}
|
|
2461
2776
|
|
|
2462
2777
|
// src/commands/merge-workspace.command.ts
|
|
2463
|
-
import { existsSync as
|
|
2778
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2464
2779
|
import { execSync } from "child_process";
|
|
2780
|
+
|
|
2781
|
+
// src/domains/validation.ts
|
|
2782
|
+
import { execFile as execFile2 } from "child_process";
|
|
2783
|
+
async function runValidationGate(issue, config) {
|
|
2784
|
+
if (!config.testCommand) return null;
|
|
2785
|
+
const cwd = issue.worktreePath ?? issue.workspacePath;
|
|
2786
|
+
if (!cwd) {
|
|
2787
|
+
logger.warn({ issueId: issue.id }, "[Validation] No workspace path \u2014 skipping gate");
|
|
2788
|
+
return null;
|
|
2789
|
+
}
|
|
2790
|
+
const command = config.testCommand;
|
|
2791
|
+
logger.info({ issueId: issue.id, command, cwd }, "[Validation] Running validation gate");
|
|
2792
|
+
return new Promise((resolve3) => {
|
|
2793
|
+
const child = execFile2("sh", ["-c", command], {
|
|
2794
|
+
cwd,
|
|
2795
|
+
encoding: "utf8",
|
|
2796
|
+
timeout: 3e5,
|
|
2797
|
+
maxBuffer: 2 * 1024 * 1024
|
|
2798
|
+
}, (err, stdout, stderr) => {
|
|
2799
|
+
const combined = (stdout || "") + (stderr || "");
|
|
2800
|
+
if (!err) {
|
|
2801
|
+
logger.info({ issueId: issue.id }, "[Validation] Gate passed");
|
|
2802
|
+
resolve3({
|
|
2803
|
+
passed: true,
|
|
2804
|
+
output: combined.slice(-2048),
|
|
2805
|
+
command,
|
|
2806
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2807
|
+
});
|
|
2808
|
+
return;
|
|
2809
|
+
}
|
|
2810
|
+
logger.warn({ issueId: issue.id, exitCode: err.code }, "[Validation] Gate failed");
|
|
2811
|
+
resolve3({
|
|
2812
|
+
passed: false,
|
|
2813
|
+
output: combined.slice(-2048) || String(err).slice(0, 2048),
|
|
2814
|
+
command,
|
|
2815
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2816
|
+
});
|
|
2817
|
+
});
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
// src/commands/merge-workspace.command.ts
|
|
2465
2822
|
async function mergeWorkspaceCommand(input, deps) {
|
|
2466
2823
|
const { issue, state } = input;
|
|
2467
|
-
if (!["
|
|
2468
|
-
throw new Error(`Issue ${issue.identifier} is in state ${issue.state}. Merge is only allowed in Reviewing,
|
|
2824
|
+
if (!["Approved", "Reviewing", "PendingDecision"].includes(issue.state)) {
|
|
2825
|
+
throw new Error(`Issue ${issue.identifier} is in state ${issue.state}. Merge is only allowed in Reviewing, PendingDecision, or Approved state.`);
|
|
2469
2826
|
}
|
|
2470
|
-
|
|
2827
|
+
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "merge issues");
|
|
2828
|
+
if (issue.state === "Reviewing" || issue.state === "PendingDecision") {
|
|
2471
2829
|
await transitionIssueCommand(
|
|
2472
|
-
{ issue, target: "
|
|
2830
|
+
{ issue, target: "Approved", note: "Approved and merged by user." },
|
|
2473
2831
|
deps
|
|
2474
2832
|
);
|
|
2475
2833
|
}
|
|
2476
2834
|
const wp = issue.worktreePath ?? issue.workspacePath;
|
|
2477
|
-
if (!wp || !
|
|
2478
|
-
throw new Error(
|
|
2835
|
+
if (!wp || !existsSync7(wp)) {
|
|
2836
|
+
throw new Error(`No mergeable workspace found for ${issue.identifier}. This issue likely ran before git was initialized for the project. Re-run the issue after git setup.`);
|
|
2479
2837
|
}
|
|
2480
2838
|
if (issue.branchName && issue.baseBranch) {
|
|
2481
2839
|
try {
|
|
@@ -2498,12 +2856,20 @@ async function mergeWorkspaceCommand(input, deps) {
|
|
|
2498
2856
|
}
|
|
2499
2857
|
} catch {
|
|
2500
2858
|
}
|
|
2859
|
+
const validation = await runValidationGate(issue, state.config);
|
|
2860
|
+
if (validation) {
|
|
2861
|
+
issue.validationResult = validation;
|
|
2862
|
+
if (!validation.passed) {
|
|
2863
|
+
throw new Error(`Validation gate failed (${validation.command}): ${validation.output.slice(0, 500)}`);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2501
2866
|
const result = mergeWorkspace(issue);
|
|
2502
2867
|
issue.mergeResult = {
|
|
2503
2868
|
copied: result.copied.length,
|
|
2504
2869
|
deleted: result.deleted.length,
|
|
2505
2870
|
skipped: result.skipped.length,
|
|
2506
|
-
conflicts: result.conflicts.length
|
|
2871
|
+
conflicts: result.conflicts.length,
|
|
2872
|
+
conflictFiles: result.conflicts.length > 0 ? result.conflicts : void 0
|
|
2507
2873
|
};
|
|
2508
2874
|
if (result.conflicts.length > 0) {
|
|
2509
2875
|
deps.eventStore.addEvent(issue.id, "error", `Merge conflicts: ${result.conflicts.join(", ")}`);
|
|
@@ -2527,6 +2893,138 @@ async function mergeWorkspaceCommand(input, deps) {
|
|
|
2527
2893
|
return result;
|
|
2528
2894
|
}
|
|
2529
2895
|
|
|
2896
|
+
// src/commands/push-workspace.command.ts
|
|
2897
|
+
import { execFileSync, execSync as execSync2 } from "child_process";
|
|
2898
|
+
function isGhAvailable() {
|
|
2899
|
+
try {
|
|
2900
|
+
execFileSync("gh", ["--version"], { stdio: "pipe", timeout: 5e3 });
|
|
2901
|
+
return true;
|
|
2902
|
+
} catch {
|
|
2903
|
+
return false;
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
function getCompareUrl(branchName, baseBranch) {
|
|
2907
|
+
try {
|
|
2908
|
+
const remote = execSync2("git remote get-url origin", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
|
|
2909
|
+
const cleanRemote = remote.replace(/\.git$/, "");
|
|
2910
|
+
return `${cleanRemote}/compare/${baseBranch}...${branchName}`;
|
|
2911
|
+
} catch {
|
|
2912
|
+
return `(branch pushed: ${branchName})`;
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
function findExistingPr(branchName) {
|
|
2916
|
+
try {
|
|
2917
|
+
const result = execFileSync(
|
|
2918
|
+
"gh",
|
|
2919
|
+
["pr", "view", branchName, "--json", "url,state", "--jq", 'select(.state == "OPEN") | .url'],
|
|
2920
|
+
{ cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe", timeout: 15e3 }
|
|
2921
|
+
).trim();
|
|
2922
|
+
return result || null;
|
|
2923
|
+
} catch {
|
|
2924
|
+
return null;
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
function createPr(branchName, baseBranch, title, body) {
|
|
2928
|
+
return execFileSync(
|
|
2929
|
+
"gh",
|
|
2930
|
+
["pr", "create", "--head", branchName, "--base", baseBranch, "--title", title, "--body", body],
|
|
2931
|
+
{ cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe", timeout: 3e4 }
|
|
2932
|
+
).trim();
|
|
2933
|
+
}
|
|
2934
|
+
async function pushWorkspaceCommand(input, deps) {
|
|
2935
|
+
const { issue, state } = input;
|
|
2936
|
+
if (!["Approved", "Reviewing", "PendingDecision"].includes(issue.state)) {
|
|
2937
|
+
throw new Error(`Issue ${issue.identifier} is in state ${issue.state}. Push is only allowed in Reviewing, PendingDecision, or Approved state.`);
|
|
2938
|
+
}
|
|
2939
|
+
ensureGitRepoReadyForWorktrees(TARGET_ROOT, "push issue branches");
|
|
2940
|
+
assertIssueHasGitWorktree(issue, "push");
|
|
2941
|
+
if (issue.state === "Reviewing" || issue.state === "PendingDecision") {
|
|
2942
|
+
await transitionIssueCommand(
|
|
2943
|
+
{ issue, target: "Approved", note: "Approved and pushed by user." },
|
|
2944
|
+
deps
|
|
2945
|
+
);
|
|
2946
|
+
}
|
|
2947
|
+
ensureWorktreeCommitted(issue);
|
|
2948
|
+
const validation = await runValidationGate(issue, state.config);
|
|
2949
|
+
if (validation) {
|
|
2950
|
+
issue.validationResult = validation;
|
|
2951
|
+
if (!validation.passed) {
|
|
2952
|
+
throw new Error(`Validation gate failed (${validation.command}): ${validation.output.slice(0, 500)}`);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
computeDiffStats(issue);
|
|
2956
|
+
const planSummary = issue.plan?.summary ?? issue.title;
|
|
2957
|
+
let diffStat = "";
|
|
2958
|
+
try {
|
|
2959
|
+
diffStat = execSync2(
|
|
2960
|
+
`git diff --stat "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
2961
|
+
{ cwd: TARGET_ROOT, encoding: "utf8", maxBuffer: 512e3, timeout: 1e4, stdio: "pipe" }
|
|
2962
|
+
).trim();
|
|
2963
|
+
} catch {
|
|
2964
|
+
}
|
|
2965
|
+
const body = `## Summary
|
|
2966
|
+
${planSummary}
|
|
2967
|
+
|
|
2968
|
+
## Diff Stats
|
|
2969
|
+
\`\`\`
|
|
2970
|
+
${diffStat || "No diff stats available"}
|
|
2971
|
+
\`\`\`
|
|
2972
|
+
|
|
2973
|
+
*Automated by fifony*`;
|
|
2974
|
+
execSync2(`git push -u origin "${issue.branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
2975
|
+
const prBase = state.config.prBaseBranch || issue.baseBranch;
|
|
2976
|
+
const ghAvailable = isGhAvailable();
|
|
2977
|
+
let prUrl;
|
|
2978
|
+
if (!ghAvailable) {
|
|
2979
|
+
prUrl = getCompareUrl(issue.branchName, prBase);
|
|
2980
|
+
logger.info({ issueId: issue.id, prUrl }, "[Push] gh CLI not available \u2014 using compare URL");
|
|
2981
|
+
} else {
|
|
2982
|
+
const existingUrl = findExistingPr(issue.branchName);
|
|
2983
|
+
if (existingUrl) {
|
|
2984
|
+
prUrl = existingUrl;
|
|
2985
|
+
logger.info({ issueId: issue.id, prUrl }, "[Push] Existing open PR found");
|
|
2986
|
+
} else {
|
|
2987
|
+
try {
|
|
2988
|
+
prUrl = createPr(issue.branchName, prBase, issue.title, body);
|
|
2989
|
+
logger.info({ issueId: issue.id, prUrl }, "[Push] PR created");
|
|
2990
|
+
} catch (err) {
|
|
2991
|
+
const ghError = (err.stderr || err.stdout || String(err)).toString().slice(0, 500);
|
|
2992
|
+
logger.error({ issueId: issue.id, ghError }, "[Push] gh pr create failed");
|
|
2993
|
+
prUrl = getCompareUrl(issue.branchName, prBase);
|
|
2994
|
+
deps.eventStore.addEvent(issue.id, "error", `gh pr create failed: ${ghError}. Branch was pushed \u2014 use the compare URL to create the PR manually.`);
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
issue.prUrl = prUrl;
|
|
2999
|
+
if (!issue.mergedReason) issue.mergedReason = "Pushed to origin and PR created.";
|
|
3000
|
+
await transitionIssueCommand(
|
|
3001
|
+
{ issue, target: "Merged", note: `Branch ${issue.branchName} pushed. PR: ${prUrl}` },
|
|
3002
|
+
deps
|
|
3003
|
+
);
|
|
3004
|
+
deps.eventStore.addEvent(issue.id, "merge", `PR created: ${prUrl}`);
|
|
3005
|
+
await deps.persistencePort.persistState(state);
|
|
3006
|
+
return { prUrl, ghAvailable };
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
// src/commands/retry-execution.command.ts
|
|
3010
|
+
async function retryExecutionCommand(input, deps) {
|
|
3011
|
+
const { issue, note } = input;
|
|
3012
|
+
if (issue.state !== "Blocked") {
|
|
3013
|
+
throw new Error(
|
|
3014
|
+
`retryExecutionCommand requires Blocked state, got ${issue.state}. Use replanIssueCommand for re-planning or the generic /retry endpoint for other states.`
|
|
3015
|
+
);
|
|
3016
|
+
}
|
|
3017
|
+
await transitionIssueCommand(
|
|
3018
|
+
{ issue, target: "Queued", note: note ?? `Retry execution for ${issue.identifier} (attempt ${issue.attempts + 1}).` },
|
|
3019
|
+
deps
|
|
3020
|
+
);
|
|
3021
|
+
deps.eventStore.addEvent(
|
|
3022
|
+
issue.id,
|
|
3023
|
+
"manual",
|
|
3024
|
+
`Execution retry requested for ${issue.identifier} \u2014 re-queued from Blocked.`
|
|
3025
|
+
);
|
|
3026
|
+
}
|
|
3027
|
+
|
|
2530
3028
|
// src/routes/state.ts
|
|
2531
3029
|
function getStateQuery(state, showAll = false) {
|
|
2532
3030
|
let issues = state.issues;
|
|
@@ -2544,12 +3042,18 @@ function getStateQuery(state, showAll = false) {
|
|
|
2544
3042
|
return {
|
|
2545
3043
|
...state,
|
|
2546
3044
|
issues,
|
|
2547
|
-
capabilities: computeCapabilityCounts(issues),
|
|
2548
3045
|
metrics: computeMetrics(issues),
|
|
2549
3046
|
_filter: showAll ? "all" : "recent",
|
|
2550
3047
|
_totalIssues: state.issues.length
|
|
2551
3048
|
};
|
|
2552
3049
|
}
|
|
3050
|
+
function getWorkspaceActionErrorStatus(error) {
|
|
3051
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3052
|
+
if (message.includes("requires a git repository") || message.includes("requires at least one commit") || message.includes("has no git worktree") || message.includes("No mergeable workspace found") || message.includes("target repository has uncommitted changes") || message.includes("current branch is")) {
|
|
3053
|
+
return 409;
|
|
3054
|
+
}
|
|
3055
|
+
return 500;
|
|
3056
|
+
}
|
|
2553
3057
|
function registerStateRoutes(app, state) {
|
|
2554
3058
|
app.get("/api/state", async (c) => {
|
|
2555
3059
|
const showAll = c.req.query("all") === "1";
|
|
@@ -2629,7 +3133,7 @@ function registerStateRoutes(app, state) {
|
|
|
2629
3133
|
);
|
|
2630
3134
|
if (issue.plan?.steps?.length) {
|
|
2631
3135
|
await transitionIssueCommand(
|
|
2632
|
-
{ issue, target: "
|
|
3136
|
+
{ issue, target: "PendingApproval", note: "Existing plan found." },
|
|
2633
3137
|
container
|
|
2634
3138
|
);
|
|
2635
3139
|
await transitionIssueCommand(
|
|
@@ -2638,11 +3142,26 @@ function registerStateRoutes(app, state) {
|
|
|
2638
3142
|
);
|
|
2639
3143
|
}
|
|
2640
3144
|
} else if (issue.state === "Blocked") {
|
|
3145
|
+
await retryExecutionCommand(
|
|
3146
|
+
{ issue, note: "Manual retry from Blocked." },
|
|
3147
|
+
container
|
|
3148
|
+
);
|
|
3149
|
+
} else if (issue.state === "Approved") {
|
|
2641
3150
|
await transitionIssueCommand(
|
|
2642
|
-
{ issue, target: "
|
|
3151
|
+
{ issue, target: "Planning", note: "Requeued for rework after merge conflicts." },
|
|
2643
3152
|
container
|
|
2644
3153
|
);
|
|
2645
|
-
|
|
3154
|
+
if (issue.plan?.steps?.length) {
|
|
3155
|
+
await transitionIssueCommand(
|
|
3156
|
+
{ issue, target: "PendingApproval", note: "Existing plan found." },
|
|
3157
|
+
container
|
|
3158
|
+
);
|
|
3159
|
+
await transitionIssueCommand(
|
|
3160
|
+
{ issue, target: "Queued", note: "Auto-queued for rework." },
|
|
3161
|
+
container
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
} else if (issue.state === "PendingApproval") {
|
|
2646
3165
|
await transitionIssueCommand(
|
|
2647
3166
|
{ issue, target: "Queued", note: "Manual retry \u2014 queued for execution." },
|
|
2648
3167
|
container
|
|
@@ -2691,25 +3210,63 @@ function registerStateRoutes(app, state) {
|
|
|
2691
3210
|
const issue = findIssue(state, issueId);
|
|
2692
3211
|
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
2693
3212
|
const container = getContainer();
|
|
3213
|
+
if (state.config.mergeMode === "push-pr") {
|
|
3214
|
+
const result2 = await pushWorkspaceCommand({ issue, state }, container);
|
|
3215
|
+
return c.json({ ok: true, prUrl: result2.prUrl, ghAvailable: result2.ghAvailable });
|
|
3216
|
+
}
|
|
2694
3217
|
const result = await mergeWorkspaceCommand({ issue, state }, container);
|
|
2695
3218
|
return c.json({ ok: true, ...result });
|
|
2696
3219
|
} catch (error) {
|
|
2697
3220
|
const issueId = parseIssue(c);
|
|
2698
3221
|
logger.error(`Failed to merge workspace for ${issueId || "<unknown>"}: ${String(error)}`);
|
|
2699
|
-
return c.json({ ok: false, error: String(error) },
|
|
3222
|
+
return c.json({ ok: false, error: String(error) }, getWorkspaceActionErrorStatus(error));
|
|
3223
|
+
}
|
|
3224
|
+
});
|
|
3225
|
+
app.get("/api/issues/:id/merge-preview", async (c) => {
|
|
3226
|
+
logger.info({ issueId: parseIssue(c) }, "[API] GET /api/issues/:id/merge-preview");
|
|
3227
|
+
try {
|
|
3228
|
+
const issueId = parseIssue(c);
|
|
3229
|
+
if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
3230
|
+
const issue = findIssue(state, issueId);
|
|
3231
|
+
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
3232
|
+
const { dryMerge } = await import("./workspace-KEHFITYR.js");
|
|
3233
|
+
const result = dryMerge(issue);
|
|
3234
|
+
return c.json({ ok: true, ...result });
|
|
3235
|
+
} catch (error) {
|
|
3236
|
+
logger.error(`Failed to preview merge for ${parseIssue(c) || "<unknown>"}: ${String(error)}`);
|
|
3237
|
+
return c.json({ ok: false, error: String(error) }, getWorkspaceActionErrorStatus(error));
|
|
3238
|
+
}
|
|
3239
|
+
});
|
|
3240
|
+
app.post("/api/issues/:id/rebase", async (c) => {
|
|
3241
|
+
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/rebase");
|
|
3242
|
+
try {
|
|
3243
|
+
const issueId = parseIssue(c);
|
|
3244
|
+
if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
3245
|
+
const issue = findIssue(state, issueId);
|
|
3246
|
+
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
3247
|
+
const { rebaseWorktree } = await import("./workspace-KEHFITYR.js");
|
|
3248
|
+
const result = rebaseWorktree(issue);
|
|
3249
|
+
if (result.success) {
|
|
3250
|
+
addEvent(state, issue.id, "info", `Branch ${issue.branchName} rebased onto ${issue.baseBranch}.`);
|
|
3251
|
+
}
|
|
3252
|
+
await persistState(state);
|
|
3253
|
+
return c.json({ ok: true, ...result });
|
|
3254
|
+
} catch (error) {
|
|
3255
|
+
logger.error(`Failed to rebase for ${parseIssue(c) || "<unknown>"}: ${String(error)}`);
|
|
3256
|
+
return c.json({ ok: false, error: String(error) }, getWorkspaceActionErrorStatus(error));
|
|
2700
3257
|
}
|
|
2701
3258
|
});
|
|
2702
3259
|
app.post("/api/issues/:id/try", async (c) => {
|
|
2703
3260
|
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/try");
|
|
2704
3261
|
return mutateIssueState(state, c, async (issue) => {
|
|
2705
|
-
if (!["Reviewing", "
|
|
3262
|
+
if (!["Reviewing", "PendingDecision"].includes(issue.state)) {
|
|
2706
3263
|
throw new Error(`Cannot apply test for issue in state ${issue.state}.`);
|
|
2707
3264
|
}
|
|
2708
3265
|
if (!issue.branchName) {
|
|
2709
3266
|
throw new Error("No branch name found for this issue.");
|
|
2710
3267
|
}
|
|
2711
3268
|
try {
|
|
2712
|
-
|
|
3269
|
+
execSync3(
|
|
2713
3270
|
`git merge --squash "${issue.branchName}"`,
|
|
2714
3271
|
{ encoding: "utf8", cwd: TARGET_ROOT, stdio: "pipe", timeout: 3e4 }
|
|
2715
3272
|
);
|
|
@@ -2724,8 +3281,8 @@ function registerStateRoutes(app, state) {
|
|
|
2724
3281
|
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/revert-try");
|
|
2725
3282
|
return mutateIssueState(state, c, async (issue) => {
|
|
2726
3283
|
try {
|
|
2727
|
-
|
|
2728
|
-
|
|
3284
|
+
execSync3("git reset --hard HEAD", { cwd: TARGET_ROOT, stdio: "pipe", timeout: 15e3 });
|
|
3285
|
+
execSync3("git clean -fd", { cwd: TARGET_ROOT, stdio: "pipe", timeout: 15e3 });
|
|
2729
3286
|
} catch (err) {
|
|
2730
3287
|
const msg = err.stderr || err.stdout || String(err);
|
|
2731
3288
|
throw new Error(`git reset/clean failed: ${msg}`);
|
|
@@ -2736,7 +3293,7 @@ function registerStateRoutes(app, state) {
|
|
|
2736
3293
|
app.post("/api/issues/:id/rollback", async (c) => {
|
|
2737
3294
|
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/rollback");
|
|
2738
3295
|
return mutateIssueState(state, c, async (issue) => {
|
|
2739
|
-
if (!["Reviewing", "
|
|
3296
|
+
if (!["Reviewing", "PendingDecision", "Approved"].includes(issue.state)) {
|
|
2740
3297
|
throw new Error(`Cannot rollback issue in state ${issue.state}. Must be in Reviewing, Reviewed, or Done.`);
|
|
2741
3298
|
}
|
|
2742
3299
|
if (issue.workspacePath) {
|
|
@@ -2766,15 +3323,15 @@ function registerStateRoutes(app, state) {
|
|
|
2766
3323
|
if (!Array.isArray(payload.files) || payload.files.length === 0) {
|
|
2767
3324
|
return c.json({ ok: false, error: "No files provided." }, 400);
|
|
2768
3325
|
}
|
|
2769
|
-
const issueAttachDir =
|
|
2770
|
-
|
|
3326
|
+
const issueAttachDir = join9(ATTACHMENTS_ROOT, issue.id);
|
|
3327
|
+
mkdirSync4(issueAttachDir, { recursive: true });
|
|
2771
3328
|
const newPaths = [];
|
|
2772
3329
|
for (const file of payload.files) {
|
|
2773
3330
|
if (typeof file.data !== "string" || !file.name) continue;
|
|
2774
3331
|
const safeExt = extname(file.name).replace(/[^a-z0-9.]/gi, "").slice(0, 10) || ".bin";
|
|
2775
3332
|
const safeName = `${randomUUID()}${safeExt}`;
|
|
2776
|
-
const dest =
|
|
2777
|
-
|
|
3333
|
+
const dest = join9(issueAttachDir, safeName);
|
|
3334
|
+
writeFileSync4(dest, Buffer.from(file.data, "base64"));
|
|
2778
3335
|
newPaths.push(dest);
|
|
2779
3336
|
}
|
|
2780
3337
|
issue.images = [...issue.images ?? [], ...newPaths];
|
|
@@ -2794,8 +3351,8 @@ function registerStateRoutes(app, state) {
|
|
|
2794
3351
|
const filename = c.req.param?.("filename") ?? c.req.params?.filename ?? "";
|
|
2795
3352
|
if (!filename) return c.json({ ok: false, error: "Filename is required." }, 400);
|
|
2796
3353
|
const safeName = basename2(filename);
|
|
2797
|
-
const filePath =
|
|
2798
|
-
if (!
|
|
3354
|
+
const filePath = join9(ATTACHMENTS_ROOT, issueId, safeName);
|
|
3355
|
+
if (!existsSync8(filePath)) return c.json({ ok: false, error: "Image not found." }, 404);
|
|
2799
3356
|
const ext = extname(safeName).toLowerCase();
|
|
2800
3357
|
const mimeMap = {
|
|
2801
3358
|
".png": "image/png",
|
|
@@ -2806,8 +3363,8 @@ function registerStateRoutes(app, state) {
|
|
|
2806
3363
|
".svg": "image/svg+xml"
|
|
2807
3364
|
};
|
|
2808
3365
|
const mime = mimeMap[ext] ?? "application/octet-stream";
|
|
2809
|
-
const { readFileSync:
|
|
2810
|
-
const data =
|
|
3366
|
+
const { readFileSync: readFileSync12 } = await import("fs");
|
|
3367
|
+
const data = readFileSync12(filePath);
|
|
2811
3368
|
return new Response(data, { headers: { "Content-Type": mime, "Cache-Control": "private, max-age=86400" } });
|
|
2812
3369
|
} catch (error) {
|
|
2813
3370
|
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
@@ -2819,7 +3376,7 @@ function registerStateRoutes(app, state) {
|
|
|
2819
3376
|
const issue = findIssue(state, issueId);
|
|
2820
3377
|
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
2821
3378
|
try {
|
|
2822
|
-
const { getIssueTransitionHistory } = await import("./issue-state-machine-
|
|
3379
|
+
const { getIssueTransitionHistory } = await import("./issue-state-machine-V2KPUYPW.js");
|
|
2823
3380
|
const limit = parseInt(c.req.query("limit") ?? "50", 10);
|
|
2824
3381
|
const offset = parseInt(c.req.query("offset") ?? "0", 10);
|
|
2825
3382
|
const transitions = await getIssueTransitionHistory(issue.id, { limit, offset });
|
|
@@ -2830,7 +3387,7 @@ function registerStateRoutes(app, state) {
|
|
|
2830
3387
|
});
|
|
2831
3388
|
app.get("/api/state-machine/transitions", async (c) => {
|
|
2832
3389
|
try {
|
|
2833
|
-
const { getStateMachineTransitions } = await import("./issue-state-machine-
|
|
3390
|
+
const { getStateMachineTransitions } = await import("./issue-state-machine-V2KPUYPW.js");
|
|
2834
3391
|
return c.json({ ok: true, transitions: getStateMachineTransitions() });
|
|
2835
3392
|
} catch (error) {
|
|
2836
3393
|
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
@@ -2838,7 +3395,7 @@ function registerStateRoutes(app, state) {
|
|
|
2838
3395
|
});
|
|
2839
3396
|
app.get("/api/state-machine/visualize", async (c) => {
|
|
2840
3397
|
try {
|
|
2841
|
-
const { visualizeStateMachine } = await import("./issue-state-machine-
|
|
3398
|
+
const { visualizeStateMachine } = await import("./issue-state-machine-V2KPUYPW.js");
|
|
2842
3399
|
const dot = visualizeStateMachine();
|
|
2843
3400
|
if (!dot) return c.json({ ok: false, error: "Visualization not available." }, 404);
|
|
2844
3401
|
return c.json({ ok: true, dot });
|
|
@@ -2923,8 +3480,8 @@ async function recoverPlanningSession() {
|
|
|
2923
3480
|
}
|
|
2924
3481
|
|
|
2925
3482
|
// src/agents/planning/plan-generator.ts
|
|
2926
|
-
import { writeFileSync as
|
|
2927
|
-
import { join as
|
|
3483
|
+
import { writeFileSync as writeFileSync6 } from "fs";
|
|
3484
|
+
import { join as join11 } from "path";
|
|
2928
3485
|
import { mkdtempSync, rmSync as rmSync2 } from "fs";
|
|
2929
3486
|
import { tmpdir } from "os";
|
|
2930
3487
|
|
|
@@ -2982,10 +3539,9 @@ function tryBuildPlan(parsed) {
|
|
|
2982
3539
|
})) : void 0,
|
|
2983
3540
|
validation: toStringArray(parsed.validation),
|
|
2984
3541
|
deliverables: toStringArray(parsed.deliverables),
|
|
2985
|
-
executionStrategy: parsed.executionStrategy || parsed.execution_strategy || void 0,
|
|
2986
|
-
toolingDecision: parsed.toolingDecision || parsed.tooling_decision || void 0,
|
|
2987
3542
|
suggestedPaths: toStringArray(parsed.suggestedPaths || parsed.suggested_paths || parsed.suggestedFilePaths || parsed.filePaths || parsed.file_paths || parsed.paths),
|
|
2988
|
-
|
|
3543
|
+
suggestedSkills: toStringArray(parsed.suggestedSkills || parsed.suggested_skills),
|
|
3544
|
+
suggestedAgents: toStringArray(parsed.suggestedAgents || parsed.suggested_agents),
|
|
2989
3545
|
suggestedEffort: parsed.suggestedEffort || parsed.suggested_effort || parsed.effortSuggestion || parsed.effort_suggestion || parsed.effort || { default: "medium" },
|
|
2990
3546
|
provider: "",
|
|
2991
3547
|
createdAt: now()
|
|
@@ -3096,28 +3652,27 @@ function extractPlanTokenUsage(raw) {
|
|
|
3096
3652
|
}
|
|
3097
3653
|
|
|
3098
3654
|
// src/agents/planning/planning-prompts.ts
|
|
3099
|
-
import { mkdirSync as
|
|
3655
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
3100
3656
|
import { spawn } from "child_process";
|
|
3101
|
-
import { join as
|
|
3657
|
+
import { join as join10 } from "path";
|
|
3102
3658
|
|
|
3103
3659
|
// src/agents/planning/planning-schema.ts
|
|
3104
3660
|
var STEP_SCHEMA = {
|
|
3105
3661
|
type: "object",
|
|
3106
3662
|
additionalProperties: false,
|
|
3107
|
-
required: ["step", "action", "files", "details", "
|
|
3663
|
+
required: ["step", "action", "files", "details", "doneWhen"],
|
|
3108
3664
|
properties: {
|
|
3109
3665
|
step: { type: "number" },
|
|
3110
3666
|
action: { type: "string" },
|
|
3111
3667
|
files: { type: "array", items: { type: "string" } },
|
|
3112
3668
|
details: { type: "string" },
|
|
3113
|
-
ownerType: { type: "string", enum: ["human", "agent", "skill", "subagent", "tool"] },
|
|
3114
3669
|
doneWhen: { type: "string" }
|
|
3115
3670
|
}
|
|
3116
3671
|
};
|
|
3117
3672
|
var PLAN_JSON_SCHEMA = JSON.stringify({
|
|
3118
3673
|
type: "object",
|
|
3119
3674
|
additionalProperties: false,
|
|
3120
|
-
required: ["summary", "steps", "
|
|
3675
|
+
required: ["summary", "steps", "estimatedComplexity", "suggestedPaths", "suggestedEffort"],
|
|
3121
3676
|
properties: {
|
|
3122
3677
|
summary: { type: "string" },
|
|
3123
3678
|
estimatedComplexity: { type: "string", enum: ["trivial", "low", "medium", "high"] },
|
|
@@ -3125,16 +3680,8 @@ var PLAN_JSON_SCHEMA = JSON.stringify({
|
|
|
3125
3680
|
constraints: { type: "array", items: { type: "string" } },
|
|
3126
3681
|
unknowns: { type: "array", items: { type: "object", additionalProperties: false, properties: { question: { type: "string" }, whyItMatters: { type: "string" }, howToResolve: { type: "string" } }, required: ["question", "whyItMatters", "howToResolve"] } },
|
|
3127
3682
|
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
3683
|
steps: { type: "array", items: STEP_SCHEMA },
|
|
3137
|
-
phases: { type: "array", items: { type: "object", additionalProperties: false, required: ["phaseName", "goal", "tasks"
|
|
3684
|
+
phases: { type: "array", items: { type: "object", additionalProperties: false, required: ["phaseName", "goal", "tasks"], properties: {
|
|
3138
3685
|
phaseName: { type: "string" },
|
|
3139
3686
|
goal: { type: "string" },
|
|
3140
3687
|
tasks: { type: "array", items: STEP_SCHEMA },
|
|
@@ -3145,8 +3692,9 @@ var PLAN_JSON_SCHEMA = JSON.stringify({
|
|
|
3145
3692
|
validation: { type: "array", items: { type: "string" } },
|
|
3146
3693
|
deliverables: { type: "array", items: { type: "string" } },
|
|
3147
3694
|
suggestedPaths: { type: "array", items: { type: "string" } },
|
|
3148
|
-
|
|
3149
|
-
|
|
3695
|
+
suggestedSkills: { type: "array", items: { type: "string" } },
|
|
3696
|
+
suggestedAgents: { type: "array", items: { type: "string" } },
|
|
3697
|
+
suggestedEffort: { type: "object", additionalProperties: false, required: ["default"], properties: { default: { type: "string" }, planner: { type: "string" }, executor: { type: "string" }, reviewer: { type: "string" } } }
|
|
3150
3698
|
}
|
|
3151
3699
|
});
|
|
3152
3700
|
var PLAN_SCHEMA_OBJECT = JSON.parse(PLAN_JSON_SCHEMA);
|
|
@@ -3166,6 +3714,9 @@ var SETTING_ID_AGENT_COMMAND = "runtime.agentCommand";
|
|
|
3166
3714
|
var SETTING_ID_DEFAULT_EFFORT = "runtime.defaultEffort";
|
|
3167
3715
|
var SETTING_ID_DETECTED_PROVIDERS = "providers.detected";
|
|
3168
3716
|
var SETTING_ID_WORKFLOW_CONFIG = "runtime.workflowConfig";
|
|
3717
|
+
var SETTING_ID_TEST_COMMAND = "runtime.testCommand";
|
|
3718
|
+
var SETTING_ID_MERGE_MODE = "runtime.mergeMode";
|
|
3719
|
+
var SETTING_ID_PR_BASE_BRANCH = "runtime.prBaseBranch";
|
|
3169
3720
|
async function loadRuntimeSettings() {
|
|
3170
3721
|
return loadPersistedSettings();
|
|
3171
3722
|
}
|
|
@@ -3181,7 +3732,10 @@ var RUNTIME_CONFIG_SETTING_IDS = /* @__PURE__ */ new Set([
|
|
|
3181
3732
|
SETTING_ID_MAX_CONCURRENT_BY_STATE,
|
|
3182
3733
|
SETTING_ID_AGENT_PROVIDER,
|
|
3183
3734
|
SETTING_ID_AGENT_COMMAND,
|
|
3184
|
-
SETTING_ID_DEFAULT_EFFORT
|
|
3735
|
+
SETTING_ID_DEFAULT_EFFORT,
|
|
3736
|
+
SETTING_ID_TEST_COMMAND,
|
|
3737
|
+
SETTING_ID_MERGE_MODE,
|
|
3738
|
+
SETTING_ID_PR_BASE_BRANCH
|
|
3185
3739
|
]);
|
|
3186
3740
|
var VALID_REASONING_EFFORTS = /* @__PURE__ */ new Set(["low", "medium", "high", "extra-high"]);
|
|
3187
3741
|
function parseIntegerSetting(value) {
|
|
@@ -3240,7 +3794,10 @@ function buildRuntimeConfigSettings(config, source) {
|
|
|
3240
3794
|
{ id: SETTING_ID_MAX_CONCURRENT_BY_STATE, scope: "runtime", value: config.maxConcurrentByState, source, updatedAt },
|
|
3241
3795
|
{ id: SETTING_ID_AGENT_PROVIDER, scope: "runtime", value: config.agentProvider, source, updatedAt },
|
|
3242
3796
|
{ 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 }
|
|
3797
|
+
{ id: SETTING_ID_DEFAULT_EFFORT, scope: "runtime", value: config.defaultEffort, source, updatedAt },
|
|
3798
|
+
{ id: SETTING_ID_TEST_COMMAND, scope: "runtime", value: config.testCommand ?? "", source, updatedAt },
|
|
3799
|
+
{ id: SETTING_ID_MERGE_MODE, scope: "runtime", value: config.mergeMode ?? "local", source, updatedAt },
|
|
3800
|
+
{ id: SETTING_ID_PR_BASE_BRANCH, scope: "runtime", value: config.prBaseBranch ?? "", source, updatedAt }
|
|
3244
3801
|
];
|
|
3245
3802
|
}
|
|
3246
3803
|
function applyPersistedSettings(config, settings) {
|
|
@@ -3331,6 +3888,24 @@ function applyPersistedSettings(config, settings) {
|
|
|
3331
3888
|
}
|
|
3332
3889
|
break;
|
|
3333
3890
|
}
|
|
3891
|
+
case SETTING_ID_TEST_COMMAND: {
|
|
3892
|
+
if (typeof setting.value === "string") {
|
|
3893
|
+
nextConfig.testCommand = setting.value.trim() || void 0;
|
|
3894
|
+
}
|
|
3895
|
+
break;
|
|
3896
|
+
}
|
|
3897
|
+
case SETTING_ID_MERGE_MODE: {
|
|
3898
|
+
if (setting.value === "local" || setting.value === "push-pr") {
|
|
3899
|
+
nextConfig.mergeMode = setting.value;
|
|
3900
|
+
}
|
|
3901
|
+
break;
|
|
3902
|
+
}
|
|
3903
|
+
case SETTING_ID_PR_BASE_BRANCH: {
|
|
3904
|
+
if (typeof setting.value === "string" && setting.value.trim()) {
|
|
3905
|
+
nextConfig.prBaseBranch = setting.value.trim();
|
|
3906
|
+
}
|
|
3907
|
+
break;
|
|
3908
|
+
}
|
|
3334
3909
|
default:
|
|
3335
3910
|
break;
|
|
3336
3911
|
}
|
|
@@ -3435,11 +4010,19 @@ async function persistWorkflowConfig(config) {
|
|
|
3435
4010
|
|
|
3436
4011
|
// src/agents/planning/planning-prompts.ts
|
|
3437
4012
|
async function buildPlanPrompt(title, description, fast = false, images) {
|
|
4013
|
+
const skills = discoverSkills(TARGET_ROOT);
|
|
4014
|
+
const agents = discoverAgents(TARGET_ROOT);
|
|
4015
|
+
const commands = discoverCommands(TARGET_ROOT);
|
|
4016
|
+
const hasCapabilities = skills.length > 0 || agents.length > 0 || commands.length > 0;
|
|
3438
4017
|
return renderPrompt("issue-planner", {
|
|
3439
4018
|
title,
|
|
3440
4019
|
description: description || "(none provided)",
|
|
3441
4020
|
fast,
|
|
3442
|
-
images: images?.length ? images : void 0
|
|
4021
|
+
images: images?.length ? images : void 0,
|
|
4022
|
+
availableCapabilities: hasCapabilities,
|
|
4023
|
+
availableSkills: skills.map((s) => ({ name: s.name, description: s.description || "", whenToUse: s.whenToUse || "" })),
|
|
4024
|
+
availableAgents: agents.map((a) => ({ name: a.name, description: a.description || "", whenToUse: a.whenToUse || "", avoidIf: a.avoidIf || "" })),
|
|
4025
|
+
availableCommands: commands.map((c) => ({ name: c.name }))
|
|
3443
4026
|
});
|
|
3444
4027
|
}
|
|
3445
4028
|
async function buildRefinePrompt(title, description, currentPlan, feedback) {
|
|
@@ -3458,11 +4041,11 @@ function getPlanCommand(provider, model, imagePaths) {
|
|
|
3458
4041
|
}
|
|
3459
4042
|
function savePlanDebugFiles(slug, prompt, output) {
|
|
3460
4043
|
try {
|
|
3461
|
-
const debugDir =
|
|
3462
|
-
|
|
4044
|
+
const debugDir = join10(STATE_ROOT, "debug");
|
|
4045
|
+
mkdirSync5(debugDir, { recursive: true });
|
|
3463
4046
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
3464
|
-
|
|
3465
|
-
if (output)
|
|
4047
|
+
writeFileSync5(join10(debugDir, `plan-${slug}-${ts}-prompt.md`), prompt, "utf8");
|
|
4048
|
+
if (output) writeFileSync5(join10(debugDir, `plan-${slug}-${ts}-output.txt`), output, "utf8");
|
|
3466
4049
|
} catch {
|
|
3467
4050
|
}
|
|
3468
4051
|
}
|
|
@@ -3620,9 +4203,9 @@ async function generatePlan(title, description, config, _workflowDefinition, opt
|
|
|
3620
4203
|
const command = getPlanCommand(preferred, planStageModel, images);
|
|
3621
4204
|
if (!command) throw new Error(`No command configured for provider ${preferred}.`);
|
|
3622
4205
|
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
|
-
|
|
4206
|
+
const tempDir = mkdtempSync(join11(tmpdir(), "fifony-plan-"));
|
|
4207
|
+
const promptFile = join11(tempDir, "fifony-plan-prompt.md");
|
|
4208
|
+
writeFileSync6(promptFile, `${prompt}
|
|
3626
4209
|
`, "utf8");
|
|
3627
4210
|
let lastProgressPersist = 0;
|
|
3628
4211
|
const PROGRESS_INTERVAL_MS = 2e3;
|
|
@@ -3699,8 +4282,8 @@ async function generatePlan(title, description, config, _workflowDefinition, opt
|
|
|
3699
4282
|
}
|
|
3700
4283
|
|
|
3701
4284
|
// src/agents/planning/plan-refiner.ts
|
|
3702
|
-
import { writeFileSync as
|
|
3703
|
-
import { join as
|
|
4285
|
+
import { writeFileSync as writeFileSync7 } from "fs";
|
|
4286
|
+
import { join as join12 } from "path";
|
|
3704
4287
|
import { mkdtempSync as mkdtempSync2, rmSync as rmSync3 } from "fs";
|
|
3705
4288
|
import { tmpdir as tmpdir2 } from "os";
|
|
3706
4289
|
async function refinePlan(issue, feedback, config, _workflowDefinition) {
|
|
@@ -3713,9 +4296,9 @@ async function refinePlan(issue, feedback, config, _workflowDefinition) {
|
|
|
3713
4296
|
{
|
|
3714
4297
|
const command = getPlanCommand(preferred, planStageModel);
|
|
3715
4298
|
if (!command) throw new Error(`No command configured for provider ${preferred}.`);
|
|
3716
|
-
const tempDir = mkdtempSync2(
|
|
3717
|
-
const promptFile =
|
|
3718
|
-
|
|
4299
|
+
const tempDir = mkdtempSync2(join12(tmpdir2(), "fifony-refine-"));
|
|
4300
|
+
const promptFile = join12(tempDir, "fifony-refine-prompt.md");
|
|
4301
|
+
writeFileSync7(promptFile, `${prompt}
|
|
3719
4302
|
`, "utf8");
|
|
3720
4303
|
const output = await runPlanningProcess({
|
|
3721
4304
|
command,
|
|
@@ -3836,10 +4419,10 @@ function refinePlanInBackground(issue, feedback, config, _workflowDefinition, ca
|
|
|
3836
4419
|
|
|
3837
4420
|
// src/agents/planning/issue-enhancer.ts
|
|
3838
4421
|
import { env as env2 } from "process";
|
|
3839
|
-
import { existsSync as
|
|
4422
|
+
import { existsSync as existsSync9, mkdtempSync as mkdtempSync3, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "fs";
|
|
3840
4423
|
import { spawn as spawn2 } from "child_process";
|
|
3841
4424
|
import { tmpdir as tmpdir3 } from "os";
|
|
3842
|
-
import { join as
|
|
4425
|
+
import { join as join13 } from "path";
|
|
3843
4426
|
function getProviderCommand(provider, config) {
|
|
3844
4427
|
return resolveAgentCommand(provider, config.agentCommand || "", "", "");
|
|
3845
4428
|
}
|
|
@@ -3908,22 +4491,22 @@ function parseCandidate(raw, expectedField) {
|
|
|
3908
4491
|
return "";
|
|
3909
4492
|
}
|
|
3910
4493
|
function readProviderOutput(resultFile, fallback) {
|
|
3911
|
-
if (
|
|
4494
|
+
if (existsSync9(resultFile)) {
|
|
3912
4495
|
try {
|
|
3913
|
-
return
|
|
4496
|
+
return readFileSync6(resultFile, "utf8").trim();
|
|
3914
4497
|
} catch {
|
|
3915
4498
|
}
|
|
3916
4499
|
}
|
|
3917
4500
|
return fallback;
|
|
3918
4501
|
}
|
|
3919
4502
|
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
|
-
|
|
4503
|
+
const tempDir = mkdtempSync3(join13(tmpdir3(), "fifony-enhance-"));
|
|
4504
|
+
const promptFile = join13(tempDir, "fifony-enhance-prompt.md");
|
|
4505
|
+
const issuePayloadFile = join13(tempDir, "fifony-issue.json");
|
|
4506
|
+
const resultFile = join13(tempDir, "fifony-result.txt");
|
|
4507
|
+
writeFileSync8(promptFile, `${prompt}
|
|
3925
4508
|
`, "utf8");
|
|
3926
|
-
|
|
4509
|
+
writeFileSync8(issuePayloadFile, JSON.stringify({ title, description, field }, null, 2), "utf8");
|
|
3927
4510
|
let effectiveCommand = command;
|
|
3928
4511
|
if (provider === "codex" && images?.length) {
|
|
3929
4512
|
const imageFlags = images.map((p) => `--image "${p}"`).join(" ");
|
|
@@ -4140,7 +4723,6 @@ function registerPlanRoutes(app, state) {
|
|
|
4140
4723
|
applyUsage: (iss, usage) => applyPlanUsage(iss, usage),
|
|
4141
4724
|
applySuggestions: (iss, plan) => {
|
|
4142
4725
|
if (plan.suggestedPaths?.length) iss.paths = plan.suggestedPaths;
|
|
4143
|
-
if (plan.suggestedLabels?.length) iss.labels = plan.suggestedLabels;
|
|
4144
4726
|
if (plan.suggestedEffort) iss.effort = plan.suggestedEffort;
|
|
4145
4727
|
}
|
|
4146
4728
|
});
|
|
@@ -4322,7 +4904,7 @@ function registerAnalyticsRoutes(app) {
|
|
|
4322
4904
|
try {
|
|
4323
4905
|
const context2 = getApiRuntimeContextOrThrow();
|
|
4324
4906
|
const doneIssues = context2.state.issues.filter(
|
|
4325
|
-
(i) => (i.state === "
|
|
4907
|
+
(i) => (i.state === "Approved" || i.state === "Merged") && i.completedAt
|
|
4326
4908
|
);
|
|
4327
4909
|
const msToDay = (ms) => ms / (1e3 * 60 * 60 * 24);
|
|
4328
4910
|
const avg = (arr) => arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : null;
|
|
@@ -4379,149 +4961,59 @@ function registerScanningRoutes(app, state) {
|
|
|
4379
4961
|
});
|
|
4380
4962
|
}
|
|
4381
4963
|
|
|
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
|
-
}
|
|
4964
|
+
// src/agents/claude-md-manager.ts
|
|
4965
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
4966
|
+
import { join as join14 } from "path";
|
|
4967
|
+
var BLOCK_START = "<!-- FIFONY:START \u2014 managed by fifony, do not edit manually -->";
|
|
4968
|
+
var BLOCK_END = "<!-- FIFONY:END -->";
|
|
4969
|
+
var BLOCK_PATTERN = /<!-- FIFONY:START[^>]*-->[\s\S]*?<!-- FIFONY:END -->/;
|
|
4970
|
+
function buildManagedBlock(skills, agents, commands) {
|
|
4971
|
+
const lines = [
|
|
4972
|
+
BLOCK_START,
|
|
4973
|
+
"## Fifony \u2014 Installed Capabilities",
|
|
4974
|
+
"",
|
|
4975
|
+
"This workspace has fifony-managed agents and skills installed.",
|
|
4976
|
+
""
|
|
4977
|
+
];
|
|
4978
|
+
if (commands.length > 0) {
|
|
4979
|
+
lines.push(`**Commands**: ${commands.map((c) => `/${c.name}`).join(", ")}`);
|
|
4980
|
+
}
|
|
4981
|
+
if (skills.length > 0) {
|
|
4982
|
+
lines.push(`**Skills**: ${skills.map((s) => s.name).join(", ")}`);
|
|
4983
|
+
}
|
|
4984
|
+
if (agents.length > 0) {
|
|
4985
|
+
lines.push(`**Agents**: ${agents.map((a) => a.name).join(", ")}`);
|
|
4986
|
+
}
|
|
4987
|
+
lines.push("");
|
|
4988
|
+
lines.push("Use these capabilities when working on tasks. For details:");
|
|
4989
|
+
lines.push("- Skills: `.claude/skills/*/SKILL.md`");
|
|
4990
|
+
lines.push("- Agents: `.claude/agents/*.md`");
|
|
4991
|
+
lines.push("- Commands: `.claude/commands/*.md`");
|
|
4992
|
+
lines.push(BLOCK_END);
|
|
4993
|
+
return lines.join("\n");
|
|
4994
|
+
}
|
|
4995
|
+
function updateClaudeMdManagedBlock(targetRoot, skills, agents, commands) {
|
|
4996
|
+
if (skills.length === 0 && agents.length === 0 && commands.length === 0) return;
|
|
4997
|
+
const claudeMdPath = join14(targetRoot, "CLAUDE.md");
|
|
4998
|
+
const newBlock = buildManagedBlock(skills, agents, commands);
|
|
4999
|
+
let existing = "";
|
|
5000
|
+
if (existsSync10(claudeMdPath)) {
|
|
5001
|
+
existing = readFileSync7(claudeMdPath, "utf8");
|
|
5002
|
+
}
|
|
5003
|
+
let updated;
|
|
5004
|
+
if (BLOCK_PATTERN.test(existing)) {
|
|
5005
|
+
updated = existing.replace(BLOCK_PATTERN, newBlock);
|
|
5006
|
+
} else if (existing) {
|
|
5007
|
+
updated = `${existing.trimEnd()}
|
|
5008
|
+
|
|
5009
|
+
${newBlock}
|
|
5010
|
+
`;
|
|
5011
|
+
} else {
|
|
5012
|
+
updated = `${newBlock}
|
|
5013
|
+
`;
|
|
4523
5014
|
}
|
|
4524
|
-
return
|
|
5015
|
+
if (updated === existing) return;
|
|
5016
|
+
writeFileSync9(claudeMdPath, updated, "utf8");
|
|
4525
5017
|
}
|
|
4526
5018
|
|
|
4527
5019
|
// src/routes/catalog.ts
|
|
@@ -4545,6 +5037,10 @@ function registerCatalogRoutes(app) {
|
|
|
4545
5037
|
}
|
|
4546
5038
|
const catalog = loadAgentCatalog();
|
|
4547
5039
|
const result = installAgents(TARGET_ROOT, agentNames, catalog);
|
|
5040
|
+
try {
|
|
5041
|
+
updateClaudeMdManagedBlock(TARGET_ROOT, discoverSkills(TARGET_ROOT), discoverAgents(TARGET_ROOT), discoverCommands(TARGET_ROOT));
|
|
5042
|
+
} catch {
|
|
5043
|
+
}
|
|
4548
5044
|
return c.json({ ok: true, ...result });
|
|
4549
5045
|
} catch (error) {
|
|
4550
5046
|
logger.error({ err: error }, "Failed to install agents");
|
|
@@ -4560,6 +5056,10 @@ function registerCatalogRoutes(app) {
|
|
|
4560
5056
|
}
|
|
4561
5057
|
const catalog = loadSkillCatalog();
|
|
4562
5058
|
const result = installSkills(TARGET_ROOT, skillNames, catalog);
|
|
5059
|
+
try {
|
|
5060
|
+
updateClaudeMdManagedBlock(TARGET_ROOT, discoverSkills(TARGET_ROOT), discoverAgents(TARGET_ROOT), discoverCommands(TARGET_ROOT));
|
|
5061
|
+
} catch {
|
|
5062
|
+
}
|
|
4563
5063
|
return c.json({ ok: true, ...result });
|
|
4564
5064
|
} catch (error) {
|
|
4565
5065
|
logger.error({ err: error }, "Failed to install skills");
|
|
@@ -4622,6 +5122,12 @@ function registerReferenceRepositoryRoutes(app) {
|
|
|
4622
5122
|
dryRun: payload?.dryRun === true,
|
|
4623
5123
|
importToGlobal: payload?.global === true
|
|
4624
5124
|
});
|
|
5125
|
+
if (!payload?.dryRun) {
|
|
5126
|
+
try {
|
|
5127
|
+
updateClaudeMdManagedBlock(TARGET_ROOT, discoverSkills(TARGET_ROOT), discoverAgents(TARGET_ROOT), discoverCommands(TARGET_ROOT));
|
|
5128
|
+
} catch {
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
4625
5131
|
return c.json({
|
|
4626
5132
|
ok: true,
|
|
4627
5133
|
...summary
|
|
@@ -4636,35 +5142,34 @@ function registerReferenceRepositoryRoutes(app) {
|
|
|
4636
5142
|
}
|
|
4637
5143
|
|
|
4638
5144
|
// src/routes/misc.ts
|
|
4639
|
-
import { execSync as
|
|
5145
|
+
import { execSync as execSync4 } from "child_process";
|
|
4640
5146
|
import {
|
|
4641
5147
|
appendFileSync,
|
|
4642
5148
|
closeSync,
|
|
4643
|
-
existsSync as
|
|
4644
|
-
mkdirSync as
|
|
5149
|
+
existsSync as existsSync11,
|
|
5150
|
+
mkdirSync as mkdirSync6,
|
|
4645
5151
|
openSync,
|
|
4646
|
-
|
|
5152
|
+
readdirSync as readdirSync3,
|
|
5153
|
+
readFileSync as readFileSync8,
|
|
4647
5154
|
readSync,
|
|
4648
5155
|
statSync,
|
|
4649
|
-
writeFileSync as
|
|
5156
|
+
writeFileSync as writeFileSync10
|
|
4650
5157
|
} from "fs";
|
|
4651
5158
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
4652
|
-
import { extname as extname2, join as
|
|
5159
|
+
import { basename as basename3, extname as extname2, join as join15 } from "path";
|
|
4653
5160
|
function registerMiscRoutes(app, state) {
|
|
4654
5161
|
app.post("/api/issues/:id/push", async (c) => {
|
|
4655
5162
|
const issueId = parseIssue(c);
|
|
4656
5163
|
if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
4657
5164
|
const issue = findIssue(state, issueId);
|
|
4658
5165
|
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
|
|
5166
|
+
if (!["Approved", "Reviewing", "PendingDecision"].includes(issue.state)) {
|
|
5167
|
+
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
5168
|
}
|
|
4662
5169
|
try {
|
|
4663
|
-
const
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
await persistState(state);
|
|
4667
|
-
return c.json({ ok: true, prUrl });
|
|
5170
|
+
const container = getContainer();
|
|
5171
|
+
const result = await pushWorkspaceCommand({ issue, state }, container);
|
|
5172
|
+
return c.json({ ok: true, prUrl: result.prUrl, ghAvailable: result.ghAvailable });
|
|
4668
5173
|
} catch (error) {
|
|
4669
5174
|
logger.error({ err: error }, `[API] Failed to push branch for ${issueId}`);
|
|
4670
5175
|
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
@@ -4687,7 +5192,7 @@ function registerMiscRoutes(app, state) {
|
|
|
4687
5192
|
const wp = issue.workspacePath;
|
|
4688
5193
|
const liveLog = wp ? `${wp}/live-output.log` : null;
|
|
4689
5194
|
let lastSize = 0;
|
|
4690
|
-
if (liveLog &&
|
|
5195
|
+
if (liveLog && existsSync11(liveLog)) {
|
|
4691
5196
|
try {
|
|
4692
5197
|
const stat = statSync(liveLog);
|
|
4693
5198
|
lastSize = stat.size;
|
|
@@ -4715,7 +5220,7 @@ function registerMiscRoutes(app, state) {
|
|
|
4715
5220
|
return;
|
|
4716
5221
|
}
|
|
4717
5222
|
const logPath = currentIssue.workspacePath ? `${currentIssue.workspacePath}/live-output.log` : null;
|
|
4718
|
-
if (logPath &&
|
|
5223
|
+
if (logPath && existsSync11(logPath)) {
|
|
4719
5224
|
try {
|
|
4720
5225
|
const stat = statSync(logPath);
|
|
4721
5226
|
if (stat.size > lastSize) {
|
|
@@ -4770,7 +5275,7 @@ function registerMiscRoutes(app, state) {
|
|
|
4770
5275
|
const liveLog = wp ? `${wp}/live-output.log` : null;
|
|
4771
5276
|
let logTail = "";
|
|
4772
5277
|
let logSize = 0;
|
|
4773
|
-
if (liveLog &&
|
|
5278
|
+
if (liveLog && existsSync11(liveLog)) {
|
|
4774
5279
|
try {
|
|
4775
5280
|
const stat = statSync(liveLog);
|
|
4776
5281
|
logSize = stat.size;
|
|
@@ -4810,13 +5315,13 @@ function registerMiscRoutes(app, state) {
|
|
|
4810
5315
|
const issue = findIssue(state, issueId);
|
|
4811
5316
|
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
4812
5317
|
const wp = issue.workspacePath;
|
|
4813
|
-
if (!wp || !
|
|
5318
|
+
if (!wp || !existsSync11(wp)) {
|
|
4814
5319
|
return c.json({ ok: true, files: [], diff: "", message: "No workspace found." });
|
|
4815
5320
|
}
|
|
4816
5321
|
let raw = "";
|
|
4817
5322
|
if (issue.branchName && issue.baseBranch) {
|
|
4818
5323
|
try {
|
|
4819
|
-
raw =
|
|
5324
|
+
raw = execSync4(
|
|
4820
5325
|
`git diff --no-color "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
4821
5326
|
{ encoding: "utf8", maxBuffer: 4 * 1024 * 1024, timeout: 15e3, cwd: TARGET_ROOT, stdio: "pipe" }
|
|
4822
5327
|
);
|
|
@@ -4824,11 +5329,11 @@ function registerMiscRoutes(app, state) {
|
|
|
4824
5329
|
raw = err.stdout || "";
|
|
4825
5330
|
}
|
|
4826
5331
|
} else {
|
|
4827
|
-
if (!
|
|
5332
|
+
if (!existsSync11(SOURCE_ROOT)) {
|
|
4828
5333
|
return c.json({ ok: true, files: [], diff: "", message: "Source root not found." });
|
|
4829
5334
|
}
|
|
4830
5335
|
try {
|
|
4831
|
-
raw =
|
|
5336
|
+
raw = execSync4(
|
|
4832
5337
|
`git diff --no-index --no-color -- "${SOURCE_ROOT}" "${wp}"`,
|
|
4833
5338
|
{ encoding: "utf8", maxBuffer: 4 * 1024 * 1024, timeout: 15e3 }
|
|
4834
5339
|
);
|
|
@@ -4876,43 +5381,17 @@ function registerMiscRoutes(app, state) {
|
|
|
4876
5381
|
});
|
|
4877
5382
|
app.get("/api/git/status", async (c) => {
|
|
4878
5383
|
try {
|
|
4879
|
-
|
|
4880
|
-
try {
|
|
4881
|
-
execSync3("git rev-parse --git-dir", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
4882
|
-
return true;
|
|
4883
|
-
} catch {
|
|
4884
|
-
return false;
|
|
4885
|
-
}
|
|
4886
|
-
})();
|
|
4887
|
-
if (!isGit) return c.json({ isGit: false, branch: null, hasCommits: false });
|
|
4888
|
-
const branch = (() => {
|
|
4889
|
-
try {
|
|
4890
|
-
return execSync3("git rev-parse --abbrev-ref HEAD", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
|
|
4891
|
-
} catch {
|
|
4892
|
-
return null;
|
|
4893
|
-
}
|
|
4894
|
-
})();
|
|
4895
|
-
const hasCommits = (() => {
|
|
4896
|
-
try {
|
|
4897
|
-
execSync3("git rev-parse HEAD", { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
4898
|
-
return true;
|
|
4899
|
-
} catch {
|
|
4900
|
-
return false;
|
|
4901
|
-
}
|
|
4902
|
-
})();
|
|
4903
|
-
return c.json({ isGit: true, branch, hasCommits });
|
|
5384
|
+
return c.json(getGitRepoStatus(TARGET_ROOT));
|
|
4904
5385
|
} catch (error) {
|
|
4905
5386
|
return c.json({ ok: false, error: String(error) }, 500);
|
|
4906
5387
|
}
|
|
4907
5388
|
});
|
|
4908
5389
|
app.post("/api/git/init", async (c) => {
|
|
4909
5390
|
try {
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
const branch = execSync3("git rev-parse --abbrev-ref HEAD", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
|
|
4913
|
-
state.config.defaultBranch = branch;
|
|
5391
|
+
const status = initializeGitRepoForWorktrees(TARGET_ROOT);
|
|
5392
|
+
state.config.defaultBranch = status.branch || state.config.defaultBranch || "main";
|
|
4914
5393
|
await persistState(state);
|
|
4915
|
-
return c.json({ ok: true,
|
|
5394
|
+
return c.json({ ok: true, ...status });
|
|
4916
5395
|
} catch (error) {
|
|
4917
5396
|
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
4918
5397
|
}
|
|
@@ -4923,7 +5402,7 @@ function registerMiscRoutes(app, state) {
|
|
|
4923
5402
|
if (!branchName || !/^[a-zA-Z0-9/_.-]+$/.test(branchName)) {
|
|
4924
5403
|
return c.json({ ok: false, error: "Invalid branch name." }, 400);
|
|
4925
5404
|
}
|
|
4926
|
-
|
|
5405
|
+
execSync4(`git checkout -b "${branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
4927
5406
|
state.config.defaultBranch = branchName;
|
|
4928
5407
|
await persistState(state);
|
|
4929
5408
|
return c.json({ ok: true, defaultBranch: branchName });
|
|
@@ -4931,6 +5410,26 @@ function registerMiscRoutes(app, state) {
|
|
|
4931
5410
|
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
4932
5411
|
}
|
|
4933
5412
|
});
|
|
5413
|
+
app.post("/api/git/switch", async (c) => {
|
|
5414
|
+
try {
|
|
5415
|
+
const { branchName } = await c.req.json();
|
|
5416
|
+
if (!branchName || !/^[a-zA-Z0-9/_.-]+$/.test(branchName)) {
|
|
5417
|
+
return c.json({ ok: false, error: "Invalid branch name." }, 400);
|
|
5418
|
+
}
|
|
5419
|
+
let created = false;
|
|
5420
|
+
try {
|
|
5421
|
+
execSync4(`git checkout "${branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
5422
|
+
} catch {
|
|
5423
|
+
execSync4(`git checkout -b "${branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
|
|
5424
|
+
created = true;
|
|
5425
|
+
}
|
|
5426
|
+
state.config.defaultBranch = branchName;
|
|
5427
|
+
await persistState(state);
|
|
5428
|
+
return c.json({ ok: true, defaultBranch: branchName, created });
|
|
5429
|
+
} catch (error) {
|
|
5430
|
+
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
5431
|
+
}
|
|
5432
|
+
});
|
|
4934
5433
|
app.get("/api/events/feed", async (c) => {
|
|
4935
5434
|
const since = c.req.query("since");
|
|
4936
5435
|
const issueId = c.req.query("issueId");
|
|
@@ -4944,11 +5443,11 @@ function registerMiscRoutes(app, state) {
|
|
|
4944
5443
|
});
|
|
4945
5444
|
app.get("/api/gitignore/status", async (c) => {
|
|
4946
5445
|
try {
|
|
4947
|
-
const gitignorePath =
|
|
4948
|
-
if (!
|
|
5446
|
+
const gitignorePath = join15(TARGET_ROOT, ".gitignore");
|
|
5447
|
+
if (!existsSync11(gitignorePath)) {
|
|
4949
5448
|
return c.json({ exists: false, hasFifony: false });
|
|
4950
5449
|
}
|
|
4951
|
-
const content =
|
|
5450
|
+
const content = readFileSync8(gitignorePath, "utf-8");
|
|
4952
5451
|
const lines = content.split("\n").map((l) => l.trim());
|
|
4953
5452
|
const hasFifony = lines.some((l) => l === ".fifony" || l === ".fifony/" || l === "/.fifony" || l === "/.fifony/");
|
|
4954
5453
|
return c.json({ exists: true, hasFifony });
|
|
@@ -4959,12 +5458,12 @@ function registerMiscRoutes(app, state) {
|
|
|
4959
5458
|
});
|
|
4960
5459
|
app.post("/api/gitignore/add", async (c) => {
|
|
4961
5460
|
try {
|
|
4962
|
-
const gitignorePath =
|
|
4963
|
-
if (!
|
|
4964
|
-
|
|
5461
|
+
const gitignorePath = join15(TARGET_ROOT, ".gitignore");
|
|
5462
|
+
if (!existsSync11(gitignorePath)) {
|
|
5463
|
+
writeFileSync10(gitignorePath, "# Fifony state directory\n.fifony/\n", "utf-8");
|
|
4965
5464
|
return c.json({ ok: true, created: true });
|
|
4966
5465
|
}
|
|
4967
|
-
const content =
|
|
5466
|
+
const content = readFileSync8(gitignorePath, "utf-8");
|
|
4968
5467
|
const lines = content.split("\n").map((l) => l.trim());
|
|
4969
5468
|
const hasFifony = lines.some((l) => l === ".fifony" || l === ".fifony/" || l === "/.fifony" || l === "/.fifony/");
|
|
4970
5469
|
if (hasFifony) {
|
|
@@ -4981,6 +5480,51 @@ function registerMiscRoutes(app, state) {
|
|
|
4981
5480
|
return c.json({ ok: false, error: "Failed to update .gitignore" }, 500);
|
|
4982
5481
|
}
|
|
4983
5482
|
});
|
|
5483
|
+
app.get("/api/issues/:id/outputs", async (c) => {
|
|
5484
|
+
try {
|
|
5485
|
+
const issueId = parseIssue(c);
|
|
5486
|
+
if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
5487
|
+
const issue = findIssue(state, issueId);
|
|
5488
|
+
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
5489
|
+
const wp = issue.workspacePath;
|
|
5490
|
+
if (!wp) return c.json({ ok: true, files: [] });
|
|
5491
|
+
const outputsDir = join15(wp, "outputs");
|
|
5492
|
+
if (!existsSync11(outputsDir)) return c.json({ ok: true, files: [] });
|
|
5493
|
+
const entries = readdirSync3(outputsDir).filter((f) => f.endsWith(".stdout.log")).map((f) => {
|
|
5494
|
+
try {
|
|
5495
|
+
const s = statSync(join15(outputsDir, f));
|
|
5496
|
+
return { name: f, size: s.size };
|
|
5497
|
+
} catch {
|
|
5498
|
+
return { name: f, size: 0 };
|
|
5499
|
+
}
|
|
5500
|
+
});
|
|
5501
|
+
return c.json({ ok: true, files: entries });
|
|
5502
|
+
} catch (error) {
|
|
5503
|
+
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
5504
|
+
}
|
|
5505
|
+
});
|
|
5506
|
+
app.get("/api/issues/:id/outputs/:filename", async (c) => {
|
|
5507
|
+
try {
|
|
5508
|
+
const issueId = parseIssue(c);
|
|
5509
|
+
if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
5510
|
+
const issue = findIssue(state, issueId);
|
|
5511
|
+
if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
|
|
5512
|
+
const filename = c.req.param?.("filename") ?? c.req.params?.filename ?? "";
|
|
5513
|
+
if (!filename) return c.json({ ok: false, error: "Filename is required." }, 400);
|
|
5514
|
+
const safeName = basename3(filename);
|
|
5515
|
+
if (safeName !== filename || !safeName.endsWith(".stdout.log")) {
|
|
5516
|
+
return c.json({ ok: false, error: "Invalid filename." }, 400);
|
|
5517
|
+
}
|
|
5518
|
+
const wp = issue.workspacePath;
|
|
5519
|
+
if (!wp) return c.json({ ok: false, error: "No workspace found." }, 404);
|
|
5520
|
+
const filePath = join15(wp, "outputs", safeName);
|
|
5521
|
+
if (!existsSync11(filePath)) return c.json({ ok: false, error: "Output file not found." }, 404);
|
|
5522
|
+
const content = readFileSync8(filePath, "utf8");
|
|
5523
|
+
return new Response(content, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
|
|
5524
|
+
} catch (error) {
|
|
5525
|
+
return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
5526
|
+
}
|
|
5527
|
+
});
|
|
4984
5528
|
app.post("/api/attachments/upload", async (c) => {
|
|
4985
5529
|
try {
|
|
4986
5530
|
const payload = await c.req.json();
|
|
@@ -4988,15 +5532,15 @@ function registerMiscRoutes(app, state) {
|
|
|
4988
5532
|
return c.json({ ok: false, error: "No files provided." }, 400);
|
|
4989
5533
|
}
|
|
4990
5534
|
const uploadId = randomUUID2();
|
|
4991
|
-
const uploadDir =
|
|
4992
|
-
|
|
5535
|
+
const uploadDir = join15(ATTACHMENTS_ROOT, "temp", uploadId);
|
|
5536
|
+
mkdirSync6(uploadDir, { recursive: true });
|
|
4993
5537
|
const paths = [];
|
|
4994
5538
|
for (const file of payload.files) {
|
|
4995
5539
|
if (typeof file.data !== "string" || !file.name) continue;
|
|
4996
5540
|
const safeExt = extname2(file.name).replace(/[^a-z0-9.]/gi, "").slice(0, 10) || ".bin";
|
|
4997
5541
|
const safeName = `${randomUUID2()}${safeExt}`;
|
|
4998
|
-
const dest =
|
|
4999
|
-
|
|
5542
|
+
const dest = join15(uploadDir, safeName);
|
|
5543
|
+
writeFileSync10(dest, Buffer.from(file.data, "base64"));
|
|
5000
5544
|
paths.push(dest);
|
|
5001
5545
|
}
|
|
5002
5546
|
return c.json({ ok: true, paths, uploadId });
|
|
@@ -5051,10 +5595,10 @@ async function startApiServer(state, port) {
|
|
|
5051
5595
|
}
|
|
5052
5596
|
setApiRuntimeContext(state);
|
|
5053
5597
|
const serveTextFile = (filePath, contentType, cacheControl = "no-cache") => {
|
|
5054
|
-
if (!
|
|
5598
|
+
if (!existsSync12(filePath)) {
|
|
5055
5599
|
return new Response("Not found", { status: 404 });
|
|
5056
5600
|
}
|
|
5057
|
-
return new Response(
|
|
5601
|
+
return new Response(readFileSync9(filePath), {
|
|
5058
5602
|
headers: {
|
|
5059
5603
|
"content-type": contentType,
|
|
5060
5604
|
"cache-control": cacheControl
|
|
@@ -5062,10 +5606,10 @@ async function startApiServer(state, port) {
|
|
|
5062
5606
|
});
|
|
5063
5607
|
};
|
|
5064
5608
|
const serveAppShell = () => {
|
|
5065
|
-
if (!
|
|
5609
|
+
if (!existsSync12(FRONTEND_INDEX)) {
|
|
5066
5610
|
return new Response("Not found", { status: 404 });
|
|
5067
5611
|
}
|
|
5068
|
-
const html =
|
|
5612
|
+
const html = readFileSync9(FRONTEND_INDEX, "utf8").replace('href="/assets/manifest.webmanifest"', 'href="/manifest.webmanifest"').replaceAll('href="/assets/icon.svg"', 'href="/icon.svg"');
|
|
5069
5613
|
return new Response(html, {
|
|
5070
5614
|
headers: {
|
|
5071
5615
|
"content-type": "text/html; charset=utf-8",
|
|
@@ -5086,6 +5630,9 @@ async function startApiServer(state, port) {
|
|
|
5086
5630
|
port,
|
|
5087
5631
|
host: "0.0.0.0",
|
|
5088
5632
|
versionPrefix: false,
|
|
5633
|
+
metrics: {
|
|
5634
|
+
logLevel: "info"
|
|
5635
|
+
},
|
|
5089
5636
|
// HTTP + WebSocket on the same port via listeners
|
|
5090
5637
|
listeners: [{
|
|
5091
5638
|
bind: { host: "0.0.0.0", port },
|
|
@@ -5434,7 +5981,6 @@ async function persistState(state) {
|
|
|
5434
5981
|
broadcastToWebSocketClients({
|
|
5435
5982
|
type: "state:update",
|
|
5436
5983
|
metrics: state.metrics,
|
|
5437
|
-
capabilities: computeCapabilityCounts(state.issues),
|
|
5438
5984
|
issues: state.issues,
|
|
5439
5985
|
events: state.events.slice(0, 50),
|
|
5440
5986
|
updatedAt: state.updatedAt
|
|
@@ -5578,12 +6124,6 @@ async function closeStateStore() {
|
|
|
5578
6124
|
|
|
5579
6125
|
// src/domains/project.ts
|
|
5580
6126
|
var SETTING_ID_PROJECT_NAME = "system.projectName";
|
|
5581
|
-
var LEGACY_PROJECT_SETTING_IDS = [
|
|
5582
|
-
"runtime.projectName",
|
|
5583
|
-
"ui.projectName",
|
|
5584
|
-
"projectName",
|
|
5585
|
-
"project.name"
|
|
5586
|
-
];
|
|
5587
6127
|
function normalizeProjectName(value) {
|
|
5588
6128
|
return typeof value === "string" ? value.trim().replace(/\s+/g, " ") : "";
|
|
5589
6129
|
}
|
|
@@ -5593,14 +6133,7 @@ function detectProjectName(targetRoot) {
|
|
|
5593
6133
|
return normalizeProjectName(basename4(normalizedPath));
|
|
5594
6134
|
}
|
|
5595
6135
|
function readSavedProjectName(settings) {
|
|
5596
|
-
|
|
5597
|
-
for (const id of settingIds) {
|
|
5598
|
-
const value = normalizeProjectName(settings.find((setting) => setting.id === id)?.value);
|
|
5599
|
-
if (value) {
|
|
5600
|
-
return value;
|
|
5601
|
-
}
|
|
5602
|
-
}
|
|
5603
|
-
return "";
|
|
6136
|
+
return normalizeProjectName(settings.find((s) => s.id === SETTING_ID_PROJECT_NAME)?.value);
|
|
5604
6137
|
}
|
|
5605
6138
|
function buildQueueTitle(projectName) {
|
|
5606
6139
|
const normalizedProjectName = normalizeProjectName(projectName);
|
|
@@ -5618,7 +6151,7 @@ function resolveProjectMetadata(settings, targetRoot) {
|
|
|
5618
6151
|
};
|
|
5619
6152
|
}
|
|
5620
6153
|
function scanProjectFiles(targetRoot) {
|
|
5621
|
-
const check = (rel) =>
|
|
6154
|
+
const check = (rel) => existsSync13(join16(targetRoot, rel));
|
|
5622
6155
|
const files = {
|
|
5623
6156
|
claudeMd: check("CLAUDE.md"),
|
|
5624
6157
|
claudeDir: check(".claude"),
|
|
@@ -5639,10 +6172,10 @@ function scanProjectFiles(targetRoot) {
|
|
|
5639
6172
|
};
|
|
5640
6173
|
const existingAgents = [];
|
|
5641
6174
|
for (const agentDir of [".claude/agents", ".codex/agents"]) {
|
|
5642
|
-
const fullPath =
|
|
5643
|
-
if (!
|
|
6175
|
+
const fullPath = join16(targetRoot, agentDir);
|
|
6176
|
+
if (!existsSync13(fullPath)) continue;
|
|
5644
6177
|
try {
|
|
5645
|
-
const entries =
|
|
6178
|
+
const entries = readdirSync4(fullPath);
|
|
5646
6179
|
for (const entry of entries) {
|
|
5647
6180
|
if (entry.endsWith(".md")) {
|
|
5648
6181
|
existingAgents.push(basename4(entry, ".md"));
|
|
@@ -5653,13 +6186,13 @@ function scanProjectFiles(targetRoot) {
|
|
|
5653
6186
|
}
|
|
5654
6187
|
const existingSkills = [];
|
|
5655
6188
|
for (const skillDir of [".claude/skills", ".codex/skills"]) {
|
|
5656
|
-
const fullPath =
|
|
5657
|
-
if (!
|
|
6189
|
+
const fullPath = join16(targetRoot, skillDir);
|
|
6190
|
+
if (!existsSync13(fullPath)) continue;
|
|
5658
6191
|
try {
|
|
5659
|
-
const entries =
|
|
6192
|
+
const entries = readdirSync4(fullPath);
|
|
5660
6193
|
for (const entry of entries) {
|
|
5661
|
-
const skillFile =
|
|
5662
|
-
if (
|
|
6194
|
+
const skillFile = join16(fullPath, entry, "SKILL.md");
|
|
6195
|
+
if (existsSync13(skillFile)) {
|
|
5663
6196
|
existingSkills.push(entry);
|
|
5664
6197
|
}
|
|
5665
6198
|
}
|
|
@@ -5667,20 +6200,20 @@ function scanProjectFiles(targetRoot) {
|
|
|
5667
6200
|
}
|
|
5668
6201
|
}
|
|
5669
6202
|
let readmeExcerpt = "";
|
|
5670
|
-
const readmePath =
|
|
5671
|
-
if (
|
|
6203
|
+
const readmePath = join16(targetRoot, "README.md");
|
|
6204
|
+
if (existsSync13(readmePath)) {
|
|
5672
6205
|
try {
|
|
5673
|
-
const content =
|
|
6206
|
+
const content = readFileSync10(readmePath, "utf8");
|
|
5674
6207
|
readmeExcerpt = content.slice(0, 200).trim();
|
|
5675
6208
|
} catch {
|
|
5676
6209
|
}
|
|
5677
6210
|
}
|
|
5678
6211
|
let packageName = "";
|
|
5679
6212
|
let packageDescription = "";
|
|
5680
|
-
const pkgPath =
|
|
5681
|
-
if (
|
|
6213
|
+
const pkgPath = join16(targetRoot, "package.json");
|
|
6214
|
+
if (existsSync13(pkgPath)) {
|
|
5682
6215
|
try {
|
|
5683
|
-
const pkg = JSON.parse(
|
|
6216
|
+
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
5684
6217
|
packageName = typeof pkg.name === "string" ? pkg.name : "";
|
|
5685
6218
|
packageDescription = typeof pkg.description === "string" ? pkg.description : "";
|
|
5686
6219
|
} catch {
|
|
@@ -5722,39 +6255,39 @@ function buildFallbackAnalysis(targetRoot) {
|
|
|
5722
6255
|
let description = "";
|
|
5723
6256
|
let readmeExcerpt = "";
|
|
5724
6257
|
for (const readmeFile of ["README.md", "README.rst", "README.txt", "README"]) {
|
|
5725
|
-
const p =
|
|
5726
|
-
if (
|
|
6258
|
+
const p = join16(targetRoot, readmeFile);
|
|
6259
|
+
if (existsSync13(p)) {
|
|
5727
6260
|
try {
|
|
5728
|
-
readmeExcerpt =
|
|
6261
|
+
readmeExcerpt = readFileSync10(p, "utf8").slice(0, 300).trim();
|
|
5729
6262
|
break;
|
|
5730
6263
|
} catch {
|
|
5731
6264
|
}
|
|
5732
6265
|
}
|
|
5733
6266
|
}
|
|
5734
|
-
const pkgPath =
|
|
5735
|
-
if (
|
|
6267
|
+
const pkgPath = join16(targetRoot, "package.json");
|
|
6268
|
+
if (existsSync13(pkgPath)) {
|
|
5736
6269
|
try {
|
|
5737
|
-
const pkg = JSON.parse(
|
|
6270
|
+
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
5738
6271
|
const name = typeof pkg.name === "string" ? pkg.name : "";
|
|
5739
6272
|
const desc = typeof pkg.description === "string" ? pkg.description : "";
|
|
5740
6273
|
if (desc) description = name ? `${name}: ${desc}` : desc;
|
|
5741
6274
|
} catch {
|
|
5742
6275
|
}
|
|
5743
6276
|
}
|
|
5744
|
-
const cargoPath =
|
|
5745
|
-
if (!description &&
|
|
6277
|
+
const cargoPath = join16(targetRoot, "Cargo.toml");
|
|
6278
|
+
if (!description && existsSync13(cargoPath)) {
|
|
5746
6279
|
try {
|
|
5747
|
-
const content =
|
|
6280
|
+
const content = readFileSync10(cargoPath, "utf8");
|
|
5748
6281
|
const descMatch = content.match(/^description\s*=\s*"([^"]+)"/m);
|
|
5749
6282
|
const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m);
|
|
5750
6283
|
if (descMatch) description = nameMatch ? `${nameMatch[1]}: ${descMatch[1]}` : descMatch[1];
|
|
5751
6284
|
} catch {
|
|
5752
6285
|
}
|
|
5753
6286
|
}
|
|
5754
|
-
const pyprojectPath =
|
|
5755
|
-
if (!description &&
|
|
6287
|
+
const pyprojectPath = join16(targetRoot, "pyproject.toml");
|
|
6288
|
+
if (!description && existsSync13(pyprojectPath)) {
|
|
5756
6289
|
try {
|
|
5757
|
-
const content =
|
|
6290
|
+
const content = readFileSync10(pyprojectPath, "utf8");
|
|
5758
6291
|
const descMatch = content.match(/^description\s*=\s*"([^"]+)"/m);
|
|
5759
6292
|
const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m);
|
|
5760
6293
|
if (descMatch) description = nameMatch ? `${nameMatch[1]}: ${descMatch[1]}` : descMatch[1];
|
|
@@ -5767,7 +6300,7 @@ function buildFallbackAnalysis(targetRoot) {
|
|
|
5767
6300
|
let language = "unknown";
|
|
5768
6301
|
const stack = [];
|
|
5769
6302
|
for (const [file, signal] of Object.entries(BUILD_FILE_SIGNALS)) {
|
|
5770
|
-
if (
|
|
6303
|
+
if (existsSync13(join16(targetRoot, file))) {
|
|
5771
6304
|
if (language === "unknown" && signal.language !== "unknown") {
|
|
5772
6305
|
language = signal.language;
|
|
5773
6306
|
}
|
|
@@ -5850,7 +6383,7 @@ function isBlockedProjectAnalysisResponse(analysis) {
|
|
|
5850
6383
|
var ANALYSIS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
5851
6384
|
function computeProjectHash(targetRoot) {
|
|
5852
6385
|
const buildFiles = Object.keys(BUILD_FILE_SIGNALS);
|
|
5853
|
-
const found = buildFiles.filter((f) =>
|
|
6386
|
+
const found = buildFiles.filter((f) => existsSync13(join16(targetRoot, f))).sort();
|
|
5854
6387
|
return createHash("sha256").update(found.join(",")).digest("hex").slice(0, 16);
|
|
5855
6388
|
}
|
|
5856
6389
|
async function loadCachedAnalysis(targetRoot) {
|
|
@@ -5902,10 +6435,10 @@ async function analyzeProjectWithCli(provider, targetRoot, options) {
|
|
|
5902
6435
|
);
|
|
5903
6436
|
return buildFallbackAnalysis(targetRoot);
|
|
5904
6437
|
}
|
|
5905
|
-
const tempDir = mkdtempSync4(
|
|
5906
|
-
const promptFile =
|
|
6438
|
+
const tempDir = mkdtempSync4(join16(tmpdir4(), "fifony-scan-"));
|
|
6439
|
+
const promptFile = join16(tempDir, "fifony-scan-prompt.txt");
|
|
5907
6440
|
const analysisPrompt = await renderPrompt("project-analysis");
|
|
5908
|
-
|
|
6441
|
+
writeFileSync11(promptFile, analysisPrompt, "utf8");
|
|
5909
6442
|
const processEnv = {};
|
|
5910
6443
|
for (const [key, value] of Object.entries(env3)) {
|
|
5911
6444
|
if (typeof value === "string") processEnv[key] = value;
|
|
@@ -6023,6 +6556,12 @@ var DEFAULT_REFERENCE_REPOSITORIES = [
|
|
|
6023
6556
|
name: "pbakaus/impeccable",
|
|
6024
6557
|
url: "https://github.com/pbakaus/impeccable.git",
|
|
6025
6558
|
description: "Frontend polish and impeccable-style quality workflows."
|
|
6559
|
+
},
|
|
6560
|
+
{
|
|
6561
|
+
id: "everything-claude-code",
|
|
6562
|
+
name: "affaan-m/everything-claude-code",
|
|
6563
|
+
url: "https://github.com/affaan-m/everything-claude-code.git",
|
|
6564
|
+
description: "28 specialized agents, 116 skills, and 59 commands \u2014 agent harness performance system."
|
|
6026
6565
|
}
|
|
6027
6566
|
];
|
|
6028
6567
|
var REPOSITORY_ROOT = resolve2(homedir3(), ".fifony", "repositories");
|
|
@@ -6047,7 +6586,7 @@ var REFERENCE_REPOSITORY_PARSERS = {
|
|
|
6047
6586
|
impeccable: collectImpeccableArtifacts
|
|
6048
6587
|
};
|
|
6049
6588
|
function runGit(args, cwd) {
|
|
6050
|
-
return
|
|
6589
|
+
return execFileSync2("git", args, {
|
|
6051
6590
|
cwd,
|
|
6052
6591
|
encoding: "utf8",
|
|
6053
6592
|
stdio: "pipe",
|
|
@@ -6074,7 +6613,7 @@ function uniqueSuffix(base, used) {
|
|
|
6074
6613
|
}
|
|
6075
6614
|
function collectDirectoryEntries(path) {
|
|
6076
6615
|
try {
|
|
6077
|
-
return
|
|
6616
|
+
return readdirSync4(path, { withFileTypes: true });
|
|
6078
6617
|
} catch {
|
|
6079
6618
|
return [];
|
|
6080
6619
|
}
|
|
@@ -6100,7 +6639,7 @@ function isMarkdownFile(value, expectedName) {
|
|
|
6100
6639
|
function isReferenceFrontMatterFile(filePath) {
|
|
6101
6640
|
let source;
|
|
6102
6641
|
try {
|
|
6103
|
-
source =
|
|
6642
|
+
source = readFileSync10(filePath, "utf8");
|
|
6104
6643
|
} catch {
|
|
6105
6644
|
return false;
|
|
6106
6645
|
}
|
|
@@ -6122,10 +6661,10 @@ function collectAgentArtifacts(agentsDir, usedNames, out) {
|
|
|
6122
6661
|
const parent = slugify(basename4(dirname2(agentsDir)));
|
|
6123
6662
|
const entries = collectDirectoryEntries(agentsDir);
|
|
6124
6663
|
for (const entry of entries) {
|
|
6125
|
-
const itemPath =
|
|
6664
|
+
const itemPath = join16(agentsDir, entry.name);
|
|
6126
6665
|
if (entry.isDirectory()) {
|
|
6127
|
-
const nestedAgentSpec =
|
|
6128
|
-
if (
|
|
6666
|
+
const nestedAgentSpec = join16(itemPath, "AGENT.md");
|
|
6667
|
+
if (existsSync13(nestedAgentSpec)) {
|
|
6129
6668
|
const name2 = uniqueSuffix(`${parent}__${slugify(entry.name)}`, usedNames);
|
|
6130
6669
|
out.push({ kind: "agent", sourcePath: nestedAgentSpec, targetName: name2 });
|
|
6131
6670
|
}
|
|
@@ -6146,10 +6685,10 @@ function collectSkillArtifacts(skillsDir, usedNames, out) {
|
|
|
6146
6685
|
const parent = slugify(basename4(dirname2(skillsDir)));
|
|
6147
6686
|
const entries = collectDirectoryEntries(skillsDir);
|
|
6148
6687
|
for (const entry of entries) {
|
|
6149
|
-
const itemPath =
|
|
6688
|
+
const itemPath = join16(skillsDir, entry.name);
|
|
6150
6689
|
if (entry.isDirectory()) {
|
|
6151
|
-
const skillFile =
|
|
6152
|
-
if (
|
|
6690
|
+
const skillFile = join16(itemPath, "SKILL.md");
|
|
6691
|
+
if (existsSync13(skillFile)) {
|
|
6153
6692
|
const name = uniqueSuffix(`${parent}__${slugify(entry.name)}`, usedNames);
|
|
6154
6693
|
out.push({ kind: "skill", sourcePath: skillFile, targetName: name });
|
|
6155
6694
|
}
|
|
@@ -6176,7 +6715,7 @@ function collectStandardArtifacts(repoPath) {
|
|
|
6176
6715
|
for (const entry of entries) {
|
|
6177
6716
|
if (!entry.isDirectory()) continue;
|
|
6178
6717
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
6179
|
-
const childPath =
|
|
6718
|
+
const childPath = join16(state.path, entry.name);
|
|
6180
6719
|
if (entry.name === "agents") {
|
|
6181
6720
|
collectAgentArtifacts(childPath, agentsUsed, artifacts);
|
|
6182
6721
|
}
|
|
@@ -6204,13 +6743,13 @@ function collectAgencyArtifacts(repoPath) {
|
|
|
6204
6743
|
if (SKIP_DIRS.has(entry.name) || AGENCY_AGENTS_EXCLUDED_DIRS.has(entry.name)) {
|
|
6205
6744
|
continue;
|
|
6206
6745
|
}
|
|
6207
|
-
queue.push({ path:
|
|
6746
|
+
queue.push({ path: join16(state.path, entry.name), depth: state.depth + 1 });
|
|
6208
6747
|
continue;
|
|
6209
6748
|
}
|
|
6210
|
-
if (!isMarkdownFile(entry.name, "readme.md") || !isReferenceFrontMatterFile(
|
|
6749
|
+
if (!isMarkdownFile(entry.name, "readme.md") || !isReferenceFrontMatterFile(join16(state.path, entry.name))) {
|
|
6211
6750
|
continue;
|
|
6212
6751
|
}
|
|
6213
|
-
const itemPath =
|
|
6752
|
+
const itemPath = join16(state.path, entry.name);
|
|
6214
6753
|
const targetName = uniqueSuffix(buildRelativeArtifactName(repoPath, itemPath), agentsUsed);
|
|
6215
6754
|
artifacts.push({
|
|
6216
6755
|
kind: "agent",
|
|
@@ -6224,13 +6763,13 @@ function collectAgencyArtifacts(repoPath) {
|
|
|
6224
6763
|
function collectImpeccableArtifacts(repoPath) {
|
|
6225
6764
|
const skillsUsed = /* @__PURE__ */ new Set();
|
|
6226
6765
|
const artifacts = [];
|
|
6227
|
-
const sourceSkills =
|
|
6228
|
-
if (
|
|
6766
|
+
const sourceSkills = join16(repoPath, "source", "skills");
|
|
6767
|
+
if (existsSync13(sourceSkills)) {
|
|
6229
6768
|
collectSkillArtifacts(sourceSkills, skillsUsed, artifacts);
|
|
6230
6769
|
return artifacts;
|
|
6231
6770
|
}
|
|
6232
|
-
const claudeSkills =
|
|
6233
|
-
if (
|
|
6771
|
+
const claudeSkills = join16(repoPath, ".claude", "skills");
|
|
6772
|
+
if (existsSync13(claudeSkills)) {
|
|
6234
6773
|
collectSkillArtifacts(claudeSkills, skillsUsed, artifacts);
|
|
6235
6774
|
}
|
|
6236
6775
|
return artifacts;
|
|
@@ -6256,19 +6795,19 @@ function getReferenceRepositoriesRoot() {
|
|
|
6256
6795
|
}
|
|
6257
6796
|
function listReferenceRepositories() {
|
|
6258
6797
|
return DEFAULT_REFERENCE_REPOSITORIES.map((repo) => {
|
|
6259
|
-
const path =
|
|
6798
|
+
const path = join16(REPOSITORY_ROOT, repo.id);
|
|
6260
6799
|
const status = {
|
|
6261
6800
|
id: repo.id,
|
|
6262
6801
|
name: repo.name,
|
|
6263
6802
|
url: repo.url,
|
|
6264
6803
|
path,
|
|
6265
|
-
present:
|
|
6804
|
+
present: existsSync13(path),
|
|
6266
6805
|
synced: false
|
|
6267
6806
|
};
|
|
6268
6807
|
if (!status.present) {
|
|
6269
6808
|
return status;
|
|
6270
6809
|
}
|
|
6271
|
-
if (!
|
|
6810
|
+
if (!existsSync13(join16(path, ".git"))) {
|
|
6272
6811
|
status.error = "Path exists but is not a git repo";
|
|
6273
6812
|
return status;
|
|
6274
6813
|
}
|
|
@@ -6292,7 +6831,7 @@ function resolveReferenceRepository(query) {
|
|
|
6292
6831
|
}
|
|
6293
6832
|
function syncReferenceRepositories(repositoryId) {
|
|
6294
6833
|
const root = REPOSITORY_ROOT;
|
|
6295
|
-
|
|
6834
|
+
mkdirSync7(root, { recursive: true });
|
|
6296
6835
|
const repos = repositoryId ? [resolveReferenceRepository(repositoryId)] : DEFAULT_REFERENCE_REPOSITORIES;
|
|
6297
6836
|
const selected = repos.filter((repo) => Boolean(repo));
|
|
6298
6837
|
if (repositoryId && selected.length === 0) {
|
|
@@ -6300,9 +6839,9 @@ function syncReferenceRepositories(repositoryId) {
|
|
|
6300
6839
|
}
|
|
6301
6840
|
const results = [];
|
|
6302
6841
|
for (const repo of selected) {
|
|
6303
|
-
const target =
|
|
6842
|
+
const target = join16(root, repo.id);
|
|
6304
6843
|
const candidates = [repo.url, ...repo.fallbackUrls ?? []];
|
|
6305
|
-
if (!
|
|
6844
|
+
if (!existsSync13(target)) {
|
|
6306
6845
|
let cloneError;
|
|
6307
6846
|
for (const candidate of candidates) {
|
|
6308
6847
|
try {
|
|
@@ -6329,7 +6868,7 @@ function syncReferenceRepositories(repositoryId) {
|
|
|
6329
6868
|
}
|
|
6330
6869
|
continue;
|
|
6331
6870
|
}
|
|
6332
|
-
if (!
|
|
6871
|
+
if (!existsSync13(join16(target, ".git"))) {
|
|
6333
6872
|
results.push({
|
|
6334
6873
|
id: repo.id,
|
|
6335
6874
|
path: target,
|
|
@@ -6364,14 +6903,14 @@ function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
|
|
|
6364
6903
|
if (!repository) {
|
|
6365
6904
|
throw new Error(`Unknown reference repository: ${repositoryId}`);
|
|
6366
6905
|
}
|
|
6367
|
-
const localPath =
|
|
6368
|
-
if (!
|
|
6906
|
+
const localPath = join16(REPOSITORY_ROOT, repository.id);
|
|
6907
|
+
if (!existsSync13(localPath)) {
|
|
6369
6908
|
throw new Error(`Repository not synced yet: ${repository.id}. Run 'fifony onboarding sync --repository ${repository.id}' first.`);
|
|
6370
6909
|
}
|
|
6371
6910
|
const basePath = resolve2(workspaceRoot);
|
|
6372
|
-
const targetBase = options.importToGlobal ?
|
|
6373
|
-
const agentsDir =
|
|
6374
|
-
const skillsDir =
|
|
6911
|
+
const targetBase = options.importToGlobal ? join16(homedir3(), ".codex") : join16(basePath, ".codex");
|
|
6912
|
+
const agentsDir = join16(targetBase, "agents");
|
|
6913
|
+
const skillsDir = join16(targetBase, "skills");
|
|
6375
6914
|
const artifacts = collectArtifacts(localPath, repository.id);
|
|
6376
6915
|
const filtered = options.kind === "all" ? artifacts : artifacts.filter((artifact) => artifact.kind === options.kind.slice(0, -1));
|
|
6377
6916
|
const summary = {
|
|
@@ -6389,16 +6928,16 @@ function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
|
|
|
6389
6928
|
return summary;
|
|
6390
6929
|
}
|
|
6391
6930
|
if (!options.dryRun) {
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6931
|
+
mkdirSync7(targetBase, { recursive: true });
|
|
6932
|
+
mkdirSync7(agentsDir, { recursive: true });
|
|
6933
|
+
mkdirSync7(skillsDir, { recursive: true });
|
|
6395
6934
|
}
|
|
6396
6935
|
for (const artifact of filtered) {
|
|
6397
6936
|
try {
|
|
6398
|
-
const source =
|
|
6937
|
+
const source = readFileSync10(artifact.sourcePath, "utf8");
|
|
6399
6938
|
if (artifact.kind === "agent") {
|
|
6400
|
-
const target =
|
|
6401
|
-
if (!options.overwrite &&
|
|
6939
|
+
const target = join16(agentsDir, `${artifact.targetName}.md`);
|
|
6940
|
+
if (!options.overwrite && existsSync13(target)) {
|
|
6402
6941
|
summary.skippedAgents.push(artifact.targetName);
|
|
6403
6942
|
continue;
|
|
6404
6943
|
}
|
|
@@ -6406,12 +6945,12 @@ function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
|
|
|
6406
6945
|
summary.importedAgents.push(artifact.targetName);
|
|
6407
6946
|
continue;
|
|
6408
6947
|
}
|
|
6409
|
-
|
|
6948
|
+
writeFileSync11(target, source, "utf8");
|
|
6410
6949
|
summary.importedAgents.push(artifact.targetName);
|
|
6411
6950
|
} else {
|
|
6412
|
-
const targetDir =
|
|
6413
|
-
const target =
|
|
6414
|
-
if (!options.overwrite &&
|
|
6951
|
+
const targetDir = join16(skillsDir, artifact.targetName);
|
|
6952
|
+
const target = join16(targetDir, "SKILL.md");
|
|
6953
|
+
if (!options.overwrite && existsSync13(target)) {
|
|
6415
6954
|
summary.skippedSkills.push(artifact.targetName);
|
|
6416
6955
|
continue;
|
|
6417
6956
|
}
|
|
@@ -6419,8 +6958,8 @@ function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
|
|
|
6419
6958
|
summary.importedSkills.push(artifact.targetName);
|
|
6420
6959
|
continue;
|
|
6421
6960
|
}
|
|
6422
|
-
|
|
6423
|
-
|
|
6961
|
+
mkdirSync7(targetDir, { recursive: true });
|
|
6962
|
+
writeFileSync11(target, source, "utf8");
|
|
6424
6963
|
summary.importedSkills.push(artifact.targetName);
|
|
6425
6964
|
}
|
|
6426
6965
|
} catch (error) {
|
|
@@ -6534,6 +7073,37 @@ function validateConfig(config) {
|
|
|
6534
7073
|
}
|
|
6535
7074
|
|
|
6536
7075
|
// src/domains/issues.ts
|
|
7076
|
+
function normalizeIssue(raw) {
|
|
7077
|
+
const id = toStringValue(raw.id, "");
|
|
7078
|
+
if (!id) return null;
|
|
7079
|
+
const createdAt = toStringValue(raw.createdAt, now());
|
|
7080
|
+
const updatedAt = toStringValue(raw.updatedAt, createdAt);
|
|
7081
|
+
const issue = {
|
|
7082
|
+
id,
|
|
7083
|
+
identifier: toStringValue(raw.identifier, id),
|
|
7084
|
+
title: toStringValue(raw.title, `Issue ${id}`),
|
|
7085
|
+
description: toStringValue(raw.description, ""),
|
|
7086
|
+
state: normalizeState(raw.state, raw.plan && typeof raw.plan === "object" ? "PendingApproval" : "Planning"),
|
|
7087
|
+
branchName: toStringValue(raw.branchName),
|
|
7088
|
+
url: toStringValue(raw.url),
|
|
7089
|
+
assigneeId: toStringValue(raw.assigneeId),
|
|
7090
|
+
labels: toStringArray(raw.labels),
|
|
7091
|
+
paths: toStringArray(raw.paths),
|
|
7092
|
+
blockedBy: toStringArray(raw.blockedBy),
|
|
7093
|
+
assignedToWorker: toBooleanValue(raw.assignedToWorker, true),
|
|
7094
|
+
createdAt,
|
|
7095
|
+
updatedAt,
|
|
7096
|
+
history: [],
|
|
7097
|
+
attempts: toNumberValue(raw.attempts, 0),
|
|
7098
|
+
maxAttempts: toNumberValue(raw.maxAttempts, 3),
|
|
7099
|
+
nextRetryAt: toStringValue(raw.nextRetryAt),
|
|
7100
|
+
planVersion: 0,
|
|
7101
|
+
executeAttempt: 0,
|
|
7102
|
+
reviewAttempt: 0,
|
|
7103
|
+
planHistory: []
|
|
7104
|
+
};
|
|
7105
|
+
return issue;
|
|
7106
|
+
}
|
|
6537
7107
|
function nextLocalIssueId(issues) {
|
|
6538
7108
|
const maxId = issues.reduce((current, issue) => {
|
|
6539
7109
|
const match = issue.identifier.match(/^#(\d+)$/);
|
|
@@ -6551,13 +7121,12 @@ function createIssueFromPayload(payload, issues, defaultBranch) {
|
|
|
6551
7121
|
const blockedBy = toStringArray(payload.blockedBy);
|
|
6552
7122
|
const paths = toStringArray(payload.paths);
|
|
6553
7123
|
const images = toStringArray(payload.images);
|
|
6554
|
-
const initialState = parseIssueState(payload.state) ?? (payload.plan ? "
|
|
7124
|
+
const initialState = parseIssueState(payload.state) ?? (payload.plan ? "PendingApproval" : "Planning");
|
|
6555
7125
|
const issue = {
|
|
6556
7126
|
id,
|
|
6557
7127
|
identifier,
|
|
6558
7128
|
title: toStringValue(payload.title, `Issue ${identifier}`),
|
|
6559
7129
|
description: toStringValue(payload.description, ""),
|
|
6560
|
-
priority: clamp(toNumberValue(payload.priority, 1), 1, 10),
|
|
6561
7130
|
state: initialState,
|
|
6562
7131
|
branchName: toStringValue(payload.branchName),
|
|
6563
7132
|
baseBranch: toStringValue(payload.baseBranch) || defaultBranch,
|
|
@@ -6565,10 +7134,6 @@ function createIssueFromPayload(payload, issues, defaultBranch) {
|
|
|
6565
7134
|
assigneeId: toStringValue(payload.assigneeId),
|
|
6566
7135
|
labels: toStringArray(payload.labels),
|
|
6567
7136
|
paths,
|
|
6568
|
-
inferredPaths: [],
|
|
6569
|
-
capabilityCategory: "",
|
|
6570
|
-
capabilityOverlays: [],
|
|
6571
|
-
capabilityRationale: [],
|
|
6572
7137
|
blockedBy,
|
|
6573
7138
|
assignedToWorker: true,
|
|
6574
7139
|
createdAt,
|
|
@@ -6590,21 +7155,10 @@ function createIssueFromPayload(payload, issues, defaultBranch) {
|
|
|
6590
7155
|
if (issue.plan.suggestedPaths?.length && !issue.paths?.length) {
|
|
6591
7156
|
issue.paths = issue.plan.suggestedPaths;
|
|
6592
7157
|
}
|
|
6593
|
-
if (issue.plan.suggestedLabels?.length && !issue.labels?.length) {
|
|
6594
|
-
issue.labels = issue.plan.suggestedLabels;
|
|
6595
|
-
}
|
|
6596
7158
|
if (issue.plan.suggestedEffort && !issue.effort) {
|
|
6597
7159
|
issue.effort = issue.plan.suggestedEffort;
|
|
6598
7160
|
}
|
|
6599
7161
|
}
|
|
6600
|
-
applyCapabilityMetadata(issue, resolveTaskCapabilities({
|
|
6601
|
-
id: issue.id,
|
|
6602
|
-
identifier: issue.identifier,
|
|
6603
|
-
title: issue.title,
|
|
6604
|
-
description: issue.description,
|
|
6605
|
-
labels: issue.labels,
|
|
6606
|
-
paths: issue.paths
|
|
6607
|
-
}, getCapabilityRoutingOptions()));
|
|
6608
7162
|
return issue;
|
|
6609
7163
|
}
|
|
6610
7164
|
function dedupHistoryEntries(issues) {
|
|
@@ -6628,13 +7182,10 @@ function buildRuntimeState(previous, config, projectMetadata = resolveProjectMet
|
|
|
6628
7182
|
identifier: toStringValue(existing.identifier, existing.id),
|
|
6629
7183
|
title: toStringValue(existing.title, `Issue ${toStringValue(existing.identifier, existing.id)}`),
|
|
6630
7184
|
description: toStringValue(existing.description, ""),
|
|
6631
|
-
state: normalizeState(existing.state, existing.plan ? "
|
|
7185
|
+
state: normalizeState(existing.state, existing.plan ? "PendingApproval" : "Planning"),
|
|
6632
7186
|
paths: toStringArray(existing.paths),
|
|
6633
|
-
inferredPaths: toStringArray(existing.inferredPaths),
|
|
6634
7187
|
labels: toStringArray(existing.labels),
|
|
6635
|
-
|
|
6636
|
-
capabilityRationale: toStringArray(existing.capabilityRationale),
|
|
6637
|
-
blockedBy: toStringArray(existing.blockedBy).length > 0 ? toStringArray(existing.blockedBy) : toStringArray(existing.blocked_by),
|
|
7188
|
+
blockedBy: toStringArray(existing.blockedBy),
|
|
6638
7189
|
history: Array.isArray(existing.history) ? existing.history : [],
|
|
6639
7190
|
attempts: clamp(toNumberValue(existing.attempts, 0), 0, config.maxAttemptsDefault),
|
|
6640
7191
|
maxAttempts: clamp(toNumberValue(existing.maxAttempts, config.maxAttemptsDefault), 1, config.maxAttemptsDefault),
|
|
@@ -6707,6 +7258,14 @@ async function transitionIssue(issue, event, context2 = {}) {
|
|
|
6707
7258
|
logger.debug({ issueId: issue.id, identifier: issue.identifier, from: issue.state, event, context: context2 }, "[State] Issue transition");
|
|
6708
7259
|
await executeTransition(issue, event, { ...context2, issue });
|
|
6709
7260
|
}
|
|
7261
|
+
function issueDependenciesResolved(issue, allIssues) {
|
|
7262
|
+
if (issue.blockedBy.length === 0) return true;
|
|
7263
|
+
const map = new Map(allIssues.map((entry) => [entry.id, entry]));
|
|
7264
|
+
return issue.blockedBy.every((dependencyId) => {
|
|
7265
|
+
const dep = map.get(dependencyId);
|
|
7266
|
+
return dep?.state === "Approved" || dep?.state === "Merged";
|
|
7267
|
+
});
|
|
7268
|
+
}
|
|
6710
7269
|
function getNextRetryAt(issue, baseMs) {
|
|
6711
7270
|
const nextAttempt = issue.attempts + 1;
|
|
6712
7271
|
const nextDelay = withRetryBackoff(nextAttempt, baseMs);
|
|
@@ -6724,7 +7283,7 @@ async function handleStatePatch(state, issue, payload) {
|
|
|
6724
7283
|
for (const event of path) {
|
|
6725
7284
|
await transitionIssue(issue, event, { note: `Manual state update: ${nextState}`, reason: toStringValue(payload.reason) });
|
|
6726
7285
|
}
|
|
6727
|
-
if (nextState === "
|
|
7286
|
+
if (nextState === "PendingApproval") {
|
|
6728
7287
|
issue.nextRetryAt = void 0;
|
|
6729
7288
|
issue.lastError = void 0;
|
|
6730
7289
|
}
|
|
@@ -6734,6 +7293,34 @@ async function handleStatePatch(state, issue, payload) {
|
|
|
6734
7293
|
addEvent(state, issue.id, "manual", `Manual state transition to ${nextState}`);
|
|
6735
7294
|
}
|
|
6736
7295
|
|
|
7296
|
+
// src/commands/request-rework.command.ts
|
|
7297
|
+
async function requestReworkCommand(input, deps) {
|
|
7298
|
+
const { issue, reviewerFeedback, note } = input;
|
|
7299
|
+
if (issue.state !== "Reviewing" && issue.state !== "PendingDecision") {
|
|
7300
|
+
throw new Error(
|
|
7301
|
+
`requestReworkCommand requires Reviewing or PendingDecision state, got ${issue.state}.`
|
|
7302
|
+
);
|
|
7303
|
+
}
|
|
7304
|
+
issue.lastError = reviewerFeedback;
|
|
7305
|
+
issue.lastFailedPhase = "review";
|
|
7306
|
+
issue.attempts += 1;
|
|
7307
|
+
if (issue.state === "Reviewing") {
|
|
7308
|
+
await transitionIssueCommand(
|
|
7309
|
+
{ issue, target: "PendingDecision", note: `Reviewer completed for ${issue.identifier}.` },
|
|
7310
|
+
deps
|
|
7311
|
+
);
|
|
7312
|
+
}
|
|
7313
|
+
await transitionIssueCommand(
|
|
7314
|
+
{ issue, target: "Queued", note: note ?? `Reviewer requested rework for ${issue.identifier}.` },
|
|
7315
|
+
deps
|
|
7316
|
+
);
|
|
7317
|
+
deps.eventStore.addEvent(
|
|
7318
|
+
issue.id,
|
|
7319
|
+
"runner",
|
|
7320
|
+
`Issue ${issue.identifier} sent back for rework by reviewer.`
|
|
7321
|
+
);
|
|
7322
|
+
}
|
|
7323
|
+
|
|
6737
7324
|
// src/agents/issue-runner.ts
|
|
6738
7325
|
async function runPlanningJob(state, issue) {
|
|
6739
7326
|
issue.planningStatus = "planning";
|
|
@@ -6742,8 +7329,8 @@ async function runPlanningJob(state, issue) {
|
|
|
6742
7329
|
issue.updatedAt = now();
|
|
6743
7330
|
markIssueDirty(issue.id);
|
|
6744
7331
|
const safeId = idToSafePath(issue.id);
|
|
6745
|
-
const workspaceDir =
|
|
6746
|
-
|
|
7332
|
+
const workspaceDir = join17(WORKSPACE_ROOT, safeId);
|
|
7333
|
+
mkdirSync8(workspaceDir, { recursive: true });
|
|
6747
7334
|
addEvent(state, issue.id, "info", `Plan generation started for ${issue.identifier} (v${(issue.planVersion ?? 0) + 1}).`);
|
|
6748
7335
|
try {
|
|
6749
7336
|
const { plan, usage, prompt } = await generatePlan(
|
|
@@ -6757,7 +7344,6 @@ async function runPlanningJob(state, issue) {
|
|
|
6757
7344
|
markIssuePlanDirty(issue.id);
|
|
6758
7345
|
issue.planVersion = Math.max(issue.planVersion ?? 0, 1);
|
|
6759
7346
|
if (plan.suggestedPaths?.length && !issue.paths?.length) issue.paths = plan.suggestedPaths;
|
|
6760
|
-
if (plan.suggestedLabels?.length && !issue.labels?.length) issue.labels = plan.suggestedLabels;
|
|
6761
7347
|
if (plan.suggestedEffort && !issue.effort) issue.effort = plan.suggestedEffort;
|
|
6762
7348
|
if (usage.totalTokens > 0) {
|
|
6763
7349
|
addTokenUsage(issue, {
|
|
@@ -6769,8 +7355,8 @@ async function runPlanningJob(state, issue) {
|
|
|
6769
7355
|
}
|
|
6770
7356
|
const pv = issue.planVersion;
|
|
6771
7357
|
try {
|
|
6772
|
-
|
|
6773
|
-
|
|
7358
|
+
writeFileSync12(join17(workspaceDir, `plan.v${pv}.json`), JSON.stringify(plan, null, 2), "utf8");
|
|
7359
|
+
writeFileSync12(join17(workspaceDir, `plan.v${pv}.prompt.md`), prompt, "utf8");
|
|
6774
7360
|
} catch (artifactErr) {
|
|
6775
7361
|
logger.warn({ err: String(artifactErr) }, "[Agent] Failed to write versioned plan artifacts");
|
|
6776
7362
|
}
|
|
@@ -6799,21 +7385,21 @@ async function handleReviewStage(state, issue, workspacePath, startTs, routedPro
|
|
|
6799
7385
|
const reviewer = routedProviders.find((p) => p.role === "reviewer");
|
|
6800
7386
|
if (!reviewer) {
|
|
6801
7387
|
issue.mergedReason = "Auto-approved: no reviewer configured.";
|
|
6802
|
-
await transitionIssueCommand({ issue, target: "
|
|
7388
|
+
await transitionIssueCommand({ issue, target: "Approved", note: `No reviewer configured; auto-approved for ${issue.identifier}.` }, container);
|
|
6803
7389
|
return;
|
|
6804
7390
|
}
|
|
6805
7391
|
addEvent(state, issue.id, "info", `Review provider: ${reviewer.role}:${reviewer.provider}${reviewer.model ? `/${reviewer.model}` : ""}${reviewer.profile ? `:${reviewer.profile}` : ""}.`);
|
|
6806
7392
|
let diffSummary = "";
|
|
6807
7393
|
try {
|
|
6808
7394
|
if (issue.baseBranch && issue.branchName) {
|
|
6809
|
-
const diffResult =
|
|
7395
|
+
const diffResult = execSync5(
|
|
6810
7396
|
`git diff --stat "${issue.baseBranch}"..."${issue.branchName}"`,
|
|
6811
7397
|
{ cwd: TARGET_ROOT, encoding: "utf8", maxBuffer: 512e3, timeout: 1e4 }
|
|
6812
7398
|
);
|
|
6813
7399
|
diffSummary = diffResult.trim();
|
|
6814
7400
|
} else {
|
|
6815
7401
|
const diffTarget = issue.worktreePath ?? workspacePath;
|
|
6816
|
-
const diffResult =
|
|
7402
|
+
const diffResult = execSync5(
|
|
6817
7403
|
`git diff --no-index --stat -- "${SOURCE_ROOT}" "${diffTarget}" 2>/dev/null`,
|
|
6818
7404
|
{ encoding: "utf8", maxBuffer: 512e3, timeout: 1e4 }
|
|
6819
7405
|
);
|
|
@@ -6822,10 +7408,10 @@ async function handleReviewStage(state, issue, workspacePath, startTs, routedPro
|
|
|
6822
7408
|
} catch (err) {
|
|
6823
7409
|
diffSummary = (err.stdout || "").trim();
|
|
6824
7410
|
}
|
|
6825
|
-
const compiled = await compileReview(issue, reviewer, workspacePath, diffSummary);
|
|
7411
|
+
const compiled = await compileReview(issue, reviewer, workspacePath, diffSummary, state.config);
|
|
6826
7412
|
const effectiveReviewer = { ...reviewer, command: compiled.command || reviewer.command };
|
|
6827
|
-
const reviewPromptFile =
|
|
6828
|
-
|
|
7413
|
+
const reviewPromptFile = join17(workspacePath, "review-prompt.md");
|
|
7414
|
+
writeFileSync12(reviewPromptFile, `${compiled.prompt}
|
|
6829
7415
|
`, "utf8");
|
|
6830
7416
|
const reviewResult = await runAgentSession(state, issue, effectiveReviewer, 1, workspacePath, compiled.prompt, reviewPromptFile);
|
|
6831
7417
|
issue.durationMs = (issue.durationMs ?? 0) + (Date.now() - startTs);
|
|
@@ -6836,27 +7422,40 @@ async function handleReviewStage(state, issue, workspacePath, startTs, routedPro
|
|
|
6836
7422
|
try {
|
|
6837
7423
|
const rpv = issue.planVersion ?? 1;
|
|
6838
7424
|
const rra = issue.reviewAttempt ?? 1;
|
|
6839
|
-
const vReviewPromptSrc =
|
|
6840
|
-
const vReviewAuditSrc =
|
|
6841
|
-
if (
|
|
6842
|
-
|
|
7425
|
+
const vReviewPromptSrc = join17(workspacePath, "review-prompt.md");
|
|
7426
|
+
const vReviewAuditSrc = join17(workspacePath, "execution-audit.json");
|
|
7427
|
+
if (existsSync14(vReviewPromptSrc)) {
|
|
7428
|
+
writeFileSync12(join17(workspacePath, `review.v${rpv}a${rra}.prompt.md`), readFileSync11(vReviewPromptSrc, "utf8"), "utf8");
|
|
6843
7429
|
}
|
|
6844
|
-
if (
|
|
6845
|
-
|
|
7430
|
+
if (existsSync14(vReviewAuditSrc)) {
|
|
7431
|
+
writeFileSync12(join17(workspacePath, `review.v${rpv}a${rra}.audit.json`), readFileSync11(vReviewAuditSrc, "utf8"), "utf8");
|
|
6846
7432
|
}
|
|
6847
7433
|
} catch (vErr) {
|
|
6848
7434
|
logger.warn({ err: String(vErr) }, "[Agent] Failed to write versioned review artifacts");
|
|
6849
7435
|
}
|
|
6850
7436
|
if (reviewResult.success) {
|
|
6851
7437
|
issue.mergedReason = `Auto-approved by reviewer in ${reviewResult.turns} turn(s).`;
|
|
6852
|
-
await transitionIssueCommand({ issue, target: "
|
|
6853
|
-
await
|
|
7438
|
+
await transitionIssueCommand({ issue, target: "PendingDecision", note: `Reviewer completed for ${issue.identifier}.` }, container);
|
|
7439
|
+
const validation = await runValidationGate(issue, state.config);
|
|
7440
|
+
if (validation) {
|
|
7441
|
+
issue.validationResult = validation;
|
|
7442
|
+
markIssueDirty(issue.id);
|
|
7443
|
+
if (!validation.passed) {
|
|
7444
|
+
addEvent(state, issue.id, "error", `Validation gate failed for ${issue.identifier}: ${validation.command}`);
|
|
7445
|
+
logger.warn({ issueId: issue.id, command: validation.command }, "[Agent] Validation gate failed \u2014 staying in Reviewed");
|
|
7446
|
+
return;
|
|
7447
|
+
}
|
|
7448
|
+
addEvent(state, issue.id, "info", `Validation gate passed for ${issue.identifier}.`);
|
|
7449
|
+
}
|
|
7450
|
+
await transitionIssueCommand({ issue, target: "Approved", note: `Reviewer approved ${issue.identifier} in ${reviewResult.turns} turn(s).` }, container);
|
|
6854
7451
|
} else if (reviewResult.continueRequested) {
|
|
6855
|
-
await
|
|
6856
|
-
|
|
6857
|
-
|
|
7452
|
+
await requestReworkCommand(
|
|
7453
|
+
{ issue, reviewerFeedback: reviewResult.output, note: `Reviewer requested rework for ${issue.identifier}.` },
|
|
7454
|
+
container
|
|
7455
|
+
);
|
|
6858
7456
|
} else {
|
|
6859
7457
|
issue.lastError = reviewResult.output;
|
|
7458
|
+
issue.lastFailedPhase = "review";
|
|
6860
7459
|
issue.attempts += 1;
|
|
6861
7460
|
if (issue.attempts >= issue.maxAttempts) {
|
|
6862
7461
|
issue.cancelledReason = `Max attempts reached (${issue.attempts}/${issue.maxAttempts}): reviewer failed or blocked.`;
|
|
@@ -6874,12 +7473,8 @@ async function handleExecutionStage(state, issue, workspacePath, promptText, pro
|
|
|
6874
7473
|
container.eventStore.addEvent(
|
|
6875
7474
|
issue.id,
|
|
6876
7475
|
"info",
|
|
6877
|
-
`
|
|
7476
|
+
`Agent providers: ${routedProviders.map((p) => `${p.role}:${p.provider}${p.model ? `/${p.model}` : ""}${p.reasoningEffort ? ` [${p.reasoningEffort}]` : ""}`).join(", ")}.`
|
|
6878
7477
|
);
|
|
6879
|
-
const routingSignals = describeRoutingSignals(issue, workspaceDerivedPaths);
|
|
6880
|
-
if (routingSignals) {
|
|
6881
|
-
container.eventStore.addEvent(issue.id, "info", `Capability routing signals: ${routingSignals}.`);
|
|
6882
|
-
}
|
|
6883
7478
|
const runResult = await runAgentPipeline(state, issue, workspacePath, promptText, promptFile, workflowConfig);
|
|
6884
7479
|
issue.durationMs = Date.now() - startTs;
|
|
6885
7480
|
issue.commandExitCode = runResult.code;
|
|
@@ -6899,13 +7494,13 @@ async function handleExecutionStage(state, issue, workspacePath, promptText, pro
|
|
|
6899
7494
|
try {
|
|
6900
7495
|
const epv = issue.planVersion ?? 1;
|
|
6901
7496
|
const eea = issue.executeAttempt ?? 1;
|
|
6902
|
-
const vExecPromptSrc =
|
|
6903
|
-
const vExecAuditSrc =
|
|
6904
|
-
if (
|
|
6905
|
-
|
|
7497
|
+
const vExecPromptSrc = join17(workspacePath, "prompt.md");
|
|
7498
|
+
const vExecAuditSrc = join17(workspacePath, "execution-audit.json");
|
|
7499
|
+
if (existsSync14(vExecPromptSrc)) {
|
|
7500
|
+
writeFileSync12(join17(workspacePath, `execute.v${epv}a${eea}.prompt.md`), readFileSync11(vExecPromptSrc, "utf8"), "utf8");
|
|
6906
7501
|
}
|
|
6907
|
-
if (
|
|
6908
|
-
|
|
7502
|
+
if (existsSync14(vExecAuditSrc)) {
|
|
7503
|
+
writeFileSync12(join17(workspacePath, `execute.v${epv}a${eea}.audit.json`), readFileSync11(vExecAuditSrc, "utf8"), "utf8");
|
|
6909
7504
|
}
|
|
6910
7505
|
} catch (vErr) {
|
|
6911
7506
|
logger.warn({ err: String(vErr) }, "[Agent] Failed to write versioned execute artifacts");
|
|
@@ -6922,6 +7517,7 @@ async function handleExecutionStage(state, issue, workspacePath, promptText, pro
|
|
|
6922
7517
|
container.eventStore.addEvent(issue.id, "runner", `Issue ${issue.identifier} queued for next turn.`);
|
|
6923
7518
|
} else {
|
|
6924
7519
|
issue.lastError = runResult.output;
|
|
7520
|
+
issue.lastFailedPhase = "execute";
|
|
6925
7521
|
issue.attempts += 1;
|
|
6926
7522
|
if (issue.attempts >= issue.maxAttempts) {
|
|
6927
7523
|
issue.commandExitCode = runResult.code;
|
|
@@ -6939,17 +7535,10 @@ async function runIssueOnce(state, issue, running) {
|
|
|
6939
7535
|
const isResuming = issue.state === "Running";
|
|
6940
7536
|
logger.info({ issueId: issue.id, identifier: issue.identifier, state: issue.state, isReviewing, isResuming, attempt: issue.attempts + 1, maxAttempts: issue.maxAttempts }, "[Agent] Starting issue execution");
|
|
6941
7537
|
if (issue.state === "Planning") {
|
|
6942
|
-
|
|
6943
|
-
runPlanningJob(state, issue).catch((err) => logger.error({ err, issueId: issue.id, identifier: issue.identifier }, "[Agent] Unexpected error in background planning job")).finally(() => {
|
|
6944
|
-
state.metrics = computeMetrics(state.issues);
|
|
6945
|
-
state.updatedAt = now();
|
|
6946
|
-
persistState(state).catch(() => {
|
|
6947
|
-
});
|
|
6948
|
-
});
|
|
7538
|
+
logger.warn({ issueId: issue.id }, "[Agent] runIssueOnce called for Planning state \u2014 skipping (queue handles planning)");
|
|
6949
7539
|
return;
|
|
6950
7540
|
}
|
|
6951
7541
|
running.add(issue.id);
|
|
6952
|
-
state.metrics.activeWorkers += 1;
|
|
6953
7542
|
issue.startedAt = issue.startedAt ?? now();
|
|
6954
7543
|
let workflowConfig = null;
|
|
6955
7544
|
try {
|
|
@@ -6974,20 +7563,10 @@ async function runIssueOnce(state, issue, running) {
|
|
|
6974
7563
|
}
|
|
6975
7564
|
try {
|
|
6976
7565
|
const workspaceDerivedPaths = hydrateIssuePathsFromWorkspace(issue);
|
|
6977
|
-
if ((issue.paths ?? []).length > 0) {
|
|
6978
|
-
issue.inferredPaths = [.../* @__PURE__ */ new Set([...issue.inferredPaths ?? [], ...inferCapabilityPaths({
|
|
6979
|
-
id: issue.id,
|
|
6980
|
-
identifier: issue.identifier,
|
|
6981
|
-
title: issue.title,
|
|
6982
|
-
description: issue.description,
|
|
6983
|
-
labels: issue.labels,
|
|
6984
|
-
paths: issue.paths
|
|
6985
|
-
})])];
|
|
6986
|
-
}
|
|
6987
7566
|
const { workspacePath, promptText, promptFile } = await prepareWorkspace(issue, state, state.config.defaultBranch);
|
|
6988
7567
|
container.issueRepository.markDirty(issue.id);
|
|
6989
7568
|
try {
|
|
6990
|
-
const { getIssueStateResource: getIssueStateResource2 } = await import("./store-
|
|
7569
|
+
const { getIssueStateResource: getIssueStateResource2 } = await import("./store-RVKQ6UEY.js");
|
|
6991
7570
|
const res = getIssueStateResource2();
|
|
6992
7571
|
if (res) {
|
|
6993
7572
|
await res.patch(issue.id, {
|
|
@@ -7009,6 +7588,7 @@ async function runIssueOnce(state, issue, running) {
|
|
|
7009
7588
|
} catch (error) {
|
|
7010
7589
|
issue.attempts += 1;
|
|
7011
7590
|
issue.lastError = String(error);
|
|
7591
|
+
issue.lastFailedPhase = issue.lastFailedPhase ?? "execute";
|
|
7012
7592
|
if (issue.attempts >= issue.maxAttempts) {
|
|
7013
7593
|
issue.cancelledReason = `Max attempts reached (${issue.attempts}/${issue.maxAttempts}): unexpected failure \u2014 ${issue.lastError?.slice(0, 120) ?? "unknown error"}.`;
|
|
7014
7594
|
await transitionIssueCommand({ issue, target: "Cancelled", note: `Issue failed unexpectedly: ${issue.lastError}` }, container);
|
|
@@ -7021,18 +7601,25 @@ async function runIssueOnce(state, issue, running) {
|
|
|
7021
7601
|
logger.info({ issueId: issue.id, identifier: issue.identifier, finalState: issue.state, elapsedMs, attempts: issue.attempts }, "[Agent] Issue execution finished");
|
|
7022
7602
|
issue.updatedAt = now();
|
|
7023
7603
|
container.issueRepository.markDirty(issue.id);
|
|
7024
|
-
state.metrics.activeWorkers = Math.max(state.metrics.activeWorkers - 1, 0);
|
|
7025
7604
|
running.delete(issue.id);
|
|
7026
7605
|
state.metrics = computeMetrics(state.issues);
|
|
7027
|
-
state.metrics.activeWorkers = Math.max(state.metrics.activeWorkers, 0);
|
|
7028
7606
|
state.updatedAt = now();
|
|
7029
7607
|
await container.persistencePort.persistState(state);
|
|
7030
7608
|
}
|
|
7031
7609
|
}
|
|
7032
7610
|
|
|
7033
7611
|
export {
|
|
7612
|
+
addTokenUsage,
|
|
7613
|
+
extractTokenUsage,
|
|
7614
|
+
tryParseJsonOutput,
|
|
7615
|
+
readAgentDirective,
|
|
7616
|
+
readAgentPid,
|
|
7617
|
+
isProcessAlive,
|
|
7034
7618
|
isAgentStillRunning,
|
|
7035
7619
|
cleanStalePidFile,
|
|
7620
|
+
loadAgentPipelineState,
|
|
7621
|
+
loadAgentPipelineSnapshotForIssue,
|
|
7622
|
+
loadAgentSessionSnapshotsForIssue,
|
|
7036
7623
|
hydrate,
|
|
7037
7624
|
recoverPlanningSession,
|
|
7038
7625
|
loadRuntimeSettings,
|
|
@@ -7042,15 +7629,27 @@ export {
|
|
|
7042
7629
|
createContainer,
|
|
7043
7630
|
runPlanningJob,
|
|
7044
7631
|
runIssueOnce,
|
|
7632
|
+
isShuttingDown,
|
|
7633
|
+
installGracefulShutdown,
|
|
7634
|
+
analyzeParallelizability,
|
|
7635
|
+
ensureNotStale,
|
|
7636
|
+
hasTerminalQueue,
|
|
7045
7637
|
deriveConfig,
|
|
7046
7638
|
applyWorkflowConfig,
|
|
7047
7639
|
validateConfig,
|
|
7640
|
+
normalizeIssue,
|
|
7641
|
+
nextLocalIssueId,
|
|
7642
|
+
createIssueFromPayload,
|
|
7643
|
+
dedupHistoryEntries,
|
|
7048
7644
|
buildRuntimeState,
|
|
7049
7645
|
addEvent,
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7646
|
+
transitionIssue,
|
|
7647
|
+
issueDependenciesResolved,
|
|
7648
|
+
getNextRetryAt,
|
|
7649
|
+
handleStatePatch,
|
|
7650
|
+
runAgentSession,
|
|
7651
|
+
runAgentPipeline,
|
|
7652
|
+
issueHasResumableSession,
|
|
7054
7653
|
startApiServer,
|
|
7055
7654
|
getStateDb,
|
|
7056
7655
|
getIssueStateResource,
|
|
@@ -7079,4 +7678,4 @@ export {
|
|
|
7079
7678
|
syncReferenceRepositories,
|
|
7080
7679
|
importReferenceArtifacts
|
|
7081
7680
|
};
|
|
7082
|
-
//# sourceMappingURL=chunk-
|
|
7681
|
+
//# sourceMappingURL=chunk-3FCJI2GK.js.map
|