@ipul-labs/forge 1.14.0 → 1.15.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 +152 -96
- package/package.json +14 -22
- package/server/forge/core/ForgeAgent.js +42 -15
- package/server/forge/core/ForgeAgent.test.js +179 -0
- package/server/forge/core/claudeClient.js +27 -5
- package/server/forge/core/configStore.js +15 -0
- package/server/forge/core/configStore.test.js +89 -0
- package/server/forge/core/localRepo.js +4 -2
- package/server/forge/core/prompts.js +124 -17
- package/server/forge/core/prompts.test.js +219 -0
- package/server/forge/core/providers/azuredevops.js +4 -3
- package/server/forge/core/providers/github.js +3 -4
- package/server/forge/core/providers/gitlab.js +6 -6
- package/server/forge/core/validator/clone.js +3 -2
- package/server/forge/core/validator.js +3 -2
- package/server/forge/routes.js +10 -7
- package/dist-lib/favicon.svg +0 -8
- package/dist-lib/index.d.ts +0 -1
- package/dist-lib/style.css +0 -1
- package/dist-lib/ui.d.ts +0 -2
- package/dist-lib/ui.js +0 -5946
- package/dist-lib/views/Forge/ForgeDashboard.d.ts +0 -6
- package/dist-lib/views/Forge/activeJobs.d.ts +0 -29
- package/dist-lib/views/Forge/analysisCache.d.ts +0 -64
- package/dist-lib/views/Forge/api.d.ts +0 -4
- package/dist-lib/views/Forge/components/AnalysisProgress.d.ts +0 -13
- package/dist-lib/views/Forge/components/CITerminal.d.ts +0 -44
- package/dist-lib/views/Forge/components/DiffViewer.d.ts +0 -37
- package/dist-lib/views/Forge/components/ForgeConnect.d.ts +0 -6
- package/dist-lib/views/Forge/components/IssueList.d.ts +0 -28
- package/dist-lib/views/Forge/components/MRStatus.d.ts +0 -9
- package/dist-lib/views/Forge/components/NewTaskModal.d.ts +0 -1
- package/dist-lib/views/Forge/components/OpenMRList.d.ts +0 -11
- package/dist-lib/views/Forge/components/PipelineStatus.d.ts +0 -12
- package/dist-lib/views/Forge/components/PipelinesList.d.ts +0 -9
- package/dist-lib/views/Forge/components/RolloutStatus.d.ts +0 -5
- package/dist-lib/views/Forge/components/UsageWidget.d.ts +0 -8
- package/dist-lib/views/Forge/components/ValidationPanel.d.ts +0 -22
- package/dist-lib/views/Forge/context/ForgeProvider.d.ts +0 -23
- package/dist-lib/views/Forge/context/ForgeRouterContext.d.ts +0 -22
- package/dist-lib/views/Forge/hooks/useConfig.d.ts +0 -54
- package/dist-lib/views/Forge/hooks/useForgeDashboard.d.ts +0 -107
- package/dist-lib/views/Forge/hooks/useForgeQuota.d.ts +0 -22
- package/dist-lib/views/Forge/phases/AnalyzingView.d.ts +0 -1
- package/dist-lib/views/Forge/phases/IdleView.d.ts +0 -1
- package/dist-lib/views/Forge/phases/MonitoringIssueTab.d.ts +0 -1
- package/dist-lib/views/Forge/phases/MonitoringPipelineTab.d.ts +0 -1
- package/dist-lib/views/Forge/phases/MonitoringView.d.ts +0 -1
- package/dist-lib/views/Forge/phases/ReviewingView.d.ts +0 -1
- package/dist-lib/views/Forge/phases/SelectedIssueTab.d.ts +0 -11
- package/dist-lib/views/Forge/phases/SelectedMRTab.d.ts +0 -1
- package/dist-lib/views/Forge/phases/SelectedView.d.ts +0 -1
- package/dist-lib/views/Forge/phases/shared/CloseIssueConfirm.d.ts +0 -1
- package/dist-lib/views/Forge/phases/shared/DetailTabs.d.ts +0 -14
- package/dist-lib/views/Forge/phases/shared/EnvironmentsPanel.d.ts +0 -9
- package/dist-lib/views/Forge/phases/shared/ForgeActivity.d.ts +0 -1
- package/dist-lib/views/Forge/phases/shared/IssueHeaderBlock.d.ts +0 -13
- package/dist-lib/views/Forge/phases/shared/MergedMRList.d.ts +0 -4
- package/dist-lib/views/Forge/phases/shared/PreviousAnalyses.d.ts +0 -1
- package/dist-lib/views/Forge/phases/shared/ResolutionCheckPanel.d.ts +0 -6
- package/dist-lib/views/Forge/settings/ConnectionRow.d.ts +0 -16
- package/dist-lib/views/Forge/settings/ConnectionsSection.d.ts +0 -7
- package/dist-lib/views/Forge/settings/DefaultsSection.d.ts +0 -7
- package/dist-lib/views/Forge/settings/ReposSection.d.ts +0 -7
- package/dist-lib/views/Forge/settings/SettingsView.d.ts +0 -1
- package/dist-lib/views/Forge/shell/ConfigBar.d.ts +0 -1
- package/dist-lib/views/Forge/shell/ForgeRouter.d.ts +0 -9
- package/dist-lib/views/Forge/shell/ForgeShell.d.ts +0 -7
- package/dist-lib/views/Forge/shell/Sidebar.d.ts +0 -1
- package/dist-lib/views/Forge/shell/SidebarFooterMenu.d.ts +0 -1
- package/dist-lib/views/Forge/shell/SidebarNav.d.ts +0 -1
- package/dist-lib/views/Forge/shell/SidebarNavItem.d.ts +0 -10
- package/dist-lib/views/Forge/shell/TopBar.d.ts +0 -5
- package/dist-lib/views/Forge/types.d.ts +0 -235
- package/dist-lib/views/Forge/utils.d.ts +0 -11
package/README.md
CHANGED
|
@@ -30,32 +30,112 @@ Everything else fills in the gaps that matter for real teams:
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
-
##
|
|
33
|
+
## Getting started
|
|
34
|
+
|
|
35
|
+
### Prerequisites
|
|
36
|
+
|
|
37
|
+
- Node.js 20+
|
|
38
|
+
- `claude` CLI installed and authenticated (`claude auth login`)
|
|
39
|
+
- Credentials for your SCM provider (see [Configuration](#configuration))
|
|
40
|
+
|
|
41
|
+
### Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://gitlab.com/ipul-labs/forge.git
|
|
45
|
+
cd forge
|
|
46
|
+
npm install
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Configure
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Interactive setup — writes .env and ~/.forge/config.json
|
|
53
|
+
npx forge init
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or copy the example env and fill in the values manually:
|
|
34
57
|
|
|
35
58
|
```bash
|
|
36
59
|
cp .env.example .env
|
|
37
|
-
# fill in FORGE_TOKEN plus your provider credentials
|
|
60
|
+
# fill in FORGE_TOKEN plus your provider credentials
|
|
61
|
+
```
|
|
38
62
|
|
|
39
|
-
|
|
63
|
+
### Launch the UI
|
|
64
|
+
|
|
65
|
+
```bash
|
|
40
66
|
npm run dev
|
|
41
|
-
#
|
|
67
|
+
# opens http://localhost:5000 (frontend) with API on :5001
|
|
42
68
|
```
|
|
43
69
|
|
|
44
|
-
|
|
70
|
+
For production:
|
|
45
71
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
72
|
+
```bash
|
|
73
|
+
npm run build:app # builds Vite SPA to dist/
|
|
74
|
+
npm start # NODE_ENV=production node server.js
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### CLI usage
|
|
78
|
+
|
|
79
|
+
The `forge` CLI drives the same orchestrator from the terminal. Install globally or use `npx`:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Global install (adds `forge` to PATH)
|
|
83
|
+
npm install -g @ipul-labs/forge
|
|
84
|
+
|
|
85
|
+
# Or run via npx from the repo
|
|
86
|
+
npx forge <command>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### End-to-end: issue to production
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
forge ship 42
|
|
93
|
+
```
|
|
49
94
|
|
|
50
|
-
|
|
95
|
+
Runs: analyze → review → validate → apply → watch CI → merge → watch deploy → close, with a `y/N` gate at every state-changing step.
|
|
51
96
|
|
|
52
|
-
|
|
97
|
+
#### Step-by-step commands
|
|
53
98
|
|
|
54
99
|
```bash
|
|
55
|
-
forge
|
|
100
|
+
forge issues # List open issues (--label X, --mine, --limit N)
|
|
101
|
+
forge issue 42 # Show full description
|
|
102
|
+
forge analyze 42 # Run two-pass analysis, cache result
|
|
103
|
+
forge show # Re-render cached analysis
|
|
104
|
+
forge refine --feedback "..." # Iterate with feedback
|
|
105
|
+
forge validate # Run lint/typecheck/test against proposed changes
|
|
106
|
+
forge apply # Push branch + open MR
|
|
107
|
+
forge watch # Poll the MR pipeline
|
|
108
|
+
forge ci-fix # Re-analyze with failed-pipeline logs
|
|
109
|
+
forge merge # Merge the MR
|
|
110
|
+
forge resolution-check # Verify merged MR closes the issue
|
|
111
|
+
forge close # Close the issue
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Options
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
-p, --port <n> Port for start
|
|
118
|
+
--repo <group/repo> Primary repo
|
|
119
|
+
--secondary <repo> Additional repo (repeatable)
|
|
120
|
+
--all-repos Include every configured repo
|
|
121
|
+
--model <id> Claude model override
|
|
122
|
+
--agent <name> Agent backend: claude | openclaw
|
|
123
|
+
--prompt <text> Extra user prompt
|
|
124
|
+
--json JSON output
|
|
125
|
+
--quiet Suppress progress
|
|
126
|
+
--yes Skip confirmation gates
|
|
127
|
+
--validate Run validate before apply
|
|
56
128
|
```
|
|
57
129
|
|
|
58
|
-
|
|
130
|
+
#### Diagnostics
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
forge doctor # Check prerequisites (node, claude CLI, env vars)
|
|
134
|
+
forge session list # List cached sessions
|
|
135
|
+
forge session clean # Remove stale session data
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
See [`docs/cli.md`](./docs/cli.md) for the full command reference and [`docs/cli-ship-workflow.md`](./docs/cli-ship-workflow.md) for the workflow design.
|
|
59
139
|
|
|
60
140
|
---
|
|
61
141
|
|
|
@@ -65,44 +145,54 @@ forge-agent ship 42
|
|
|
65
145
|
|
|
66
146
|
| Variable | Required | Description |
|
|
67
147
|
|---|---|---|
|
|
68
|
-
| `FORGE_TOKEN` |
|
|
148
|
+
| `FORGE_TOKEN` | Yes | Dashboard password — also auto-fills the UI login form |
|
|
149
|
+
| `FORGE_REPOS` | Yes | Comma-separated repo list; first entry is the primary project |
|
|
69
150
|
| `SCM_PROVIDER` | — | `gitlab` (default) · `github` · `azuredevops` |
|
|
70
|
-
| `FORGE_REPOS` | — | Comma-separated repo list for multi-repo support |
|
|
71
151
|
| `ANTHROPIC_API_KEY` | — | Enables live model list from Anthropic; falls back to hardcoded list |
|
|
72
152
|
| `FORGE_MODEL` | — | Override default model (default: `claude-sonnet-4-6`) |
|
|
73
|
-
| `VITE_FORGE_TOKEN` | — | Baked into the UI — skips the token entry form on load |
|
|
74
153
|
| `K8S_CLUSTER_TOKEN` | — | K8s service account token for rollout monitoring |
|
|
75
154
|
| `K8S_CLUSTER_SERVER` | — | K8s API server URL |
|
|
76
155
|
| `PORT` | — | HTTP server port (default: `5001`) |
|
|
77
156
|
| `OPENCLAW_GATEWAY_URL` | — | Enables OpenClaw agent backend; point at your local Gateway |
|
|
78
157
|
| `OPENCLAW_TOKEN` | — | Bearer token for OpenClaw Gateway auth |
|
|
79
158
|
|
|
80
|
-
###
|
|
159
|
+
### Provider credentials
|
|
160
|
+
|
|
161
|
+
SCM tokens can be set here **or** in the Forge Settings UI (Settings → Connections). The UI value takes precedence over env vars.
|
|
162
|
+
|
|
163
|
+
#### GitLab (`SCM_PROVIDER=gitlab`, default)
|
|
81
164
|
|
|
82
165
|
| Variable | Required | Description |
|
|
83
166
|
|---|---|---|
|
|
84
|
-
| `
|
|
85
|
-
| `GITLAB_TOKEN` | ✅ | Personal access token with `api` + `write_repository` scopes |
|
|
167
|
+
| `GITLAB_TOKEN` | Yes | Personal access token with `api` + `write_repository` scopes |
|
|
86
168
|
|
|
87
|
-
|
|
169
|
+
#### GitHub (`SCM_PROVIDER=github`)
|
|
88
170
|
|
|
89
171
|
| Variable | Required | Description |
|
|
90
172
|
|---|---|---|
|
|
91
|
-
| `GITHUB_TOKEN` |
|
|
92
|
-
| `GITHUB_OWNER` |
|
|
93
|
-
| `GITHUB_REPO` |
|
|
173
|
+
| `GITHUB_TOKEN` | Yes | Personal access token with `repo` + `workflow` scopes |
|
|
174
|
+
| `GITHUB_OWNER` | Yes | Organization or user, e.g. `ipul-labs` |
|
|
175
|
+
| `GITHUB_REPO` | Yes | Repository name, e.g. `web-app` |
|
|
94
176
|
| `GITHUB_BASE_BRANCH` | — | Default target branch (default: `main`) |
|
|
95
177
|
|
|
96
|
-
|
|
178
|
+
#### Azure DevOps (`SCM_PROVIDER=azuredevops`)
|
|
97
179
|
|
|
98
180
|
| Variable | Required | Description |
|
|
99
181
|
|---|---|---|
|
|
100
|
-
| `AZURE_DEVOPS_TOKEN` |
|
|
101
|
-
| `AZURE_DEVOPS_ORG` |
|
|
102
|
-
| `AZURE_DEVOPS_PROJECT` |
|
|
182
|
+
| `AZURE_DEVOPS_TOKEN` | Yes | Personal access token (Full access or Code + Work Items) |
|
|
183
|
+
| `AZURE_DEVOPS_ORG` | Yes | Organization name, e.g. `ipul-labs` |
|
|
184
|
+
| `AZURE_DEVOPS_PROJECT` | Yes | Team project name, e.g. `web-app` |
|
|
103
185
|
| `AZURE_DEVOPS_REPO` | — | Git repo name (defaults to project name) |
|
|
104
186
|
| `AZURE_DEVOPS_BASE_BRANCH` | — | Default target branch (default: `main`) |
|
|
105
187
|
|
|
188
|
+
### Deprecated (still work as fallbacks)
|
|
189
|
+
|
|
190
|
+
| Variable | Replaced by |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `FORGE_GITLAB_PROJECT` | First entry in `FORGE_REPOS` |
|
|
193
|
+
| `VITE_FORGE_TOKEN` | Auto-derived from `FORGE_TOKEN` |
|
|
194
|
+
| `FORGE_REFINE_REPO_ACCESS` | Settings UI toggle (Settings → Defaults) |
|
|
195
|
+
|
|
106
196
|
See `.env.example` for a full annotated list.
|
|
107
197
|
|
|
108
198
|
---
|
|
@@ -131,75 +221,19 @@ FORGE_REPOS=gitlab:ipul/web-app,github:my-org/mobile
|
|
|
131
221
|
|
|
132
222
|
| Feature | GitLab | GitHub | Azure DevOps |
|
|
133
223
|
|---|---|---|---|
|
|
134
|
-
| Issues / Work Items |
|
|
135
|
-
| Multi-file commits |
|
|
136
|
-
| Merge / Pull Requests |
|
|
137
|
-
| CI pipelines |
|
|
138
|
-
| Job log traces |
|
|
139
|
-
| Server-side rebase |
|
|
140
|
-
|
|
141
|
-
---
|
|
142
|
-
|
|
143
|
-
## Using as an npm package
|
|
144
|
-
|
|
145
|
-
Mount the Forge Express router alongside your own app:
|
|
146
|
-
|
|
147
|
-
```js
|
|
148
|
-
import express from "express";
|
|
149
|
-
import forgeRouter from "forge-agent";
|
|
150
|
-
|
|
151
|
-
const app = express();
|
|
152
|
-
app.use("/api/forge", express.json(), forgeRouter);
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
The router reads all config from `process.env` — no constructor arguments needed.
|
|
156
|
-
|
|
157
|
-
### Available exports
|
|
158
|
-
|
|
159
|
-
```js
|
|
160
|
-
import forgeRouter from "forge-agent"; // Express router
|
|
161
|
-
import { ForgeAgent } from "forge-agent/agent"; // Core pipeline class
|
|
162
|
-
import { createProvider } from "forge-agent/providers"; // SCM provider factory
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
#### `ForgeAgent`
|
|
166
|
-
|
|
167
|
-
```js
|
|
168
|
-
const agent = new ForgeAgent({ project: "group/repo" });
|
|
169
|
-
|
|
170
|
-
// Analyze an issue and stream progress events
|
|
171
|
-
const result = await agent.analyzeIssue(issueIid, (event) => {
|
|
172
|
-
console.log(event); // { type: "progress" | "result" | "error" | "usage", ... }
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// Apply the analysis: creates branch + commit + MR/PR
|
|
176
|
-
const { branchName, mr } = await agent.applyFix(result);
|
|
177
|
-
|
|
178
|
-
// Refine with reviewer feedback
|
|
179
|
-
await agent.refineIssue(issueIid, result.changes, "use the existing logger utility");
|
|
180
|
-
|
|
181
|
-
// Push to an existing branch (for CI fixes)
|
|
182
|
-
await agent.commitToBranch(branchName, result);
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
#### `createProvider`
|
|
186
|
-
|
|
187
|
-
```js
|
|
188
|
-
import { createProvider } from "forge-agent/providers";
|
|
189
|
-
|
|
190
|
-
const provider = createProvider("github:my-org/my-repo");
|
|
191
|
-
|
|
192
|
-
const issues = await provider.listOpenIssues();
|
|
193
|
-
const mr = await provider.getMR(42);
|
|
194
|
-
const diffs = await provider.getMRDiffs(42);
|
|
195
|
-
```
|
|
224
|
+
| Issues / Work Items | Yes | Yes | Yes (Work Items) |
|
|
225
|
+
| Multi-file commits | Yes | Yes (Git Data API) | Yes (Pushes API) |
|
|
226
|
+
| Merge / Pull Requests | Yes | Yes | Yes |
|
|
227
|
+
| CI pipelines | Yes (GitLab CI) | Yes (GitHub Actions) | Yes (Azure Pipelines) |
|
|
228
|
+
| Job log traces | Yes | Yes | Yes |
|
|
229
|
+
| Server-side rebase | Yes | — | — |
|
|
196
230
|
|
|
197
231
|
---
|
|
198
232
|
|
|
199
233
|
## Production deployment
|
|
200
234
|
|
|
201
235
|
```bash
|
|
202
|
-
npm run build
|
|
236
|
+
npm run build:app # builds Vite SPA to dist/
|
|
203
237
|
NODE_ENV=production node server.js
|
|
204
238
|
```
|
|
205
239
|
|
|
@@ -215,15 +249,23 @@ docker run -p 5001:5001 --env-file .env forge
|
|
|
215
249
|
## Architecture
|
|
216
250
|
|
|
217
251
|
```
|
|
252
|
+
bin/
|
|
253
|
+
forge-agent.js # CLI entry point (forge command)
|
|
254
|
+
cli/
|
|
255
|
+
ship.js # End-to-end orchestrator (Ink UI)
|
|
256
|
+
analyze.js # Two-pass issue analysis
|
|
257
|
+
resolution.js # Post-merge resolution check
|
|
258
|
+
sessionStore.js # ~/.forge/sessions/ persistence
|
|
259
|
+
...
|
|
260
|
+
|
|
218
261
|
server/
|
|
219
262
|
forge/
|
|
220
263
|
routes.js # Express router — all /api/forge/* endpoints
|
|
221
264
|
core/
|
|
222
|
-
ForgeAgent.js # Orchestrates the full pipeline (analyze
|
|
265
|
+
ForgeAgent.js # Orchestrates the full pipeline (analyze -> apply -> refine)
|
|
223
266
|
claudeClient.js # Shells out to the claude CLI for structured output
|
|
224
267
|
openclawClient.js # OpenClaw Gateway HTTP client
|
|
225
268
|
agentClient.js # Dispatcher: routes to claudeClient or openclawClient
|
|
226
|
-
gitlabClient.js # Legacy shim (kept for backward compatibility)
|
|
227
269
|
k8s.js # Kubernetes client for rollout monitoring
|
|
228
270
|
providers/
|
|
229
271
|
base.js # Abstract ScmProvider — normalized shapes + interface
|
|
@@ -238,11 +280,7 @@ src/
|
|
|
238
280
|
api.ts # Token management + fetch helper
|
|
239
281
|
types.ts # Shared TypeScript types
|
|
240
282
|
utils.ts # SSE helper + state utilities
|
|
241
|
-
components/
|
|
242
|
-
AnalysisProgress.tsx # Streaming progress display (analyze + ci-fix modes)
|
|
243
|
-
PipelineStatus.tsx # CI pipeline + per-job log traces
|
|
244
|
-
MRStatus.tsx # MR conflict/rebase banner + inline diff viewer
|
|
245
|
-
OpenMRList.tsx # All open Forge MRs with pipeline badges
|
|
283
|
+
components/ # Analysis progress, pipeline status, diff viewer
|
|
246
284
|
hooks/
|
|
247
285
|
useForgeDashboard.ts # All state, phase transitions, and API calls
|
|
248
286
|
```
|
|
@@ -268,6 +306,24 @@ All three SCM providers implement `ScmProvider` from `providers/base.js`. The co
|
|
|
268
306
|
|
|
269
307
|
---
|
|
270
308
|
|
|
309
|
+
## Programmatic usage
|
|
310
|
+
|
|
311
|
+
Mount the Forge Express router or use the core classes directly:
|
|
312
|
+
|
|
313
|
+
```js
|
|
314
|
+
import forgeRouter from "@ipul-labs/forge"; // Express router
|
|
315
|
+
import { ForgeAgent } from "@ipul-labs/forge/agent"; // Core pipeline class
|
|
316
|
+
import { createProvider } from "@ipul-labs/forge/providers"; // SCM factory
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
```js
|
|
320
|
+
const agent = new ForgeAgent({ project: "group/repo" });
|
|
321
|
+
const result = await agent.analyzeIssue(42, (event) => console.log(event));
|
|
322
|
+
const { branchName, mr } = await agent.applyFix(result);
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
271
327
|
## License
|
|
272
328
|
|
|
273
329
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ipul-labs/forge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "AI-powered code change & deploy control panel — connects Claude, GitLab, and Kubernetes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server/forge/routes.js",
|
|
@@ -10,26 +10,20 @@
|
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./server/forge/routes.js",
|
|
12
12
|
"./agent": "./server/forge/core/ForgeAgent.js",
|
|
13
|
-
"./
|
|
14
|
-
"./ui": {
|
|
15
|
-
"import": "./dist-lib/ui.js",
|
|
16
|
-
"types": "./dist-lib/ui.d.ts"
|
|
17
|
-
},
|
|
18
|
-
"./style.css": "./dist-lib/style.css"
|
|
13
|
+
"./providers": "./server/forge/core/providers/index.js"
|
|
19
14
|
},
|
|
20
15
|
"files": [
|
|
21
16
|
"bin",
|
|
22
17
|
"server/forge",
|
|
23
|
-
"dist"
|
|
24
|
-
"dist-lib"
|
|
18
|
+
"dist"
|
|
25
19
|
],
|
|
26
20
|
"scripts": {
|
|
27
21
|
"dev": "trap 'kill 0' EXIT INT TERM; FORGE_NO_VITE=1 node --watch-path=./server --watch-path=./server.js server.js & vite; wait",
|
|
28
|
-
"build": "vite build
|
|
29
|
-
"build:app": "vite build",
|
|
30
|
-
"prepare": "npm run build",
|
|
22
|
+
"build": "vite build",
|
|
31
23
|
"start": "NODE_ENV=production node server.js",
|
|
32
|
-
"typecheck": "tsc --noEmit"
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest"
|
|
33
27
|
},
|
|
34
28
|
"dependencies": {
|
|
35
29
|
"@kubernetes/client-node": "^1.0.0",
|
|
@@ -40,7 +34,10 @@
|
|
|
40
34
|
"ink": "^7.0.1",
|
|
41
35
|
"ink-select-input": "^6.2.0",
|
|
42
36
|
"ink-spinner": "^5.0.0",
|
|
43
|
-
"ink-text-input": "^6.0.0"
|
|
37
|
+
"ink-text-input": "^6.0.0",
|
|
38
|
+
"react": "^19.0.0",
|
|
39
|
+
"react-dom": "^19.0.0",
|
|
40
|
+
"react-router-dom": "^7.14.1"
|
|
44
41
|
},
|
|
45
42
|
"devDependencies": {
|
|
46
43
|
"@semantic-release/changelog": "^6.0.3",
|
|
@@ -52,19 +49,14 @@
|
|
|
52
49
|
"@types/express": "^5.0.0",
|
|
53
50
|
"@types/react": "^19.0.0",
|
|
54
51
|
"@types/react-dom": "^19.0.0",
|
|
55
|
-
"@vitejs/plugin-react": "^
|
|
52
|
+
"@vitejs/plugin-react": "^6.0.0",
|
|
56
53
|
"autoprefixer": "^10.4.20",
|
|
57
54
|
"postcss": "^8.4.47",
|
|
58
55
|
"semantic-release": "^25.0.3",
|
|
59
56
|
"tailwindcss": "^3.4.14",
|
|
60
57
|
"typescript": "^5.6.0",
|
|
61
|
-
"vite": "^
|
|
62
|
-
"
|
|
63
|
-
},
|
|
64
|
-
"peerDependencies": {
|
|
65
|
-
"react": "^18.0.0 || ^19.0.0",
|
|
66
|
-
"react-dom": "^18.0.0 || ^19.0.0",
|
|
67
|
-
"react-router-dom": "^7.0.0"
|
|
58
|
+
"vite": "^8.0.0",
|
|
59
|
+
"vitest": "^4.1.0"
|
|
68
60
|
},
|
|
69
61
|
"engines": {
|
|
70
62
|
"node": ">=20.0.0"
|
|
@@ -26,6 +26,7 @@ import { checkResolution, identifyRelevantFiles, proposeFix, refineOrAsk } from
|
|
|
26
26
|
import { branchNameFor } from "./branchName.js";
|
|
27
27
|
import * as configStore from "./configStore.js";
|
|
28
28
|
import { ensureWorkingTree, overlayChanges } from "./localRepo.js";
|
|
29
|
+
import { classifyIssue } from "./prompts.js";
|
|
29
30
|
import { createProvider } from "./providers/index.js";
|
|
30
31
|
import { reattributeChanges } from "./validator/attribute.js";
|
|
31
32
|
|
|
@@ -100,19 +101,26 @@ function isLikelyResumeFailure(err) {
|
|
|
100
101
|
*/
|
|
101
102
|
|
|
102
103
|
const MAX_FILE_BYTES = 40_000; // ~40 KB per file to keep the context window healthy
|
|
103
|
-
const MAX_FILES_TOTAL = 12; // hard cap across all repos
|
|
104
|
-
const MAX_FILES_PER_REPO = 8; // cap per repo so one repo can't starve another
|
|
105
104
|
const CONV_PREFIX = /^(?:feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(?:\([^)]+\))?!?:\s*/i;
|
|
106
105
|
|
|
106
|
+
const FILE_CAPS = {
|
|
107
|
+
feature: { total: 18, perRepo: 15 },
|
|
108
|
+
refactor: { total: 18, perRepo: 15 },
|
|
109
|
+
fix: { total: 12, perRepo: 8 },
|
|
110
|
+
chore: { total: 12, perRepo: 8 },
|
|
111
|
+
};
|
|
112
|
+
|
|
107
113
|
/**
|
|
108
114
|
* Builds a conventional-commit subject line from the analysis. Prefers
|
|
109
115
|
* `analysis.mrTitle` (the model's own commit-style summary), falling back to
|
|
110
|
-
* the issue title with a
|
|
116
|
+
* the issue title with a prefix derived from the issue kind.
|
|
111
117
|
*/
|
|
112
118
|
function buildCommitSubject(analysis) {
|
|
113
119
|
const mrTitle = analysis.mrTitle?.trim();
|
|
114
120
|
if (mrTitle) return mrTitle;
|
|
115
|
-
|
|
121
|
+
const kind = classifyIssue(analysis.issue);
|
|
122
|
+
const prefix = kind === "feature" ? "feat" : kind === "refactor" ? "refactor" : "fix";
|
|
123
|
+
return `${prefix}: ${analysis.issue.title.replace(CONV_PREFIX, "")}`;
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
/**
|
|
@@ -194,7 +202,7 @@ export class ForgeAgent {
|
|
|
194
202
|
* @param {{ project?: string, secondaryProjects?: string[], agent?: string }} [opts]
|
|
195
203
|
*/
|
|
196
204
|
constructor(opts = {}) {
|
|
197
|
-
const primary = opts.project ??
|
|
205
|
+
const primary = opts.project ?? configStore.getPrimaryProject();
|
|
198
206
|
if (!primary) throw new Error("ForgeAgent requires a primary project.");
|
|
199
207
|
|
|
200
208
|
const secondaries = (opts.secondaryProjects ?? [])
|
|
@@ -235,7 +243,7 @@ export class ForgeAgent {
|
|
|
235
243
|
const results = await Promise.all(
|
|
236
244
|
this.allRepos.map(async (repo) => {
|
|
237
245
|
try {
|
|
238
|
-
const files = await this._provider(repo).getFileTree("
|
|
246
|
+
const files = await this._provider(repo).getFileTree("", "main");
|
|
239
247
|
return { repo, files };
|
|
240
248
|
} catch (err) {
|
|
241
249
|
emit({ type: "log", message: `Could not scan ${repo}: ${err.message}` });
|
|
@@ -315,13 +323,14 @@ export class ForgeAgent {
|
|
|
315
323
|
}
|
|
316
324
|
|
|
317
325
|
/** Enforces the per-repo and total caps, preserving model order. */
|
|
318
|
-
_capSelections(entries) {
|
|
326
|
+
_capSelections(entries, issueKind) {
|
|
327
|
+
const caps = FILE_CAPS[issueKind] ?? FILE_CAPS.fix;
|
|
319
328
|
const capped = [];
|
|
320
329
|
const perRepo = new Map();
|
|
321
330
|
for (const entry of entries) {
|
|
322
|
-
if (capped.length >=
|
|
331
|
+
if (capped.length >= caps.total) break;
|
|
323
332
|
const count = perRepo.get(entry.repo) ?? 0;
|
|
324
|
-
if (count >=
|
|
333
|
+
if (count >= caps.perRepo) continue;
|
|
325
334
|
capped.push(entry);
|
|
326
335
|
perRepo.set(entry.repo, count + 1);
|
|
327
336
|
}
|
|
@@ -350,7 +359,7 @@ export class ForgeAgent {
|
|
|
350
359
|
* @param {string} [userPrompt]
|
|
351
360
|
* @returns {Promise<AnalysisResult>}
|
|
352
361
|
*/
|
|
353
|
-
async analyzeIssue(issueIid, onProgress, model, ciContext, userPrompt, kind = "fresh", opts = {}) {
|
|
362
|
+
async analyzeIssue(issueIid, onProgress, model, ciContext, userPrompt, kind = "fresh", opts = {}, previousChanges = null) {
|
|
354
363
|
const emit = onProgress ?? (() => {});
|
|
355
364
|
const { signal } = opts;
|
|
356
365
|
|
|
@@ -391,9 +400,12 @@ export class ForgeAgent {
|
|
|
391
400
|
cacheReadTokens: cr1 = 0,
|
|
392
401
|
} = ident;
|
|
393
402
|
|
|
403
|
+
const issueKind = classifyIssue(issue);
|
|
404
|
+
emit({ type: "log", message: `Issue classified as: ${issueKind}` });
|
|
405
|
+
|
|
394
406
|
const normalized = this._normalizeFileSelections(rawFiles);
|
|
395
407
|
const { valid, skipped } = this._validateSelections(normalized, repoTrees);
|
|
396
|
-
const capped = this._capSelections(valid);
|
|
408
|
+
const capped = this._capSelections(valid, issueKind);
|
|
397
409
|
|
|
398
410
|
if (capped.length) {
|
|
399
411
|
emit({ type: "log", message: `Identified ${capped.length} file(s): ${capped.map((f) => `${f.repo}:${f.path}`).join(", ")}` });
|
|
@@ -413,10 +425,11 @@ export class ForgeAgent {
|
|
|
413
425
|
|
|
414
426
|
// Step 5 — propose fix (pass 2)
|
|
415
427
|
emit({ type: "progress", step: "generate_fix", message: msgs.generate_fix });
|
|
428
|
+
const promptRules = configStore.get().defaults?.promptRules || "";
|
|
416
429
|
const propose = await proposeFix(
|
|
417
430
|
issue,
|
|
418
431
|
fileContents,
|
|
419
|
-
{ ciContext, userPrompt, repos: this.allRepos },
|
|
432
|
+
{ ciContext, userPrompt, previousChanges, repos: this.allRepos, promptRules },
|
|
420
433
|
model,
|
|
421
434
|
this.agent,
|
|
422
435
|
{ signal },
|
|
@@ -506,10 +519,11 @@ export class ForgeAgent {
|
|
|
506
519
|
// the caller expects fresh changes.
|
|
507
520
|
if (ciContext) {
|
|
508
521
|
emit({ type: "progress", step: "generate_fix", message: `Fixing CI failure with ${this._agentLabel}...` });
|
|
522
|
+
const ciRefineRules = configStore.get().defaults?.promptRules || "";
|
|
509
523
|
const propose = await proposeFix(
|
|
510
524
|
issue,
|
|
511
525
|
fileContents,
|
|
512
|
-
{ feedback, previousChanges, ciContext, repos: this.allRepos },
|
|
526
|
+
{ feedback, previousChanges, ciContext, repos: this.allRepos, promptRules: ciRefineRules },
|
|
513
527
|
model,
|
|
514
528
|
this.agent,
|
|
515
529
|
{ signal },
|
|
@@ -567,6 +581,7 @@ export class ForgeAgent {
|
|
|
567
581
|
emit({ type: "progress", step: "generate_fix", message: `Refining fix with ${this._agentLabel}...` });
|
|
568
582
|
if (sessionId) emit({ type: "session", sessionId, resumed: resumeSession });
|
|
569
583
|
|
|
584
|
+
const refinePromptRules = configStore.get().defaults?.promptRules || "";
|
|
570
585
|
const callRefine = (sid, resume) =>
|
|
571
586
|
refineOrAsk(
|
|
572
587
|
issue,
|
|
@@ -579,6 +594,7 @@ export class ForgeAgent {
|
|
|
579
594
|
repoCwd,
|
|
580
595
|
repoTrees,
|
|
581
596
|
repoAccessMode: repoCwd ? "tools" : "tree",
|
|
597
|
+
promptRules: refinePromptRules,
|
|
582
598
|
...(sid ? { sessionId: sid, resumeSession: resume } : {}),
|
|
583
599
|
},
|
|
584
600
|
model,
|
|
@@ -735,7 +751,7 @@ export class ForgeAgent {
|
|
|
735
751
|
} = ident;
|
|
736
752
|
|
|
737
753
|
const { valid } = this._validateSelections(this._normalizeFileSelections(rawFiles), repoTrees);
|
|
738
|
-
const capped = this._capSelections(valid);
|
|
754
|
+
const capped = this._capSelections(valid, "fix");
|
|
739
755
|
|
|
740
756
|
emit({
|
|
741
757
|
type: "progress",
|
|
@@ -745,10 +761,11 @@ export class ForgeAgent {
|
|
|
745
761
|
const fileContents = await this._fetchFiles(capped, emit);
|
|
746
762
|
|
|
747
763
|
emit({ type: "progress", step: "generate_fix", message: `Fixing CI failure with ${this._agentLabel}...` });
|
|
764
|
+
const ciPromptRules = configStore.get().defaults?.promptRules || "";
|
|
748
765
|
const propose = await proposeFix(
|
|
749
766
|
issue,
|
|
750
767
|
fileContents,
|
|
751
|
-
{ ciContext, repos: this.allRepos },
|
|
768
|
+
{ ciContext, repos: this.allRepos, promptRules: ciPromptRules },
|
|
752
769
|
model,
|
|
753
770
|
this.agent,
|
|
754
771
|
{ signal },
|
|
@@ -859,6 +876,16 @@ export class ForgeAgent {
|
|
|
859
876
|
}
|
|
860
877
|
|
|
861
878
|
const provider = this._provider(repo);
|
|
879
|
+
|
|
880
|
+
// Rebase onto latest main before pushing so CI runs against current code.
|
|
881
|
+
try {
|
|
882
|
+
const mrs = await provider.listAllForgeMRs();
|
|
883
|
+
const mr = mrs.find((m) => m.source_branch === branchName);
|
|
884
|
+
if (mr && provider.supportsRebase) {
|
|
885
|
+
await provider.rebaseMR(mr.iid);
|
|
886
|
+
}
|
|
887
|
+
} catch { /* rebase failure is non-fatal — push will still work */ }
|
|
888
|
+
|
|
862
889
|
const actions = changes.map((c) => ({ action: c.action, file_path: c.file, content: c.proposed }));
|
|
863
890
|
const subject = buildCommitSubject(analysis);
|
|
864
891
|
const commitMessage = buildCommitMessage(subject, changes, analysis, "ci-fix");
|