forge-cc 0.1.41 → 1.0.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 +454 -338
- package/dist/cli.js +194 -935
- package/dist/cli.js.map +1 -1
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.js +49 -56
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +37 -125
- package/dist/config/schema.js +13 -28
- package/dist/config/schema.js.map +1 -1
- package/dist/doctor.d.ts +10 -0
- package/dist/doctor.js +148 -0
- package/dist/doctor.js.map +1 -0
- package/dist/gates/index.d.ts +14 -12
- package/dist/gates/index.js +53 -105
- package/dist/gates/index.js.map +1 -1
- package/dist/gates/lint-gate.d.ts +2 -2
- package/dist/gates/lint-gate.js +60 -66
- package/dist/gates/lint-gate.js.map +1 -1
- package/dist/gates/tests-gate.d.ts +2 -4
- package/dist/gates/tests-gate.js +75 -203
- package/dist/gates/tests-gate.js.map +1 -1
- package/dist/gates/types-gate.d.ts +2 -2
- package/dist/gates/types-gate.js +53 -59
- package/dist/gates/types-gate.js.map +1 -1
- package/dist/linear/client.d.ts +31 -108
- package/dist/linear/client.js +88 -388
- package/dist/linear/client.js.map +1 -1
- package/dist/linear/sync.d.ts +15 -0
- package/dist/linear/sync.js +102 -0
- package/dist/linear/sync.js.map +1 -0
- package/dist/runner/loop.d.ts +4 -0
- package/dist/runner/loop.js +168 -0
- package/dist/runner/loop.js.map +1 -0
- package/dist/runner/prompt.d.ts +14 -0
- package/dist/runner/prompt.js +59 -0
- package/dist/runner/prompt.js.map +1 -0
- package/dist/runner/update.d.ts +1 -0
- package/dist/runner/update.js +72 -0
- package/dist/runner/update.js.map +1 -0
- package/dist/server.d.ts +6 -2
- package/dist/server.js +43 -101
- package/dist/server.js.map +1 -1
- package/dist/setup.d.ts +5 -0
- package/dist/setup.js +208 -0
- package/dist/setup.js.map +1 -0
- package/dist/state/cache.d.ts +3 -0
- package/dist/state/cache.js +23 -0
- package/dist/state/cache.js.map +1 -0
- package/dist/state/status.d.ts +66 -0
- package/dist/state/status.js +96 -0
- package/dist/state/status.js.map +1 -0
- package/dist/types.d.ts +46 -114
- package/dist/worktree/manager.d.ts +6 -103
- package/dist/worktree/manager.js +25 -296
- package/dist/worktree/manager.js.map +1 -1
- package/hooks/pre-commit-verify.js +109 -109
- package/package.json +3 -2
- package/skills/forge-go.md +20 -13
- package/skills/forge-setup.md +149 -388
- package/skills/forge-spec.md +367 -342
- package/skills/forge-triage.md +179 -133
- package/skills/forge-update.md +87 -93
package/dist/cli.js
CHANGED
|
@@ -1,985 +1,244 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { forgeConfigTemplate, claudeMdTemplate, lessonsMdTemplate, globalClaudeMdTemplate, gitignoreForgeLines, } from "./setup/templates.js";
|
|
13
|
-
import { loadRegistry, detectStaleSessions, deregisterSession } from "./worktree/session.js";
|
|
14
|
-
import { countPendingMilestones } from "./go/auto-chain.js";
|
|
15
|
-
import { discoverPRDs } from "./state/prd-status.js";
|
|
16
|
-
import { PRDQueue } from "./go/prd-queue.js";
|
|
17
|
-
import { getRepoRoot, cleanupStaleWorktrees, cleanupMergedBranches } from "./worktree/manager.js";
|
|
18
|
-
import { formatSessionsReport } from "./reporter/human.js";
|
|
19
|
-
const __filename_cli = fileURLToPath(import.meta.url);
|
|
20
|
-
const __dirname_cli = dirname(__filename_cli);
|
|
21
|
-
const cliPkgVersion = JSON.parse(readFileSync(join(__dirname_cli, "..", "package.json"), "utf-8")).version;
|
|
22
|
-
const program = new Command();
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import { loadConfig } from './config/loader.js';
|
|
4
|
+
import { registerGate, runPipeline } from './gates/index.js';
|
|
5
|
+
import { typesGate } from './gates/types-gate.js';
|
|
6
|
+
import { lintGate } from './gates/lint-gate.js';
|
|
7
|
+
import { testsGate } from './gates/tests-gate.js';
|
|
8
|
+
import { writeVerifyCache } from './state/cache.js';
|
|
9
|
+
import { readStatus, discoverStatuses, findNextPending } from './state/status.js';
|
|
10
|
+
import { ForgeLinearClient } from './linear/client.js';
|
|
11
|
+
import { syncMilestoneStart, syncMilestoneComplete, syncProjectDone } from './linear/sync.js';
|
|
23
12
|
program
|
|
24
|
-
.name(
|
|
25
|
-
.
|
|
26
|
-
.
|
|
13
|
+
.name('forge')
|
|
14
|
+
.version('1.0.0')
|
|
15
|
+
.description('Forge — verification harness for Claude Code agents');
|
|
27
16
|
program
|
|
28
|
-
.command(
|
|
29
|
-
.description(
|
|
30
|
-
.option(
|
|
31
|
-
.option(
|
|
32
|
-
.option("--prd <path>", "Path to PRD for acceptance criteria matching")
|
|
33
|
-
.option("--before-only", "Capture visual baseline screenshots and exit (no verification)")
|
|
34
|
-
.option("--after-only", "Run visual verification comparing against stored baseline")
|
|
17
|
+
.command('verify')
|
|
18
|
+
.description('Run verification gates')
|
|
19
|
+
.option('--gate <gates>', 'Comma-separated list of gates to run')
|
|
20
|
+
.option('--json', 'Output results as JSON')
|
|
35
21
|
.action(async (opts) => {
|
|
36
|
-
try {
|
|
37
|
-
const projectDir = process.cwd();
|
|
38
|
-
const config = loadConfig(projectDir);
|
|
39
|
-
const appDir = config.appDir ? resolve(projectDir, config.appDir) : undefined;
|
|
40
|
-
const targetDir = appDir ?? projectDir;
|
|
41
|
-
const pages = config.pages ?? ["/"];
|
|
42
|
-
// --before-only: capture baseline screenshots to disk and exit
|
|
43
|
-
if (opts.beforeOnly) {
|
|
44
|
-
console.log("Capturing visual baseline screenshots...");
|
|
45
|
-
try {
|
|
46
|
-
await captureBeforeSnapshots(targetDir, pages, {
|
|
47
|
-
devServerCommand: config.devServer?.command,
|
|
48
|
-
devServerPort: config.devServer?.port,
|
|
49
|
-
});
|
|
50
|
-
console.log(`Visual baseline captured for ${pages.length} page(s): ${pages.join(", ")}`);
|
|
51
|
-
console.log("Snapshots saved to .forge/screenshots/before/");
|
|
52
|
-
process.exit(0);
|
|
53
|
-
}
|
|
54
|
-
catch (err) {
|
|
55
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
56
|
-
console.error(`Error: Visual baseline capture failed — ${message}`);
|
|
57
|
-
process.exit(1);
|
|
58
|
-
}
|
|
59
|
-
finally {
|
|
60
|
-
try {
|
|
61
|
-
await closeBrowser();
|
|
62
|
-
}
|
|
63
|
-
catch { /* non-fatal */ }
|
|
64
|
-
}
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
// --after-only: run only the visual gate (comparison against stored baseline)
|
|
68
|
-
if (opts.afterOnly) {
|
|
69
|
-
const { verifyVisual } = await import("./gates/visual-gate.js");
|
|
70
|
-
console.log("Running visual verification against baseline...");
|
|
71
|
-
try {
|
|
72
|
-
const result = await verifyVisual(targetDir, pages, {
|
|
73
|
-
devServerCommand: config.devServer?.command,
|
|
74
|
-
devServerPort: config.devServer?.port,
|
|
75
|
-
});
|
|
76
|
-
if (opts.json) {
|
|
77
|
-
console.log(JSON.stringify(result, null, 2));
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
const status = result.passed ? "PASSED" : "FAILED";
|
|
81
|
-
console.log(`Visual gate: ${status}`);
|
|
82
|
-
if (result.warnings.length > 0) {
|
|
83
|
-
for (const w of result.warnings)
|
|
84
|
-
console.log(` Warning: ${w}`);
|
|
85
|
-
}
|
|
86
|
-
if (result.errors.length > 0) {
|
|
87
|
-
for (const e of result.errors)
|
|
88
|
-
console.log(` Error: ${e.message}`);
|
|
89
|
-
}
|
|
90
|
-
if (result.screenshots.length > 0) {
|
|
91
|
-
console.log(`Screenshots saved to .forge/screenshots/after/`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
process.exit(result.passed ? 0 : 1);
|
|
95
|
-
}
|
|
96
|
-
catch (err) {
|
|
97
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
98
|
-
console.error(`Error: Visual verification failed — ${message}`);
|
|
99
|
-
process.exit(1);
|
|
100
|
-
}
|
|
101
|
-
finally {
|
|
102
|
-
try {
|
|
103
|
-
await closeBrowser();
|
|
104
|
-
}
|
|
105
|
-
catch { /* non-fatal */ }
|
|
106
|
-
}
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
// Standard verify: run the full pipeline
|
|
110
|
-
const gates = opts.gate ? opts.gate.split(",").map((g) => g.trim()) : config.gates;
|
|
111
|
-
const prdPath = opts.prd ?? config.prdPath;
|
|
112
|
-
const result = await runPipeline({
|
|
113
|
-
projectDir,
|
|
114
|
-
appDir,
|
|
115
|
-
gates,
|
|
116
|
-
prdPath,
|
|
117
|
-
pages: config.pages,
|
|
118
|
-
maxIterations: config.maxIterations,
|
|
119
|
-
devServerCommand: config.devServer?.command,
|
|
120
|
-
devServerPort: config.devServer?.port,
|
|
121
|
-
reviewBlocking: config.review?.blocking,
|
|
122
|
-
});
|
|
123
|
-
// Generate report if pipeline didn't produce one
|
|
124
|
-
if (!result.report) {
|
|
125
|
-
result.report = formatReport(result);
|
|
126
|
-
}
|
|
127
|
-
// Write verify cache (non-fatal if this fails)
|
|
128
|
-
try {
|
|
129
|
-
writeVerifyCache(projectDir, result);
|
|
130
|
-
}
|
|
131
|
-
catch (cacheErr) {
|
|
132
|
-
const msg = cacheErr instanceof Error ? cacheErr.message : String(cacheErr);
|
|
133
|
-
console.error(`Warning: Could not write verify cache: ${msg}`);
|
|
134
|
-
}
|
|
135
|
-
// Output
|
|
136
|
-
if (opts.json) {
|
|
137
|
-
console.log(JSON.stringify(result, null, 2));
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
console.log(result.report);
|
|
141
|
-
}
|
|
142
|
-
process.exit(result.passed ? 0 : 1);
|
|
143
|
-
}
|
|
144
|
-
catch (err) {
|
|
145
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
146
|
-
console.error(`Error: forge verify failed — ${message}`);
|
|
147
|
-
process.exit(1);
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
program
|
|
151
|
-
.command("status")
|
|
152
|
-
.description("Print current project state")
|
|
153
|
-
.action(async () => {
|
|
154
22
|
const projectDir = process.cwd();
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const cache = JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
171
|
-
const status = cache.passed ? "PASSED" : "FAILED";
|
|
172
|
-
const age = Math.round((Date.now() - new Date(cache.timestamp).getTime()) / 60_000);
|
|
173
|
-
console.log(`**Last Verify:** ${status} (${age}min ago on ${cache.branch})`);
|
|
174
|
-
for (const gate of cache.gates) {
|
|
175
|
-
const icon = gate.passed ? "[x]" : "[ ]";
|
|
176
|
-
console.log(` - ${icon} ${gate.gate}: ${gate.passed ? "PASS" : "FAIL"}`);
|
|
177
|
-
}
|
|
23
|
+
const config = await loadConfig(projectDir);
|
|
24
|
+
// Register default gates
|
|
25
|
+
registerGate(typesGate);
|
|
26
|
+
registerGate(lintGate);
|
|
27
|
+
registerGate(testsGate);
|
|
28
|
+
// Filter gates if --gate flag provided
|
|
29
|
+
if (opts.gate) {
|
|
30
|
+
const requested = opts.gate.split(',').map((g) => g.trim());
|
|
31
|
+
config.gates = requested;
|
|
32
|
+
}
|
|
33
|
+
const pipeline = await runPipeline(config, projectDir);
|
|
34
|
+
// Write cache
|
|
35
|
+
await writeVerifyCache(projectDir, pipeline);
|
|
36
|
+
if (opts.json) {
|
|
37
|
+
console.log(JSON.stringify(pipeline, null, 2));
|
|
178
38
|
}
|
|
179
39
|
else {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
console.log(`**Config:** auto-detected (no .forge.json)`);
|
|
190
|
-
}
|
|
191
|
-
// Sessions
|
|
192
|
-
try {
|
|
193
|
-
const repoRoot = getRepoRoot(projectDir);
|
|
194
|
-
detectStaleSessions(repoRoot);
|
|
195
|
-
const registry = loadRegistry(repoRoot);
|
|
196
|
-
if (registry.sessions.length > 0) {
|
|
197
|
-
console.log("");
|
|
198
|
-
console.log(formatSessionsReport(registry.sessions));
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
catch {
|
|
202
|
-
// Not a git repo or no session registry — skip silently
|
|
203
|
-
}
|
|
204
|
-
// Per-PRD status
|
|
205
|
-
try {
|
|
206
|
-
const prds = await discoverPRDs(projectDir);
|
|
207
|
-
if (prds.length > 0) {
|
|
208
|
-
console.log("");
|
|
209
|
-
console.log("### PRDs");
|
|
210
|
-
for (const prd of prds) {
|
|
211
|
-
const milestones = Object.entries(prd.status.milestones);
|
|
212
|
-
const complete = milestones.filter(([, m]) => m.status === "complete").length;
|
|
213
|
-
const total = milestones.length;
|
|
214
|
-
console.log(`- **${prd.slug}** (${prd.status.branch}): ${complete}/${total} milestones complete`);
|
|
40
|
+
// Human-readable output
|
|
41
|
+
for (const gate of pipeline.gates) {
|
|
42
|
+
const status = gate.passed ? 'PASS' : 'FAIL';
|
|
43
|
+
console.log(`${gate.gate}: ${status} (${gate.durationMs}ms)`);
|
|
44
|
+
for (const err of gate.errors) {
|
|
45
|
+
const loc = err.file ? `${err.file}:${err.line}` : '(no file)';
|
|
46
|
+
console.log(` - ${loc}: ${err.message}`);
|
|
215
47
|
}
|
|
216
48
|
}
|
|
49
|
+
console.log(`\nResult: ${pipeline.result} (${pipeline.durationMs}ms)`);
|
|
217
50
|
}
|
|
218
|
-
|
|
51
|
+
process.exit(pipeline.result === 'PASSED' ? 0 : 1);
|
|
219
52
|
});
|
|
220
|
-
// ── Skill installation helper ──────────────────────────────────────
|
|
221
|
-
function getPackageRoot() {
|
|
222
|
-
return resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
223
|
-
}
|
|
224
|
-
function installSkills() {
|
|
225
|
-
const skillsDir = join(getPackageRoot(), "skills");
|
|
226
|
-
const targetDir = join(homedir(), ".claude", "commands", "forge");
|
|
227
|
-
mkdirSync(targetDir, { recursive: true });
|
|
228
|
-
const installed = [];
|
|
229
|
-
const files = readdirSync(skillsDir).filter((f) => f.startsWith("forge-") && f.endsWith(".md"));
|
|
230
|
-
for (const file of files) {
|
|
231
|
-
const targetName = file.replace(/^forge-/, "");
|
|
232
|
-
copyFileSync(join(skillsDir, file), join(targetDir, targetName));
|
|
233
|
-
installed.push(targetName);
|
|
234
|
-
}
|
|
235
|
-
return installed;
|
|
236
|
-
}
|
|
237
|
-
// ── setup command ──────────────────────────────────────────────────
|
|
238
53
|
program
|
|
239
|
-
.command(
|
|
240
|
-
.description(
|
|
241
|
-
.
|
|
242
|
-
.option("--with-visual", "Auto-install Playwright without prompting")
|
|
243
|
-
.option("--skip-deps", "Skip optional dependency checks")
|
|
54
|
+
.command('run')
|
|
55
|
+
.description('Execute milestones via Ralph loop')
|
|
56
|
+
.requiredOption('--prd <slug>', 'PRD slug to execute')
|
|
244
57
|
.action(async (opts) => {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
console.log(`Installed ${installed.length} skills to ~/.claude/commands/forge/`);
|
|
248
|
-
for (const s of installed) {
|
|
249
|
-
console.log(` - ${s}`);
|
|
250
|
-
}
|
|
251
|
-
if (opts.skillsOnly) {
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
// Optional dependency check
|
|
255
|
-
if (!opts.skipDeps) {
|
|
256
|
-
const checks = await checkEnvironment();
|
|
257
|
-
const playwrightCheck = checks.find((c) => c.name === "Playwright");
|
|
258
|
-
const playwrightMissing = playwrightCheck?.status !== "ok";
|
|
259
|
-
console.log("\n## Environment\n");
|
|
260
|
-
for (const check of checks) {
|
|
261
|
-
if (check.status === "ok") {
|
|
262
|
-
const ver = check.version ? ` ${check.version}` : "";
|
|
263
|
-
const extra = check.detail ? ` (${check.detail})` : "";
|
|
264
|
-
console.log(` \u2713 ${check.name}${ver}${extra}`);
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
const msg = check.message ? ` \u2014 ${check.message}` : "";
|
|
268
|
-
console.log(` \u2717 ${check.name}${msg}`);
|
|
269
|
-
if (check.fix) {
|
|
270
|
-
console.log(` \u2192 ${check.fix}`);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
console.log("");
|
|
275
|
-
if (playwrightMissing) {
|
|
276
|
-
let shouldInstall = false;
|
|
277
|
-
if (opts.withVisual) {
|
|
278
|
-
shouldInstall = true;
|
|
279
|
-
}
|
|
280
|
-
else if (process.stdout.isTTY) {
|
|
281
|
-
shouldInstall = await askYesNo("Playwright enables visual regression + runtime testing. Install now? (Y/n): ");
|
|
282
|
-
}
|
|
283
|
-
if (shouldInstall) {
|
|
284
|
-
console.log("\nInstalling Playwright + Chromium...\n");
|
|
285
|
-
try {
|
|
286
|
-
execSync("npm install -g playwright && npx playwright install chromium", {
|
|
287
|
-
stdio: "inherit",
|
|
288
|
-
});
|
|
289
|
-
console.log("\nPlaywright installed successfully.");
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
console.error("\nPlaywright installation failed. Run manually:\n npm install -g playwright && npx playwright install chromium");
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
// Check if project already initialized
|
|
298
|
-
const projectDir = process.cwd();
|
|
299
|
-
if (existsSync(join(projectDir, ".forge.json"))) {
|
|
300
|
-
console.log("\nProject already initialized. Run `/forge:setup` to refresh.");
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
// Scaffold project files
|
|
304
|
-
const projectName = basename(projectDir);
|
|
305
|
-
const ctx = {
|
|
306
|
-
projectName,
|
|
307
|
-
techStack: "TypeScript, Node.js",
|
|
308
|
-
description: "Project description — customize in CLAUDE.md",
|
|
309
|
-
gates: ["types", "lint", "tests"],
|
|
310
|
-
date: new Date().toISOString().split("T")[0],
|
|
311
|
-
};
|
|
312
|
-
mkdirSync(join(projectDir, ".planning"), { recursive: true });
|
|
313
|
-
mkdirSync(join(projectDir, "tasks"), { recursive: true });
|
|
314
|
-
writeFileSync(join(projectDir, ".forge.json"), forgeConfigTemplate(ctx));
|
|
315
|
-
writeFileSync(join(projectDir, "CLAUDE.md"), claudeMdTemplate(ctx));
|
|
316
|
-
writeFileSync(join(projectDir, "tasks", "lessons.md"), lessonsMdTemplate(ctx));
|
|
317
|
-
// Append to .gitignore
|
|
318
|
-
const gitignorePath = join(projectDir, ".gitignore");
|
|
319
|
-
const forgeLines = gitignoreForgeLines();
|
|
320
|
-
if (existsSync(gitignorePath)) {
|
|
321
|
-
const content = readFileSync(gitignorePath, "utf-8");
|
|
322
|
-
if (!content.includes(".forge/")) {
|
|
323
|
-
writeFileSync(gitignorePath, content + "\n" + forgeLines);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
else {
|
|
327
|
-
writeFileSync(gitignorePath, forgeLines);
|
|
328
|
-
}
|
|
329
|
-
// Create global CLAUDE.md if needed
|
|
330
|
-
const globalClaudeMdPath = join(homedir(), ".claude", "CLAUDE.md");
|
|
331
|
-
if (!existsSync(globalClaudeMdPath)) {
|
|
332
|
-
mkdirSync(dirname(globalClaudeMdPath), { recursive: true });
|
|
333
|
-
writeFileSync(globalClaudeMdPath, globalClaudeMdTemplate());
|
|
334
|
-
console.log("\nCreated ~/.claude/CLAUDE.md");
|
|
335
|
-
}
|
|
336
|
-
console.log(`\n## Forge Setup Complete`);
|
|
337
|
-
console.log(`**Project:** ${projectName}`);
|
|
338
|
-
console.log(`**Gates:** ${ctx.gates.join(", ")}`);
|
|
339
|
-
console.log(`\nFiles created:`);
|
|
340
|
-
console.log(` - .forge.json`);
|
|
341
|
-
console.log(` - CLAUDE.md`);
|
|
342
|
-
console.log(` - tasks/lessons.md`);
|
|
343
|
-
console.log(` - .gitignore (forge lines)`);
|
|
344
|
-
console.log(`\nNext: Review CLAUDE.md, then run \`npx forge verify\``);
|
|
58
|
+
const { runRalphLoop } = await import('./runner/loop.js');
|
|
59
|
+
await runRalphLoop({ slug: opts.prd, projectDir: process.cwd() });
|
|
345
60
|
});
|
|
346
|
-
// ── update command ─────────────────────────────────────────────────
|
|
347
61
|
program
|
|
348
|
-
.command(
|
|
349
|
-
.description(
|
|
350
|
-
.action(() => {
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
let latestVersion;
|
|
356
|
-
try {
|
|
357
|
-
latestVersion = execSync("npm view forge-cc version", {
|
|
358
|
-
encoding: "utf-8",
|
|
359
|
-
}).trim();
|
|
360
|
-
}
|
|
361
|
-
catch {
|
|
362
|
-
console.error("Could not reach npm registry. Check your internet connection.");
|
|
363
|
-
process.exit(1);
|
|
364
|
-
}
|
|
365
|
-
console.log(`## Forge Version Check\n`);
|
|
366
|
-
console.log(`**Installed:** v${currentVersion}`);
|
|
367
|
-
console.log(`**Latest:** v${latestVersion}`);
|
|
368
|
-
if (currentVersion === latestVersion) {
|
|
369
|
-
console.log(`**Status:** Up to date\n`);
|
|
370
|
-
console.log("You're on the latest version.");
|
|
62
|
+
.command('status')
|
|
63
|
+
.description('Show PRD progress')
|
|
64
|
+
.action(async () => {
|
|
65
|
+
const projectDir = process.cwd();
|
|
66
|
+
const statuses = await discoverStatuses(projectDir);
|
|
67
|
+
if (statuses.length === 0) {
|
|
68
|
+
console.log('No PRD status files found.');
|
|
371
69
|
return;
|
|
372
70
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
71
|
+
const pending = findNextPending(statuses);
|
|
72
|
+
const pendingMap = new Map(pending.map((p) => [p.slug, p.milestone]));
|
|
73
|
+
// Calculate column widths
|
|
74
|
+
const rows = statuses.map((s) => {
|
|
75
|
+
const keys = Object.keys(s.milestones);
|
|
76
|
+
const complete = keys.filter((k) => s.milestones[k].status === 'complete').length;
|
|
77
|
+
const total = keys.length;
|
|
78
|
+
const next = pendingMap.get(s.slug);
|
|
79
|
+
const linearState = s.linearProjectId ? 'linked' : '-';
|
|
80
|
+
return {
|
|
81
|
+
project: s.project,
|
|
82
|
+
branch: s.branch,
|
|
83
|
+
progress: `${complete}/${total}`,
|
|
84
|
+
next: complete === total ? '(done)' : next ?? '-',
|
|
85
|
+
linear: linearState,
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
const headers = { project: 'Project', branch: 'Branch', progress: 'Progress', next: 'Next', linear: 'Linear' };
|
|
89
|
+
const cols = Object.keys(headers).map((key) => {
|
|
90
|
+
const max = Math.max(headers[key].length, ...rows.map((r) => r[key].length));
|
|
91
|
+
return { key, width: max };
|
|
92
|
+
});
|
|
93
|
+
const headerLine = cols.map((c) => headers[c.key].padEnd(c.width)).join(' ');
|
|
94
|
+
console.log(headerLine);
|
|
95
|
+
console.log(cols.map((c) => '-'.repeat(c.width)).join(' '));
|
|
96
|
+
for (const row of rows) {
|
|
97
|
+
console.log(cols.map((c) => row[c.key].padEnd(c.width)).join(' '));
|
|
390
98
|
}
|
|
391
99
|
});
|
|
392
|
-
// ── cleanup command ────────────────────────────────────────────────
|
|
393
100
|
program
|
|
394
|
-
.command(
|
|
395
|
-
.description(
|
|
396
|
-
.
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
catch {
|
|
402
|
-
console.error("Error: not a git repository. Run this from inside a git project.");
|
|
403
|
-
process.exit(1);
|
|
404
|
-
return; // unreachable but helps TypeScript narrow
|
|
405
|
-
}
|
|
406
|
-
console.log("## Forge Cleanup\n");
|
|
407
|
-
// Detect and mark stale sessions (mutates registry)
|
|
408
|
-
detectStaleSessions(repoRoot);
|
|
409
|
-
// Load registry and filter for stale sessions
|
|
410
|
-
const registry = loadRegistry(repoRoot);
|
|
411
|
-
const staleSessions = registry.sessions.filter((s) => s.status === "stale");
|
|
412
|
-
if (staleSessions.length === 0) {
|
|
413
|
-
console.log("No stale sessions found.");
|
|
414
|
-
}
|
|
415
|
-
else {
|
|
416
|
-
console.log(`Found ${staleSessions.length} stale session${staleSessions.length === 1 ? "" : "s"}.\n`);
|
|
417
|
-
// Remove worktrees
|
|
418
|
-
const result = cleanupStaleWorktrees(repoRoot, staleSessions);
|
|
419
|
-
// Deregister successfully removed sessions and print results
|
|
420
|
-
for (const removed of result.removed) {
|
|
421
|
-
deregisterSession(repoRoot, removed.sessionId);
|
|
422
|
-
console.log(`- Removed: ${removed.sessionId} (${removed.branch}) — worktree deleted`);
|
|
423
|
-
}
|
|
424
|
-
for (const err of result.errors) {
|
|
425
|
-
console.log(`- Error: ${err.sessionId} — ${err.error}`);
|
|
426
|
-
}
|
|
427
|
-
// Summary
|
|
428
|
-
const cleanedCount = result.removed.length;
|
|
429
|
-
const errorCount = result.errors.length;
|
|
430
|
-
console.log("");
|
|
431
|
-
if (errorCount === 0) {
|
|
432
|
-
console.log(`Cleaned up ${cleanedCount} session${cleanedCount === 1 ? "" : "s"}.`);
|
|
433
|
-
}
|
|
434
|
-
else {
|
|
435
|
-
console.log(`Cleaned up ${cleanedCount} session${cleanedCount === 1 ? "" : "s"}, ${errorCount} error${errorCount === 1 ? "" : "s"}.`);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
// Clean up branches whose remote tracking branch is gone (PR merged)
|
|
439
|
-
const branchResult = cleanupMergedBranches(repoRoot);
|
|
440
|
-
if (branchResult.deleted.length > 0) {
|
|
441
|
-
console.log(`\nDeleted ${branchResult.deleted.length} merged branch${branchResult.deleted.length === 1 ? "" : "es"}:`);
|
|
442
|
-
for (const branch of branchResult.deleted) {
|
|
443
|
-
console.log(` - ${branch}`);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
if (branchResult.errors.length > 0) {
|
|
447
|
-
for (const err of branchResult.errors) {
|
|
448
|
-
console.log(` - Error deleting ${err.branch}: ${err.error}`);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
101
|
+
.command('setup')
|
|
102
|
+
.description('Initialize forge for a project')
|
|
103
|
+
.option('--skills-only', 'Only sync skill files')
|
|
104
|
+
.action(async (opts) => {
|
|
105
|
+
const { runSetup } = await import('./setup.js');
|
|
106
|
+
await runSetup({ projectDir: process.cwd(), skillsOnly: opts.skillsOnly });
|
|
451
107
|
});
|
|
452
|
-
// ── linear-sync command ────────────────────────────────────────────
|
|
453
108
|
const linearSync = program
|
|
454
|
-
.command(
|
|
455
|
-
.description(
|
|
109
|
+
.command('linear-sync')
|
|
110
|
+
.description('Sync milestone state with Linear');
|
|
456
111
|
linearSync
|
|
457
|
-
.command(
|
|
458
|
-
.description(
|
|
459
|
-
.requiredOption(
|
|
460
|
-
.requiredOption(
|
|
112
|
+
.command('start')
|
|
113
|
+
.description('Start a milestone sync')
|
|
114
|
+
.requiredOption('--slug <slug>', 'PRD slug')
|
|
115
|
+
.requiredOption('--milestone <n>', 'Milestone number')
|
|
461
116
|
.action(async (opts) => {
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (result) {
|
|
467
|
-
console.log(JSON.stringify(result, null, 2));
|
|
117
|
+
const apiKey = process.env.LINEAR_API_KEY;
|
|
118
|
+
if (!apiKey) {
|
|
119
|
+
console.log('[forge] LINEAR_API_KEY not set, skipping Linear sync');
|
|
120
|
+
return;
|
|
468
121
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const milestoneNumber = parseInt(opts.milestone, 10);
|
|
481
|
-
const isLastMilestone = opts.last === true;
|
|
482
|
-
const result = await cliSyncComplete(projectDir, opts.slug, milestoneNumber, isLastMilestone, opts.prUrl);
|
|
483
|
-
if (result) {
|
|
484
|
-
console.log(JSON.stringify(result, null, 2));
|
|
122
|
+
try {
|
|
123
|
+
const projectDir = process.cwd();
|
|
124
|
+
const status = await readStatus(projectDir, opts.slug);
|
|
125
|
+
if (!status.linearTeamId) {
|
|
126
|
+
console.log('[forge] No linearTeamId in status file, skipping Linear sync');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const client = new ForgeLinearClient({ apiKey, teamId: status.linearTeamId });
|
|
130
|
+
const config = await loadConfig(projectDir);
|
|
131
|
+
await syncMilestoneStart(client, config, status, opts.milestone);
|
|
132
|
+
console.log(`[forge] linear-sync start complete for ${opts.slug} ${opts.milestone}`);
|
|
485
133
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
.command("list-issues")
|
|
489
|
-
.description("List all Linear issue identifiers for a project")
|
|
490
|
-
.requiredOption("--slug <slug>", "PRD slug")
|
|
491
|
-
.action(async (opts) => {
|
|
492
|
-
const { cliFetchIssueIdentifiers } = await import("./go/linear-sync-cli.js");
|
|
493
|
-
const projectDir = process.cwd();
|
|
494
|
-
const result = await cliFetchIssueIdentifiers(projectDir, opts.slug);
|
|
495
|
-
if (result) {
|
|
496
|
-
console.log(JSON.stringify(result.identifiers));
|
|
134
|
+
catch (err) {
|
|
135
|
+
console.warn('[forge] linear-sync start failed:', err);
|
|
497
136
|
}
|
|
498
137
|
});
|
|
499
138
|
linearSync
|
|
500
|
-
.command(
|
|
501
|
-
.description(
|
|
502
|
-
.requiredOption(
|
|
139
|
+
.command('complete')
|
|
140
|
+
.description('Complete a milestone sync')
|
|
141
|
+
.requiredOption('--slug <slug>', 'PRD slug')
|
|
142
|
+
.requiredOption('--milestone <n>', 'Milestone number')
|
|
143
|
+
.option('--last', 'This is the last milestone')
|
|
144
|
+
.option('--pr-url <url>', 'PR URL to include in comments')
|
|
503
145
|
.action(async (opts) => {
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
console.log(JSON.stringify(result, null, 2));
|
|
146
|
+
const apiKey = process.env.LINEAR_API_KEY;
|
|
147
|
+
if (!apiKey) {
|
|
148
|
+
console.log('[forge] LINEAR_API_KEY not set, skipping Linear sync');
|
|
149
|
+
return;
|
|
509
150
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
.requiredOption("--repo <repo>", "GitHub repository name")
|
|
517
|
-
.requiredOption("--pr <number>", "Pull request number", parseInt)
|
|
518
|
-
.option("--interval <ms>", "Poll interval in milliseconds", parseInt, 60_000)
|
|
519
|
-
.option("--max-polls <n>", "Maximum number of polls", parseInt, 8)
|
|
520
|
-
.action(async (opts) => {
|
|
521
|
-
const { pollForCodexComments } = await import("./gates/codex-gate.js");
|
|
522
|
-
const maxPolls = opts.maxPolls;
|
|
523
|
-
const intervalMs = opts.interval;
|
|
524
|
-
const total = Math.ceil((maxPolls * intervalMs) / 1000);
|
|
525
|
-
process.stderr.write(`Polling for Codex comments on ${opts.owner}/${opts.repo}#${opts.pr} ` +
|
|
526
|
-
`(${maxPolls} polls, ${intervalMs / 1000}s interval, ~${total}s max)\n`);
|
|
527
|
-
const comments = await pollForCodexComments({
|
|
528
|
-
owner: opts.owner,
|
|
529
|
-
repo: opts.repo,
|
|
530
|
-
prNumber: opts.pr,
|
|
531
|
-
pollIntervalMs: intervalMs,
|
|
532
|
-
maxPolls,
|
|
533
|
-
projectDir: process.cwd(),
|
|
534
|
-
});
|
|
535
|
-
process.stderr.write(comments.length > 0
|
|
536
|
-
? `Found ${comments.length} comment(s)\n`
|
|
537
|
-
: `No comments found after ${maxPolls} polls\n`);
|
|
538
|
-
console.log(JSON.stringify(comments));
|
|
539
|
-
});
|
|
540
|
-
// ── doctor command ─────────────────────────────────────────────────
|
|
541
|
-
program
|
|
542
|
-
.command("doctor")
|
|
543
|
-
.description("Check environment health and optional dependency status")
|
|
544
|
-
.action(async () => {
|
|
545
|
-
const checks = await checkEnvironment();
|
|
546
|
-
console.log("## Forge Environment\n");
|
|
547
|
-
for (const check of checks) {
|
|
548
|
-
if (check.status === "ok") {
|
|
549
|
-
const ver = check.version ? ` ${check.version}` : "";
|
|
550
|
-
const extra = check.detail ? ` (${check.detail})` : "";
|
|
551
|
-
console.log(` \u2713 ${check.name}${ver}${extra}`);
|
|
552
|
-
}
|
|
553
|
-
else {
|
|
554
|
-
const msg = check.message ? ` \u2014 ${check.message}` : "";
|
|
555
|
-
console.log(` \u2717 ${check.name}${msg}`);
|
|
556
|
-
if (check.fix) {
|
|
557
|
-
console.log(` \u2192 ${check.fix}`);
|
|
558
|
-
}
|
|
151
|
+
try {
|
|
152
|
+
const projectDir = process.cwd();
|
|
153
|
+
const status = await readStatus(projectDir, opts.slug);
|
|
154
|
+
if (!status.linearTeamId) {
|
|
155
|
+
console.log('[forge] No linearTeamId in status file, skipping Linear sync');
|
|
156
|
+
return;
|
|
559
157
|
}
|
|
158
|
+
const client = new ForgeLinearClient({ apiKey, teamId: status.linearTeamId });
|
|
159
|
+
const config = await loadConfig(projectDir);
|
|
160
|
+
await syncMilestoneComplete(client, config, status, opts.milestone, !!opts.last);
|
|
161
|
+
console.log(`[forge] linear-sync complete finished for ${opts.slug} ${opts.milestone}`);
|
|
560
162
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
if (issues.length === 0) {
|
|
564
|
-
console.log("All checks passed.");
|
|
565
|
-
process.exit(0);
|
|
566
|
-
}
|
|
567
|
-
else {
|
|
568
|
-
console.log(`${issues.length} issue${issues.length === 1 ? "" : "s"} found. Run the commands above to fix.`);
|
|
569
|
-
process.exit(1);
|
|
163
|
+
catch (err) {
|
|
164
|
+
console.warn('[forge] linear-sync complete failed:', err);
|
|
570
165
|
}
|
|
571
166
|
});
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
// Pass prompt as CLI argument (not stdin) — matches Ralphy pattern
|
|
577
|
-
const child = spawn("claude", [
|
|
578
|
-
"-p",
|
|
579
|
-
"--dangerously-skip-permissions",
|
|
580
|
-
"--output-format",
|
|
581
|
-
"stream-json",
|
|
582
|
-
"--verbose",
|
|
583
|
-
prompt,
|
|
584
|
-
], { stdio: ["ignore", "pipe", "inherit"], cwd, env });
|
|
585
|
-
let buffer = "";
|
|
586
|
-
let finalResult;
|
|
587
|
-
child.stdout.on("data", (chunk) => {
|
|
588
|
-
buffer += chunk.toString();
|
|
589
|
-
const lines = buffer.split("\n");
|
|
590
|
-
buffer = lines.pop() ?? ""; // keep incomplete last line
|
|
591
|
-
for (const line of lines) {
|
|
592
|
-
if (!line.trim())
|
|
593
|
-
continue;
|
|
594
|
-
let evt;
|
|
595
|
-
try {
|
|
596
|
-
evt = JSON.parse(line);
|
|
597
|
-
}
|
|
598
|
-
catch {
|
|
599
|
-
continue;
|
|
600
|
-
}
|
|
601
|
-
if (evt.type === "assistant" && evt.message?.content) {
|
|
602
|
-
for (const block of evt.message.content) {
|
|
603
|
-
if (block.type === "text" && block.text) {
|
|
604
|
-
process.stdout.write(block.text);
|
|
605
|
-
}
|
|
606
|
-
else if (block.type === "tool_use" && block.name) {
|
|
607
|
-
const summary = formatToolInput(block.name, block.input);
|
|
608
|
-
process.stdout.write(` [${block.name}] ${summary}\n`);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
else if (evt.type === "result") {
|
|
613
|
-
finalResult = evt.result;
|
|
614
|
-
const turns = evt.num_turns ?? 0;
|
|
615
|
-
const cost = evt.total_cost_usd
|
|
616
|
-
? `$${evt.total_cost_usd.toFixed(2)}`
|
|
617
|
-
: "";
|
|
618
|
-
const dur = evt.duration_ms
|
|
619
|
-
? `${Math.round(evt.duration_ms / 1000)}s`
|
|
620
|
-
: "";
|
|
621
|
-
const parts = [
|
|
622
|
-
`${turns} turns`,
|
|
623
|
-
dur,
|
|
624
|
-
cost,
|
|
625
|
-
].filter(Boolean);
|
|
626
|
-
console.log(`\n--- Session complete (${parts.join(", ")}) ---`);
|
|
627
|
-
if (evt.is_error) {
|
|
628
|
-
console.error("Session ended with error.");
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
});
|
|
633
|
-
child.on("close", (code) => {
|
|
634
|
-
resolve({ exitCode: code ?? 1, result: finalResult });
|
|
635
|
-
});
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
function formatToolInput(name, input) {
|
|
639
|
-
if (!input)
|
|
640
|
-
return "";
|
|
641
|
-
switch (name) {
|
|
642
|
-
case "Read":
|
|
643
|
-
return String(input.file_path ?? "");
|
|
644
|
-
case "Write":
|
|
645
|
-
return String(input.file_path ?? "");
|
|
646
|
-
case "Edit":
|
|
647
|
-
return String(input.file_path ?? "");
|
|
648
|
-
case "Bash":
|
|
649
|
-
return String(input.command ?? "").substring(0, 120);
|
|
650
|
-
case "Glob":
|
|
651
|
-
return String(input.pattern ?? "");
|
|
652
|
-
case "Grep":
|
|
653
|
-
return String(input.pattern ?? "");
|
|
654
|
-
case "Skill":
|
|
655
|
-
return `${input.skill ?? ""}${input.args ? " " + input.args : ""}`;
|
|
656
|
-
case "TeamCreate":
|
|
657
|
-
return String(input.team_name ?? "");
|
|
658
|
-
case "TeamDelete":
|
|
659
|
-
return "";
|
|
660
|
-
case "SendMessage":
|
|
661
|
-
return `→ ${input.recipient ?? "all"}: ${String(input.summary ?? "").substring(0, 80)}`;
|
|
662
|
-
case "Task":
|
|
663
|
-
return String(input.description ?? "").substring(0, 80);
|
|
664
|
-
case "TaskUpdate":
|
|
665
|
-
return `#${input.taskId ?? ""} → ${input.status ?? ""}`;
|
|
666
|
-
default:
|
|
667
|
-
return JSON.stringify(input).substring(0, 100);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
// ── run command ────────────────────────────────────────────────────
|
|
671
|
-
program
|
|
672
|
-
.command("run")
|
|
673
|
-
.description("Execute all remaining milestones autonomously in fresh Claude sessions (Ralph Loop pattern)")
|
|
674
|
-
.option("--max-iterations <n>", "Maximum iterations before stopping (safety cap)", "20")
|
|
675
|
-
.option("--prd <slug>", "Run milestones for a specific PRD")
|
|
676
|
-
.option("--all", "Run all PRDs with pending milestones (parallel worktrees for independent PRDs)")
|
|
167
|
+
linearSync
|
|
168
|
+
.command('done')
|
|
169
|
+
.description('Mark project as done in Linear')
|
|
170
|
+
.requiredOption('--slug <slug>', 'PRD slug')
|
|
677
171
|
.action(async (opts) => {
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
const prds = await discoverPRDs(projectDir);
|
|
682
|
-
if (prds.length === 0) {
|
|
683
|
-
console.error("Error: No PRD status files found in .planning/status/. Run /forge:spec first.");
|
|
684
|
-
process.exit(1);
|
|
685
|
-
}
|
|
686
|
-
// --all mode: run all PRDs with pending milestones
|
|
687
|
-
if (opts.all) {
|
|
688
|
-
const queue = new PRDQueue(projectDir);
|
|
689
|
-
const readyPRDs = await queue.getReadyPRDs();
|
|
690
|
-
if (readyPRDs.length === 0) {
|
|
691
|
-
console.log("All PRDs complete! Nothing to run.");
|
|
692
|
-
console.log('Create a PR with `gh pr create` or run `/forge:spec` to start a new project.');
|
|
693
|
-
process.exit(0);
|
|
694
|
-
}
|
|
695
|
-
console.log("## Forge Multi-PRD Auto-Chain\n");
|
|
696
|
-
console.log(`**PRDs with pending milestones:** ${readyPRDs.length}`);
|
|
697
|
-
console.log(`**Max iterations per PRD:** ${maxIterations}`);
|
|
698
|
-
console.log(`**Stop:** Ctrl+C\n`);
|
|
699
|
-
// Display per-PRD status
|
|
700
|
-
console.log("### PRD Queue");
|
|
701
|
-
for (const entry of readyPRDs) {
|
|
702
|
-
const next = entry.nextMilestone !== null ? `next: M${entry.nextMilestone}` : "none pending";
|
|
703
|
-
console.log(`- **${entry.slug}** (${entry.branch}): ${entry.pendingMilestones} pending, ${next}`);
|
|
704
|
-
}
|
|
705
|
-
console.log("");
|
|
706
|
-
// Run each PRD sequentially (each PRD runs its milestones in order)
|
|
707
|
-
for (const entry of readyPRDs) {
|
|
708
|
-
console.log(`\n--- Running PRD: ${entry.slug} ---\n`);
|
|
709
|
-
let prdPending = entry.pendingMilestones;
|
|
710
|
-
const prompt = [
|
|
711
|
-
"You are executing one milestone of a forge auto-chain.",
|
|
712
|
-
`Use the Skill tool: skill="forge:go", args="--single --prd ${entry.slug}"`,
|
|
713
|
-
"After the skill completes, stop.",
|
|
714
|
-
].join("\n");
|
|
715
|
-
for (let i = 0; i < maxIterations && prdPending > 0; i++) {
|
|
716
|
-
const iteration = i + 1;
|
|
717
|
-
console.log(`\n=== ${entry.slug} — Iteration ${iteration} (${prdPending} milestones remaining) ===\n`);
|
|
718
|
-
const { exitCode } = await runClaudeSession(prompt, projectDir);
|
|
719
|
-
if (exitCode !== 0) {
|
|
720
|
-
console.error(`\nError: Claude session for ${entry.slug} exited with code ${exitCode}. Skipping to next PRD.`);
|
|
721
|
-
break;
|
|
722
|
-
}
|
|
723
|
-
const newPending = await countPendingMilestones(projectDir, entry.slug);
|
|
724
|
-
if (newPending === 0) {
|
|
725
|
-
console.log(`\n${entry.slug}: All milestones complete!`);
|
|
726
|
-
break;
|
|
727
|
-
}
|
|
728
|
-
if (newPending >= prdPending) {
|
|
729
|
-
console.error(`\nStall detected for ${entry.slug}: pending count did not decrease (was ${prdPending}, now ${newPending}). Skipping to next PRD.`);
|
|
730
|
-
break;
|
|
731
|
-
}
|
|
732
|
-
prdPending = newPending;
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
// Final summary
|
|
736
|
-
console.log("\n---\n");
|
|
737
|
-
console.log("## Multi-PRD Run Summary\n");
|
|
738
|
-
const allEntries = await queue.scanPRDs();
|
|
739
|
-
for (const entry of allEntries) {
|
|
740
|
-
const status = entry.pendingMilestones === 0 ? "COMPLETE" : `${entry.pendingMilestones} pending`;
|
|
741
|
-
console.log(`- **${entry.slug}**: ${status}`);
|
|
742
|
-
}
|
|
743
|
-
const totalPending = allEntries.reduce((sum, e) => sum + e.pendingMilestones, 0);
|
|
744
|
-
if (totalPending === 0) {
|
|
745
|
-
console.log("\nAll PRDs complete!");
|
|
746
|
-
}
|
|
747
|
-
else {
|
|
748
|
-
console.log(`\n${totalPending} milestone${totalPending === 1 ? "" : "s"} remaining across all PRDs.`);
|
|
749
|
-
}
|
|
750
|
-
process.exit(totalPending === 0 ? 0 : 1);
|
|
172
|
+
const apiKey = process.env.LINEAR_API_KEY;
|
|
173
|
+
if (!apiKey) {
|
|
174
|
+
console.log('[forge] LINEAR_API_KEY not set, skipping Linear sync');
|
|
751
175
|
return;
|
|
752
176
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
process.exit(1);
|
|
177
|
+
try {
|
|
178
|
+
const projectDir = process.cwd();
|
|
179
|
+
const status = await readStatus(projectDir, opts.slug);
|
|
180
|
+
if (!status.linearTeamId) {
|
|
181
|
+
console.log('[forge] No linearTeamId in status file, skipping Linear sync');
|
|
182
|
+
return;
|
|
760
183
|
}
|
|
184
|
+
const client = new ForgeLinearClient({ apiKey, teamId: status.linearTeamId });
|
|
185
|
+
const config = await loadConfig(projectDir);
|
|
186
|
+
await syncProjectDone(client, config, status);
|
|
187
|
+
console.log(`[forge] linear-sync done complete for ${opts.slug}`);
|
|
761
188
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
if (pending === 0) {
|
|
765
|
-
console.log("All milestones complete! Nothing to run.");
|
|
766
|
-
console.log('Create a PR with `gh pr create` or run `/forge:spec` to start a new project.');
|
|
767
|
-
process.exit(0);
|
|
768
|
-
}
|
|
769
|
-
// Banner
|
|
770
|
-
console.log("## Forge Auto-Chain (Ralph Loop)\n");
|
|
771
|
-
console.log(`**Milestones remaining:** ${pending}`);
|
|
772
|
-
console.log(`**Max iterations:** ${maxIterations}`);
|
|
773
|
-
console.log(`**Stop:** Ctrl+C\n`);
|
|
774
|
-
console.log("Each milestone runs in a fresh Claude session with full /forge:go pipeline.");
|
|
775
|
-
console.log("Output streams inline below.\n");
|
|
776
|
-
console.log("---\n");
|
|
777
|
-
const skillArgs = opts.prd ? `--single --prd ${opts.prd}` : "--single";
|
|
778
|
-
const prompt = [
|
|
779
|
-
"You are executing one milestone of a forge auto-chain.",
|
|
780
|
-
`Use the Skill tool: skill="forge:go", args="${skillArgs}"`,
|
|
781
|
-
"After the skill completes, stop.",
|
|
782
|
-
].join("\n");
|
|
783
|
-
for (let i = 0; i < maxIterations; i++) {
|
|
784
|
-
const iteration = i + 1;
|
|
785
|
-
console.log(`\n=== Iteration ${iteration} (${pending} milestones remaining) ===\n`);
|
|
786
|
-
const { exitCode } = await runClaudeSession(prompt, projectDir);
|
|
787
|
-
// Check exit code
|
|
788
|
-
if (exitCode !== 0) {
|
|
789
|
-
console.error(`\nError: Claude session exited with code ${exitCode}. Stopping.`);
|
|
790
|
-
console.log("Fix the issue, then run `npx forge run` again to resume.");
|
|
791
|
-
process.exit(1);
|
|
792
|
-
}
|
|
793
|
-
// Check pending count (stall detection)
|
|
794
|
-
const newPending = await countPendingMilestones(projectDir, opts.prd);
|
|
795
|
-
if (newPending === 0) {
|
|
796
|
-
console.log("\n---\n");
|
|
797
|
-
console.log("## All Milestones Complete!\n");
|
|
798
|
-
console.log(`Completed in ${iteration} iteration${iteration === 1 ? "" : "s"}.`);
|
|
799
|
-
console.log('Create a PR with `gh pr create` or run `/forge:spec` to start a new project.');
|
|
800
|
-
process.exit(0);
|
|
801
|
-
}
|
|
802
|
-
if (newPending >= pending) {
|
|
803
|
-
console.error(`\nStall detected: pending count did not decrease (was ${pending}, now ${newPending}). Stopping.`);
|
|
804
|
-
console.log("Fix the issue, then run `npx forge run` again to resume.");
|
|
805
|
-
process.exit(1);
|
|
806
|
-
}
|
|
807
|
-
pending = newPending;
|
|
189
|
+
catch (err) {
|
|
190
|
+
console.warn('[forge] linear-sync done failed:', err);
|
|
808
191
|
}
|
|
809
|
-
console.error(`\nReached max iterations (${maxIterations}). Stopping.`);
|
|
810
|
-
console.log(`${pending} milestone${pending === 1 ? "" : "s"} remaining. Run \`npx forge run\` again to continue.`);
|
|
811
|
-
process.exit(1);
|
|
812
192
|
});
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
branchName = execSync("git branch --show-current", { encoding: "utf-8" }).trim();
|
|
823
|
-
}
|
|
824
|
-
catch {
|
|
825
|
-
branchName = "unknown";
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
const slug = branchName.replace(/\//g, "-").toLowerCase();
|
|
829
|
-
return join(projectDir, ".forge", "verify-cache", `${slug}.json`);
|
|
830
|
-
}
|
|
831
|
-
function writeVerifyCache(projectDir, result) {
|
|
832
|
-
let branch = "unknown";
|
|
833
|
-
try {
|
|
834
|
-
branch = execSync("git branch --show-current", { encoding: "utf-8" }).trim();
|
|
835
|
-
}
|
|
836
|
-
catch { /* not a git repo */ }
|
|
837
|
-
const cachePath = getVerifyCachePath(projectDir, branch);
|
|
838
|
-
mkdirSync(dirname(cachePath), { recursive: true });
|
|
839
|
-
const cache = {
|
|
840
|
-
passed: result.passed,
|
|
841
|
-
timestamp: new Date().toISOString(),
|
|
842
|
-
gates: result.gates,
|
|
843
|
-
branch,
|
|
844
|
-
};
|
|
845
|
-
writeFileSync(cachePath, JSON.stringify(cache, null, 2));
|
|
846
|
-
}
|
|
847
|
-
async function checkEnvironment() {
|
|
848
|
-
const checks = [];
|
|
849
|
-
// forge-cc
|
|
850
|
-
checks.push({ name: "forge-cc", status: "ok", version: `v${cliPkgVersion}` });
|
|
851
|
-
// Node.js
|
|
852
|
-
checks.push({ name: "Node.js", status: "ok", version: process.version });
|
|
853
|
-
// git
|
|
854
|
-
try {
|
|
855
|
-
const gitOut = execSync("git --version", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
856
|
-
checks.push({ name: "git", status: "ok", version: gitOut.replace("git version ", "") });
|
|
857
|
-
}
|
|
858
|
-
catch {
|
|
859
|
-
checks.push({
|
|
860
|
-
name: "git",
|
|
861
|
-
status: "missing",
|
|
862
|
-
message: "not installed",
|
|
863
|
-
fix: "Install git: https://git-scm.com/",
|
|
864
|
-
});
|
|
865
|
-
}
|
|
866
|
-
// gh CLI + auth
|
|
867
|
-
try {
|
|
868
|
-
const ghOut = execSync("gh --version", { encoding: "utf-8", stdio: "pipe" }).trim().split("\n")[0];
|
|
869
|
-
const ghVersion = ghOut.replace(/^gh version\s+/, "").split(" ")[0];
|
|
870
|
-
let authenticated = false;
|
|
871
|
-
try {
|
|
872
|
-
execSync("gh auth status", { encoding: "utf-8", stdio: "pipe" });
|
|
873
|
-
authenticated = true;
|
|
874
|
-
}
|
|
875
|
-
catch {
|
|
876
|
-
// not authenticated
|
|
877
|
-
}
|
|
878
|
-
if (authenticated) {
|
|
879
|
-
checks.push({ name: "gh CLI", status: "ok", version: ghVersion, detail: "authenticated" });
|
|
880
|
-
}
|
|
881
|
-
else {
|
|
882
|
-
checks.push({ name: "gh CLI", status: "ok", version: ghVersion });
|
|
883
|
-
checks.push({
|
|
884
|
-
name: "gh auth",
|
|
885
|
-
status: "error",
|
|
886
|
-
message: "not authenticated",
|
|
887
|
-
fix: "gh auth login",
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
catch {
|
|
892
|
-
checks.push({
|
|
893
|
-
name: "gh CLI",
|
|
894
|
-
status: "missing",
|
|
895
|
-
message: "not installed",
|
|
896
|
-
fix: "Install gh: https://cli.github.com/",
|
|
897
|
-
});
|
|
193
|
+
linearSync
|
|
194
|
+
.command('list-issues')
|
|
195
|
+
.description('List all Linear issue identifiers for a PRD slug')
|
|
196
|
+
.requiredOption('--slug <slug>', 'PRD slug')
|
|
197
|
+
.action(async (opts) => {
|
|
198
|
+
const apiKey = process.env.LINEAR_API_KEY;
|
|
199
|
+
if (!apiKey) {
|
|
200
|
+
console.log('[forge] LINEAR_API_KEY not set, skipping Linear sync');
|
|
201
|
+
return;
|
|
898
202
|
}
|
|
899
|
-
// Playwright
|
|
900
|
-
let playwrightAvailable = false;
|
|
901
203
|
try {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
checks.push({
|
|
908
|
-
name: "Playwright",
|
|
909
|
-
status: "missing",
|
|
910
|
-
message: "not installed",
|
|
911
|
-
fix: "npm install -g playwright && npx playwright install chromium",
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
// Chromium browser
|
|
915
|
-
if (playwrightAvailable) {
|
|
916
|
-
try {
|
|
917
|
-
const { chromium } = await import("playwright");
|
|
918
|
-
const browser = await chromium.launch();
|
|
919
|
-
await browser.close();
|
|
920
|
-
checks.push({ name: "Chromium browser", status: "ok" });
|
|
921
|
-
}
|
|
922
|
-
catch {
|
|
923
|
-
checks.push({
|
|
924
|
-
name: "Chromium browser",
|
|
925
|
-
status: "missing",
|
|
926
|
-
message: "not installed",
|
|
927
|
-
fix: "npx playwright install chromium",
|
|
928
|
-
});
|
|
204
|
+
const projectDir = process.cwd();
|
|
205
|
+
const status = await readStatus(projectDir, opts.slug);
|
|
206
|
+
if (!status.linearProjectId) {
|
|
207
|
+
console.log('[forge] No linearProjectId in status file');
|
|
208
|
+
return;
|
|
929
209
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
name: "Chromium browser",
|
|
934
|
-
status: "missing",
|
|
935
|
-
message: "not installed",
|
|
936
|
-
fix: "npx playwright install chromium",
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
return checks;
|
|
940
|
-
}
|
|
941
|
-
function askYesNo(question) {
|
|
942
|
-
return new Promise((resolve) => {
|
|
943
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
944
|
-
rl.question(question, (answer) => {
|
|
945
|
-
rl.close();
|
|
946
|
-
const trimmed = answer.trim().toLowerCase();
|
|
947
|
-
resolve(trimmed === "" || trimmed === "y" || trimmed === "yes");
|
|
948
|
-
});
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
function formatReport(result) {
|
|
952
|
-
const lines = [];
|
|
953
|
-
const status = result.passed ? "PASSED" : "FAILED";
|
|
954
|
-
lines.push("## Verification Report");
|
|
955
|
-
lines.push(`**Status:** ${status}`);
|
|
956
|
-
const totalMs = result.gates.reduce((sum, g) => sum + g.duration_ms, 0);
|
|
957
|
-
lines.push(`**Duration:** ${(totalMs / 1000).toFixed(1)}s`);
|
|
958
|
-
lines.push("");
|
|
959
|
-
lines.push("### Gates");
|
|
960
|
-
for (const gate of result.gates) {
|
|
961
|
-
const icon = gate.passed ? "[x]" : "[ ]";
|
|
962
|
-
const dur = `${(gate.duration_ms / 1000).toFixed(1)}s`;
|
|
963
|
-
let suffix = "";
|
|
964
|
-
if (!gate.passed && gate.errors.length > 0) {
|
|
965
|
-
suffix = ` — ${gate.errors.length} error${gate.errors.length === 1 ? "" : "s"}`;
|
|
210
|
+
if (!status.linearTeamId) {
|
|
211
|
+
console.log('[forge] No linearTeamId in status file, skipping list-issues');
|
|
212
|
+
return;
|
|
966
213
|
}
|
|
967
|
-
|
|
214
|
+
const client = new ForgeLinearClient({ apiKey, teamId: status.linearTeamId });
|
|
215
|
+
const issues = await client.listIssuesByProject(status.linearProjectId);
|
|
216
|
+
const identifiers = issues.map((i) => i.identifier);
|
|
217
|
+
console.log(JSON.stringify(identifiers));
|
|
968
218
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
if (withErrors.length > 0) {
|
|
972
|
-
lines.push("");
|
|
973
|
-
lines.push("### Errors");
|
|
974
|
-
for (const gate of withErrors) {
|
|
975
|
-
lines.push(`#### ${gate.gate}`);
|
|
976
|
-
for (const err of gate.errors) {
|
|
977
|
-
const loc = err.file ? `${err.file}${err.line ? `:${err.line}` : ""}` : "";
|
|
978
|
-
lines.push(`- ${loc ? `${loc}: ` : ""}${err.message}`);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
console.warn('[forge] linear-sync list-issues failed:', err);
|
|
981
221
|
}
|
|
982
|
-
|
|
983
|
-
|
|
222
|
+
});
|
|
223
|
+
program
|
|
224
|
+
.command('doctor')
|
|
225
|
+
.description('Check environment')
|
|
226
|
+
.action(async () => {
|
|
227
|
+
const { runDoctor } = await import('./doctor.js');
|
|
228
|
+
const result = await runDoctor(process.cwd());
|
|
229
|
+
for (const check of result.checks) {
|
|
230
|
+
const icon = check.status === 'ok' ? '\u2713' : check.status === 'warn' ? '!' : '\u2717';
|
|
231
|
+
console.log(` ${icon} ${check.name}: ${check.message}`);
|
|
232
|
+
}
|
|
233
|
+
console.log(result.ok ? '\nEnvironment ready.' : '\nSome checks failed.');
|
|
234
|
+
process.exit(result.ok ? 0 : 1);
|
|
235
|
+
});
|
|
236
|
+
program
|
|
237
|
+
.command('update')
|
|
238
|
+
.description('Check for and install forge updates')
|
|
239
|
+
.action(async () => {
|
|
240
|
+
const { checkForUpdate } = await import('./runner/update.js');
|
|
241
|
+
await checkForUpdate(process.cwd());
|
|
242
|
+
});
|
|
984
243
|
program.parse();
|
|
985
244
|
//# sourceMappingURL=cli.js.map
|