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.
Files changed (62) hide show
  1. package/README.md +454 -338
  2. package/dist/cli.js +194 -935
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/loader.d.ts +1 -1
  5. package/dist/config/loader.js +49 -56
  6. package/dist/config/loader.js.map +1 -1
  7. package/dist/config/schema.d.ts +37 -125
  8. package/dist/config/schema.js +13 -28
  9. package/dist/config/schema.js.map +1 -1
  10. package/dist/doctor.d.ts +10 -0
  11. package/dist/doctor.js +148 -0
  12. package/dist/doctor.js.map +1 -0
  13. package/dist/gates/index.d.ts +14 -12
  14. package/dist/gates/index.js +53 -105
  15. package/dist/gates/index.js.map +1 -1
  16. package/dist/gates/lint-gate.d.ts +2 -2
  17. package/dist/gates/lint-gate.js +60 -66
  18. package/dist/gates/lint-gate.js.map +1 -1
  19. package/dist/gates/tests-gate.d.ts +2 -4
  20. package/dist/gates/tests-gate.js +75 -203
  21. package/dist/gates/tests-gate.js.map +1 -1
  22. package/dist/gates/types-gate.d.ts +2 -2
  23. package/dist/gates/types-gate.js +53 -59
  24. package/dist/gates/types-gate.js.map +1 -1
  25. package/dist/linear/client.d.ts +31 -108
  26. package/dist/linear/client.js +88 -388
  27. package/dist/linear/client.js.map +1 -1
  28. package/dist/linear/sync.d.ts +15 -0
  29. package/dist/linear/sync.js +102 -0
  30. package/dist/linear/sync.js.map +1 -0
  31. package/dist/runner/loop.d.ts +4 -0
  32. package/dist/runner/loop.js +168 -0
  33. package/dist/runner/loop.js.map +1 -0
  34. package/dist/runner/prompt.d.ts +14 -0
  35. package/dist/runner/prompt.js +59 -0
  36. package/dist/runner/prompt.js.map +1 -0
  37. package/dist/runner/update.d.ts +1 -0
  38. package/dist/runner/update.js +72 -0
  39. package/dist/runner/update.js.map +1 -0
  40. package/dist/server.d.ts +6 -2
  41. package/dist/server.js +43 -101
  42. package/dist/server.js.map +1 -1
  43. package/dist/setup.d.ts +5 -0
  44. package/dist/setup.js +208 -0
  45. package/dist/setup.js.map +1 -0
  46. package/dist/state/cache.d.ts +3 -0
  47. package/dist/state/cache.js +23 -0
  48. package/dist/state/cache.js.map +1 -0
  49. package/dist/state/status.d.ts +66 -0
  50. package/dist/state/status.js +96 -0
  51. package/dist/state/status.js.map +1 -0
  52. package/dist/types.d.ts +46 -114
  53. package/dist/worktree/manager.d.ts +6 -103
  54. package/dist/worktree/manager.js +25 -296
  55. package/dist/worktree/manager.js.map +1 -1
  56. package/hooks/pre-commit-verify.js +109 -109
  57. package/package.json +3 -2
  58. package/skills/forge-go.md +20 -13
  59. package/skills/forge-setup.md +149 -388
  60. package/skills/forge-spec.md +367 -342
  61. package/skills/forge-triage.md +179 -133
  62. package/skills/forge-update.md +87 -93
package/dist/cli.js CHANGED
@@ -1,985 +1,244 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from "commander";
3
- import { execSync, spawn } from "node:child_process";
4
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from "node:fs";
5
- import { basename, dirname, join, resolve } from "node:path";
6
- import { homedir } from "node:os";
7
- import { createInterface } from "node:readline";
8
- import { fileURLToPath } from "node:url";
9
- import { runPipeline, captureBeforeSnapshots } from "./gates/index.js";
10
- import { closeBrowser } from "./utils/browser.js";
11
- import { loadConfig } from "./config/loader.js";
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("forge")
25
- .description("forge-cc — verification + workflow CLI for Claude Code agents")
26
- .version(cliPkgVersion);
13
+ .name('forge')
14
+ .version('1.0.0')
15
+ .description('Forge — verification harness for Claude Code agents');
27
16
  program
28
- .command("verify")
29
- .description("Run verification gates against the current project")
30
- .option("--gate <gates>", "Comma-separated list of gates to run (e.g., types,lint,tests)")
31
- .option("--json", "Output structured JSON instead of human-readable report")
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
- // Branch
156
- let branch = "unknown";
157
- try {
158
- branch = execSync("git branch --show-current", { encoding: "utf-8" }).trim();
159
- }
160
- catch { /* not a git repo */ }
161
- console.log(`## Forge Status`);
162
- console.log(`**Branch:** ${branch}`);
163
- // Last verify — try per-branch first, fall back to old path
164
- const perBranchCachePath = getVerifyCachePath(projectDir, branch);
165
- const legacyCachePath = join(projectDir, ".forge", "last-verify.json");
166
- const cachePath = existsSync(perBranchCachePath)
167
- ? perBranchCachePath
168
- : legacyCachePath;
169
- if (existsSync(cachePath)) {
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
- console.log(`**Last Verify:** none`);
181
- }
182
- // Config
183
- const configPath = join(projectDir, ".forge.json");
184
- if (existsSync(configPath)) {
185
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
186
- console.log(`**Config:** .forge.json (gates: ${config.gates?.join(", ") ?? "default"})`);
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
- catch { /* prd-status not available */ }
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("setup")
240
- .description("Initialize forge project and install skills")
241
- .option("--skills-only", "Only install skills to ~/.claude/commands/forge/")
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
- // Always install skills
246
- const installed = installSkills();
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("update")
349
- .description("Check for updates and install latest forge-cc")
350
- .action(() => {
351
- // Get current version from our own package.json
352
- const pkgPath = join(getPackageRoot(), "package.json");
353
- const currentVersion = JSON.parse(readFileSync(pkgPath, "utf-8")).version;
354
- // Get latest version from npm registry
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
- console.log(`**Status:** Update available\n`);
374
- console.log(`Updating forge-cc to v${latestVersion}...`);
375
- try {
376
- execSync("npm install -g forge-cc@latest", { stdio: "inherit" });
377
- }
378
- catch {
379
- console.error("Update failed. Try manually: npm install -g forge-cc@latest");
380
- process.exit(1);
381
- }
382
- // Re-sync skills after update
383
- const installed = installSkills();
384
- console.log(`\nSynced ${installed.length} skills to ~/.claude/commands/forge/`);
385
- console.log(`\n## Update Complete`);
386
- console.log(`**Previous:** v${currentVersion}`);
387
- console.log(`**Current:** v${latestVersion}`);
388
- if (existsSync(join(process.cwd(), ".forge.json"))) {
389
- console.log(`\nConsider running \`/forge:setup\` (Refresh) to update project files.`);
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("cleanup")
395
- .description("Remove stale worktrees, deregister dead sessions, reclaim disk space")
396
- .action(() => {
397
- let repoRoot;
398
- try {
399
- repoRoot = getRepoRoot(process.cwd());
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("linear-sync")
455
- .description("Sync milestone state with Linear (programmatic)");
109
+ .command('linear-sync')
110
+ .description('Sync milestone state with Linear');
456
111
  linearSync
457
- .command("start")
458
- .description("Transition milestone issues and project to In Progress")
459
- .requiredOption("--slug <slug>", "PRD slug")
460
- .requiredOption("--milestone <number>", "Milestone number")
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 { cliSyncStart } = await import("./go/linear-sync-cli.js");
463
- const projectDir = process.cwd();
464
- const milestoneNumber = parseInt(opts.milestone, 10);
465
- const result = await cliSyncStart(projectDir, opts.slug, milestoneNumber);
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
- linearSync
471
- .command("complete")
472
- .description("Transition milestone issues and project on completion")
473
- .requiredOption("--slug <slug>", "PRD slug")
474
- .requiredOption("--milestone <number>", "Milestone number")
475
- .option("--last", "This is the last milestone (transition to In Review)")
476
- .option("--pr-url <url>", "PR URL to attach as comments")
477
- .action(async (opts) => {
478
- const { cliSyncComplete } = await import("./go/linear-sync-cli.js");
479
- const projectDir = process.cwd();
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
- linearSync
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("done")
501
- .description("Transition all project issues and the project to Done (post-merge)")
502
- .requiredOption("--slug <slug>", "PRD slug")
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 { cliSyncDone } = await import("./go/linear-sync-cli.js");
505
- const projectDir = process.cwd();
506
- const result = await cliSyncDone(projectDir, opts.slug);
507
- if (result) {
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
- // ── codex-poll command ────────────────────────────────────────────
512
- program
513
- .command("codex-poll")
514
- .description("Poll for Codex review comments on a PR (deterministic 8-poll loop)")
515
- .requiredOption("--owner <owner>", "GitHub repository owner")
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
- const issues = checks.filter((c) => c.status !== "ok");
562
- console.log("");
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
- function runClaudeSession(prompt, cwd) {
573
- return new Promise((resolve) => {
574
- const env = { ...process.env };
575
- delete env.CLAUDECODE;
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 projectDir = process.cwd();
679
- const maxIterations = parseInt(opts.maxIterations, 10);
680
- // Pre-flight: check for PRD status files
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
- // Single PRD mode (existing behavior)
754
- // Validate --prd slug if provided
755
- if (opts.prd) {
756
- const slugExists = prds.some((p) => p.slug === opts.prd);
757
- if (!slugExists) {
758
- console.error(`Error: PRD "${opts.prd}" not found. Available PRDs: ${prds.map((p) => p.slug).join(", ")}`);
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
- // Pre-flight: check pending milestones
763
- let pending = await countPendingMilestones(projectDir, opts.prd);
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
- // ── helpers ────────────────────────────────────────────────────────
814
- /**
815
- * Get the verify cache path for the current branch.
816
- * Returns: .forge/verify-cache/<branch-slug>.json
817
- */
818
- function getVerifyCachePath(projectDir, branch) {
819
- let branchName = branch;
820
- if (!branchName) {
821
- try {
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
- await import("playwright");
903
- playwrightAvailable = true;
904
- checks.push({ name: "Playwright", status: "ok" });
905
- }
906
- catch {
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
- else {
932
- checks.push({
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
- lines.push(`- ${icon} ${gate.gate}: ${gate.passed ? "PASS" : "FAIL"} (${dur})${suffix}`);
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
- // Errors detail section
970
- const withErrors = result.gates.filter(g => g.errors.length > 0);
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
- return lines.join("\n");
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