@ipul-labs/forge 1.16.8 → 1.17.1
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 +39 -21
- package/lib/bin/cli/analyze.d.ts +9 -0
- package/lib/bin/cli/analyze.d.ts.map +1 -0
- package/lib/bin/cli/analyze.js +491 -0
- package/lib/bin/cli/analyze.js.map +1 -0
- package/lib/bin/cli/apply.d.ts +2 -0
- package/lib/bin/cli/apply.d.ts.map +1 -0
- package/lib/bin/cli/apply.js +262 -0
- package/lib/bin/cli/apply.js.map +1 -0
- package/lib/bin/cli/cifix.d.ts +2 -0
- package/lib/bin/cli/cifix.d.ts.map +1 -0
- package/lib/bin/cli/cifix.js +310 -0
- package/lib/bin/cli/cifix.js.map +1 -0
- package/lib/bin/cli/close.d.ts +2 -0
- package/lib/bin/cli/close.d.ts.map +1 -0
- package/lib/bin/cli/close.js +113 -0
- package/lib/bin/cli/close.js.map +1 -0
- package/lib/bin/cli/createIssue.d.ts +2 -0
- package/lib/bin/cli/createIssue.d.ts.map +1 -0
- package/lib/bin/cli/createIssue.js +108 -0
- package/lib/bin/cli/createIssue.js.map +1 -0
- package/lib/bin/cli/doctor.d.ts +2 -0
- package/lib/bin/cli/doctor.d.ts.map +1 -0
- package/lib/bin/cli/doctor.js +145 -0
- package/lib/bin/cli/doctor.js.map +1 -0
- package/lib/bin/cli/init.d.ts +4 -0
- package/lib/bin/cli/init.d.ts.map +1 -0
- package/lib/bin/cli/init.js +288 -0
- package/lib/bin/cli/init.js.map +1 -0
- package/lib/bin/cli/ink.d.ts +23 -0
- package/lib/bin/cli/ink.d.ts.map +1 -0
- package/{bin → lib/bin}/cli/ink.js +18 -19
- package/lib/bin/cli/ink.js.map +1 -0
- package/lib/bin/cli/issue.d.ts +2 -0
- package/lib/bin/cli/issue.d.ts.map +1 -0
- package/lib/bin/cli/issue.js +114 -0
- package/lib/bin/cli/issue.js.map +1 -0
- package/lib/bin/cli/issues.d.ts +2 -0
- package/lib/bin/cli/issues.d.ts.map +1 -0
- package/lib/bin/cli/issues.js +149 -0
- package/lib/bin/cli/issues.js.map +1 -0
- package/lib/bin/cli/merge.d.ts +2 -0
- package/lib/bin/cli/merge.d.ts.map +1 -0
- package/lib/bin/cli/merge.js +162 -0
- package/lib/bin/cli/merge.js.map +1 -0
- package/lib/bin/cli/refine.d.ts +2 -0
- package/lib/bin/cli/refine.d.ts.map +1 -0
- package/lib/bin/cli/refine.js +249 -0
- package/lib/bin/cli/refine.js.map +1 -0
- package/lib/bin/cli/repl.d.ts +2 -0
- package/lib/bin/cli/repl.d.ts.map +1 -0
- package/lib/bin/cli/repl.js +309 -0
- package/lib/bin/cli/repl.js.map +1 -0
- package/lib/bin/cli/repos.d.ts +2 -0
- package/lib/bin/cli/repos.d.ts.map +1 -0
- package/lib/bin/cli/repos.js +86 -0
- package/lib/bin/cli/repos.js.map +1 -0
- package/lib/bin/cli/resolution.d.ts +2 -0
- package/lib/bin/cli/resolution.d.ts.map +1 -0
- package/lib/bin/cli/resolution.js +139 -0
- package/lib/bin/cli/resolution.js.map +1 -0
- package/lib/bin/cli/serverClient.d.ts +26 -0
- package/lib/bin/cli/serverClient.d.ts.map +1 -0
- package/lib/bin/cli/serverClient.js +111 -0
- package/lib/bin/cli/serverClient.js.map +1 -0
- package/lib/bin/cli/session.d.ts +8 -0
- package/lib/bin/cli/session.d.ts.map +1 -0
- package/lib/bin/cli/session.js +212 -0
- package/lib/bin/cli/session.js.map +1 -0
- package/lib/bin/cli/sessionStore.d.ts +69 -0
- package/lib/bin/cli/sessionStore.d.ts.map +1 -0
- package/lib/bin/cli/sessionStore.js +196 -0
- package/lib/bin/cli/sessionStore.js.map +1 -0
- package/lib/bin/cli/ship.d.ts +33 -0
- package/lib/bin/cli/ship.d.ts.map +1 -0
- package/lib/bin/cli/ship.js +737 -0
- package/lib/bin/cli/ship.js.map +1 -0
- package/lib/bin/cli/types.d.ts +59 -0
- package/lib/bin/cli/types.d.ts.map +1 -0
- package/lib/bin/cli/types.js +2 -0
- package/lib/bin/cli/types.js.map +1 -0
- package/lib/bin/cli/update.d.ts +2 -0
- package/lib/bin/cli/update.d.ts.map +1 -0
- package/lib/bin/cli/update.js +87 -0
- package/lib/bin/cli/update.js.map +1 -0
- package/lib/bin/cli/utils.d.ts +43 -0
- package/lib/bin/cli/utils.d.ts.map +1 -0
- package/lib/bin/cli/utils.js +181 -0
- package/lib/bin/cli/utils.js.map +1 -0
- package/lib/bin/cli/validate.d.ts +2 -0
- package/lib/bin/cli/validate.d.ts.map +1 -0
- package/lib/bin/cli/validate.js +283 -0
- package/lib/bin/cli/validate.js.map +1 -0
- package/lib/bin/cli/watch.d.ts +5 -0
- package/lib/bin/cli/watch.d.ts.map +1 -0
- package/lib/bin/cli/watch.js +263 -0
- package/lib/bin/cli/watch.js.map +1 -0
- package/lib/bin/forge-agent.d.ts +3 -0
- package/lib/bin/forge-agent.d.ts.map +1 -0
- package/{bin → lib/bin}/forge-agent.js +104 -105
- package/lib/bin/forge-agent.js.map +1 -0
- package/lib/server/forge/core/ForgeAgent.d.ts +260 -0
- package/lib/server/forge/core/ForgeAgent.d.ts.map +1 -0
- package/lib/server/forge/core/ForgeAgent.js +995 -0
- package/lib/server/forge/core/ForgeAgent.js.map +1 -0
- package/lib/server/forge/core/agentClient.d.ts +39 -0
- package/lib/server/forge/core/agentClient.d.ts.map +1 -0
- package/{server → lib/server}/forge/core/agentClient.js +7 -12
- package/lib/server/forge/core/agentClient.js.map +1 -0
- package/lib/server/forge/core/branchName.d.ts +20 -0
- package/lib/server/forge/core/branchName.d.ts.map +1 -0
- package/lib/server/forge/core/branchName.js +39 -0
- package/lib/server/forge/core/branchName.js.map +1 -0
- package/lib/server/forge/core/claudeClient.d.ts +60 -0
- package/lib/server/forge/core/claudeClient.d.ts.map +1 -0
- package/lib/server/forge/core/claudeClient.js +347 -0
- package/lib/server/forge/core/claudeClient.js.map +1 -0
- package/lib/server/forge/core/configStore.d.ts +76 -0
- package/lib/server/forge/core/configStore.d.ts.map +1 -0
- package/lib/server/forge/core/configStore.js +213 -0
- package/lib/server/forge/core/configStore.js.map +1 -0
- package/lib/server/forge/core/gitlabClient.d.ts +144 -0
- package/lib/server/forge/core/gitlabClient.d.ts.map +1 -0
- package/{server → lib/server}/forge/core/gitlabClient.js +98 -180
- package/lib/server/forge/core/gitlabClient.js.map +1 -0
- package/lib/server/forge/core/jobs.d.ts +67 -0
- package/lib/server/forge/core/jobs.d.ts.map +1 -0
- package/lib/server/forge/core/jobs.js +185 -0
- package/lib/server/forge/core/jobs.js.map +1 -0
- package/lib/server/forge/core/k8s.d.ts +18 -0
- package/lib/server/forge/core/k8s.d.ts.map +1 -0
- package/lib/server/forge/core/k8s.js +49 -0
- package/lib/server/forge/core/k8s.js.map +1 -0
- package/lib/server/forge/core/localRepo.d.ts +36 -0
- package/lib/server/forge/core/localRepo.d.ts.map +1 -0
- package/lib/server/forge/core/localRepo.js +165 -0
- package/lib/server/forge/core/localRepo.js.map +1 -0
- package/lib/server/forge/core/openclawClient.d.ts +61 -0
- package/lib/server/forge/core/openclawClient.d.ts.map +1 -0
- package/lib/server/forge/core/openclawClient.js +111 -0
- package/lib/server/forge/core/openclawClient.js.map +1 -0
- package/lib/server/forge/core/prompts.d.ts +96 -0
- package/lib/server/forge/core/prompts.d.ts.map +1 -0
- package/lib/server/forge/core/prompts.js +450 -0
- package/lib/server/forge/core/prompts.js.map +1 -0
- package/lib/server/forge/core/providers/azuredevops.d.ts +134 -0
- package/lib/server/forge/core/providers/azuredevops.d.ts.map +1 -0
- package/lib/server/forge/core/providers/azuredevops.js +360 -0
- package/lib/server/forge/core/providers/azuredevops.js.map +1 -0
- package/lib/server/forge/core/providers/base.d.ts +114 -0
- package/lib/server/forge/core/providers/base.d.ts.map +1 -0
- package/lib/server/forge/core/providers/base.js +98 -0
- package/lib/server/forge/core/providers/base.js.map +1 -0
- package/lib/server/forge/core/providers/github.d.ts +133 -0
- package/lib/server/forge/core/providers/github.d.ts.map +1 -0
- package/lib/server/forge/core/providers/github.js +307 -0
- package/lib/server/forge/core/providers/github.js.map +1 -0
- package/lib/server/forge/core/providers/gitlab.d.ts +144 -0
- package/lib/server/forge/core/providers/gitlab.d.ts.map +1 -0
- package/lib/server/forge/core/providers/gitlab.js +407 -0
- package/lib/server/forge/core/providers/gitlab.js.map +1 -0
- package/lib/server/forge/core/providers/index.d.ts +21 -0
- package/lib/server/forge/core/providers/index.d.ts.map +1 -0
- package/lib/server/forge/core/providers/index.js +43 -0
- package/lib/server/forge/core/providers/index.js.map +1 -0
- package/lib/server/forge/core/providers/types.d.ts +80 -0
- package/lib/server/forge/core/providers/types.d.ts.map +1 -0
- package/lib/server/forge/core/providers/types.js +2 -0
- package/lib/server/forge/core/providers/types.js.map +1 -0
- package/lib/server/forge/core/quotaClient.d.ts +48 -0
- package/lib/server/forge/core/quotaClient.d.ts.map +1 -0
- package/lib/server/forge/core/quotaClient.js +88 -0
- package/lib/server/forge/core/quotaClient.js.map +1 -0
- package/lib/server/forge/core/types.d.ts +160 -0
- package/lib/server/forge/core/types.d.ts.map +1 -0
- package/lib/server/forge/core/types.js +2 -0
- package/lib/server/forge/core/types.js.map +1 -0
- package/lib/server/forge/core/validator/attribute.d.ts +31 -0
- package/lib/server/forge/core/validator/attribute.d.ts.map +1 -0
- package/lib/server/forge/core/validator/attribute.js +82 -0
- package/lib/server/forge/core/validator/attribute.js.map +1 -0
- package/lib/server/forge/core/validator/clone.d.ts +14 -0
- package/lib/server/forge/core/validator/clone.d.ts.map +1 -0
- package/lib/server/forge/core/validator/clone.js +99 -0
- package/lib/server/forge/core/validator/clone.js.map +1 -0
- package/lib/server/forge/core/validator/cmd.d.ts +23 -0
- package/lib/server/forge/core/validator/cmd.d.ts.map +1 -0
- package/lib/server/forge/core/validator/cmd.js +87 -0
- package/lib/server/forge/core/validator/cmd.js.map +1 -0
- package/lib/server/forge/core/validator/fs.d.ts +5 -0
- package/lib/server/forge/core/validator/fs.d.ts.map +1 -0
- package/{server → lib/server}/forge/core/validator/fs.js +8 -3
- package/lib/server/forge/core/validator/fs.js.map +1 -0
- package/lib/server/forge/core/validator/overlay.d.ts +13 -0
- package/lib/server/forge/core/validator/overlay.d.ts.map +1 -0
- package/lib/server/forge/core/validator/overlay.js +53 -0
- package/lib/server/forge/core/validator/overlay.js.map +1 -0
- package/lib/server/forge/core/validator/projectType.d.ts +20 -0
- package/lib/server/forge/core/validator/projectType.d.ts.map +1 -0
- package/lib/server/forge/core/validator/projectType.js +94 -0
- package/lib/server/forge/core/validator/projectType.js.map +1 -0
- package/lib/server/forge/core/validator/runners/go.d.ts +21 -0
- package/lib/server/forge/core/validator/runners/go.d.ts.map +1 -0
- package/lib/server/forge/core/validator/runners/go.js +99 -0
- package/lib/server/forge/core/validator/runners/go.js.map +1 -0
- package/lib/server/forge/core/validator/runners/node.d.ts +12 -0
- package/lib/server/forge/core/validator/runners/node.d.ts.map +1 -0
- package/lib/server/forge/core/validator/runners/node.js +75 -0
- package/lib/server/forge/core/validator/runners/node.js.map +1 -0
- package/lib/server/forge/core/validator/tdd.d.ts +30 -0
- package/lib/server/forge/core/validator/tdd.d.ts.map +1 -0
- package/lib/server/forge/core/validator/tdd.js +96 -0
- package/lib/server/forge/core/validator/tdd.js.map +1 -0
- package/lib/server/forge/core/validator.d.ts +58 -0
- package/lib/server/forge/core/validator.d.ts.map +1 -0
- package/lib/server/forge/core/validator.js +150 -0
- package/lib/server/forge/core/validator.js.map +1 -0
- package/lib/server/forge/routes.d.ts +22 -0
- package/lib/server/forge/routes.d.ts.map +1 -0
- package/lib/server/forge/routes.js +984 -0
- package/lib/server/forge/routes.js.map +1 -0
- package/lib/server.d.ts +2 -0
- package/lib/server.d.ts.map +1 -0
- package/lib/server.js +52 -0
- package/lib/server.js.map +1 -0
- package/lib/shared/events.d.ts +12 -0
- package/lib/shared/events.d.ts.map +1 -0
- package/lib/shared/events.js +12 -0
- package/lib/shared/events.js.map +1 -0
- package/package.json +15 -13
- package/scripts/fix-shebang.cjs +11 -0
- package/bin/cli/analyze.js +0 -516
- package/bin/cli/apply.js +0 -264
- package/bin/cli/cifix.js +0 -300
- package/bin/cli/close.js +0 -125
- package/bin/cli/createIssue.js +0 -112
- package/bin/cli/doctor.js +0 -161
- package/bin/cli/init.js +0 -306
- package/bin/cli/issue.js +0 -116
- package/bin/cli/issues.js +0 -155
- package/bin/cli/merge.js +0 -175
- package/bin/cli/refine.js +0 -253
- package/bin/cli/repl.js +0 -301
- package/bin/cli/repos.js +0 -91
- package/bin/cli/resolution.js +0 -126
- package/bin/cli/session.js +0 -230
- package/bin/cli/sessionStore.js +0 -211
- package/bin/cli/ship.js +0 -762
- package/bin/cli/update.js +0 -95
- package/bin/cli/utils.js +0 -145
- package/bin/cli/validate.js +0 -280
- package/bin/cli/watch.js +0 -274
- package/server/forge/core/ForgeAgent.js +0 -1179
- package/server/forge/core/ForgeAgent.test.js +0 -179
- package/server/forge/core/branchName.js +0 -41
- package/server/forge/core/claudeClient.js +0 -378
- package/server/forge/core/configStore.js +0 -229
- package/server/forge/core/configStore.test.js +0 -89
- package/server/forge/core/jobs.js +0 -180
- package/server/forge/core/k8s.js +0 -54
- package/server/forge/core/localRepo.js +0 -163
- package/server/forge/core/openclawClient.js +0 -125
- package/server/forge/core/prompts.js +0 -483
- package/server/forge/core/prompts.test.js +0 -219
- package/server/forge/core/providers/azuredevops.js +0 -409
- package/server/forge/core/providers/base.js +0 -208
- package/server/forge/core/providers/github.js +0 -348
- package/server/forge/core/providers/gitlab.js +0 -529
- package/server/forge/core/providers/index.js +0 -47
- package/server/forge/core/quotaClient.js +0 -101
- package/server/forge/core/validator/attribute.js +0 -84
- package/server/forge/core/validator/clone.js +0 -116
- package/server/forge/core/validator/cmd.js +0 -98
- package/server/forge/core/validator/overlay.js +0 -52
- package/server/forge/core/validator/projectType.js +0 -87
- package/server/forge/core/validator/runners/go.js +0 -107
- package/server/forge/core/validator/runners/node.js +0 -84
- package/server/forge/core/validator/tdd.js +0 -105
- package/server/forge/core/validator.js +0 -167
- package/server/forge/routes.js +0 -1027
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
|
|
3
|
-
// We test the module-level helpers and ForgeAgent internals by importing the
|
|
4
|
-
// module. The class constructor requires a project, so we mock the provider.
|
|
5
|
-
// For the pure functions (buildCommitSubject, FILE_CAPS), we test them through
|
|
6
|
-
// a thin wrapper or by constructing a ForgeAgent with mocked providers.
|
|
7
|
-
|
|
8
|
-
// ─── Module-level helpers ────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
// buildCommitSubject and FILE_CAPS are not exported, so we test them through
|
|
11
|
-
// ForgeAgent's public surface or by importing the module and inspecting.
|
|
12
|
-
// Since they're internal, we test via the class methods that use them.
|
|
13
|
-
|
|
14
|
-
import * as configStore from "./configStore.js";
|
|
15
|
-
|
|
16
|
-
// Mock configStore.getPrimaryProject for ForgeAgent constructor
|
|
17
|
-
vi.spyOn(configStore, "getPrimaryProject").mockReturnValue("group/web-app");
|
|
18
|
-
vi.spyOn(configStore, "get").mockReturnValue({
|
|
19
|
-
repos: [{ id: "group/web-app", label: "web-app" }],
|
|
20
|
-
defaults: { promptRules: "" },
|
|
21
|
-
connections: {},
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
// Mock the provider factory
|
|
25
|
-
vi.mock("./providers/index.js", () => ({
|
|
26
|
-
createProvider: () => ({
|
|
27
|
-
getIssue: vi.fn(),
|
|
28
|
-
getFileTree: vi.fn().mockResolvedValue([]),
|
|
29
|
-
getFileContent: vi.fn(),
|
|
30
|
-
createBranch: vi.fn(),
|
|
31
|
-
commitFiles: vi.fn(),
|
|
32
|
-
createMR: vi.fn(),
|
|
33
|
-
listAllForgeMRs: vi.fn().mockResolvedValue([]),
|
|
34
|
-
getMRPipelines: vi.fn().mockResolvedValue([]),
|
|
35
|
-
}),
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
// Dynamic import after mocks are set up
|
|
39
|
-
const { ForgeAgent } = await import("./ForgeAgent.js");
|
|
40
|
-
|
|
41
|
-
describe("ForgeAgent", () => {
|
|
42
|
-
describe("_capSelections", () => {
|
|
43
|
-
let agent;
|
|
44
|
-
|
|
45
|
-
beforeEach(() => {
|
|
46
|
-
agent = new ForgeAgent({ project: "group/web-app" });
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("applies fix caps (12 total, 8 per repo)", () => {
|
|
50
|
-
const entries = Array.from({ length: 15 }, (_, i) => ({
|
|
51
|
-
repo: "group/web-app",
|
|
52
|
-
path: `src/file${i}.ts`,
|
|
53
|
-
}));
|
|
54
|
-
|
|
55
|
-
const capped = agent._capSelections(entries, "fix");
|
|
56
|
-
expect(capped).toHaveLength(8);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("applies feature caps (18 total, 15 per repo)", () => {
|
|
60
|
-
const entries = Array.from({ length: 20 }, (_, i) => ({
|
|
61
|
-
repo: "group/web-app",
|
|
62
|
-
path: `src/file${i}.ts`,
|
|
63
|
-
}));
|
|
64
|
-
|
|
65
|
-
const capped = agent._capSelections(entries, "feature");
|
|
66
|
-
expect(capped).toHaveLength(15);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("enforces total cap across multiple repos for features", () => {
|
|
70
|
-
const entries = [
|
|
71
|
-
...Array.from({ length: 12 }, (_, i) => ({
|
|
72
|
-
repo: "group/web-app",
|
|
73
|
-
path: `src/file${i}.ts`,
|
|
74
|
-
})),
|
|
75
|
-
...Array.from({ length: 12 }, (_, i) => ({
|
|
76
|
-
repo: "group/api",
|
|
77
|
-
path: `internal/file${i}.go`,
|
|
78
|
-
})),
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
const capped = agent._capSelections(entries, "feature");
|
|
82
|
-
expect(capped).toHaveLength(18);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("defaults to fix caps for unknown kinds", () => {
|
|
86
|
-
const entries = Array.from({ length: 15 }, (_, i) => ({
|
|
87
|
-
repo: "group/web-app",
|
|
88
|
-
path: `src/file${i}.ts`,
|
|
89
|
-
}));
|
|
90
|
-
|
|
91
|
-
const capped = agent._capSelections(entries, "unknown");
|
|
92
|
-
expect(capped).toHaveLength(8);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("refactor gets same caps as feature", () => {
|
|
96
|
-
const entries = Array.from({ length: 20 }, (_, i) => ({
|
|
97
|
-
repo: "group/web-app",
|
|
98
|
-
path: `src/file${i}.ts`,
|
|
99
|
-
}));
|
|
100
|
-
|
|
101
|
-
const capped = agent._capSelections(entries, "refactor");
|
|
102
|
-
expect(capped).toHaveLength(15);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe("_normalizeFileSelections", () => {
|
|
107
|
-
let agent;
|
|
108
|
-
|
|
109
|
-
beforeEach(() => {
|
|
110
|
-
agent = new ForgeAgent({ project: "group/web-app" });
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("normalizes string entries to primary repo", () => {
|
|
114
|
-
const result = agent._normalizeFileSelections(["src/App.tsx"]);
|
|
115
|
-
expect(result).toEqual([{ repo: "group/web-app", path: "src/App.tsx" }]);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("passes through {repo, path} objects", () => {
|
|
119
|
-
const result = agent._normalizeFileSelections([
|
|
120
|
-
{ repo: "group/api", path: "main.go" },
|
|
121
|
-
]);
|
|
122
|
-
expect(result).toEqual([{ repo: "group/api", path: "main.go" }]);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("defaults repo when only path is present", () => {
|
|
126
|
-
const result = agent._normalizeFileSelections([{ path: "src/a.ts" }]);
|
|
127
|
-
expect(result).toEqual([{ repo: "group/web-app", path: "src/a.ts" }]);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("filters out null and invalid entries", () => {
|
|
131
|
-
const result = agent._normalizeFileSelections([null, undefined, {}, "src/a.ts"]);
|
|
132
|
-
expect(result).toHaveLength(1);
|
|
133
|
-
expect(result[0].path).toBe("src/a.ts");
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
describe("_validateSelections", () => {
|
|
138
|
-
let agent;
|
|
139
|
-
|
|
140
|
-
beforeEach(() => {
|
|
141
|
-
agent = new ForgeAgent({ project: "group/web-app" });
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("keeps files that exist in the tree", () => {
|
|
145
|
-
const entries = [{ repo: "group/web-app", path: "src/App.tsx" }];
|
|
146
|
-
const trees = [{ repo: "group/web-app", files: ["src/App.tsx", "src/index.ts"] }];
|
|
147
|
-
const { valid, skipped } = agent._validateSelections(entries, trees);
|
|
148
|
-
expect(valid).toHaveLength(1);
|
|
149
|
-
expect(skipped).toHaveLength(0);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("skips files not in the tree", () => {
|
|
153
|
-
const entries = [{ repo: "group/web-app", path: "src/Missing.tsx" }];
|
|
154
|
-
const trees = [{ repo: "group/web-app", files: ["src/App.tsx"] }];
|
|
155
|
-
const { valid, skipped } = agent._validateSelections(entries, trees);
|
|
156
|
-
expect(valid).toHaveLength(0);
|
|
157
|
-
expect(skipped).toHaveLength(1);
|
|
158
|
-
expect(skipped[0]).toContain("not in tree");
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("skips files from unknown repos", () => {
|
|
162
|
-
const entries = [{ repo: "group/unknown", path: "src/a.ts" }];
|
|
163
|
-
const trees = [{ repo: "group/web-app", files: ["src/a.ts"] }];
|
|
164
|
-
const { valid, skipped } = agent._validateSelections(entries, trees);
|
|
165
|
-
expect(valid).toHaveLength(0);
|
|
166
|
-
expect(skipped[0]).toContain("unknown repo");
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("deduplicates entries", () => {
|
|
170
|
-
const entries = [
|
|
171
|
-
{ repo: "group/web-app", path: "src/App.tsx" },
|
|
172
|
-
{ repo: "group/web-app", path: "src/App.tsx" },
|
|
173
|
-
];
|
|
174
|
-
const trees = [{ repo: "group/web-app", files: ["src/App.tsx"] }];
|
|
175
|
-
const { valid } = agent._validateSelections(entries, trees);
|
|
176
|
-
expect(valid).toHaveLength(1);
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
});
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* server/forge/core/branchName.js
|
|
3
|
-
*
|
|
4
|
-
* Shared branch-name derivation so the validator's local workspace checkout
|
|
5
|
-
* and the provider's remote branch end up on the same name — "follow the code
|
|
6
|
-
* change intention, no timestamps".
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const CONV_PREFIX = /^(?:feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(?:\([^)]+\))?!?:\s*/i;
|
|
10
|
-
|
|
11
|
-
const MAX_SLUG_LEN = 32;
|
|
12
|
-
|
|
13
|
-
export function slugifyIntent(text) {
|
|
14
|
-
const stripped = String(text || "")
|
|
15
|
-
.replace(CONV_PREFIX, "")
|
|
16
|
-
.toLowerCase()
|
|
17
|
-
.normalize("NFKD")
|
|
18
|
-
.replace(/[^\w\s-]/g, "")
|
|
19
|
-
.trim()
|
|
20
|
-
.replace(/[\s_]+/g, "-")
|
|
21
|
-
.replace(/-+/g, "-")
|
|
22
|
-
.replace(/^-+|-+$/g, "");
|
|
23
|
-
if (stripped.length <= MAX_SLUG_LEN) return stripped;
|
|
24
|
-
// Truncate on a word boundary so the branch name reads like a phrase,
|
|
25
|
-
// not a mid-word cutoff ("...div-attri"). Fall back to hard cut if the
|
|
26
|
-
// first word is already too long.
|
|
27
|
-
const hardCut = stripped.slice(0, MAX_SLUG_LEN);
|
|
28
|
-
const lastDash = hardCut.lastIndexOf("-");
|
|
29
|
-
const soft = lastDash > 0 ? hardCut.slice(0, lastDash) : hardCut;
|
|
30
|
-
return soft.replace(/-+$/g, "");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Branch name derives from the *issue title* rather than the model's mrTitle,
|
|
35
|
-
* so the branch stays stable across refines / CI-fix loops (which rewrite
|
|
36
|
-
* mrTitle to describe the latest edit instead of the overall change body).
|
|
37
|
-
*/
|
|
38
|
-
export function branchNameFor(issueIid, analysis) {
|
|
39
|
-
const slug = slugifyIntent(analysis?.issue?.title || analysis?.mrTitle || "");
|
|
40
|
-
return slug ? `forge/issue-${issueIid}-${slug}` : `forge/issue-${issueIid}`;
|
|
41
|
-
}
|
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* server/forge/core/claudeClient.js
|
|
3
|
-
*
|
|
4
|
-
* Shells out to the `claude` CLI (Claude Code) instead of the Anthropic SDK.
|
|
5
|
-
* Uses your Claude Code subscription — no separate API credits needed.
|
|
6
|
-
*
|
|
7
|
-
* Requires: `claude` CLI installed and authenticated (`claude auth login`)
|
|
8
|
-
*
|
|
9
|
-
* Quota headers are NOT available via the CLI — see `quotaClient.js` for a
|
|
10
|
-
* user-triggered probe that hits the Messages API once to read them.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { spawn } from "child_process";
|
|
14
|
-
import {
|
|
15
|
-
SELECT_FILES_SCHEMA,
|
|
16
|
-
PROPOSE_FIX_SCHEMA,
|
|
17
|
-
CI_FIX_SCHEMA,
|
|
18
|
-
REFINE_OR_ASK_SCHEMA,
|
|
19
|
-
RESOLUTION_CHECK_SCHEMA,
|
|
20
|
-
buildIdentifyFilesPrompt,
|
|
21
|
-
buildProposeFixPrompt,
|
|
22
|
-
buildRefineOrAskPrompt,
|
|
23
|
-
buildRefineResumePrompt,
|
|
24
|
-
buildCheckResolutionPrompt,
|
|
25
|
-
classifyIssue,
|
|
26
|
-
} from "./prompts.js";
|
|
27
|
-
|
|
28
|
-
// FORGE_MODEL env var overrides the default. Set to a cheaper model to reduce cost.
|
|
29
|
-
// e.g. FORGE_MODEL=claude-haiku-4-5-20251001
|
|
30
|
-
const MODEL = process.env.FORGE_MODEL ?? "claude-sonnet-4-6";
|
|
31
|
-
const FALLBACK_MODEL = process.env.FORGE_FALLBACK_MODEL ?? "claude-haiku-4-5-20251001";
|
|
32
|
-
const CLAUDE_BIN = process.env.CLAUDE_BIN ?? "claude";
|
|
33
|
-
|
|
34
|
-
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
function extractJSON(text) {
|
|
37
|
-
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
38
|
-
if (fenced) return JSON.parse(fenced[1].trim());
|
|
39
|
-
const start = text.indexOf("{");
|
|
40
|
-
const end = text.lastIndexOf("}");
|
|
41
|
-
if (start !== -1 && end !== -1) return JSON.parse(text.slice(start, end + 1));
|
|
42
|
-
throw new Error("No JSON found in response");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function shortenPath(p, cwd) {
|
|
46
|
-
if (!p || typeof p !== "string") return p;
|
|
47
|
-
if (cwd && p.startsWith(cwd + "/")) return p.slice(cwd.length + 1);
|
|
48
|
-
if (cwd && p === cwd) return ".";
|
|
49
|
-
return p;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Translate a stream-json `tool_use` block into a user-friendly one-line log.
|
|
54
|
-
* Returns null for tools we don't want to surface (filters out noise).
|
|
55
|
-
*/
|
|
56
|
-
function formatToolUse(block, cwd) {
|
|
57
|
-
const name = block?.name;
|
|
58
|
-
const input = block?.input ?? {};
|
|
59
|
-
switch (name) {
|
|
60
|
-
case "Read": {
|
|
61
|
-
const path = shortenPath(input.file_path, cwd);
|
|
62
|
-
return path ? `Reading ${path}` : null;
|
|
63
|
-
}
|
|
64
|
-
case "Glob": {
|
|
65
|
-
if (!input.pattern) return null;
|
|
66
|
-
const where = input.path ? ` in ${shortenPath(input.path, cwd)}` : "";
|
|
67
|
-
return `Globbing ${input.pattern}${where}`;
|
|
68
|
-
}
|
|
69
|
-
case "Grep": {
|
|
70
|
-
if (!input.pattern) return null;
|
|
71
|
-
const where = input.path ? ` in ${shortenPath(input.path, cwd)}` : "";
|
|
72
|
-
return `Grepping ${JSON.stringify(input.pattern)}${where}`;
|
|
73
|
-
}
|
|
74
|
-
default:
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function runClaude(prompt, jsonSchema, systemPrompt, model, options = {}) {
|
|
80
|
-
const { cwd, allowedTools, onEvent, sessionId, resumeSession, signal } = options;
|
|
81
|
-
if (signal?.aborted) {
|
|
82
|
-
const err = new Error("Claude CLI call cancelled before start.");
|
|
83
|
-
err.name = "AbortError";
|
|
84
|
-
throw err;
|
|
85
|
-
}
|
|
86
|
-
const streaming = typeof onEvent === "function";
|
|
87
|
-
const args = [
|
|
88
|
-
"--print",
|
|
89
|
-
"--output-format", streaming ? "stream-json" : "json",
|
|
90
|
-
...(streaming ? ["--verbose"] : []),
|
|
91
|
-
"--model", model ?? MODEL,
|
|
92
|
-
"--fallback-model", FALLBACK_MODEL,
|
|
93
|
-
// Kill every tool by default: without this, Claude sometimes picks up
|
|
94
|
-
// TodoWrite/Task/WebSearch and spends turns on them, returning
|
|
95
|
-
// stop_reason=end_turn with an empty result. When the caller explicitly
|
|
96
|
-
// passes allowedTools (e.g. refine with repo access), allow only those.
|
|
97
|
-
// NOTE: we can't use `--bare` — it disables keychain/OAuth reads, which
|
|
98
|
-
// breaks the Claude Code subscription auth path.
|
|
99
|
-
...(allowedTools?.length
|
|
100
|
-
? ["--allowed-tools", allowedTools.join(",")]
|
|
101
|
-
: ["--tools", ""]),
|
|
102
|
-
// Skip user/project/local settings (hooks, agents, CLAUDE.md cascade,
|
|
103
|
-
// auto-mode, memory). Loading those was causing extra turns that came
|
|
104
|
-
// back with stop_reason=end_turn and result="". Verified: same prompt
|
|
105
|
-
// goes from failing at turns=2/3 → passing at turns=1 with this flag.
|
|
106
|
-
"--setting-sources", "",
|
|
107
|
-
];
|
|
108
|
-
|
|
109
|
-
// Session continuity for multi-turn refine:
|
|
110
|
-
// resumeSession=true → --resume <uuid> (replays the conversation so Claude
|
|
111
|
-
// keeps its prior reasoning, tool-call history, and prompt cache).
|
|
112
|
-
// sessionId without resume → --session-id <uuid> (first turn; fixes the
|
|
113
|
-
// UUID so subsequent turns can resume by the id we own).
|
|
114
|
-
if (resumeSession && sessionId) {
|
|
115
|
-
args.push("--resume", sessionId);
|
|
116
|
-
} else if (sessionId) {
|
|
117
|
-
args.push("--session-id", sessionId);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// NOTE: we deliberately do NOT pass --json-schema anymore. The CLI's schema
|
|
121
|
-
// validator rejects the model's first turn and retries on a second turn; if
|
|
122
|
-
// that also fails, we get stop_reason=end_turn with result="". The prompts
|
|
123
|
-
// already tell Claude to emit raw JSON and `extractJSON` is lenient enough
|
|
124
|
-
// to unwrap a stray fence or preamble. We validate shape on our side instead.
|
|
125
|
-
void jsonSchema;
|
|
126
|
-
// On resume, the system prompt is baked into the session from the first
|
|
127
|
-
// turn — re-passing it would stack a second system message.
|
|
128
|
-
if (systemPrompt && !resumeSession) args.push("--system-prompt", systemPrompt);
|
|
129
|
-
|
|
130
|
-
return new Promise((resolve, reject) => {
|
|
131
|
-
// Strip ANTHROPIC_API_KEY so the CLI uses the OAuth session
|
|
132
|
-
// (Claude Code subscription) rather than falling back to API credits.
|
|
133
|
-
const { ANTHROPIC_API_KEY: _removed, ...cleanEnv } = process.env;
|
|
134
|
-
const proc = spawn(CLAUDE_BIN, args, {
|
|
135
|
-
env: { ...cleanEnv, FORCE_COLOR: "0" },
|
|
136
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
137
|
-
...(cwd ? { cwd } : {}),
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
let stdout = "";
|
|
141
|
-
let stderr = "";
|
|
142
|
-
let streamBuffer = "";
|
|
143
|
-
let streamedResult = null;
|
|
144
|
-
|
|
145
|
-
const timer = setTimeout(() => {
|
|
146
|
-
proc.kill();
|
|
147
|
-
reject(new Error("claude CLI timed out after 5 minutes"));
|
|
148
|
-
}, 300_000);
|
|
149
|
-
|
|
150
|
-
// External abort (e.g. user cancelled the analysis). Kill the subprocess
|
|
151
|
-
// so we stop billing the subscription / token window for work the user
|
|
152
|
-
// has already discarded.
|
|
153
|
-
const onAbort = () => {
|
|
154
|
-
try { proc.kill(); } catch { /* ignore */ }
|
|
155
|
-
clearTimeout(timer);
|
|
156
|
-
const err = new Error("Claude CLI call cancelled.");
|
|
157
|
-
err.name = "AbortError";
|
|
158
|
-
reject(err);
|
|
159
|
-
};
|
|
160
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
161
|
-
proc.on("close", () => { signal?.removeEventListener("abort", onAbort); });
|
|
162
|
-
|
|
163
|
-
proc.stdout.on("data", (d) => {
|
|
164
|
-
stdout += d;
|
|
165
|
-
if (!streaming) return;
|
|
166
|
-
streamBuffer += d.toString("utf8");
|
|
167
|
-
let nl;
|
|
168
|
-
while ((nl = streamBuffer.indexOf("\n")) !== -1) {
|
|
169
|
-
const line = streamBuffer.slice(0, nl).trim();
|
|
170
|
-
streamBuffer = streamBuffer.slice(nl + 1);
|
|
171
|
-
if (!line) continue;
|
|
172
|
-
let evt;
|
|
173
|
-
try { evt = JSON.parse(line); } catch { continue; }
|
|
174
|
-
if (evt.type === "assistant") {
|
|
175
|
-
const blocks = evt.message?.content ?? [];
|
|
176
|
-
for (const block of blocks) {
|
|
177
|
-
if (block.type !== "tool_use") continue;
|
|
178
|
-
const msg = formatToolUse(block, cwd);
|
|
179
|
-
if (msg) {
|
|
180
|
-
try { onEvent({ type: "log", message: msg }); } catch { /* swallow */ }
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
} else if (evt.type === "result") {
|
|
184
|
-
streamedResult = evt;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
proc.stderr.on("data", (d) => { stderr += d; });
|
|
189
|
-
|
|
190
|
-
proc.on("close", (code) => {
|
|
191
|
-
clearTimeout(timer);
|
|
192
|
-
if (code !== 0) {
|
|
193
|
-
try {
|
|
194
|
-
const wrapper = streamedResult ?? JSON.parse(stdout.trim());
|
|
195
|
-
const msg = wrapper.result || wrapper.error || stderr.trim() || stdout.trim();
|
|
196
|
-
reject(new Error(`claude CLI error: ${msg}`));
|
|
197
|
-
} catch {
|
|
198
|
-
reject(new Error(`claude CLI error: ${stderr.trim() || stdout.trim()}`));
|
|
199
|
-
}
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
let wrapper;
|
|
203
|
-
if (streaming) {
|
|
204
|
-
if (!streamedResult) {
|
|
205
|
-
reject(new Error(`claude CLI stream ended without a result event (${stdout.length} bytes). First 200 chars: ${stdout.slice(0, 200)}`));
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
wrapper = streamedResult;
|
|
209
|
-
} else {
|
|
210
|
-
try {
|
|
211
|
-
wrapper = JSON.parse(stdout.trim());
|
|
212
|
-
} catch {
|
|
213
|
-
reject(new Error(`claude CLI returned non-JSON output (${stdout.length} bytes). First 200 chars: ${stdout.slice(0, 200)}`));
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const result = wrapper.result ?? "";
|
|
219
|
-
if (!result.trim()) {
|
|
220
|
-
// Claude succeeded but emitted no content. Usually means the model
|
|
221
|
-
// got stuck in a tool-use loop or was blocked by --disallowed-tools.
|
|
222
|
-
// Surface a clean, actionable message instead of dumping the wrapper.
|
|
223
|
-
const stopReason = wrapper.stop_reason ?? "unknown";
|
|
224
|
-
const turns = wrapper.num_turns ?? 0;
|
|
225
|
-
const hint = stopReason === "tool_use"
|
|
226
|
-
? "model tried to use a blocked tool — retry or allow the tool."
|
|
227
|
-
: "model produced empty output — retry or switch model.";
|
|
228
|
-
reject(new Error(`Claude returned no content (stop_reason: ${stopReason}, turns: ${turns}). ${hint}`));
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
let data;
|
|
233
|
-
try {
|
|
234
|
-
data = extractJSON(result);
|
|
235
|
-
} catch (err) {
|
|
236
|
-
reject(new Error(`Claude response was not valid JSON — ${err.message}. First 200 chars of result: ${result.slice(0, 200)}`));
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// The CLI echoes the session id it actually used. If it doesn't match
|
|
241
|
-
// the one we passed (CLI version drift, resume silently forked to a
|
|
242
|
-
// new session, etc.), warn — the caller should persist the CLI's id
|
|
243
|
-
// going forward or we'll resume the wrong conversation next turn.
|
|
244
|
-
const wrapperSessionId = wrapper.session_id ?? null;
|
|
245
|
-
if (sessionId && wrapperSessionId && wrapperSessionId !== sessionId) {
|
|
246
|
-
console.warn(
|
|
247
|
-
`[claudeClient] session_id mismatch: passed ${sessionId}, CLI returned ${wrapperSessionId}. Using CLI value.`,
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
resolve({
|
|
252
|
-
data,
|
|
253
|
-
cost: wrapper.total_cost_usd ?? 0,
|
|
254
|
-
serviceTier: wrapper.usage?.service_tier ?? "unknown",
|
|
255
|
-
inputTokens: wrapper.usage?.input_tokens ?? 0,
|
|
256
|
-
outputTokens: wrapper.usage?.output_tokens ?? 0,
|
|
257
|
-
cacheCreationTokens: wrapper.usage?.cache_creation_input_tokens ?? 0,
|
|
258
|
-
cacheReadTokens: wrapper.usage?.cache_read_input_tokens ?? 0,
|
|
259
|
-
sessionId: wrapperSessionId ?? sessionId ?? null,
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
proc.stdin.write(prompt, "utf8");
|
|
264
|
-
proc.stdin.end();
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
269
|
-
|
|
270
|
-
export async function checkResolution(issue, diffs, model, opts = {}) {
|
|
271
|
-
const { data, cost, serviceTier, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens } = await runClaude(
|
|
272
|
-
buildCheckResolutionPrompt(issue, diffs),
|
|
273
|
-
RESOLUTION_CHECK_SCHEMA,
|
|
274
|
-
"You are Forge, a code review assistant. Assess whether code changes actually fix the reported issue. Be concise and direct.",
|
|
275
|
-
model,
|
|
276
|
-
{ signal: opts.signal },
|
|
277
|
-
);
|
|
278
|
-
return { ...data, cost, serviceTier, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens };
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
export async function identifyRelevantFiles(issue, fileTree, model, opts = {}) {
|
|
282
|
-
const { data, cost, serviceTier, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens } = await runClaude(
|
|
283
|
-
buildIdentifyFilesPrompt(issue, fileTree),
|
|
284
|
-
SELECT_FILES_SCHEMA,
|
|
285
|
-
"You are Forge, a code review assistant. You only output raw JSON — never markdown or prose.",
|
|
286
|
-
model,
|
|
287
|
-
{ signal: opts.signal },
|
|
288
|
-
);
|
|
289
|
-
return { ...data, cost, serviceTier, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens };
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export async function refineOrAsk(issue, fileContents, options = {}, model, onEvent, opts = {}) {
|
|
293
|
-
const { repoCwd, sessionId, resumeSession } = options;
|
|
294
|
-
const hasRepoAccess = Boolean(repoCwd);
|
|
295
|
-
const accessLine = hasRepoAccess
|
|
296
|
-
? `You ARE running inside a checkout of the repo at the current working directory, with the user's proposed changes already overlaid on the working tree. You can use Read, Glob, and Grep to inspect any file in the repo. Use them when you need context beyond the files listed in the prompt.`
|
|
297
|
-
: `The prompt lists the file paths in the repo and the full content of the files currently in scope. Treat those as authoritative — do NOT claim you lack repo access. If you need a file that isn't attached, name the path and ask the user to share it.`;
|
|
298
|
-
|
|
299
|
-
// When resuming, the system prompt + issue/files/previousChanges are already
|
|
300
|
-
// in Claude's context from the initial turn — send only what's new.
|
|
301
|
-
const prompt = resumeSession
|
|
302
|
-
? buildRefineResumePrompt(options)
|
|
303
|
-
: buildRefineOrAskPrompt(issue, fileContents, options);
|
|
304
|
-
|
|
305
|
-
const runOptions = hasRepoAccess
|
|
306
|
-
? { cwd: repoCwd, allowedTools: ["Read", "Glob", "Grep"], onEvent }
|
|
307
|
-
: { onEvent };
|
|
308
|
-
if (sessionId) runOptions.sessionId = sessionId;
|
|
309
|
-
if (resumeSession) runOptions.resumeSession = true;
|
|
310
|
-
if (opts.signal) runOptions.signal = opts.signal;
|
|
311
|
-
|
|
312
|
-
const { data, cost, serviceTier, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, sessionId: returnedSessionId } = await runClaude(
|
|
313
|
-
prompt,
|
|
314
|
-
REFINE_OR_ASK_SCHEMA,
|
|
315
|
-
`You are Forge, an AI code assistant in a production React/TypeScript/Express codebase.
|
|
316
|
-
${accessLine}
|
|
317
|
-
You auto-classify the user's message: answer questions about state, or produce a refined code change when given a directive.
|
|
318
|
-
Rules when producing changes:
|
|
319
|
-
- Changes must be minimal and surgical.
|
|
320
|
-
- Match the existing code style exactly (TypeScript strict, no barrel imports, Tailwind for UI).
|
|
321
|
-
- For "proposed", always return the COMPLETE file content.
|
|
322
|
-
- Maximum 5 files changed.
|
|
323
|
-
- Never add console.* — use the existing log utility.
|
|
324
|
-
- Never add comments unless the logic is non-obvious.
|
|
325
|
-
Rules when answering:
|
|
326
|
-
- Ground statements in the validation state, the proposed changes, and (when available) files you read from the working tree.
|
|
327
|
-
- If validation hasn't been run yet, say so plainly.
|
|
328
|
-
- Be concise — one to four short paragraphs at most.
|
|
329
|
-
Your final message MUST be a single raw JSON object matching the schema — no prose, no code fences, no preamble.`,
|
|
330
|
-
model,
|
|
331
|
-
runOptions,
|
|
332
|
-
);
|
|
333
|
-
return { ...data, cost, serviceTier, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, sessionId: returnedSessionId };
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
export async function proposeFix(issue, fileContents, options = {}, model, opts = {}) {
|
|
337
|
-
const schema = options.ciContext ? CI_FIX_SCHEMA : PROPOSE_FIX_SCHEMA;
|
|
338
|
-
const kind = classifyIssue(issue);
|
|
339
|
-
const isFeature = kind === "feature" || kind === "refactor";
|
|
340
|
-
|
|
341
|
-
const systemPrompt = isFeature
|
|
342
|
-
? `You are Forge, an AI code assistant in a production React/TypeScript/Express codebase.
|
|
343
|
-
Rules:
|
|
344
|
-
- Match the existing code style exactly (TypeScript strict, no barrel imports, Tailwind for UI).
|
|
345
|
-
- For "proposed", always return the COMPLETE file content, not a diff or snippet.
|
|
346
|
-
- Maximum 12 files changed. Prioritise core changes over secondary ones — if the feature touches more files than the cap, implement the most important changes and note the rest in "explanation".
|
|
347
|
-
- Never add console.* — use the existing log utility.
|
|
348
|
-
- Never add comments unless the logic is non-obvious.
|
|
349
|
-
- Before returning, verify TypeScript correctness: no implicit any, no missing imports, no type mismatches, all imported symbols must actually be exported from their source.
|
|
350
|
-
Feature discipline:
|
|
351
|
-
- Follow the issue specification. Only make changes the issue asks for — do not clean up, refactor, or "improve" unrelated code.
|
|
352
|
-
- When the issue says to ADD an entry to an existing list or config (nav items, routes, type unions), add the entry — do not restructure the existing code around it.
|
|
353
|
-
- Base new files on the patterns and conventions visible in the relevant files.
|
|
354
|
-
- Use a "feat:" conventional-commit prefix in the title, not "fix:".`
|
|
355
|
-
: `You are Forge, an AI code assistant in a production React/TypeScript/Express codebase.
|
|
356
|
-
Rules:
|
|
357
|
-
- Changes must be minimal and surgical.
|
|
358
|
-
- Match the existing code style exactly (TypeScript strict, no barrel imports, Tailwind for UI).
|
|
359
|
-
- For "proposed", always return the COMPLETE file content, not a diff or snippet.
|
|
360
|
-
- Maximum 5 files changed.
|
|
361
|
-
- Never add console.* — use the existing log utility.
|
|
362
|
-
- Never add comments unless the logic is non-obvious.
|
|
363
|
-
- Before returning, verify TypeScript correctness: no implicit any, no missing imports, no type mismatches, all imported symbols must actually be exported from their source.
|
|
364
|
-
- If fixing a CI failure, address only what the logs report — do not change unrelated code.
|
|
365
|
-
Root-cause discipline:
|
|
366
|
-
- A fix that only improves an error message or adds a guard at the point of failure is a symptom suppressor, not a fix. Reject it and find the upstream origin of the bad input.
|
|
367
|
-
- When a value is undefined/null at a check, trace one or two frames up (the caller of the function, the prop passed into the component, the handler bound to an onClick). Default function parameters are defeated when the caller passes an unexpected argument (e.g. React forwarding a MouseEvent) — look for that class of bug before patching the consumer.
|
|
368
|
-
- In "explanation", name the origin of the bad value and state why the reported symptom can no longer occur after your change.`;
|
|
369
|
-
|
|
370
|
-
const { data, cost, serviceTier, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens } = await runClaude(
|
|
371
|
-
buildProposeFixPrompt(issue, fileContents, options),
|
|
372
|
-
schema,
|
|
373
|
-
systemPrompt,
|
|
374
|
-
model,
|
|
375
|
-
{ signal: opts.signal },
|
|
376
|
-
);
|
|
377
|
-
return { ...data, cost, serviceTier, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens };
|
|
378
|
-
}
|