@ipul-labs/forge 1.13.0
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 +273 -0
- package/bin/README.md +210 -0
- package/bin/cli/analyze.js +516 -0
- package/bin/cli/apply.js +264 -0
- package/bin/cli/cifix.js +300 -0
- package/bin/cli/close.js +125 -0
- package/bin/cli/doctor.js +161 -0
- package/bin/cli/init.js +306 -0
- package/bin/cli/ink.js +57 -0
- package/bin/cli/issue.js +116 -0
- package/bin/cli/issues.js +155 -0
- package/bin/cli/merge.js +175 -0
- package/bin/cli/refine.js +253 -0
- package/bin/cli/repos.js +91 -0
- package/bin/cli/resolution.js +126 -0
- package/bin/cli/session.js +230 -0
- package/bin/cli/sessionStore.js +214 -0
- package/bin/cli/ship.js +762 -0
- package/bin/cli/utils.js +145 -0
- package/bin/cli/validate.js +280 -0
- package/bin/cli/watch.js +274 -0
- package/bin/forge-agent.js +174 -0
- package/dist/assets/index-CNTCMbW9.js +80 -0
- package/dist/assets/index-InpYS-zg.css +1 -0
- package/dist/favicon.svg +8 -0
- package/dist/index.html +19 -0
- package/dist-lib/favicon.svg +8 -0
- package/dist-lib/index.d.ts +1 -0
- package/dist-lib/style.css +1 -0
- package/dist-lib/ui.d.ts +2 -0
- package/dist-lib/ui.js +7390 -0
- package/dist-lib/views/Forge/ForgeDashboard.d.ts +5 -0
- package/dist-lib/views/Forge/activeJobs.d.ts +29 -0
- package/dist-lib/views/Forge/analysisCache.d.ts +64 -0
- package/dist-lib/views/Forge/api.d.ts +4 -0
- package/dist-lib/views/Forge/components/AnalysisProgress.d.ts +13 -0
- package/dist-lib/views/Forge/components/CITerminal.d.ts +44 -0
- package/dist-lib/views/Forge/components/DiffViewer.d.ts +37 -0
- package/dist-lib/views/Forge/components/ForgeConnect.d.ts +6 -0
- package/dist-lib/views/Forge/components/IssueList.d.ts +28 -0
- package/dist-lib/views/Forge/components/MRStatus.d.ts +9 -0
- package/dist-lib/views/Forge/components/NewTaskModal.d.ts +1 -0
- package/dist-lib/views/Forge/components/OpenMRList.d.ts +11 -0
- package/dist-lib/views/Forge/components/PipelineStatus.d.ts +12 -0
- package/dist-lib/views/Forge/components/PipelinesList.d.ts +9 -0
- package/dist-lib/views/Forge/components/RolloutStatus.d.ts +5 -0
- package/dist-lib/views/Forge/components/UsageWidget.d.ts +8 -0
- package/dist-lib/views/Forge/components/ValidationPanel.d.ts +22 -0
- package/dist-lib/views/Forge/context/ForgeProvider.d.ts +23 -0
- package/dist-lib/views/Forge/hooks/useConfig.d.ts +54 -0
- package/dist-lib/views/Forge/hooks/useForgeDashboard.d.ts +107 -0
- package/dist-lib/views/Forge/hooks/useForgeQuota.d.ts +22 -0
- package/dist-lib/views/Forge/phases/AnalyzingView.d.ts +1 -0
- package/dist-lib/views/Forge/phases/IdleView.d.ts +1 -0
- package/dist-lib/views/Forge/phases/MonitoringIssueTab.d.ts +1 -0
- package/dist-lib/views/Forge/phases/MonitoringPipelineTab.d.ts +1 -0
- package/dist-lib/views/Forge/phases/MonitoringView.d.ts +1 -0
- package/dist-lib/views/Forge/phases/ReviewingView.d.ts +1 -0
- package/dist-lib/views/Forge/phases/SelectedIssueTab.d.ts +11 -0
- package/dist-lib/views/Forge/phases/SelectedMRTab.d.ts +1 -0
- package/dist-lib/views/Forge/phases/SelectedView.d.ts +1 -0
- package/dist-lib/views/Forge/phases/shared/CloseIssueConfirm.d.ts +1 -0
- package/dist-lib/views/Forge/phases/shared/DetailTabs.d.ts +14 -0
- package/dist-lib/views/Forge/phases/shared/EnvironmentsPanel.d.ts +9 -0
- package/dist-lib/views/Forge/phases/shared/ForgeActivity.d.ts +1 -0
- package/dist-lib/views/Forge/phases/shared/IssueHeaderBlock.d.ts +13 -0
- package/dist-lib/views/Forge/phases/shared/MergedMRList.d.ts +4 -0
- package/dist-lib/views/Forge/phases/shared/PreviousAnalyses.d.ts +1 -0
- package/dist-lib/views/Forge/phases/shared/ResolutionCheckPanel.d.ts +6 -0
- package/dist-lib/views/Forge/settings/ConnectionRow.d.ts +16 -0
- package/dist-lib/views/Forge/settings/ConnectionsSection.d.ts +7 -0
- package/dist-lib/views/Forge/settings/DefaultsSection.d.ts +7 -0
- package/dist-lib/views/Forge/settings/ReposSection.d.ts +7 -0
- package/dist-lib/views/Forge/settings/SettingsView.d.ts +1 -0
- package/dist-lib/views/Forge/shell/ConfigBar.d.ts +1 -0
- package/dist-lib/views/Forge/shell/ForgeRouter.d.ts +9 -0
- package/dist-lib/views/Forge/shell/ForgeShell.d.ts +7 -0
- package/dist-lib/views/Forge/shell/Sidebar.d.ts +1 -0
- package/dist-lib/views/Forge/shell/SidebarFooterMenu.d.ts +1 -0
- package/dist-lib/views/Forge/shell/SidebarNav.d.ts +1 -0
- package/dist-lib/views/Forge/shell/SidebarNavItem.d.ts +10 -0
- package/dist-lib/views/Forge/shell/TopBar.d.ts +5 -0
- package/dist-lib/views/Forge/types.d.ts +235 -0
- package/dist-lib/views/Forge/utils.d.ts +11 -0
- package/package.json +88 -0
- package/server/forge/core/ForgeAgent.js +1144 -0
- package/server/forge/core/agentClient.js +36 -0
- package/server/forge/core/branchName.js +41 -0
- package/server/forge/core/claudeClient.js +356 -0
- package/server/forge/core/configStore.js +214 -0
- package/server/forge/core/gitlabClient.js +340 -0
- package/server/forge/core/jobs.js +180 -0
- package/server/forge/core/k8s.js +54 -0
- package/server/forge/core/localRepo.js +136 -0
- package/server/forge/core/openclawClient.js +125 -0
- package/server/forge/core/prompts.js +369 -0
- package/server/forge/core/providers/azuredevops.js +408 -0
- package/server/forge/core/providers/base.js +208 -0
- package/server/forge/core/providers/github.js +349 -0
- package/server/forge/core/providers/gitlab.js +529 -0
- package/server/forge/core/providers/index.js +47 -0
- package/server/forge/core/quotaClient.js +101 -0
- package/server/forge/core/validator/attribute.js +84 -0
- package/server/forge/core/validator/clone.js +91 -0
- package/server/forge/core/validator/cmd.js +98 -0
- package/server/forge/core/validator/fs.js +9 -0
- package/server/forge/core/validator/overlay.js +52 -0
- package/server/forge/core/validator/projectType.js +87 -0
- package/server/forge/core/validator/runners/go.js +107 -0
- package/server/forge/core/validator/runners/node.js +84 -0
- package/server/forge/core/validator/tdd.js +105 -0
- package/server/forge/core/validator.js +166 -0
- package/server/forge/routes.js +1012 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import Spinner from "ink-spinner";
|
|
5
|
+
import { createPatch } from "diff";
|
|
6
|
+
import { h, Box, Text, useApp, runApp } from "./ink.js";
|
|
7
|
+
import {
|
|
8
|
+
projectRoot,
|
|
9
|
+
getFlag,
|
|
10
|
+
resolveRepoSet,
|
|
11
|
+
prefixDivergence,
|
|
12
|
+
} from "./utils.js";
|
|
13
|
+
import {
|
|
14
|
+
PHASES,
|
|
15
|
+
appendEvent,
|
|
16
|
+
setHead,
|
|
17
|
+
updateState,
|
|
18
|
+
writeAnalysis,
|
|
19
|
+
} from "./sessionStore.js";
|
|
20
|
+
|
|
21
|
+
const DIFF_MAX_LINES = 60;
|
|
22
|
+
|
|
23
|
+
function StepRow({ step, message, status }) {
|
|
24
|
+
let glyph;
|
|
25
|
+
if (status === "running")
|
|
26
|
+
glyph = h`<${Text} color="cyan"><${Spinner} type="dots"/></>`;
|
|
27
|
+
else if (status === "done") glyph = h`<${Text} color="green">✓</>`;
|
|
28
|
+
else glyph = h`<${Text} color="red">✘</>`;
|
|
29
|
+
return h`<${Box}>
|
|
30
|
+
${glyph}
|
|
31
|
+
<${Text} dimColor> [${step}]</>
|
|
32
|
+
<${Text}> ${message}</>
|
|
33
|
+
</>`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ItemRow({ item }) {
|
|
37
|
+
if (item.kind === "step") return h`<${StepRow} ...${item} />`;
|
|
38
|
+
if (item.kind === "log")
|
|
39
|
+
return h`<${Box}><${Text} dimColor> ${item.text}</></>`;
|
|
40
|
+
if (item.kind === "usage") {
|
|
41
|
+
const cost = typeof item.cost === "number" ? `$${item.cost.toFixed(4)}` : "$?";
|
|
42
|
+
return h`<${Box}>
|
|
43
|
+
<${Text} dimColor> ${item.inputTokens}in / ${item.outputTokens}out tokens, ${cost}</>
|
|
44
|
+
</>`;
|
|
45
|
+
}
|
|
46
|
+
if (item.kind === "error")
|
|
47
|
+
return h`<${Box}><${Text} color="red">ERROR</><${Text}>: ${item.message}</></>`;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function colorForDiffLine(line) {
|
|
52
|
+
if (line.startsWith("+++") || line.startsWith("---")) return "white";
|
|
53
|
+
if (line.startsWith("@@")) return "cyan";
|
|
54
|
+
if (line.startsWith("+")) return "green";
|
|
55
|
+
if (line.startsWith("-")) return "red";
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function DiffBlock({ original, proposed, file }) {
|
|
60
|
+
const patch = createPatch(file, original ?? "", proposed ?? "", "current", "proposed");
|
|
61
|
+
const lines = patch.split("\n").slice(2);
|
|
62
|
+
const truncated = lines.length > DIFF_MAX_LINES;
|
|
63
|
+
const shown = truncated ? lines.slice(0, DIFF_MAX_LINES) : lines;
|
|
64
|
+
return h`<${Box} flexDirection="column" marginLeft=${4}>
|
|
65
|
+
${shown.map((line, i) => {
|
|
66
|
+
const color = colorForDiffLine(line);
|
|
67
|
+
return h`<${Text} key=${i} color=${color} dimColor=${color === undefined}>${line || " "}</>`;
|
|
68
|
+
})}
|
|
69
|
+
${truncated
|
|
70
|
+
? h`<${Text} dimColor>${` … ${lines.length - DIFF_MAX_LINES} more diff lines (use --json to see full payload)`}</>`
|
|
71
|
+
: null}
|
|
72
|
+
</>`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function ChangeBlock({ change, index }) {
|
|
76
|
+
const isCreate = change.action === "create";
|
|
77
|
+
const isDelete = change.action === "delete";
|
|
78
|
+
const showDiff = !isDelete; // create: diff vs empty; delete: skip
|
|
79
|
+
const header = ` ${index + 1}. ${change.action.toUpperCase()} ${change.repo}:${change.file}`;
|
|
80
|
+
return h`<${Box} flexDirection="column" marginTop=${1}>
|
|
81
|
+
<${Text} color="yellow">${header}</>
|
|
82
|
+
${change.summary
|
|
83
|
+
? h`<${Box} marginLeft=${5}><${Text} dimColor>${change.summary}</></>`
|
|
84
|
+
: null}
|
|
85
|
+
${showDiff
|
|
86
|
+
? h`<${DiffBlock} file=${change.file} original=${isCreate ? "" : change.original ?? ""} proposed=${change.proposed ?? ""} />`
|
|
87
|
+
: null}
|
|
88
|
+
</>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function DivergenceWarning({ divergence }) {
|
|
92
|
+
if (!divergence) return null;
|
|
93
|
+
const text = `⚠ Issue prefix "${divergence.issue}:" but proposed MR prefix "${divergence.mr}:" — model may have misinterpreted the issue.`;
|
|
94
|
+
return h`<${Box} marginTop=${1}>
|
|
95
|
+
<${Text} color="yellow">${text}</>
|
|
96
|
+
</>`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function ResultBlock({ result }) {
|
|
100
|
+
const changes = result.changes ?? [];
|
|
101
|
+
const issueTitle = result.issue?.title ?? "";
|
|
102
|
+
const divergence = prefixDivergence(issueTitle, result.mrTitle);
|
|
103
|
+
return h`<${Box} flexDirection="column" marginTop=${1}>
|
|
104
|
+
<${Text} dimColor>${"────────────── result ──────────────"}</>
|
|
105
|
+
<${Box}>
|
|
106
|
+
<${Text} color="yellow">Issue: </>
|
|
107
|
+
<${Text}>#${result.issue?.iid}: ${issueTitle}</>
|
|
108
|
+
</>
|
|
109
|
+
${result.mrTitle
|
|
110
|
+
? h`<${Box}>
|
|
111
|
+
<${Text} color="yellow">MR: </>
|
|
112
|
+
<${Text}>${result.mrTitle}</>
|
|
113
|
+
</>`
|
|
114
|
+
: null}
|
|
115
|
+
${result.repos?.length
|
|
116
|
+
? h`<${Box}>
|
|
117
|
+
<${Text} color="yellow">Repos: </>
|
|
118
|
+
<${Text}>${result.repos.join(", ")}</>
|
|
119
|
+
</>`
|
|
120
|
+
: null}
|
|
121
|
+
<${DivergenceWarning} divergence=${divergence} />
|
|
122
|
+
${result.explanation
|
|
123
|
+
? h`<${Box} flexDirection="column" marginTop=${1}>
|
|
124
|
+
<${Text} dimColor>Explanation</>
|
|
125
|
+
<${Text}>${result.explanation}</>
|
|
126
|
+
</>`
|
|
127
|
+
: null}
|
|
128
|
+
<${Box} marginTop=${1}>
|
|
129
|
+
<${Text} dimColor>${`Changes (${changes.length}):`}</>
|
|
130
|
+
</>
|
|
131
|
+
${changes.length === 0
|
|
132
|
+
? h`<${Box} marginLeft=${2}><${Text} dimColor>(none — model proposed no changes)</></>`
|
|
133
|
+
: changes.map((c, i) => h`<${ChangeBlock} key=${i} change=${c} index=${i} />`)}
|
|
134
|
+
</>`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function AnalyzeView({
|
|
138
|
+
iid,
|
|
139
|
+
primary,
|
|
140
|
+
secondaries,
|
|
141
|
+
model,
|
|
142
|
+
agentKind,
|
|
143
|
+
userPrompt,
|
|
144
|
+
eventSink,
|
|
145
|
+
onDone,
|
|
146
|
+
}) {
|
|
147
|
+
const { exit } = useApp();
|
|
148
|
+
const [items, setItems] = useState([]);
|
|
149
|
+
const [result, setResult] = useState(null);
|
|
150
|
+
const [finished, setFinished] = useState(null);
|
|
151
|
+
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (!finished) return;
|
|
154
|
+
onDone(finished);
|
|
155
|
+
exit();
|
|
156
|
+
}, [finished]);
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
let cancelled = false;
|
|
160
|
+
(async () => {
|
|
161
|
+
try {
|
|
162
|
+
const { ForgeAgent } = await import(
|
|
163
|
+
path.join(projectRoot, "server/forge/core/ForgeAgent.js")
|
|
164
|
+
);
|
|
165
|
+
const agent = new ForgeAgent({
|
|
166
|
+
project: primary,
|
|
167
|
+
secondaryProjects: secondaries,
|
|
168
|
+
agent: agentKind,
|
|
169
|
+
});
|
|
170
|
+
let final = null;
|
|
171
|
+
|
|
172
|
+
const onEvent = (evt) => {
|
|
173
|
+
if (cancelled) return;
|
|
174
|
+
eventSink?.(evt);
|
|
175
|
+
if (evt.type === "result") {
|
|
176
|
+
final = evt.data;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
setItems((prev) => {
|
|
180
|
+
const closeRunning = (list) =>
|
|
181
|
+
list.map((it) =>
|
|
182
|
+
it.kind === "step" && it.status === "running"
|
|
183
|
+
? { ...it, status: "done" }
|
|
184
|
+
: it,
|
|
185
|
+
);
|
|
186
|
+
switch (evt.type) {
|
|
187
|
+
case "progress":
|
|
188
|
+
return [
|
|
189
|
+
...closeRunning(prev),
|
|
190
|
+
{
|
|
191
|
+
kind: "step",
|
|
192
|
+
step: evt.step,
|
|
193
|
+
message: evt.message,
|
|
194
|
+
status: "running",
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
case "log":
|
|
198
|
+
return [...prev, { kind: "log", text: evt.message }];
|
|
199
|
+
case "usage":
|
|
200
|
+
return [
|
|
201
|
+
...prev,
|
|
202
|
+
{
|
|
203
|
+
kind: "usage",
|
|
204
|
+
inputTokens: evt.inputTokens,
|
|
205
|
+
outputTokens: evt.outputTokens,
|
|
206
|
+
cost: evt.cost,
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
case "error":
|
|
210
|
+
return [
|
|
211
|
+
...prev.map((it) =>
|
|
212
|
+
it.kind === "step" && it.status === "running"
|
|
213
|
+
? { ...it, status: "failed" }
|
|
214
|
+
: it,
|
|
215
|
+
),
|
|
216
|
+
{ kind: "error", message: evt.message },
|
|
217
|
+
];
|
|
218
|
+
default:
|
|
219
|
+
return prev;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
await agent.analyzeIssue(iid, onEvent, model, undefined, userPrompt, "fresh");
|
|
225
|
+
if (cancelled) return;
|
|
226
|
+
|
|
227
|
+
setItems((prev) =>
|
|
228
|
+
prev.map((it) =>
|
|
229
|
+
it.kind === "step" && it.status === "running"
|
|
230
|
+
? { ...it, status: "done" }
|
|
231
|
+
: it,
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
if (final) setResult(final);
|
|
235
|
+
setFinished({ ok: true, result: final });
|
|
236
|
+
} catch (err) {
|
|
237
|
+
if (cancelled) return;
|
|
238
|
+
setItems((prev) => [
|
|
239
|
+
...prev.map((it) =>
|
|
240
|
+
it.kind === "step" && it.status === "running"
|
|
241
|
+
? { ...it, status: "failed" }
|
|
242
|
+
: it,
|
|
243
|
+
),
|
|
244
|
+
{ kind: "error", message: err.message || String(err) },
|
|
245
|
+
]);
|
|
246
|
+
setFinished({ ok: false, error: err });
|
|
247
|
+
}
|
|
248
|
+
})();
|
|
249
|
+
return () => {
|
|
250
|
+
cancelled = true;
|
|
251
|
+
};
|
|
252
|
+
}, []);
|
|
253
|
+
|
|
254
|
+
return h`<${Box} flexDirection="column">
|
|
255
|
+
${items.map((it, i) => h`<${ItemRow} key=${i} item=${it} />`)}
|
|
256
|
+
${result ? h`<${ResultBlock} result=${result} />` : null}
|
|
257
|
+
</>`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Render a cached analysis (read from session) through the same ResultBlock
|
|
262
|
+
* the live analyze view uses. No streaming, no progress — just the final
|
|
263
|
+
* payload. Used by `forge-agent show`.
|
|
264
|
+
*/
|
|
265
|
+
export async function renderCachedResult(result) {
|
|
266
|
+
await runApp(
|
|
267
|
+
h`<${Box} flexDirection="column">
|
|
268
|
+
<${ResultBlock} result=${result} />
|
|
269
|
+
</>`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Persist analyze outcome to ~/.forge/sessions/<repo>/<iid>/. Writes
|
|
275
|
+
* analysis.json (if a result is present), state.json with the new phase,
|
|
276
|
+
* events.ndjson with the full event buffer, and bumps HEAD so the next
|
|
277
|
+
* <iid>-less command resumes here. Errors during persistence are
|
|
278
|
+
* surfaced on stderr but never abort the caller.
|
|
279
|
+
*/
|
|
280
|
+
async function persistAnalyzeOutcome({ repo, iid, primary, secondaries, result, events, error }) {
|
|
281
|
+
if (!repo || !iid) return;
|
|
282
|
+
try {
|
|
283
|
+
if (result) await writeAnalysis(repo, iid, result);
|
|
284
|
+
await updateState(repo, iid, {
|
|
285
|
+
repo,
|
|
286
|
+
iid: Number(iid),
|
|
287
|
+
primary,
|
|
288
|
+
secondaries,
|
|
289
|
+
phase: result ? PHASES.ANALYZED : "analyze-failed",
|
|
290
|
+
lastError: error?.message ?? null,
|
|
291
|
+
});
|
|
292
|
+
await appendEvent(repo, iid, { type: "session:analyze-start" });
|
|
293
|
+
for (const evt of events) await appendEvent(repo, iid, evt);
|
|
294
|
+
await appendEvent(repo, iid, {
|
|
295
|
+
type: "session:analyze-end",
|
|
296
|
+
ok: !error,
|
|
297
|
+
error: error?.message,
|
|
298
|
+
});
|
|
299
|
+
await setHead(repo, iid);
|
|
300
|
+
} catch (err) {
|
|
301
|
+
process.stderr.write(`forge: failed to persist session: ${err.message}\n`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Render a saved NDJSON event stream through the same Ink view, without
|
|
307
|
+
* re-running the agent. Useful for verifying renderer changes against a
|
|
308
|
+
* known-good payload, and for users who want to re-inspect a prior result.
|
|
309
|
+
*/
|
|
310
|
+
async function cmdAnalyzeReplay(replayPath) {
|
|
311
|
+
const raw = await fs.readFile(replayPath, "utf8");
|
|
312
|
+
const events = raw
|
|
313
|
+
.split("\n")
|
|
314
|
+
.filter(Boolean)
|
|
315
|
+
.map((line) => {
|
|
316
|
+
try {
|
|
317
|
+
return JSON.parse(line);
|
|
318
|
+
} catch {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
.filter(Boolean);
|
|
323
|
+
|
|
324
|
+
let outcome = { ok: true };
|
|
325
|
+
await runApp(
|
|
326
|
+
h`<${ReplayView} events=${events} onDone=${(o) => (outcome = o)} />`,
|
|
327
|
+
);
|
|
328
|
+
if (!outcome.ok) process.exit(2);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function ReplayView({ events, onDone }) {
|
|
332
|
+
const { exit } = useApp();
|
|
333
|
+
const [items, setItems] = useState([]);
|
|
334
|
+
const [result, setResult] = useState(null);
|
|
335
|
+
const [finished, setFinished] = useState(null);
|
|
336
|
+
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
if (!finished) return;
|
|
339
|
+
onDone(finished);
|
|
340
|
+
exit();
|
|
341
|
+
}, [finished]);
|
|
342
|
+
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
let final = null;
|
|
345
|
+
const next = [];
|
|
346
|
+
const closeRunning = (list) =>
|
|
347
|
+
list.map((it) =>
|
|
348
|
+
it.kind === "step" && it.status === "running" ? { ...it, status: "done" } : it,
|
|
349
|
+
);
|
|
350
|
+
for (const evt of events) {
|
|
351
|
+
if (evt.type === "result") {
|
|
352
|
+
final = evt.data;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
switch (evt.type) {
|
|
356
|
+
case "progress": {
|
|
357
|
+
const closed = closeRunning(next);
|
|
358
|
+
next.length = 0;
|
|
359
|
+
next.push(...closed, {
|
|
360
|
+
kind: "step",
|
|
361
|
+
step: evt.step,
|
|
362
|
+
message: evt.message,
|
|
363
|
+
status: "running",
|
|
364
|
+
});
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
case "log":
|
|
368
|
+
next.push({ kind: "log", text: evt.message });
|
|
369
|
+
break;
|
|
370
|
+
case "usage":
|
|
371
|
+
next.push({
|
|
372
|
+
kind: "usage",
|
|
373
|
+
inputTokens: evt.inputTokens,
|
|
374
|
+
outputTokens: evt.outputTokens,
|
|
375
|
+
cost: evt.cost,
|
|
376
|
+
});
|
|
377
|
+
break;
|
|
378
|
+
case "error": {
|
|
379
|
+
const closed = next.map((it) =>
|
|
380
|
+
it.kind === "step" && it.status === "running"
|
|
381
|
+
? { ...it, status: "failed" }
|
|
382
|
+
: it,
|
|
383
|
+
);
|
|
384
|
+
next.length = 0;
|
|
385
|
+
next.push(...closed, { kind: "error", message: evt.message });
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
default:
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
setItems(closeRunning(next));
|
|
392
|
+
if (final) setResult(final);
|
|
393
|
+
setFinished({ ok: true });
|
|
394
|
+
}, []);
|
|
395
|
+
|
|
396
|
+
return h`<${Box} flexDirection="column">
|
|
397
|
+
${items.map((it, i) => h`<${ItemRow} key=${i} item=${it} />`)}
|
|
398
|
+
${result ? h`<${ResultBlock} result=${result} />` : null}
|
|
399
|
+
</>`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function cmdAnalyze(argv) {
|
|
403
|
+
const replayPath = getFlag(argv, "--replay");
|
|
404
|
+
if (replayPath) return cmdAnalyzeReplay(replayPath);
|
|
405
|
+
|
|
406
|
+
const iid = Number(argv[1]);
|
|
407
|
+
if (!Number.isInteger(iid) || iid <= 0) {
|
|
408
|
+
console.error("Usage: forge-agent analyze <iid> [options]");
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const { primary, secondaries } = await resolveRepoSet(argv);
|
|
413
|
+
if (!primary) {
|
|
414
|
+
console.error(
|
|
415
|
+
"No repo. Pass --repo <group/repo>, set FORGE_GITLAB_PROJECT, or run `forge-agent init`.",
|
|
416
|
+
);
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const model = getFlag(argv, "--model") || undefined;
|
|
421
|
+
const agentKind = getFlag(argv, "--agent") || "claude";
|
|
422
|
+
const userPrompt = getFlag(argv, "--prompt") || undefined;
|
|
423
|
+
const json = argv.includes("--json");
|
|
424
|
+
const quiet = argv.includes("--quiet");
|
|
425
|
+
|
|
426
|
+
const { ForgeAgent } = await import(
|
|
427
|
+
path.join(projectRoot, "server/forge/core/ForgeAgent.js")
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
if (json || quiet) {
|
|
431
|
+
const agent = new ForgeAgent({
|
|
432
|
+
project: primary,
|
|
433
|
+
secondaryProjects: secondaries,
|
|
434
|
+
agent: agentKind,
|
|
435
|
+
});
|
|
436
|
+
let finalResult = null;
|
|
437
|
+
const events = [];
|
|
438
|
+
const onEvent = (evt) => {
|
|
439
|
+
if (evt.type === "result") finalResult = evt.data;
|
|
440
|
+
events.push(evt);
|
|
441
|
+
if (json) process.stdout.write(`${JSON.stringify(evt)}\n`);
|
|
442
|
+
};
|
|
443
|
+
try {
|
|
444
|
+
await agent.analyzeIssue(iid, onEvent, model, undefined, userPrompt, "fresh");
|
|
445
|
+
} catch (err) {
|
|
446
|
+
if (json) {
|
|
447
|
+
process.stdout.write(
|
|
448
|
+
`${JSON.stringify({ type: "error", message: err.message })}\n`,
|
|
449
|
+
);
|
|
450
|
+
} else {
|
|
451
|
+
process.stderr.write(`ERROR: ${err.message}\n`);
|
|
452
|
+
}
|
|
453
|
+
await persistAnalyzeOutcome({
|
|
454
|
+
repo: primary,
|
|
455
|
+
iid,
|
|
456
|
+
primary,
|
|
457
|
+
secondaries,
|
|
458
|
+
result: null,
|
|
459
|
+
events,
|
|
460
|
+
error: err,
|
|
461
|
+
});
|
|
462
|
+
process.exit(2);
|
|
463
|
+
}
|
|
464
|
+
if (!finalResult) {
|
|
465
|
+
if (!json) console.error("Analysis produced no result.");
|
|
466
|
+
await persistAnalyzeOutcome({
|
|
467
|
+
repo: primary,
|
|
468
|
+
iid,
|
|
469
|
+
primary,
|
|
470
|
+
secondaries,
|
|
471
|
+
result: null,
|
|
472
|
+
events,
|
|
473
|
+
error: new Error("no result"),
|
|
474
|
+
});
|
|
475
|
+
process.exit(2);
|
|
476
|
+
}
|
|
477
|
+
await persistAnalyzeOutcome({
|
|
478
|
+
repo: primary,
|
|
479
|
+
iid,
|
|
480
|
+
primary,
|
|
481
|
+
secondaries,
|
|
482
|
+
result: finalResult,
|
|
483
|
+
events,
|
|
484
|
+
});
|
|
485
|
+
if (quiet && !json) console.log(JSON.stringify(finalResult, null, 2));
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
let outcome = { ok: true };
|
|
490
|
+
const events = [];
|
|
491
|
+
await runApp(
|
|
492
|
+
h`<${AnalyzeView}
|
|
493
|
+
iid=${iid}
|
|
494
|
+
primary=${primary}
|
|
495
|
+
secondaries=${secondaries}
|
|
496
|
+
model=${model}
|
|
497
|
+
agentKind=${agentKind}
|
|
498
|
+
userPrompt=${userPrompt}
|
|
499
|
+
eventSink=${(e) => events.push(e)}
|
|
500
|
+
onDone=${(o) => (outcome = o)} />`,
|
|
501
|
+
);
|
|
502
|
+
await persistAnalyzeOutcome({
|
|
503
|
+
repo: primary,
|
|
504
|
+
iid,
|
|
505
|
+
primary,
|
|
506
|
+
secondaries,
|
|
507
|
+
result: outcome.result ?? null,
|
|
508
|
+
events,
|
|
509
|
+
error: outcome.error,
|
|
510
|
+
});
|
|
511
|
+
if (!outcome.ok) process.exit(2);
|
|
512
|
+
if (!outcome.result) {
|
|
513
|
+
console.error("Analysis produced no result.");
|
|
514
|
+
process.exit(2);
|
|
515
|
+
}
|
|
516
|
+
}
|