@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.
Files changed (75) hide show
  1. package/README.md +152 -96
  2. package/package.json +14 -22
  3. package/server/forge/core/ForgeAgent.js +42 -15
  4. package/server/forge/core/ForgeAgent.test.js +179 -0
  5. package/server/forge/core/claudeClient.js +27 -5
  6. package/server/forge/core/configStore.js +15 -0
  7. package/server/forge/core/configStore.test.js +89 -0
  8. package/server/forge/core/localRepo.js +4 -2
  9. package/server/forge/core/prompts.js +124 -17
  10. package/server/forge/core/prompts.test.js +219 -0
  11. package/server/forge/core/providers/azuredevops.js +4 -3
  12. package/server/forge/core/providers/github.js +3 -4
  13. package/server/forge/core/providers/gitlab.js +6 -6
  14. package/server/forge/core/validator/clone.js +3 -2
  15. package/server/forge/core/validator.js +3 -2
  16. package/server/forge/routes.js +10 -7
  17. package/dist-lib/favicon.svg +0 -8
  18. package/dist-lib/index.d.ts +0 -1
  19. package/dist-lib/style.css +0 -1
  20. package/dist-lib/ui.d.ts +0 -2
  21. package/dist-lib/ui.js +0 -5946
  22. package/dist-lib/views/Forge/ForgeDashboard.d.ts +0 -6
  23. package/dist-lib/views/Forge/activeJobs.d.ts +0 -29
  24. package/dist-lib/views/Forge/analysisCache.d.ts +0 -64
  25. package/dist-lib/views/Forge/api.d.ts +0 -4
  26. package/dist-lib/views/Forge/components/AnalysisProgress.d.ts +0 -13
  27. package/dist-lib/views/Forge/components/CITerminal.d.ts +0 -44
  28. package/dist-lib/views/Forge/components/DiffViewer.d.ts +0 -37
  29. package/dist-lib/views/Forge/components/ForgeConnect.d.ts +0 -6
  30. package/dist-lib/views/Forge/components/IssueList.d.ts +0 -28
  31. package/dist-lib/views/Forge/components/MRStatus.d.ts +0 -9
  32. package/dist-lib/views/Forge/components/NewTaskModal.d.ts +0 -1
  33. package/dist-lib/views/Forge/components/OpenMRList.d.ts +0 -11
  34. package/dist-lib/views/Forge/components/PipelineStatus.d.ts +0 -12
  35. package/dist-lib/views/Forge/components/PipelinesList.d.ts +0 -9
  36. package/dist-lib/views/Forge/components/RolloutStatus.d.ts +0 -5
  37. package/dist-lib/views/Forge/components/UsageWidget.d.ts +0 -8
  38. package/dist-lib/views/Forge/components/ValidationPanel.d.ts +0 -22
  39. package/dist-lib/views/Forge/context/ForgeProvider.d.ts +0 -23
  40. package/dist-lib/views/Forge/context/ForgeRouterContext.d.ts +0 -22
  41. package/dist-lib/views/Forge/hooks/useConfig.d.ts +0 -54
  42. package/dist-lib/views/Forge/hooks/useForgeDashboard.d.ts +0 -107
  43. package/dist-lib/views/Forge/hooks/useForgeQuota.d.ts +0 -22
  44. package/dist-lib/views/Forge/phases/AnalyzingView.d.ts +0 -1
  45. package/dist-lib/views/Forge/phases/IdleView.d.ts +0 -1
  46. package/dist-lib/views/Forge/phases/MonitoringIssueTab.d.ts +0 -1
  47. package/dist-lib/views/Forge/phases/MonitoringPipelineTab.d.ts +0 -1
  48. package/dist-lib/views/Forge/phases/MonitoringView.d.ts +0 -1
  49. package/dist-lib/views/Forge/phases/ReviewingView.d.ts +0 -1
  50. package/dist-lib/views/Forge/phases/SelectedIssueTab.d.ts +0 -11
  51. package/dist-lib/views/Forge/phases/SelectedMRTab.d.ts +0 -1
  52. package/dist-lib/views/Forge/phases/SelectedView.d.ts +0 -1
  53. package/dist-lib/views/Forge/phases/shared/CloseIssueConfirm.d.ts +0 -1
  54. package/dist-lib/views/Forge/phases/shared/DetailTabs.d.ts +0 -14
  55. package/dist-lib/views/Forge/phases/shared/EnvironmentsPanel.d.ts +0 -9
  56. package/dist-lib/views/Forge/phases/shared/ForgeActivity.d.ts +0 -1
  57. package/dist-lib/views/Forge/phases/shared/IssueHeaderBlock.d.ts +0 -13
  58. package/dist-lib/views/Forge/phases/shared/MergedMRList.d.ts +0 -4
  59. package/dist-lib/views/Forge/phases/shared/PreviousAnalyses.d.ts +0 -1
  60. package/dist-lib/views/Forge/phases/shared/ResolutionCheckPanel.d.ts +0 -6
  61. package/dist-lib/views/Forge/settings/ConnectionRow.d.ts +0 -16
  62. package/dist-lib/views/Forge/settings/ConnectionsSection.d.ts +0 -7
  63. package/dist-lib/views/Forge/settings/DefaultsSection.d.ts +0 -7
  64. package/dist-lib/views/Forge/settings/ReposSection.d.ts +0 -7
  65. package/dist-lib/views/Forge/settings/SettingsView.d.ts +0 -1
  66. package/dist-lib/views/Forge/shell/ConfigBar.d.ts +0 -1
  67. package/dist-lib/views/Forge/shell/ForgeRouter.d.ts +0 -9
  68. package/dist-lib/views/Forge/shell/ForgeShell.d.ts +0 -7
  69. package/dist-lib/views/Forge/shell/Sidebar.d.ts +0 -1
  70. package/dist-lib/views/Forge/shell/SidebarFooterMenu.d.ts +0 -1
  71. package/dist-lib/views/Forge/shell/SidebarNav.d.ts +0 -1
  72. package/dist-lib/views/Forge/shell/SidebarNavItem.d.ts +0 -10
  73. package/dist-lib/views/Forge/shell/TopBar.d.ts +0 -5
  74. package/dist-lib/views/Forge/types.d.ts +0 -235
  75. 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
- ## Quick start
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 (see Configuration below)
60
+ # fill in FORGE_TOKEN plus your provider credentials
61
+ ```
38
62
 
39
- npm install
63
+ ### Launch the UI
64
+
65
+ ```bash
40
66
  npm run dev
41
- # open http://localhost:5000
67
+ # opens http://localhost:5000 (frontend) with API on :5001
42
68
  ```
43
69
 
44
- ### Prerequisites
70
+ For production:
45
71
 
46
- - Node.js 20+
47
- - `claude` CLI installed and authenticated (`claude auth login`)
48
- - Credentials for your SCM provider (see below)
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
- ### Headless / CLI
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
- The `forge-agent` bin drives the same orchestrator from the terminal — one command takes an issue all the way to production:
97
+ #### Step-by-step commands
53
98
 
54
99
  ```bash
55
- forge-agent ship 42
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
- …analyze → review → validate → apply → watch CI → merge → watch deploy → close, with a `y/N` gate at every state-changing step. See [`bin/README.md`](./bin/README.md) for a quick start, [`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.
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` | ✅ | Dashboard password — users enter this to access Forge |
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
- ### GitLab (`SCM_PROVIDER=gitlab`, default)
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
- | `FORGE_GITLAB_PROJECT` | ✅ | Default project path, e.g. `group/repo` |
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
- ### GitHub (`SCM_PROVIDER=github`)
169
+ #### GitHub (`SCM_PROVIDER=github`)
88
170
 
89
171
  | Variable | Required | Description |
90
172
  |---|---|---|
91
- | `GITHUB_TOKEN` | ✅ | Personal access token with `repo` + `workflow` scopes |
92
- | `GITHUB_OWNER` | ✅ | Organization or user, e.g. `ipul-labs` |
93
- | `GITHUB_REPO` | ✅ | Repository name, e.g. `web-app` |
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
- ### Azure DevOps (`SCM_PROVIDER=azuredevops`)
178
+ #### Azure DevOps (`SCM_PROVIDER=azuredevops`)
97
179
 
98
180
  | Variable | Required | Description |
99
181
  |---|---|---|
100
- | `AZURE_DEVOPS_TOKEN` | ✅ | Personal access token (Full access or Code + Work Items) |
101
- | `AZURE_DEVOPS_ORG` | ✅ | Organization name, e.g. `ipul-labs` |
102
- | `AZURE_DEVOPS_PROJECT` | ✅ | Team project name, e.g. `web-app` |
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 | ✅ | ✅ | ✅ (Work Items) |
135
- | Multi-file commits | ✅ | ✅ (Git Data API) | ✅ (Pushes API) |
136
- | Merge / Pull Requests | ✅ | ✅ | ✅ |
137
- | CI pipelines | ✅ GitLab CI | ✅ GitHub Actions | ✅ Azure 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 # builds Vite SPA → dist/
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 → apply → refine)
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.14.0",
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
- "./gitlab": "./server/forge/core/gitlabClient.js",
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 --mode lib",
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": "^4.3.0",
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": "^5.4.0",
62
- "vite-plugin-dts": "^4.3.0"
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 `fix:` prefix when the model didn't supply one.
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
- return `fix: ${analysis.issue.title.replace(CONV_PREFIX, "")}`;
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 ?? process.env.FORGE_GITLAB_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("src", "main");
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 >= MAX_FILES_TOTAL) break;
331
+ if (capped.length >= caps.total) break;
323
332
  const count = perRepo.get(entry.repo) ?? 0;
324
- if (count >= MAX_FILES_PER_REPO) continue;
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");