forgecraft 1.4.0 → 2.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.
@@ -1,3129 +0,0 @@
1
- // src/core/adapters/nextjs.ts
2
- var nextjsAdapter = {
3
- id: "nextjs",
4
- name: "Next.js",
5
- language: "typescript",
6
- scaffoldCommands: [
7
- "npx create-next-app@latest . --typescript --tailwind --eslint --app --src-dir --import-alias '@/*' --use-npm --no-turbopack"
8
- ],
9
- buildCommand: "npm run build",
10
- lintCommand: "npm run lint",
11
- typecheckCommand: "npx tsc --noEmit",
12
- devCommand: "npm run dev",
13
- devPort: 3e3,
14
- designSupport: true,
15
- packageManager: "npm",
16
- requiredFiles: ["package.json", "tsconfig.json", "next.config.ts", "tailwind.config.ts"],
17
- buildPromptAdditions: `
18
- FOR NEXT.JS:
19
- - Use App Router (not Pages Router)
20
- - Server Components by default \u2014 only use "use client" when genuinely needed
21
- - Server Actions for mutations
22
- - Proper metadata and SEO in layout.tsx
23
- - Dynamic imports for heavy components
24
- - Use next/image for all images with width, height, alt
25
- - Use next/link for all internal navigation
26
- - Use next/font for custom fonts (no Google Fonts CDN)
27
-
28
- ASSETS & SEO (REQUIRED):
29
- - Create favicon.ico, icon.svg, and apple-touch-icon.png in public/
30
- - Create og.png (1200x630 placeholder) for Open Graph sharing
31
- - Add complete metadata in layout.tsx: title, description, Open Graph tags, Twitter cards
32
- - Create robots.txt, sitemap.xml, and manifest.json in public/
33
- - Ensure every page has a unique <title> and meta description
34
- `.trim(),
35
- designPromptAdditions: `
36
- NEXT.JS DESIGN:
37
- - Components use shadcn/ui + Tailwind CSS
38
- - Use CSF3 format for Storybook stories
39
- - Include viewport decorators for mobile (375px) and desktop (1440px)
40
- `.trim(),
41
- fileStructure: `
42
- src/
43
- \u251C\u2500\u2500 app/
44
- \u2502 \u251C\u2500\u2500 layout.tsx # Root layout with metadata
45
- \u2502 \u251C\u2500\u2500 page.tsx # Home page
46
- \u2502 \u251C\u2500\u2500 globals.css # Global styles + Tailwind
47
- \u2502 \u2514\u2500\u2500 [route]/
48
- \u2502 \u2514\u2500\u2500 page.tsx # Route pages
49
- \u251C\u2500\u2500 components/
50
- \u2502 \u251C\u2500\u2500 ui/ # shadcn/ui components
51
- \u2502 \u2514\u2500\u2500 [feature]/ # Feature-specific components
52
- \u251C\u2500\u2500 lib/
53
- \u2502 \u2514\u2500\u2500 utils.ts # Shared utilities
54
- \u2514\u2500\u2500 types/
55
- \u2514\u2500\u2500 index.ts # TypeScript types
56
- public/
57
- \u251C\u2500\u2500 favicon.ico
58
- \u251C\u2500\u2500 og.png
59
- \u251C\u2500\u2500 robots.txt
60
- \u2514\u2500\u2500 sitemap.xml
61
- `.trim()
62
- };
63
-
64
- // src/core/adapters/react-vite.ts
65
- var reactViteAdapter = {
66
- id: "react",
67
- name: "React + Vite",
68
- language: "typescript",
69
- scaffoldCommands: [
70
- "npm create vite@latest . -- --template react-ts",
71
- "npm install",
72
- "npm install -D tailwindcss @tailwindcss/vite",
73
- "npm install react-router-dom"
74
- ],
75
- buildCommand: "npm run build",
76
- lintCommand: "npm run lint",
77
- typecheckCommand: "npx tsc --noEmit",
78
- devCommand: "npm run dev",
79
- devPort: 5173,
80
- designSupport: true,
81
- packageManager: "npm",
82
- requiredFiles: ["package.json", "tsconfig.json", "vite.config.ts", "index.html"],
83
- buildPromptAdditions: `
84
- FOR REACT + VITE:
85
- - Use React Router v6 with createBrowserRouter for routing
86
- - Functional components only \u2014 no class components
87
- - Use TypeScript strict mode throughout
88
- - Tailwind CSS for styling (configured via @tailwindcss/vite plugin)
89
- - Use React.lazy() and Suspense for code splitting
90
- - State management: React context + useReducer for simple state, or Zustand if complex
91
- - No SSR \u2014 this is a client-side SPA
92
- - Put entry point in src/main.tsx, root component in src/App.tsx
93
- - Environment variables: import.meta.env.VITE_* prefix
94
-
95
- VITE CONFIG:
96
- - Configure in vite.config.ts
97
- - Add @tailwindcss/vite plugin
98
- - Set resolve aliases: "@/" -> "./src/"
99
-
100
- ASSETS & SEO:
101
- - favicon.ico and icon.svg in public/
102
- - og.png (1200x630) in public/
103
- - Update index.html with proper <title>, meta description, og tags
104
- - robots.txt in public/
105
- `.trim(),
106
- designPromptAdditions: `
107
- REACT + VITE DESIGN:
108
- - Components use Tailwind CSS utility classes
109
- - Use CSF3 format for Storybook stories
110
- - No Next.js-specific features (no next/image, next/link, etc.)
111
- - Use standard <img>, <a> tags or react-router <Link>
112
- `.trim(),
113
- fileStructure: `
114
- src/
115
- \u251C\u2500\u2500 main.tsx # Entry point
116
- \u251C\u2500\u2500 App.tsx # Root component with router
117
- \u251C\u2500\u2500 index.css # Global styles + Tailwind
118
- \u251C\u2500\u2500 components/
119
- \u2502 \u251C\u2500\u2500 ui/ # Reusable UI components
120
- \u2502 \u2514\u2500\u2500 [feature]/ # Feature-specific components
121
- \u251C\u2500\u2500 pages/
122
- \u2502 \u2514\u2500\u2500 [PageName].tsx # Route pages
123
- \u251C\u2500\u2500 hooks/
124
- \u2502 \u2514\u2500\u2500 use[Hook].ts # Custom hooks
125
- \u251C\u2500\u2500 lib/
126
- \u2502 \u2514\u2500\u2500 utils.ts # Shared utilities
127
- \u2514\u2500\u2500 types/
128
- \u2514\u2500\u2500 index.ts # TypeScript types
129
- public/
130
- \u251C\u2500\u2500 favicon.ico
131
- \u251C\u2500\u2500 og.png
132
- \u2514\u2500\u2500 robots.txt
133
- `.trim()
134
- };
135
-
136
- // src/core/adapters/django.ts
137
- var isWin = process.platform === "win32";
138
- var venvBin = isWin ? "venv\\Scripts" : "venv/bin";
139
- var python = isWin ? "python" : "python3";
140
- var venvPython = isWin ? `${venvBin}\\python` : `${venvBin}/python`;
141
- var venvPip = isWin ? `${venvBin}\\pip` : `${venvBin}/pip`;
142
- var venvDjangoAdmin = isWin ? `${venvBin}\\django-admin` : `${venvBin}/django-admin`;
143
- var djangoAdapter = {
144
- id: "django",
145
- name: "Django",
146
- language: "python",
147
- scaffoldCommands: [
148
- `${python} -m venv venv`,
149
- `${venvPip} install django djangorestframework django-cors-headers python-dotenv`,
150
- `${venvDjangoAdmin} startproject config .`,
151
- `${venvPython} manage.py startapp core`
152
- ],
153
- buildCommand: `${venvPython} manage.py check --deploy`,
154
- lintCommand: `${venvPython} -m py_compile manage.py`,
155
- typecheckCommand: isWin ? "echo Type checking skipped (Python)" : "echo 'Type checking skipped (Python)'",
156
- devCommand: `${venvPython} manage.py runserver`,
157
- devPort: 8e3,
158
- designSupport: false,
159
- packageManager: "pip",
160
- requiredFiles: ["manage.py", "config/settings.py", "config/urls.py", "requirements.txt"],
161
- buildPromptAdditions: `
162
- FOR DJANGO:
163
- - Use Django 5.x with Django REST Framework for APIs
164
- - Class-Based Views for CRUD, function-based views for custom logic
165
- - Use Django models with proper field types, validators, and Meta classes
166
- - Migrations: always run makemigrations + migrate after model changes
167
- - URL patterns in config/urls.py, app-level urls in each app's urls.py
168
- - Use Django templates (Jinja-style) for server-rendered pages
169
- - Static files in static/, templates in templates/
170
- - Settings: use python-dotenv for environment variables
171
- - Always create a superuser: python manage.py createsuperuser --noinput
172
- (set DJANGO_SUPERUSER_USERNAME, DJANGO_SUPERUSER_PASSWORD, DJANGO_SUPERUSER_EMAIL)
173
- - Register models in admin.py for Django Admin access
174
- - Use django-cors-headers for API CORS configuration
175
-
176
- PLATFORM NOTE:
177
- - On Windows, use venv\\Scripts\\python instead of venv/bin/python
178
- - On Unix/macOS, use venv/bin/python
179
- - Detect the platform and use the correct path
180
-
181
- SECURITY:
182
- - CSRF protection enabled by default \u2014 don't disable it
183
- - Use Django's built-in auth system (User model, login/logout views)
184
- - Set DEBUG=False in production settings
185
- - Configure ALLOWED_HOSTS properly
186
-
187
- AFTER WRITING CODE:
188
- 1. Run: ${venvPython} manage.py makemigrations
189
- 2. Run: ${venvPython} manage.py migrate
190
- 3. Run: ${venvPython} manage.py check --deploy
191
- 4. Fix any warnings or errors before proceeding
192
-
193
- ALWAYS generate a requirements.txt with: ${venvPip} freeze > requirements.txt
194
- `.trim(),
195
- designPromptAdditions: `
196
- DJANGO DESIGN:
197
- - No Storybook \u2014 Django uses server-rendered templates
198
- - Design phase is skipped for Django projects
199
- - Focus on clean, functional UI with Django templates + CSS
200
- `.trim(),
201
- fileStructure: `
202
- config/
203
- \u251C\u2500\u2500 __init__.py
204
- \u251C\u2500\u2500 settings.py # Django settings
205
- \u251C\u2500\u2500 urls.py # Root URL configuration
206
- \u251C\u2500\u2500 wsgi.py # WSGI entry point
207
- \u2514\u2500\u2500 asgi.py # ASGI entry point
208
- core/
209
- \u251C\u2500\u2500 __init__.py
210
- \u251C\u2500\u2500 admin.py # Admin registrations
211
- \u251C\u2500\u2500 apps.py # App config
212
- \u251C\u2500\u2500 models.py # Database models
213
- \u251C\u2500\u2500 views.py # Views (CBV/FBV)
214
- \u251C\u2500\u2500 urls.py # App URL patterns
215
- \u251C\u2500\u2500 serializers.py # DRF serializers
216
- \u251C\u2500\u2500 forms.py # Django forms
217
- \u251C\u2500\u2500 tests.py # Tests
218
- \u251C\u2500\u2500 migrations/
219
- \u2502 \u2514\u2500\u2500 __init__.py
220
- \u2514\u2500\u2500 templates/
221
- \u2514\u2500\u2500 core/ # App templates
222
- static/
223
- \u251C\u2500\u2500 css/
224
- \u251C\u2500\u2500 js/
225
- \u2514\u2500\u2500 images/
226
- templates/
227
- \u2514\u2500\u2500 base.html # Base template
228
- manage.py # Django CLI
229
- requirements.txt # Python dependencies
230
- .env # Environment variables
231
- `.trim()
232
- };
233
-
234
- // src/core/adapters/generic.ts
235
- var genericAdapter = {
236
- id: "generic",
237
- name: "Custom Stack",
238
- language: "typescript",
239
- // default, but prompts tell the agent to auto-detect
240
- scaffoldCommands: [],
241
- buildCommand: "AUTO_DETECT",
242
- lintCommand: "AUTO_DETECT",
243
- typecheckCommand: "AUTO_DETECT",
244
- devCommand: "npm run dev",
245
- devPort: 3e3,
246
- designSupport: false,
247
- packageManager: "npm",
248
- requiredFiles: [],
249
- buildPromptAdditions: `
250
- CUSTOM TECH STACK \u2014 IMPORTANT:
251
- This project uses a tech stack chosen by the user. You MUST auto-detect everything.
252
-
253
- STEP 1 \u2014 DETECT THE STACK:
254
- Before writing ANY code, read these files (whichever exist):
255
- - package.json (Node.js/JS/TS projects)
256
- - requirements.txt / pyproject.toml / Pipfile (Python projects)
257
- - go.mod (Go projects)
258
- - Cargo.toml (Rust projects)
259
- - pom.xml / build.gradle (Java/Kotlin projects)
260
- - pubspec.yaml (Flutter/Dart projects)
261
- - Gemfile (Ruby projects)
262
- - composer.json (PHP projects)
263
- - Any existing source files to understand patterns
264
-
265
- STEP 2 \u2014 FOLLOW EXISTING CONVENTIONS:
266
- - If the project already has code, match its style exactly (naming, file structure, patterns)
267
- - If starting from scratch, use the standard project structure for the detected tech stack
268
- - Use the package manager the project already uses (npm, yarn, pnpm, pip, poetry, cargo, go, etc.)
269
-
270
- STEP 3 \u2014 BUILD VERIFICATION:
271
- - Detect the correct build/lint/test commands from the project config
272
- - For Node.js: check package.json "scripts" for build, lint, typecheck commands
273
- - For Python: use pytest, flake8/ruff, mypy if configured
274
- - For Go: use go build, go vet, go test
275
- - For Rust: use cargo build, cargo clippy, cargo test
276
- - If no build script exists, skip the build step \u2014 do NOT fail
277
- - If a command does not exist or is not configured, skip it gracefully
278
-
279
- STEP 4 \u2014 SCAFFOLDING:
280
- - If the project directory is empty, scaffold the project using the standard tooling for the stack
281
- (e.g., npm init, cargo init, go mod init, django-admin startproject, etc.)
282
- - Install dependencies after scaffolding
283
- `.trim(),
284
- designPromptAdditions: "",
285
- fileStructure: `
286
- (auto-detected \u2014 read the project's existing files to determine structure)
287
- `.trim()
288
- };
289
-
290
- // src/core/adapters/index.ts
291
- var adapters = {
292
- nextjs: nextjsAdapter,
293
- react: reactViteAdapter,
294
- django: djangoAdapter,
295
- generic: genericAdapter
296
- };
297
- function getAdapter(framework) {
298
- const adapter = adapters[framework];
299
- if (!adapter) {
300
- throw new Error(
301
- `Unknown framework "${framework}". Supported: ${Object.keys(adapters).join(", ")}`
302
- );
303
- }
304
- return adapter;
305
- }
306
- function listAdapters() {
307
- return Object.values(adapters);
308
- }
309
-
310
- // src/core/utils/sound.ts
311
- import { exec } from "child_process";
312
- import { platform } from "os";
313
- function playSound() {
314
- if (!process.stdout.isTTY) return;
315
- try {
316
- switch (platform()) {
317
- case "darwin":
318
- exec("afplay /System/Library/Sounds/Blow.aiff", () => {
319
- });
320
- break;
321
- case "win32":
322
- exec(
323
- 'powershell -NoProfile -NonInteractive -Command "[System.Media.SystemSounds]::Exclamation.Play()"',
324
- { shell: "cmd.exe" },
325
- () => {
326
- }
327
- );
328
- break;
329
- case "linux":
330
- exec(
331
- "paplay /usr/share/sounds/freedesktop/stereo/complete.oga",
332
- (err) => {
333
- if (err) process.stdout.write("\x07");
334
- }
335
- );
336
- break;
337
- default:
338
- process.stdout.write("\x07");
339
- }
340
- } catch {
341
- }
342
- }
343
-
344
- // src/core/orchestrator/index.ts
345
- import { query } from "@anthropic-ai/claude-agent-sdk";
346
- import chalk from "chalk";
347
-
348
- // src/core/orchestrator/prompts.ts
349
- var ORCHESTRATOR_SYSTEM_PROMPT = `
350
- You are the Orchestrator \u2014 a senior tech lead managing an AI development sprint for the "Forge" framework.
351
-
352
- YOUR ROLE:
353
- You NEVER write code. You plan, delegate, and review. You are the brain of the operation.
354
-
355
- RESPONSIBILITIES:
356
- 1. Break user requirements into epics and stories
357
- 2. Craft detailed, specific prompts for the Worker agent
358
- 3. Route user feedback to the correct Worker mode
359
- 4. Review Worker output and decide next steps
360
- 5. Maintain project context and sprint state
361
-
362
- WHEN CREATING STORIES:
363
- - Each story must be independently buildable
364
- - UI stories need design phase; backend-only stories skip it
365
- - Order stories by dependency (foundation/setup first, then data layer, then UI, then integration)
366
- - Each story should result in working, testable code
367
- - Story IDs should be kebab-case: "auth-login", "dashboard-layout"
368
- - First story should always be project setup/scaffolding
369
-
370
- WHEN CRAFTING WORKER PROMPTS:
371
- - Include EXACT file paths to create or modify
372
- - Reference approved designs when available
373
- - Specify the framework patterns to follow (App Router, Server Components, etc.)
374
- - Include acceptance criteria \u2014 what "done" looks like
375
- - Never be vague \u2014 the Worker should not have to guess anything
376
- - Include the full project context the Worker needs (but not more)
377
-
378
- WHEN ROUTING USER FEEDBACK:
379
- - Visual tweak (color, spacing, font, border) \u2192 Worker fix mode, direct code change
380
- - Layout/UX redesign (page structure, navigation, flow) \u2192 Worker design mode first, then build
381
- - Bug fix (something broken, error, crash) \u2192 Worker fix mode with debug focus
382
- - New feature (something that doesn't exist yet) \u2192 Create new story, full pipeline
383
- - Content change (text, labels, copy) \u2192 Worker fix mode, string replacement
384
- - Question about the project \u2192 Answer directly, don't route
385
- - If Worker is currently busy \u2192 Queue the change for after current task
386
-
387
- WHEN REVIEWING WORKER OUTPUT:
388
- - Does the output match what was requested?
389
- - Did the Worker run build/lint/typecheck?
390
- - Are there any obvious issues?
391
- - Minor issues: send back to Worker fix mode
392
- - Major issues: send back to Worker build mode with specific instructions
393
-
394
- OUTPUT FORMAT:
395
- Always respond with valid JSON when asked for structured data.
396
- When answering questions, respond in plain text.
397
- Never include markdown code fences around JSON output.
398
-
399
- DESIGN QUALITY STANDARDS:
400
- When the Worker is in design mode, ensure designs reference real products:
401
- - Finance/Banking \u2192 Stripe, Mercury, Wise
402
- - Dashboard/Admin \u2192 Linear, Vercel, Raycast
403
- - E-commerce \u2192 Shopify Admin, Gumroad
404
- - Social/Community \u2192 Discord, Slack
405
- - Content/Blog \u2192 Medium, Ghost, Substack
406
- - SaaS/Productivity \u2192 Notion, Figma
407
- - Healthcare/Church/Nonprofit \u2192 warm, trustworthy, accessible
408
-
409
- NEVER accept generic AI-looking designs (purple gradients, Inter font, rounded white cards on gray).
410
- `.trim();
411
- var ROUTING_CLASSIFICATION_PROMPT = `
412
- Classify the user's message into one of these categories:
413
-
414
- 1. VISUAL_TWEAK \u2014 Small CSS/styling change
415
- Examples: "make it blue", "bigger font", "add shadow", "more padding"
416
-
417
- 2. REDESIGN \u2014 Significant layout or UX change
418
- Examples: "redesign the dashboard", "switch to sidebar nav", "make it look like Stripe"
419
-
420
- 3. BUG_FIX \u2014 Something is broken or not working
421
- Examples: "button doesn't work", "page is blank", "getting an error"
422
-
423
- 4. NEW_FEATURE \u2014 Something that doesn't exist yet
424
- Examples: "add dark mode", "add recurring donations", "add export to PDF"
425
-
426
- 5. CONTENT_CHANGE \u2014 Text, labels, or copy changes
427
- Examples: "change 'Submit' to 'Send'", "update the footer text"
428
-
429
- 6. QUESTION \u2014 Asking for information
430
- Examples: "what stack are we using?", "how many stories left?", "show me the schema"
431
-
432
- 7. PRIORITY_CHANGE \u2014 Wants to reorder or skip stories
433
- Examples: "do reports next", "skip the settings page for now"
434
-
435
- Respond with ONLY the category name and a brief routing instruction.
436
- `.trim();
437
-
438
- // src/core/orchestrator/index.ts
439
- var Orchestrator = class {
440
- config;
441
- plan = null;
442
- constructor(config) {
443
- this.config = config;
444
- }
445
- // ── Plan Generation ───────────────────────────────────────
446
- async generatePlan(description) {
447
- const adapter = getAdapter(this.config.framework);
448
- const isGeneric = adapter.id === "generic";
449
- const frameworkHint = isGeneric ? `Tech stack: Detect from the user's description below. The user may specify their own framework, language, and tools.` : `Framework: ${adapter.name} (${this.config.framework})
450
- Language: ${adapter.language}`;
451
- const prompt = `
452
- The user wants to build the following application:
453
- "${description}"
454
-
455
- ${frameworkHint}
456
- Design support: ${adapter.designSupport ? "yes (Storybook)" : "no"}
457
-
458
- IMPORTANT: If the user references any files (like .md, .txt, .pdf, or any document),
459
- READ those files first to understand the full requirements before planning.
460
- Also read any existing project files (package.json, requirements.txt, etc.) to understand the current state.
461
-
462
- Break this down into epics and stories. Each story should be:
463
- - Small enough to build in one agent session
464
- - Independently testable
465
- - Ordered by dependency (foundation first)
466
-
467
- Story types:
468
- - "ui" = has a visual component ${adapter.designSupport ? "(needs design phase)" : "(no design phase for this framework)"}
469
- - "backend" = API/database only (skip design phase)
470
- - "fullstack" = both UI and backend
471
-
472
- Return ONLY valid JSON with this structure:
473
- {
474
- "project": "project name",
475
- "framework": "${this.config.framework}",
476
- "description": "brief description",
477
- "epics": [
478
- {
479
- "id": "epic-id",
480
- "title": "Epic Title",
481
- "stories": [
482
- {
483
- "id": "story-id",
484
- "title": "Story Title",
485
- "description": "What to build",
486
- "type": "ui" | "backend" | "fullstack",
487
- "priority": 1,
488
- "dependencies": ["other-story-id"]
489
- }
490
- ]
491
- }
492
- ]
493
- }
494
-
495
- CRITICAL: Return ONLY the raw JSON object. No explanation, no markdown, no text before or after.
496
- Your entire response must be a single valid JSON object starting with { and ending with }.
497
- `;
498
- const resultText = await this.runQuery(prompt, { maxTurns: 10 });
499
- let rawPlan;
500
- try {
501
- rawPlan = JSON.parse(this.cleanJson(resultText));
502
- } catch {
503
- const preview = resultText.slice(0, 200).replace(/\n/g, " ");
504
- throw new Error(
505
- `Failed to parse plan \u2014 Claude returned text instead of JSON.
506
- Preview: "${preview}${resultText.length > 200 ? "..." : ""}"
507
- Try a more specific description, e.g.: forge auto "build a todo app with React and Express"`
508
- );
509
- }
510
- if (!rawPlan.epics || !Array.isArray(rawPlan.epics)) {
511
- throw new Error("Invalid plan: missing epics array. Try regenerating.");
512
- }
513
- const plan = {
514
- project: rawPlan.project || "Untitled",
515
- framework: rawPlan.framework || this.config.framework,
516
- description: rawPlan.description || description,
517
- created: (/* @__PURE__ */ new Date()).toISOString(),
518
- epics: rawPlan.epics.map((epic) => ({
519
- id: epic.id || `epic-${Date.now()}`,
520
- title: epic.title || "Untitled Epic",
521
- status: "planned",
522
- stories: (epic.stories || []).map((story) => ({
523
- id: story.id || `story-${Date.now()}`,
524
- title: story.title || "Untitled Story",
525
- description: story.description || "",
526
- type: story.type || "fullstack",
527
- status: "planned",
528
- branch: null,
529
- designApproved: false,
530
- tags: [],
531
- priority: story.priority || 1,
532
- dependencies: story.dependencies || []
533
- }))
534
- }))
535
- };
536
- this.plan = plan;
537
- return plan;
538
- }
539
- // ── User Input Routing ────────────────────────────────────
540
- async routeUserInput(message, currentState) {
541
- const routingPrompt = `
542
- Current sprint state:
543
- - Phase: ${currentState.currentPhase}
544
- - Current story: ${currentState.currentStory || "none"}
545
- - Worker mode: ${currentState.workerMode || "idle"}
546
- - Framework: ${this.config.framework}
547
-
548
- The user said: "${message}"
549
-
550
- Classify this input and decide what to do.
551
- Return ONLY valid JSON with this structure:
552
- {
553
- "action": "route-to-worker" | "add-story" | "reprioritize" | "answer" | "queue-change",
554
- "workerMode": "design" | "build" | "review" | "fix" (if routing to worker),
555
- "response": "your response to the user" (if answering directly),
556
- "prompt": "detailed prompt for the worker" (if routing to worker),
557
- "story": { "title": "...", "description": "...", "type": "..." } (if adding a new story)
558
- }
559
-
560
- Routing rules:
561
- - Visual tweak (color, spacing, font) \u2192 route-to-worker, mode: fix
562
- - Layout/UX redesign \u2192 route-to-worker, mode: design
563
- - Bug fix \u2192 route-to-worker, mode: fix
564
- - New feature \u2192 add-story (include title, description, type)
565
- - Content change (text, labels) \u2192 route-to-worker, mode: fix
566
- - Question \u2192 answer directly
567
- - If worker is currently active \u2192 queue-change (apply after current task)
568
- `;
569
- const resultText = await this.runQuery(routingPrompt, { maxTurns: 1 });
570
- try {
571
- return JSON.parse(this.cleanJson(resultText));
572
- } catch {
573
- return { action: "answer", response: resultText };
574
- }
575
- }
576
- // ── Prompt Crafting ───────────────────────────────────────
577
- craftWorkerPrompt(story, mode, context) {
578
- const adapter = getAdapter(this.config.framework);
579
- switch (mode) {
580
- case "design":
581
- return this.craftDesignPrompt(story, context, adapter);
582
- case "build":
583
- return this.craftBuildPrompt(story, context, adapter);
584
- case "review":
585
- return this.craftReviewPrompt(story, context, adapter);
586
- case "fix":
587
- return this.craftFixPrompt(story, context, adapter);
588
- }
589
- }
590
- craftDesignPrompt(story, context, adapter) {
591
- const refsDir = ".forge/designs/references";
592
- return `
593
- You are designing the UI for: "${story.title}"
594
-
595
- Description: ${story.description}
596
- App: ${context.plan.project} (${adapter.name})
597
- Full app context: ${context.plan.description}
598
-
599
- DESIGN REFERENCES:
600
- Check if ${refsDir}/ exists and contains reference images.
601
- If so, read them and match their visual style, color palette, typography, and layout.
602
-
603
- Create a Storybook story file that renders this component/page.
604
- The file should be at: stories/${story.id}.stories.tsx
605
-
606
- Requirements:
607
- - Must be responsive (mobile-first: 375px, then 768px, then 1440px)
608
- - Use Tailwind CSS for styling
609
- - Include realistic placeholder data (not lorem ipsum)
610
- - Show default, loading, empty, and error states as separate stories
611
- - Use a distinctive, professional design (not generic AI aesthetics)
612
- - Choose fonts and colors that match the app's purpose
613
-
614
- DO NOT write the actual app code. Only the Storybook preview.
615
- `;
616
- }
617
- craftBuildPrompt(story, context, adapter) {
618
- const isGeneric = adapter.id === "generic";
619
- const designRef = context.designMeta ? `
620
- Approved design: Follow the design in stories/${story.id}.stories.tsx exactly.` : "";
621
- const existingRef = context.existingFiles?.length ? `
622
- Existing files to reference for patterns:
623
- ${context.existingFiles.map((f) => ` - ${f}`).join("\n")}` : "";
624
- const verifySteps = isGeneric ? `
625
- After writing code:
626
- 1. Detect the project's build/lint/typecheck commands from its config files (package.json scripts, Makefile, pyproject.toml, etc.)
627
- 2. Run whichever commands exist \u2014 skip any that aren't configured
628
- 3. Fix any errors before finishing
629
- ` : `
630
- After writing code:
631
- 1. Run: ${adapter.buildCommand} (fix any errors)
632
- 2. Run: ${adapter.lintCommand} (fix any warnings)
633
- 3. Run: ${adapter.typecheckCommand} (fix any type errors)
634
- `;
635
- const structureRef = isGeneric ? `
636
- Project structure:
637
- Read the existing project files to understand the structure.
638
- If starting from scratch, use the standard conventions for this tech stack.
639
- ` : `
640
- Expected project structure:
641
- ${adapter.fileStructure}
642
- `;
643
- return `
644
- Implement: "${story.title}"
645
-
646
- Description: ${story.description}
647
- App: ${context.plan.project} (${adapter.name})
648
- ${designRef}
649
- ${existingRef}
650
-
651
- ${structureRef}
652
-
653
- Technical requirements:
654
- - Follow existing code patterns in the project
655
- - Small, focused components/functions
656
- - Proper error handling
657
- - Responsive design (mobile-first)
658
-
659
- ${verifySteps}
660
-
661
- If any command fails, read the error, fix it, and re-run.
662
- Do NOT leave broken code.
663
- `;
664
- }
665
- craftReviewPrompt(story, context, adapter) {
666
- const isGeneric = adapter.id === "generic";
667
- const runCmds = isGeneric ? `Run the project's build/lint/test commands (detect from config files). Skip any that aren't configured.` : `Run:
668
- - ${adapter.buildCommand}
669
- - ${adapter.lintCommand}
670
- - ${adapter.typecheckCommand}`;
671
- return `
672
- Review the code for: "${story.title}"
673
-
674
- Framework: ${adapter.name}
675
-
676
- Check:
677
- 1. Does the implementation match the story description?
678
- ${context.designMeta ? "2. Does it match the approved design?" : ""}
679
- 3. Code quality \u2014 no shortcuts, proper types
680
- 4. Responsive design \u2014 works on mobile and desktop
681
- 5. Error handling \u2014 graceful failures, loading states
682
- 6. Accessibility \u2014 semantic HTML, ARIA labels
683
- 7. No debug logs, no commented-out code, no TODOs
684
-
685
- ${runCmds}
686
-
687
- If you find issues:
688
- - Minor (formatting, missing type): fix them directly
689
- - Major (broken logic, missing feature): list them and do NOT fix
690
-
691
- Return a summary of what you found and what you fixed.
692
- `;
693
- }
694
- craftFixPrompt(story, context, adapter) {
695
- const isGeneric = adapter.id === "generic";
696
- const verifyCmds = isGeneric ? "After fixing, run the project's build/test commands to verify (detect from config files)." : `After fixing, run ${adapter.buildCommand} and ${adapter.typecheckCommand} to verify.`;
697
- return `
698
- Fix an issue in: "${story.title}"
699
- Framework: ${adapter.name}
700
-
701
- Make the smallest possible change. Do not refactor.
702
- ${verifyCmds}
703
- `;
704
- }
705
- // ── SDK Query Helper ──────────────────────────────────────
706
- async runQuery(prompt, opts = {}) {
707
- const maxRetries = 3;
708
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
709
- try {
710
- return await this.executeQuery(prompt, opts);
711
- } catch (error) {
712
- const msg = error instanceof Error ? error.message : String(error);
713
- if (this.isAuthError(msg) && attempt < maxRetries) {
714
- playSound();
715
- console.log(chalk.yellow("\n Authentication expired"));
716
- console.log(chalk.dim(" Run: claude login"));
717
- console.log(chalk.dim(` Retrying in 30s (attempt ${attempt}/${maxRetries})...
718
- `));
719
- await new Promise((r) => setTimeout(r, 3e4));
720
- continue;
721
- }
722
- throw error;
723
- }
724
- }
725
- throw new Error("Max retries exceeded");
726
- }
727
- async executeQuery(prompt, opts = {}) {
728
- let resultText = "";
729
- try {
730
- for await (const msg of query({
731
- prompt,
732
- options: {
733
- model: this.config.model,
734
- systemPrompt: ORCHESTRATOR_SYSTEM_PROMPT,
735
- maxTurns: opts.maxTurns,
736
- allowedTools: ["Read", "Glob", "Grep"]
737
- }
738
- })) {
739
- if (msg.type === "result") {
740
- const hasError = "is_error" in msg && msg.is_error;
741
- const errors = "errors" in msg && Array.isArray(msg.errors) ? msg.errors : [];
742
- const subtype = "subtype" in msg ? String(msg.subtype) : "";
743
- if (hasError || errors.length > 0) {
744
- const errorParts = [];
745
- if (subtype) errorParts.push(subtype);
746
- for (const e of errors) {
747
- const eStr = typeof e === "string" ? e : JSON.stringify(e);
748
- if (eStr && eStr.length > 0) errorParts.push(eStr);
749
- }
750
- const errorMsg = errorParts.length > 0 ? errorParts.join(" \u2014 ") : "Unknown error from Claude. Check your auth with: claude login";
751
- throw new Error(errorMsg);
752
- }
753
- if ("result" in msg && typeof msg.result === "string") {
754
- resultText = msg.result;
755
- }
756
- }
757
- if (msg.type === "assistant" && msg.message?.content) {
758
- const textBlocks = msg.message.content.filter((b) => b.type === "text").map((b) => b.text);
759
- if (textBlocks.length > 0) {
760
- resultText = textBlocks.join("\n");
761
- }
762
- }
763
- }
764
- } catch (error) {
765
- if (error instanceof Error && !error.message.includes("EPIPE") && !error.message.includes("ENOENT")) {
766
- throw error;
767
- }
768
- const msg = error instanceof Error ? error.message : String(error);
769
- throw new Error(
770
- `Failed to communicate with Claude. ${msg}
771
- Possible fixes:
772
- 1. Run: claude login
773
- 2. Check your internet connection
774
- 3. Run: forge doctor`
775
- );
776
- }
777
- if (!resultText) {
778
- throw new Error(
779
- "No response from Claude.\n Possible fixes:\n 1. Run: claude login\n 2. Check your internet connection\n 3. Run: forge doctor"
780
- );
781
- }
782
- return resultText;
783
- }
784
- isAuthError(msg) {
785
- const lower = msg.toLowerCase();
786
- return lower.includes("401") || lower.includes("unauthorized") || lower.includes("auth") || lower.includes("token expired") || lower.includes("session expired") || lower.includes("not authenticated");
787
- }
788
- cleanJson(text) {
789
- let cleaned = text.replace(/```json\s*/g, "").replace(/```\s*/g, "").trim();
790
- try {
791
- JSON.parse(cleaned);
792
- return cleaned;
793
- } catch {
794
- }
795
- const firstBrace = cleaned.indexOf("{");
796
- const lastBrace = cleaned.lastIndexOf("}");
797
- if (firstBrace !== -1 && lastBrace > firstBrace) {
798
- const extracted = cleaned.slice(firstBrace, lastBrace + 1);
799
- try {
800
- JSON.parse(extracted);
801
- return extracted;
802
- } catch {
803
- return extracted;
804
- }
805
- }
806
- return cleaned;
807
- }
808
- };
809
-
810
- // src/core/worker/index.ts
811
- import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
812
- import chalk2 from "chalk";
813
-
814
- // src/core/worker/prompts/index.ts
815
- function getDesignPrompt(framework) {
816
- const adapter = framework ? getAdapter(framework) : null;
817
- const additions = adapter?.designPromptAdditions || "";
818
- return `
819
- You are a senior UI/UX designer working within the Forge development framework.
820
- Your job is to create beautiful, professional component previews as Storybook stories.
821
-
822
- DESIGN PRINCIPLES:
823
- - Mobile-first: design for 375px, then scale up to 768px and 1440px
824
- - Use the project's design system (shadcn/ui + Tailwind CSS)
825
- - Choose distinctive typography \u2014 NEVER use Inter, Arial, or Roboto
826
- - Commit to a cohesive color palette with a dominant color and sharp accents
827
- - Add micro-interactions (hover states, transitions)
828
- - Use realistic placeholder data (real names, real numbers, not "Lorem ipsum")
829
- - Include all states: default, loading, empty, error, success
830
-
831
- STORYBOOK OUTPUT FORMAT:
832
- - Create .stories.tsx files in the stories/ directory
833
- - Each story file should export a default meta and named story variants
834
- - Use CSF3 format (Component Story Format)
835
- - Include args/controls for interactive props
836
- - Add viewport decorators for mobile/desktop
837
-
838
- QUALITY BAR:
839
- Your designs should look like they came from a professional design agency.
840
- Reference real, well-designed products:
841
- - Finance \u2192 Stripe, Mercury, Wise
842
- - Dashboards \u2192 Linear, Vercel
843
- - E-commerce \u2192 Shopify, Gumroad
844
- - Social \u2192 Discord, Slack
845
-
846
- NEVER produce:
847
- - Purple gradient backgrounds
848
- - Generic rounded white cards on gray
849
- - Stock-photo placeholder images
850
- - Inconsistent spacing or alignment
851
- - Missing hover/focus states
852
-
853
- ${additions}
854
- `.trim();
855
- }
856
- function getBuildPrompt(framework) {
857
- const adapter = framework ? getAdapter(framework) : null;
858
- const additions = adapter?.buildPromptAdditions || "";
859
- const isGeneric = adapter?.id === "generic";
860
- const verifyBlock = isGeneric ? `
861
- AFTER WRITING CODE:
862
- 1. Detect the project's build/lint/typecheck/test commands from its config files
863
- (package.json scripts, Makefile, pyproject.toml, Cargo.toml, etc.)
864
- 2. Run whichever commands exist \u2014 skip any that aren't configured
865
- 3. Fix ALL errors before proceeding
866
- 4. If no build system is configured, verify the code is syntactically valid
867
- ` : `
868
- AFTER WRITING CODE:
869
- 1. Run: ${adapter?.buildCommand || "npm run build"} \u2014 fix ALL errors before proceeding
870
- 2. Run: ${adapter?.lintCommand || "npm run lint"} \u2014 fix warnings
871
- 3. Run: ${adapter?.typecheckCommand || "npx tsc --noEmit"} \u2014 fix type errors
872
- `;
873
- return `
874
- You are a senior fullstack developer working within the Forge development framework.
875
- Your job is to implement features based on approved designs and story requirements.
876
-
877
- CODING STANDARDS:
878
- - Small, focused components and functions (< 100 lines per file)
879
- - Proper error handling \u2014 try/catch, error boundaries, loading states
880
- - Responsive design \u2014 mobile-first with Tailwind breakpoints
881
- - Semantic HTML with accessibility (ARIA labels, keyboard navigation)
882
- - Clean imports, no circular dependencies
883
-
884
- ${additions}
885
-
886
- ${verifyBlock}
887
-
888
- If any command fails:
889
- - Read the full error output
890
- - Identify the root cause
891
- - Fix it
892
- - Re-run the command
893
- - Repeat until all checks pass
894
-
895
- NEVER leave code in a broken state. Every commit must build successfully.
896
-
897
- GIT:
898
- - Make small, atomic changes
899
- - Write descriptive file names and function names
900
- `.trim();
901
- }
902
- function getReviewPrompt(framework) {
903
- const adapter = framework ? getAdapter(framework) : null;
904
- const isGeneric = adapter?.id === "generic";
905
- const lang = adapter?.language || "typescript";
906
- const langCheck = isGeneric ? "3. Code quality \u2014 follows the language's best practices and idioms" : lang === "typescript" ? "3. TypeScript strict compliance (no any, no ts-ignore, no ts-expect-error)" : "3. Python code quality (no bare except, proper type hints where used)";
907
- const runBlock = isGeneric ? `
908
- RUN THESE COMMANDS:
909
- - Detect build/lint/test commands from the project's config files (package.json, Makefile, pyproject.toml, Cargo.toml, etc.)
910
- - Run whichever exist \u2014 skip any that aren't configured
911
- - Run tests if a test runner is configured
912
- ` : `
913
- RUN THESE COMMANDS:
914
- - ${adapter?.buildCommand || "npm run build"}
915
- - ${adapter?.lintCommand || "npm run lint"}
916
- - ${adapter?.typecheckCommand || "npx tsc --noEmit"}
917
- - npm run test (if tests exist)
918
- `;
919
- return `
920
- You are a QA engineer reviewing code within the Forge development framework.
921
- Your job is to catch issues before code is merged to main.
922
-
923
- REVIEW CHECKLIST:
924
- 1. Implementation matches the story description
925
- 2. If design was approved \u2014 implementation matches the design
926
- ${langCheck}
927
- 4. Responsive design works at 375px, 768px, 1440px
928
- 5. Error handling \u2014 what happens when things fail?
929
- 6. Loading states \u2014 what does the user see while waiting?
930
- 7. Empty states \u2014 what if there's no data?
931
- 8. Accessibility \u2014 semantic HTML, focus management, screen reader support
932
- 9. No debug logs (console.log / print() for debugging)
933
- 10. No commented-out code
934
- 11. No TODO/FIXME/HACK comments left behind
935
- 12. No hardcoded values that should be configurable
936
-
937
- ${runBlock}
938
-
939
- ISSUE CLASSIFICATION:
940
- - MINOR: formatting, missing type annotation, unused import
941
- \u2192 Fix it directly, note what you fixed
942
- - MAJOR: broken logic, missing error handling, security issue, missing feature
943
- \u2192 List the issue with file path and line, do NOT attempt to fix
944
-
945
- OUTPUT:
946
- Provide a structured review summary:
947
- - Files reviewed
948
- - Issues found (with severity)
949
- - Issues auto-fixed
950
- - Commands run and their results
951
- - PASS or FAIL recommendation
952
- `.trim();
953
- }
954
- function getFixPrompt(framework) {
955
- const adapter = framework ? getAdapter(framework) : null;
956
- const isGeneric = adapter?.id === "generic";
957
- const verifyBlock = isGeneric ? `
958
- AFTER EVERY FIX:
959
- 1. Detect the project's build/test commands from its config files
960
- 2. Run whichever exist to verify nothing is broken
961
- 3. Verify the fix works
962
- ` : `
963
- AFTER EVERY FIX:
964
- 1. Run: ${adapter?.buildCommand || "npm run build"}
965
- 2. Run: ${adapter?.typecheckCommand || "npx tsc --noEmit"}
966
- 3. Verify the fix works
967
- `;
968
- return `
969
- You are a debugger and problem solver within the Forge development framework.
970
- Your job is to make targeted fixes without breaking anything else.
971
-
972
- PRINCIPLES:
973
- - Make the SMALLEST possible change to fix the issue
974
- - Do NOT refactor surrounding code \u2014 you're here to fix, not improve
975
- - Read the existing code carefully before changing anything
976
- - Understand the full context of what you're changing
977
-
978
- FOR VISUAL TWEAKS:
979
- - Change only the specific CSS/Tailwind classes needed
980
- - Verify the change doesn't break other viewports
981
- - Check both mobile and desktop after changing
982
-
983
- FOR BUG FIXES:
984
- - Read error messages and stack traces carefully
985
- - Trace the issue to its ROOT CAUSE
986
- - Fix the cause, not the symptom
987
- - Check if the same bug pattern exists elsewhere
988
- - Verify the fix by running the relevant command
989
-
990
- ${verifyBlock}
991
-
992
- If your fix introduces new errors, undo it and try a different approach.
993
- `.trim();
994
- }
995
- var DESIGN_SYSTEM_PROMPT = getDesignPrompt();
996
- var BUILD_SYSTEM_PROMPT = getBuildPrompt();
997
- var REVIEW_SYSTEM_PROMPT = getReviewPrompt();
998
- var FIX_SYSTEM_PROMPT = getFixPrompt();
999
-
1000
- // src/core/worker/index.ts
1001
- var MODE_TOOLS = {
1002
- design: ["Read", "Write", "Glob", "LS"],
1003
- build: ["Read", "Write", "Edit", "Bash", "Glob", "LS", "Grep"],
1004
- review: ["Read", "Bash", "Glob", "LS", "Grep"],
1005
- fix: ["Read", "Write", "Edit", "Bash", "Glob", "LS", "Grep"]
1006
- };
1007
- function getModePrompt(mode, framework) {
1008
- switch (mode) {
1009
- case "design":
1010
- return getDesignPrompt(framework);
1011
- case "build":
1012
- return getBuildPrompt(framework);
1013
- case "review":
1014
- return getReviewPrompt(framework);
1015
- case "fix":
1016
- return getFixPrompt(framework);
1017
- }
1018
- }
1019
- var MODE_MAX_TURNS = {
1020
- design: 30,
1021
- build: 50,
1022
- review: 20,
1023
- fix: 15
1024
- };
1025
- var MAX_AUTH_RETRIES = 3;
1026
- var AUTH_RETRY_DELAY_MS = 3e4;
1027
- var Worker = class {
1028
- config;
1029
- sandboxOpts;
1030
- mute;
1031
- constructor(config, sandboxOpts = {}, mute = false) {
1032
- this.config = config;
1033
- this.sandboxOpts = sandboxOpts;
1034
- this.mute = mute;
1035
- }
1036
- async run(mode, prompt, options = {}) {
1037
- for (let attempt = 1; attempt <= MAX_AUTH_RETRIES; attempt++) {
1038
- const result = await this.executeQuery(mode, prompt, options);
1039
- if (!result.success && this.isAuthError(result.errors)) {
1040
- if (attempt < MAX_AUTH_RETRIES) {
1041
- this.notifyAuthError(attempt);
1042
- await this.waitForReauth();
1043
- result.errors = result.errors.filter((e) => !this.isAuthErrorMsg(e));
1044
- continue;
1045
- }
1046
- result.errors.push(
1047
- "Authentication failed after retries. Run: claude login"
1048
- );
1049
- }
1050
- return result;
1051
- }
1052
- return {
1053
- success: false,
1054
- filesCreated: [],
1055
- filesModified: [],
1056
- errors: ["Max retries exceeded"],
1057
- summary: "",
1058
- usage: { inputTokens: 0, outputTokens: 0, costUsd: 0, durationMs: 0 }
1059
- };
1060
- }
1061
- async executeQuery(mode, prompt, options = {}) {
1062
- let workingDir = options.workingDir || this.sandboxOpts.workingDir;
1063
- if (!workingDir) {
1064
- try {
1065
- workingDir = process.cwd();
1066
- } catch {
1067
- return {
1068
- success: false,
1069
- filesCreated: [],
1070
- filesModified: [],
1071
- errors: ["Working directory no longer exists. Please cd to your project and retry."],
1072
- summary: "",
1073
- usage: { inputTokens: 0, outputTokens: 0, costUsd: 0, durationMs: 0 }
1074
- };
1075
- }
1076
- }
1077
- const { onProgress } = options;
1078
- const result = {
1079
- success: false,
1080
- filesCreated: [],
1081
- filesModified: [],
1082
- errors: [],
1083
- summary: "",
1084
- usage: { inputTokens: 0, outputTokens: 0, costUsd: 0, durationMs: 0 }
1085
- };
1086
- const sdkOptions = {
1087
- model: this.config.model,
1088
- systemPrompt: getModePrompt(mode, this.config.framework),
1089
- allowedTools: MODE_TOOLS[mode],
1090
- cwd: workingDir,
1091
- maxTurns: MODE_MAX_TURNS[mode]
1092
- };
1093
- if (this.sandboxOpts.yes && !this.sandboxOpts.sandbox) {
1094
- sdkOptions.permissionMode = "bypassPermissions";
1095
- }
1096
- if (this.sandboxOpts.sandbox) {
1097
- sdkOptions.permissionMode = "bypassPermissions";
1098
- sdkOptions.sandbox = {
1099
- enabled: true,
1100
- autoAllowBashIfSandboxed: true,
1101
- filesystem: {
1102
- allowWrite: [workingDir]
1103
- },
1104
- network: {
1105
- allowedDomains: this.sandboxOpts.allowedDomains || [
1106
- "registry.npmjs.org",
1107
- "api.anthropic.com"
1108
- ]
1109
- }
1110
- };
1111
- }
1112
- try {
1113
- for await (const msg of query2({ prompt, options: sdkOptions })) {
1114
- if (msg.type === "assistant") {
1115
- if (msg.message?.usage) {
1116
- result.usage.inputTokens += msg.message.usage.input_tokens || 0;
1117
- result.usage.outputTokens += msg.message.usage.output_tokens || 0;
1118
- }
1119
- for (const block of msg.message?.content || []) {
1120
- if (block.type === "text") {
1121
- onProgress?.({ type: "thinking", content: block.text.slice(0, 120) });
1122
- }
1123
- if (block.type === "tool_use") {
1124
- const detail = this.describeToolUse(block);
1125
- onProgress?.({ type: "tool_use", content: detail, tool: block.name });
1126
- if (block.name === "Write" && block.input?.file_path) {
1127
- result.filesCreated.push(block.input.file_path);
1128
- }
1129
- if (block.name === "Edit" && block.input?.file_path) {
1130
- result.filesModified.push(block.input.file_path);
1131
- }
1132
- }
1133
- }
1134
- }
1135
- if (msg.type === "tool_progress") {
1136
- onProgress?.({
1137
- type: "tool_running",
1138
- content: msg.tool_name,
1139
- tool: msg.tool_name,
1140
- elapsed: msg.elapsed_time_seconds
1141
- });
1142
- }
1143
- if (msg.type === "tool_use_summary") {
1144
- onProgress?.({ type: "tool_done", content: msg.summary });
1145
- }
1146
- if (msg.type === "result") {
1147
- if ("result" in msg && typeof msg.result === "string") {
1148
- result.summary = msg.result;
1149
- }
1150
- const hasError = "is_error" in msg && msg.is_error;
1151
- const errors = "errors" in msg && Array.isArray(msg.errors) ? msg.errors : [];
1152
- const subtype = "subtype" in msg ? String(msg.subtype || "") : "";
1153
- if (errors.length > 0) {
1154
- for (const e of errors) {
1155
- const eStr = typeof e === "string" ? e : JSON.stringify(e);
1156
- if (eStr && eStr.length > 0) result.errors.push(eStr);
1157
- }
1158
- }
1159
- if (hasError && result.errors.length === 0) {
1160
- result.errors.push(
1161
- subtype || "Claude encountered an error. Run: claude login"
1162
- );
1163
- }
1164
- if (msg.usage) {
1165
- result.usage.inputTokens = msg.usage.input_tokens || 0;
1166
- result.usage.outputTokens = msg.usage.output_tokens || 0;
1167
- }
1168
- if (typeof msg.total_cost_usd === "number") {
1169
- result.usage.costUsd = msg.total_cost_usd;
1170
- }
1171
- if (typeof msg.duration_ms === "number") {
1172
- result.usage.durationMs = msg.duration_ms;
1173
- }
1174
- if ("is_error" in msg && msg.subtype === "error_max_turns") {
1175
- result.errors.push(`Hit ${MODE_MAX_TURNS[mode]}-turn safety cap \u2014 story may be incomplete`);
1176
- }
1177
- }
1178
- }
1179
- result.success = result.errors.length === 0;
1180
- if (!result.summary) {
1181
- result.summary = "Completed without summary.";
1182
- }
1183
- } catch (error) {
1184
- const msg = error instanceof Error ? error.message : String(error);
1185
- result.success = false;
1186
- result.errors.push(msg);
1187
- }
1188
- return result;
1189
- }
1190
- // ── Auth Error Detection ──────────────────────────────────
1191
- isAuthErrorMsg(msg) {
1192
- const lower = msg.toLowerCase();
1193
- return lower.includes("401") || lower.includes("unauthorized") || lower.includes("auth") || lower.includes("token expired") || lower.includes("session expired") || lower.includes("not authenticated") || lower.includes("login required") || lower.includes("credential");
1194
- }
1195
- isAuthError(errors) {
1196
- return errors.some((e) => this.isAuthErrorMsg(e));
1197
- }
1198
- notifyAuthError(attempt) {
1199
- if (!this.mute) playSound();
1200
- console.log("");
1201
- console.log(chalk2.yellow(" \u26A0 Authentication expired"));
1202
- console.log(chalk2.dim(" Your Claude session token has expired."));
1203
- console.log(chalk2.dim(" Please re-authenticate:"));
1204
- console.log(chalk2.white(" claude login"));
1205
- console.log(chalk2.dim(`
1206
- Waiting to retry (attempt ${attempt}/${MAX_AUTH_RETRIES})...`));
1207
- console.log(chalk2.dim(" Forge will resume automatically once auth is renewed.\n"));
1208
- }
1209
- waitForReauth() {
1210
- return new Promise((resolve) => setTimeout(resolve, AUTH_RETRY_DELAY_MS));
1211
- }
1212
- /** Turn a tool_use block into a short human-readable string */
1213
- describeToolUse(block) {
1214
- const name = block.name;
1215
- const input = block.input || {};
1216
- switch (name) {
1217
- case "Write":
1218
- return `Write ${this.shortPath(input.file_path)}`;
1219
- case "Edit":
1220
- return `Edit ${this.shortPath(input.file_path)}`;
1221
- case "Read":
1222
- return `Read ${this.shortPath(input.file_path)}`;
1223
- case "Bash":
1224
- return `Run ${(input.command || "").slice(0, 60)}`;
1225
- case "Glob":
1226
- return `Search ${input.pattern || ""}`;
1227
- case "Grep":
1228
- return `Grep ${input.pattern || ""}`;
1229
- default:
1230
- return name;
1231
- }
1232
- }
1233
- shortPath(p) {
1234
- if (!p) return "";
1235
- const parts = p.split(/[/\\]/);
1236
- return parts.length > 2 ? ".../" + parts.slice(-2).join("/") : p;
1237
- }
1238
- };
1239
-
1240
- // src/core/git/index.ts
1241
- import simpleGit from "simple-git";
1242
- import fs from "fs/promises";
1243
- import path from "path";
1244
- var GitManager = class {
1245
- git;
1246
- basePath;
1247
- cachedBranch = null;
1248
- repoVerified = false;
1249
- constructor(basePath = process.cwd()) {
1250
- this.basePath = basePath;
1251
- this.git = simpleGit(basePath);
1252
- }
1253
- // ── Branch Operations ─────────────────────────────────────
1254
- async createBranch(name) {
1255
- await this.stashDirtyState();
1256
- const currentBranch = await this.getCurrentBranch();
1257
- if (currentBranch !== "main") {
1258
- await this.git.checkout("main");
1259
- }
1260
- await this.git.checkoutLocalBranch(name);
1261
- }
1262
- async checkout(branch) {
1263
- await this.stashDirtyState();
1264
- await this.git.checkout(branch);
1265
- this.cachedBranch = branch;
1266
- }
1267
- async merge(branch) {
1268
- await this.git.merge([branch, "--no-ff"]);
1269
- }
1270
- async deleteBranch(branch) {
1271
- await this.git.deleteLocalBranch(branch, true);
1272
- }
1273
- async getCurrentBranch() {
1274
- if (this.cachedBranch) return this.cachedBranch;
1275
- try {
1276
- const status = await this.git.status();
1277
- if (status.current) {
1278
- this.cachedBranch = status.current;
1279
- return this.cachedBranch;
1280
- }
1281
- } catch {
1282
- }
1283
- return "main";
1284
- }
1285
- async listBranches() {
1286
- const result = await this.git.branchLocal();
1287
- return result.all;
1288
- }
1289
- // ── Commit Operations ─────────────────────────────────────
1290
- async commitAll(message) {
1291
- await this.git.add(".");
1292
- try {
1293
- await this.git.commit(message);
1294
- } catch (err) {
1295
- if (!err?.message?.includes("nothing to commit")) throw err;
1296
- }
1297
- }
1298
- async commitState(message) {
1299
- await this.git.add(".forge/");
1300
- await this.git.add("forge.config.json");
1301
- const status = await this.git.status();
1302
- if (status.staged.length > 0) {
1303
- await this.git.commit(`forge: ${message}`);
1304
- }
1305
- }
1306
- // ── Tag Operations ────────────────────────────────────────
1307
- async tag(name) {
1308
- await this.git.addTag(name);
1309
- }
1310
- async listTags() {
1311
- const result = await this.git.tags();
1312
- return result.all.filter((t) => t.startsWith("forge/"));
1313
- }
1314
- async checkoutTag(tagName) {
1315
- await this.stashDirtyState();
1316
- await this.git.checkout(tagName);
1317
- }
1318
- // ── Diff & History ────────────────────────────────────────
1319
- async getDiff(branch) {
1320
- return this.git.diff(["main", branch]);
1321
- }
1322
- /** Diff between any two refs (tags, commits, branches) */
1323
- async getDiff2(ref1, ref2) {
1324
- return this.git.diff([ref1, ref2]);
1325
- }
1326
- async getLog(count = 10) {
1327
- const log = await this.git.log({ maxCount: count });
1328
- return log.all;
1329
- }
1330
- /** Get the current HEAD commit hash */
1331
- async getHead() {
1332
- const log = await this.git.log({ maxCount: 1 });
1333
- return log.latest?.hash || "";
1334
- }
1335
- /** Revert a specific commit (creates a new revert commit) */
1336
- async revertCommit(hash) {
1337
- await this.git.raw(["revert", "--no-edit", hash]);
1338
- }
1339
- /** Get forge-related commits (feat:, fix:, forge:, docs:, ci:) */
1340
- async getForgeLog(count = 20) {
1341
- const log = await this.git.log({ maxCount: count });
1342
- return log.all.map((entry) => ({
1343
- hash: entry.hash,
1344
- date: entry.date,
1345
- message: entry.message
1346
- }));
1347
- }
1348
- /** Get tags pointing at a specific commit */
1349
- async getTagsAtCommit(hash) {
1350
- try {
1351
- const result = await this.git.raw(["tag", "--points-at", hash]);
1352
- return result.trim().split("\n").filter(Boolean);
1353
- } catch {
1354
- return [];
1355
- }
1356
- }
1357
- // ── Push Operations ─────────────────────────────────────
1358
- /** Check if a remote named 'origin' exists */
1359
- async hasRemote() {
1360
- try {
1361
- const remotes = await this.git.getRemotes();
1362
- return remotes.some((r) => r.name === "origin");
1363
- } catch {
1364
- return false;
1365
- }
1366
- }
1367
- /** Push current branch to origin */
1368
- async push() {
1369
- const branch = await this.getCurrentBranch();
1370
- await this.git.push("origin", branch, ["--set-upstream"]);
1371
- }
1372
- /** Push tags to origin */
1373
- async pushTags() {
1374
- await this.git.pushTags("origin");
1375
- }
1376
- // ── Status ────────────────────────────────────────────────
1377
- async isClean() {
1378
- const status = await this.git.status();
1379
- return status.isClean();
1380
- }
1381
- async hasUncommittedChanges() {
1382
- const status = await this.git.status();
1383
- return !status.isClean();
1384
- }
1385
- // ── Init (for new projects) ───────────────────────────────
1386
- async ensureRepo() {
1387
- if (this.repoVerified) return;
1388
- const isRepo = await this.git.checkIsRepo();
1389
- if (!isRepo) {
1390
- await this.git.init();
1391
- }
1392
- await this.ensureGitignore();
1393
- const log = await this.git.log().catch(() => null);
1394
- if (!log || log.total === 0) {
1395
- await this.git.add(".");
1396
- await this.git.commit("Initial commit");
1397
- }
1398
- this.repoVerified = true;
1399
- }
1400
- async ensureMainBranch() {
1401
- const current = await this.getCurrentBranch();
1402
- if (current !== "main") {
1403
- const branches = await this.listBranches();
1404
- if (!branches.includes("main")) {
1405
- await this.git.branch(["-M", "main"]);
1406
- this.cachedBranch = "main";
1407
- }
1408
- }
1409
- }
1410
- // ── Internal ──────────────────────────────────────────────
1411
- /** Commit any uncommitted changes before branch operations */
1412
- async stashDirtyState() {
1413
- const status = await this.git.status();
1414
- if (status.isClean()) return;
1415
- await this.git.add(".");
1416
- await this.git.commit("forge: save state before branch switch");
1417
- }
1418
- /** Ensure .gitignore covers build artifacts and forge internals */
1419
- async ensureGitignore() {
1420
- const gitignorePath = path.join(this.basePath, ".gitignore");
1421
- const requiredEntries = [
1422
- "node_modules/",
1423
- ".next/",
1424
- ".forge/snapshots/",
1425
- "dist/",
1426
- ".env",
1427
- ".env.local"
1428
- ];
1429
- let content = "";
1430
- try {
1431
- content = await fs.readFile(gitignorePath, "utf-8");
1432
- } catch {
1433
- }
1434
- const missing = requiredEntries.filter((e) => !content.includes(e));
1435
- if (missing.length > 0) {
1436
- const addition = (content ? "\n" : "") + missing.join("\n") + "\n";
1437
- await fs.appendFile(gitignorePath, addition);
1438
- }
1439
- }
1440
- };
1441
-
1442
- // src/state/index.ts
1443
- import fs2 from "fs/promises";
1444
- import path2 from "path";
1445
- var FORGE_DIR = ".forge";
1446
- var PLAN_FILE = "plan.json";
1447
- var STATE_FILE = "state.json";
1448
- var CONFIG_FILE = "forge.config.json";
1449
- var SNAPSHOTS_DIR = "snapshots";
1450
- var StateManager = class {
1451
- basePath;
1452
- cache = /* @__PURE__ */ new Map();
1453
- dirCreated = /* @__PURE__ */ new Set();
1454
- constructor(basePath = process.cwd()) {
1455
- this.basePath = basePath;
1456
- }
1457
- /** Clear all caches (call when basePath changes) */
1458
- clearCache() {
1459
- this.cache.clear();
1460
- this.dirCreated.clear();
1461
- }
1462
- // ── Paths ─────────────────────────────────────────────────
1463
- forgePath(...parts) {
1464
- return path2.join(this.basePath, FORGE_DIR, ...parts);
1465
- }
1466
- rootPath(...parts) {
1467
- return path2.join(this.basePath, ...parts);
1468
- }
1469
- // ── Plan ──────────────────────────────────────────────────
1470
- async savePlan(plan) {
1471
- await this.writeJson(this.forgePath(PLAN_FILE), plan);
1472
- }
1473
- async getPlan() {
1474
- return this.readJson(this.forgePath(PLAN_FILE));
1475
- }
1476
- async hasPlan() {
1477
- return this.fileExists(this.forgePath(PLAN_FILE));
1478
- }
1479
- // ── State ─────────────────────────────────────────────────
1480
- async getState() {
1481
- const state = await this.readJson(
1482
- this.forgePath(STATE_FILE)
1483
- );
1484
- return state || {
1485
- currentPhase: "init",
1486
- currentStory: null,
1487
- workerMode: null,
1488
- queue: [],
1489
- history: []
1490
- };
1491
- }
1492
- async updatePhase(phase) {
1493
- const state = await this.getState();
1494
- state.currentPhase = phase;
1495
- await this.writeJson(this.forgePath(STATE_FILE), state);
1496
- }
1497
- async updateState(updates) {
1498
- const state = await this.getState();
1499
- Object.assign(state, updates);
1500
- await this.writeJson(this.forgePath(STATE_FILE), state);
1501
- }
1502
- async addHistoryEntry(entry) {
1503
- const state = await this.getState();
1504
- state.history.push({
1505
- ...entry,
1506
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1507
- snapshotId: entry.snapshotId || null
1508
- });
1509
- await this.writeJson(this.forgePath(STATE_FILE), state);
1510
- }
1511
- // ── Config ────────────────────────────────────────────────
1512
- async getConfig() {
1513
- return this.readJson(this.rootPath(CONFIG_FILE));
1514
- }
1515
- async isInitialized() {
1516
- return this.fileExists(this.rootPath(CONFIG_FILE));
1517
- }
1518
- // ── Snapshots ─────────────────────────────────────────────
1519
- async saveSnapshot(data) {
1520
- const state = await this.getState();
1521
- const id = String(state.history.length + 1).padStart(3, "0");
1522
- const snapshot = {
1523
- id,
1524
- action: data.action,
1525
- storyId: data.storyId,
1526
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1527
- files: [],
1528
- branch: data.branch,
1529
- commitBefore: data.commitBefore || ""
1530
- };
1531
- await this.writeJson(
1532
- this.forgePath(SNAPSHOTS_DIR, `${id}-pre-${data.action}-${data.storyId || "global"}.json`),
1533
- snapshot
1534
- );
1535
- await this.addHistoryEntry({
1536
- action: data.action,
1537
- storyId: data.storyId,
1538
- details: `Snapshot taken before ${data.action}`,
1539
- snapshotId: id
1540
- });
1541
- return id;
1542
- }
1543
- async getSnapshot(id) {
1544
- const dir = this.forgePath(SNAPSHOTS_DIR);
1545
- const files = await fs2.readdir(dir).catch(() => []);
1546
- const match = files.find((f) => f.startsWith(id));
1547
- if (!match) return null;
1548
- return this.readJson(path2.join(dir, match));
1549
- }
1550
- async listSnapshots() {
1551
- const dir = this.forgePath(SNAPSHOTS_DIR);
1552
- const files = await fs2.readdir(dir).catch(() => []);
1553
- const snapshots = [];
1554
- for (const file of files.filter((f) => f.endsWith(".json"))) {
1555
- const snapshot = await this.readJson(path2.join(dir, file));
1556
- if (snapshot) snapshots.push(snapshot);
1557
- }
1558
- return snapshots.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
1559
- }
1560
- // ── Design References ──────────────────────────────────────
1561
- async saveDesignReferences(files) {
1562
- const refDir = this.forgePath("designs", "references");
1563
- await fs2.mkdir(refDir, { recursive: true });
1564
- const manifest = [];
1565
- for (const file of files) {
1566
- const basename = path2.basename(file);
1567
- const dest = path2.join(refDir, basename);
1568
- await fs2.copyFile(file, dest);
1569
- manifest.push({ original: file, saved: dest });
1570
- }
1571
- await this.writeJson(this.forgePath("designs", "references.json"), manifest);
1572
- }
1573
- async getDesignReferences() {
1574
- const manifest = await this.readJson(
1575
- this.forgePath("designs", "references.json")
1576
- );
1577
- return manifest?.map((r) => r.saved) || [];
1578
- }
1579
- // ── Helpers ───────────────────────────────────────────────
1580
- async readJson(filePath) {
1581
- try {
1582
- const stat = await fs2.stat(filePath);
1583
- const mtime = stat.mtimeMs;
1584
- const cached = this.cache.get(filePath);
1585
- if (cached && cached.mtime === mtime) {
1586
- return cached.data;
1587
- }
1588
- const content = await fs2.readFile(filePath, "utf-8");
1589
- const data = JSON.parse(content);
1590
- this.cache.set(filePath, { data, mtime });
1591
- return data;
1592
- } catch (err) {
1593
- if (err?.code === "ENOENT") return null;
1594
- if (err instanceof SyntaxError) return null;
1595
- throw err;
1596
- }
1597
- }
1598
- async writeJson(filePath, data) {
1599
- const dir = path2.dirname(filePath);
1600
- if (!this.dirCreated.has(dir)) {
1601
- await fs2.mkdir(dir, { recursive: true });
1602
- this.dirCreated.add(dir);
1603
- }
1604
- const json = JSON.stringify(data, null, 2);
1605
- await fs2.writeFile(filePath, json);
1606
- const stat = await fs2.stat(filePath);
1607
- this.cache.set(filePath, { data, mtime: stat.mtimeMs });
1608
- }
1609
- async fileExists(filePath) {
1610
- try {
1611
- await fs2.access(filePath);
1612
- return true;
1613
- } catch {
1614
- return false;
1615
- }
1616
- }
1617
- };
1618
- var stateManager = new StateManager();
1619
-
1620
- // src/core/pipeline/index.ts
1621
- import chalk3 from "chalk";
1622
- import ora from "ora";
1623
- import inquirer from "inquirer";
1624
- var MAX_REGEN_ATTEMPTS = 5;
1625
- var Pipeline = class {
1626
- orchestrator;
1627
- worker;
1628
- git;
1629
- config;
1630
- plan = null;
1631
- changeQueue = [];
1632
- constructor(config) {
1633
- this.config = config;
1634
- this.orchestrator = new Orchestrator(config);
1635
- this.worker = new Worker(config, {});
1636
- this.git = new GitManager();
1637
- }
1638
- // ── Full Sprint (all phases) ──────────────────────────────
1639
- async runSprint(description) {
1640
- console.log(chalk3.bold("\n forge") + chalk3.dim(" sprint\n"));
1641
- await this.git.ensureRepo();
1642
- await this.git.ensureMainBranch();
1643
- this.plan = await this.runPlanPhase(description);
1644
- if (!this.plan) return;
1645
- const adapter = getAdapter(this.config.framework);
1646
- if (adapter.designSupport) {
1647
- await this.runDesignPhase(this.plan);
1648
- } else {
1649
- console.log(chalk3.dim(`
1650
- Design skipped (${adapter.name} \u2014 no Storybook support)
1651
- `));
1652
- }
1653
- await this.runBuildPhase(this.plan);
1654
- await this.runReviewPhase(this.plan);
1655
- console.log(chalk3.dim("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1656
- console.log(chalk3.bold(" Sprint complete\n"));
1657
- console.log(chalk3.dim(" Run your app and test it."));
1658
- console.log(chalk3.dim(" Use " + chalk3.white('forge fix "description"') + " to make changes."));
1659
- console.log(chalk3.dim(" Use " + chalk3.white("forge undo") + " to revert.\n"));
1660
- }
1661
- // ── Phase 1: Plan ────────────────────────────────────────
1662
- async runPlanPhase(description, attempt = 1) {
1663
- if (attempt === 1) {
1664
- console.log(chalk3.bold(" Plan\n"));
1665
- }
1666
- const spinner = ora({ text: "Analyzing requirements...", indent: 2 }).start();
1667
- let plan;
1668
- try {
1669
- plan = await this.orchestrator.generatePlan(description);
1670
- } catch (err) {
1671
- spinner.fail("Plan generation failed");
1672
- console.log(chalk3.red(` ${err instanceof Error ? err.message : err}`));
1673
- return null;
1674
- }
1675
- spinner.succeed("Plan generated");
1676
- this.displayPlan(plan);
1677
- const { action } = await inquirer.prompt([
1678
- {
1679
- type: "list",
1680
- name: "action",
1681
- message: "Action",
1682
- choices: [
1683
- { name: "Approve plan", value: "approve" },
1684
- { name: "Edit (describe changes)", value: "edit" },
1685
- { name: "Regenerate", value: "regen" },
1686
- { name: "Cancel", value: "cancel" }
1687
- ]
1688
- }
1689
- ]);
1690
- switch (action) {
1691
- case "approve":
1692
- await stateManager.savePlan(plan);
1693
- await stateManager.updatePhase("plan");
1694
- await this.git.commitState("Sprint plan approved");
1695
- await this.git.tag("forge/v0.0-plan");
1696
- console.log(chalk3.green(" Plan saved\n"));
1697
- return plan;
1698
- case "edit": {
1699
- if (attempt >= MAX_REGEN_ATTEMPTS) {
1700
- console.log(chalk3.yellow(` Max edit attempts (${MAX_REGEN_ATTEMPTS}) reached. Approving current plan.`));
1701
- await stateManager.savePlan(plan);
1702
- return plan;
1703
- }
1704
- const { changes } = await inquirer.prompt([
1705
- { type: "input", name: "changes", message: "Describe changes:" }
1706
- ]);
1707
- console.log(chalk3.dim(" Re-planning..."));
1708
- return this.runPlanPhase(`${description}
1709
-
1710
- User edits: ${changes}`, attempt + 1);
1711
- }
1712
- case "regen": {
1713
- if (attempt >= MAX_REGEN_ATTEMPTS) {
1714
- console.log(chalk3.yellow(` Max regeneration attempts (${MAX_REGEN_ATTEMPTS}) reached.`));
1715
- return null;
1716
- }
1717
- return this.runPlanPhase(description, attempt + 1);
1718
- }
1719
- case "cancel":
1720
- console.log(chalk3.dim(" Cancelled."));
1721
- return null;
1722
- default:
1723
- return null;
1724
- }
1725
- }
1726
- // ── Phase 2: Design ──────────────────────────────────────
1727
- async runDesignPhase(plan) {
1728
- const uiStories = this.getStoriesByType(plan, ["ui", "fullstack"]);
1729
- if (uiStories.length === 0) {
1730
- console.log(chalk3.dim("\n No UI stories, skipping design.\n"));
1731
- return;
1732
- }
1733
- console.log(chalk3.bold(`
1734
- Design`) + chalk3.dim(` \xB7 ${uiStories.length} stories
1735
- `));
1736
- for (const story of uiStories) {
1737
- const spinner = ora({ text: story.title, indent: 4 }).start();
1738
- const prompt = this.orchestrator.craftWorkerPrompt(story, "design", { plan });
1739
- const result = await this.worker.run("design", prompt, {
1740
- onProgress: (event) => {
1741
- if (event.type === "tool_use") {
1742
- spinner.text = event.content;
1743
- }
1744
- }
1745
- });
1746
- if (result.success) {
1747
- spinner.succeed(story.title);
1748
- } else {
1749
- spinner.fail(story.title);
1750
- console.log(chalk3.red(` ${result.errors.join(", ")}`));
1751
- continue;
1752
- }
1753
- const adapter = getAdapter(this.config.framework);
1754
- console.log(chalk3.dim(` Preview: http://localhost:${adapter.devPort}
1755
- `));
1756
- const { approval } = await inquirer.prompt([
1757
- {
1758
- type: "list",
1759
- name: "approval",
1760
- message: `Approve "${story.title}"?`,
1761
- choices: [
1762
- { name: "Approve", value: "approve" },
1763
- { name: "Request changes", value: "change" },
1764
- { name: "Skip", value: "skip" }
1765
- ]
1766
- }
1767
- ]);
1768
- if (approval === "approve") {
1769
- story.designApproved = true;
1770
- story.status = "design-approved";
1771
- console.log(chalk3.green(` Approved
1772
- `));
1773
- } else if (approval === "change") {
1774
- const { feedback } = await inquirer.prompt([
1775
- { type: "input", name: "feedback", message: "Describe changes:" }
1776
- ]);
1777
- const fixResult = await this.worker.run("fix", `Revise design for "${story.title}": ${feedback}`);
1778
- if (fixResult.success) {
1779
- console.log(chalk3.dim(" Revised.\n"));
1780
- }
1781
- }
1782
- }
1783
- await stateManager.savePlan(plan);
1784
- await stateManager.updatePhase("design");
1785
- await this.git.commitAll("forge: designs reviewed");
1786
- await this.git.tag("forge/v0.1-designs");
1787
- console.log(chalk3.dim(" Design phase complete\n"));
1788
- }
1789
- // ── Phase 3: Build ───────────────────────────────────────
1790
- async runBuildPhase(plan) {
1791
- const buildableStories = this.getAllStories(plan).filter(
1792
- (s) => s.status === "design-approved" || s.status === "planned"
1793
- );
1794
- if (buildableStories.length === 0) {
1795
- console.log(chalk3.dim("\n No stories to build.\n"));
1796
- return;
1797
- }
1798
- console.log(chalk3.bold(`
1799
- Build`) + chalk3.dim(` \xB7 ${buildableStories.length} stories
1800
- `));
1801
- for (const story of buildableStories) {
1802
- const spinner = ora({ text: story.title, indent: 4 }).start();
1803
- const headBefore = await this.git.getHead();
1804
- await stateManager.saveSnapshot({
1805
- action: "build",
1806
- storyId: story.id,
1807
- branch: "main",
1808
- commitBefore: headBefore
1809
- });
1810
- story.status = "building";
1811
- await stateManager.savePlan(plan);
1812
- const prompt = this.orchestrator.craftWorkerPrompt(story, "build", {
1813
- plan,
1814
- designMeta: story.designApproved ? { storyId: story.id } : void 0
1815
- });
1816
- const result = await this.worker.run("build", prompt, {
1817
- onProgress: (event) => {
1818
- if (event.type === "tool_use") {
1819
- spinner.text = event.content;
1820
- }
1821
- }
1822
- });
1823
- if (result.success) {
1824
- await this.git.commitAll(`feat: ${story.title}`);
1825
- story.status = "reviewing";
1826
- spinner.succeed(story.title + chalk3.dim(` \xB7 ${result.filesCreated.length} files`));
1827
- } else {
1828
- story.status = "blocked";
1829
- spinner.fail(story.title);
1830
- for (const error of result.errors) {
1831
- console.log(chalk3.red(` ${error}`));
1832
- }
1833
- }
1834
- await stateManager.savePlan(plan);
1835
- await this.processQueue(plan);
1836
- }
1837
- await stateManager.updatePhase("build");
1838
- console.log(chalk3.dim("\n Build phase complete\n"));
1839
- }
1840
- // ── Phase 4: Review ──────────────────────────────────────
1841
- async runReviewPhase(plan) {
1842
- const reviewableStories = this.getAllStories(plan).filter(
1843
- (s) => s.status === "reviewing"
1844
- );
1845
- if (reviewableStories.length === 0) return;
1846
- console.log(chalk3.bold(`
1847
- Review`) + chalk3.dim(` \xB7 ${reviewableStories.length} stories
1848
- `));
1849
- for (const story of reviewableStories) {
1850
- const spinner = ora({ text: story.title, indent: 4 }).start();
1851
- const prompt = this.orchestrator.craftWorkerPrompt(story, "review", {
1852
- plan,
1853
- designMeta: story.designApproved ? { storyId: story.id } : void 0
1854
- });
1855
- let result;
1856
- try {
1857
- result = await this.worker.run("review", prompt);
1858
- } catch (err) {
1859
- spinner.fail(story.title + chalk3.dim(" \u2014 review error"));
1860
- console.log(chalk3.red(` ${err instanceof Error ? err.message : err}`));
1861
- continue;
1862
- }
1863
- if (result.success) {
1864
- const tagName = `forge/v0.${this.getNextTagNumber()}-${story.id}`;
1865
- await this.git.tag(tagName);
1866
- story.tags.push(tagName);
1867
- story.status = "done";
1868
- spinner.succeed(story.title + chalk3.dim(` [${tagName}]`));
1869
- } else {
1870
- spinner.fail(story.title);
1871
- console.log(chalk3.dim(` ${result.summary}`));
1872
- }
1873
- await stateManager.savePlan(plan);
1874
- }
1875
- await stateManager.updatePhase("review");
1876
- console.log(chalk3.dim("\n Review phase complete\n"));
1877
- }
1878
- // ── Change Queue ─────────────────────────────────────────
1879
- queueChange(change) {
1880
- this.changeQueue.push(change);
1881
- console.log(chalk3.dim(` Queued: "${change.message}"`));
1882
- }
1883
- async processQueue(plan) {
1884
- if (this.changeQueue.length === 0) return;
1885
- console.log(chalk3.dim(`
1886
- Processing ${this.changeQueue.length} queued changes...
1887
- `));
1888
- for (const change of this.changeQueue) {
1889
- const decision = await this.orchestrator.routeUserInput(
1890
- change.message,
1891
- await stateManager.getState()
1892
- );
1893
- if (decision.action === "route-to-worker" && decision.prompt) {
1894
- const mode = decision.workerMode || "fix";
1895
- const spinner = ora({ text: change.message, indent: 4 }).start();
1896
- const result = await this.worker.run(mode, decision.prompt);
1897
- if (result.success) {
1898
- await this.git.commitAll(`fix: ${change.message}`);
1899
- spinner.succeed(change.message);
1900
- } else {
1901
- spinner.fail(change.message);
1902
- }
1903
- }
1904
- }
1905
- this.changeQueue = [];
1906
- }
1907
- // ── Helpers ──────────────────────────────────────────────
1908
- displayPlan(plan) {
1909
- console.log(chalk3.bold(`
1910
- ${plan.project}`) + chalk3.dim(` \xB7 ${plan.framework}`));
1911
- console.log(chalk3.dim(` ${plan.description}
1912
- `));
1913
- for (const epic of plan.epics) {
1914
- console.log(chalk3.dim(` ${epic.title}`));
1915
- for (const story of epic.stories) {
1916
- const tag = story.type === "ui" ? "ui" : story.type === "backend" ? "api" : "full";
1917
- console.log(` ${chalk3.dim(`[${tag}]`)} ${story.id} ${chalk3.dim("\u2014")} ${story.title}`);
1918
- }
1919
- console.log("");
1920
- }
1921
- }
1922
- getAllStories(plan) {
1923
- return plan.epics.flatMap((epic) => epic.stories);
1924
- }
1925
- getStoriesByType(plan, types) {
1926
- return this.getAllStories(plan).filter((s) => types.includes(s.type));
1927
- }
1928
- tagCounter = 2;
1929
- getNextTagNumber() {
1930
- return this.tagCounter++;
1931
- }
1932
- };
1933
-
1934
- // src/core/pipeline/auto.ts
1935
- import chalk4 from "chalk";
1936
- import ora2 from "ora";
1937
- import readline from "readline";
1938
- import inquirer2 from "inquirer";
1939
-
1940
- // src/core/github/index.ts
1941
- import { spawnSync } from "child_process";
1942
- var GitHubSync = class {
1943
- repo;
1944
- constructor(repo) {
1945
- this.repo = repo;
1946
- }
1947
- /** Check if `gh` CLI is installed and authenticated */
1948
- static isAvailable() {
1949
- const result = spawnSync("gh", ["auth", "status"], { stdio: "ignore", shell: true });
1950
- return result.status === 0;
1951
- }
1952
- /** Create GitHub labels for forge story tracking */
1953
- async ensureLabels() {
1954
- const labels = [
1955
- { name: "forge", color: "000000", description: "Managed by ForgeAI" },
1956
- { name: "type:ui", color: "7057ff", description: "UI component" },
1957
- { name: "type:backend", color: "0075ca", description: "Backend/API" },
1958
- { name: "type:fullstack", color: "008672", description: "Full stack" },
1959
- { name: "status:planned", color: "d4c5f9", description: "Planned" },
1960
- { name: "status:in-progress", color: "fbca04", description: "Building" },
1961
- { name: "status:review", color: "f9d0c4", description: "In review" },
1962
- { name: "status:done", color: "0e8a16", description: "Complete" },
1963
- { name: "status:blocked", color: "e11d48", description: "Blocked" }
1964
- ];
1965
- for (const label of labels) {
1966
- spawnSync("gh", [
1967
- "label",
1968
- "create",
1969
- label.name,
1970
- "--repo",
1971
- this.repo,
1972
- "--color",
1973
- label.color,
1974
- "--description",
1975
- label.description,
1976
- "--force"
1977
- ], { stdio: "ignore", shell: true });
1978
- }
1979
- }
1980
- /** Sync all stories in a plan to GitHub Issues */
1981
- async syncPlan(plan) {
1982
- let created = 0;
1983
- let updated = 0;
1984
- for (const epic of plan.epics) {
1985
- for (const story of epic.stories) {
1986
- const result = await this.syncStory(story, epic.title);
1987
- if (result === "created") created++;
1988
- else if (result === "updated") updated++;
1989
- }
1990
- }
1991
- return { created, updated };
1992
- }
1993
- /** Sync a single story to a GitHub Issue */
1994
- async syncStory(story, epicTitle) {
1995
- const title = `[${epicTitle}] ${story.title}`;
1996
- const labels = this.getLabels(story);
1997
- const body = [
1998
- `**Story ID:** \`${story.id}\``,
1999
- `**Type:** ${story.type}`,
2000
- `**Priority:** ${story.priority}`,
2001
- `**Status:** ${story.status}`,
2002
- "",
2003
- story.description,
2004
- "",
2005
- "---",
2006
- "_Managed by [ForgeAI](https://github.com/joeljohn159/forgeai)_"
2007
- ].join("\n");
2008
- const searchResult = spawnSync("gh", [
2009
- "issue",
2010
- "list",
2011
- "--repo",
2012
- this.repo,
2013
- "--search",
2014
- story.id,
2015
- "--label",
2016
- "forge",
2017
- "--json",
2018
- "number",
2019
- "--jq",
2020
- ".[0].number",
2021
- "--state",
2022
- "all"
2023
- ], { encoding: "utf-8", shell: true });
2024
- const existingNumber = searchResult.stdout?.trim();
2025
- if (existingNumber && /^\d+$/.test(existingNumber)) {
2026
- const args = [
2027
- "issue",
2028
- "edit",
2029
- existingNumber,
2030
- "--repo",
2031
- this.repo,
2032
- "--title",
2033
- title,
2034
- "--body",
2035
- body
2036
- ];
2037
- for (const label of labels) {
2038
- args.push("--add-label", label);
2039
- }
2040
- spawnSync("gh", args, { stdio: "ignore" });
2041
- if (story.status === "done") {
2042
- spawnSync("gh", [
2043
- "issue",
2044
- "close",
2045
- existingNumber,
2046
- "--repo",
2047
- this.repo
2048
- ], { stdio: "ignore", shell: true });
2049
- }
2050
- return "updated";
2051
- }
2052
- const createArgs = [
2053
- "issue",
2054
- "create",
2055
- "--repo",
2056
- this.repo,
2057
- "--title",
2058
- title,
2059
- "--body",
2060
- body
2061
- ];
2062
- for (const label of labels) {
2063
- createArgs.push("--label", label);
2064
- }
2065
- const createResult = spawnSync("gh", createArgs, { stdio: "ignore" });
2066
- if (createResult.status === 0) {
2067
- return "created";
2068
- }
2069
- return "skipped";
2070
- }
2071
- /** Map story status to GitHub labels */
2072
- getLabels(story) {
2073
- const labels = ["forge", `type:${story.type}`];
2074
- switch (story.status) {
2075
- case "planned":
2076
- case "designing":
2077
- case "design-approved":
2078
- labels.push("status:planned");
2079
- break;
2080
- case "building":
2081
- labels.push("status:in-progress");
2082
- break;
2083
- case "reviewing":
2084
- labels.push("status:review");
2085
- break;
2086
- case "done":
2087
- labels.push("status:done");
2088
- break;
2089
- case "blocked":
2090
- labels.push("status:blocked");
2091
- break;
2092
- }
2093
- return labels;
2094
- }
2095
- };
2096
-
2097
- // src/core/utils/attachments.ts
2098
- import fs3 from "fs";
2099
- import path3 from "path";
2100
- var IMAGE_EXTS = /* @__PURE__ */ new Set([
2101
- ".png",
2102
- ".jpg",
2103
- ".jpeg",
2104
- ".gif",
2105
- ".webp",
2106
- ".svg",
2107
- ".bmp",
2108
- ".ico",
2109
- ".tiff",
2110
- ".avif"
2111
- ]);
2112
- var DOCUMENT_EXTS = /* @__PURE__ */ new Set([
2113
- ".pdf",
2114
- ".doc",
2115
- ".docx",
2116
- ".txt",
2117
- ".md",
2118
- ".csv",
2119
- ".xls",
2120
- ".xlsx",
2121
- ".ppt",
2122
- ".pptx",
2123
- ".rtf",
2124
- ".html",
2125
- ".json",
2126
- ".yaml",
2127
- ".yml",
2128
- ".xml",
2129
- ".figma",
2130
- ".sketch",
2131
- ".xd"
2132
- ]);
2133
- var PATH_PATTERN = /(?:"([^"]+\.[\w]+)")|(?:'([^']+\.[\w]+)')|(?:((?:\/|[A-Z]:\\)(?:[^\s,;]|\\[ ])+\.[\w]+))/g;
2134
- function parseAttachments(input) {
2135
- const attachments = [];
2136
- let imageCount = 0;
2137
- let docCount = 0;
2138
- let cleaned = input.replace(PATH_PATTERN, (match, quoted, singleQuoted, raw) => {
2139
- const filePath = (quoted || singleQuoted || raw || "").replace(/\\ /g, " ").trim();
2140
- if (!filePath) return match;
2141
- const resolved = path3.resolve(filePath);
2142
- if (!fs3.existsSync(resolved)) return match;
2143
- const ext = path3.extname(resolved).toLowerCase();
2144
- const fileName = path3.basename(resolved);
2145
- let type;
2146
- let label;
2147
- if (IMAGE_EXTS.has(ext)) {
2148
- imageCount++;
2149
- type = "image";
2150
- label = `[Image${imageCount}]`;
2151
- } else if (DOCUMENT_EXTS.has(ext)) {
2152
- docCount++;
2153
- type = "document";
2154
- label = `[Document${docCount}]`;
2155
- } else {
2156
- docCount++;
2157
- type = "document";
2158
- label = `[Document${docCount}]`;
2159
- }
2160
- attachments.push({
2161
- label,
2162
- originalPath: match,
2163
- resolvedPath: resolved,
2164
- type,
2165
- ext,
2166
- fileName
2167
- });
2168
- return label;
2169
- });
2170
- cleaned = cleaned.replace(/\s{2,}/g, " ").trim();
2171
- return { description: cleaned, attachments };
2172
- }
2173
- function stageAttachments(attachments) {
2174
- if (attachments.length === 0) return [];
2175
- const attachDir = path3.join(process.cwd(), ".forge", "attachments");
2176
- fs3.mkdirSync(attachDir, { recursive: true });
2177
- const stagedPaths = [];
2178
- for (const att of attachments) {
2179
- const stagedName = att.label.replace(/[\[\]]/g, "").toLowerCase() + att.ext;
2180
- const dest = path3.join(attachDir, stagedName);
2181
- try {
2182
- fs3.copyFileSync(att.resolvedPath, dest);
2183
- stagedPaths.push(dest);
2184
- } catch {
2185
- }
2186
- }
2187
- return stagedPaths;
2188
- }
2189
- function formatAttachmentList(attachments) {
2190
- if (attachments.length === 0) return "";
2191
- return attachments.map((a) => ` ${a.label} ${a.fileName}`).join("\n");
2192
- }
2193
- function buildAttachmentPrompt(attachments) {
2194
- if (attachments.length === 0) return "";
2195
- const lines = attachments.map((a) => {
2196
- const relPath = path3.relative(process.cwd(), a.resolvedPath);
2197
- if (a.type === "image") {
2198
- return `- ${a.label}: Image file at ${relPath} \u2014 read this file to see the visual reference`;
2199
- }
2200
- return `- ${a.label}: Document at ${relPath} \u2014 read this file for requirements/specs`;
2201
- });
2202
- return `
2203
- ATTACHMENTS (provided by the user):
2204
- ${lines.join("\n")}
2205
-
2206
- Use these attachments as reference material. For images, match the visual style/layout.
2207
- For documents, extract requirements and specifications from them.
2208
- `;
2209
- }
2210
-
2211
- // src/core/pipeline/auto.ts
2212
- var AutoPipeline = class {
2213
- orchestrator;
2214
- worker;
2215
- git;
2216
- config;
2217
- plan = null;
2218
- options;
2219
- chatQueue = [];
2220
- rl = null;
2221
- startTime = 0;
2222
- totalUsage = { inputTokens: 0, outputTokens: 0, costUsd: 0, durationMs: 0 };
2223
- shuttingDown = false;
2224
- activeSpinner = null;
2225
- tagCounter = 1;
2226
- constructor(config, options = {}) {
2227
- this.config = config;
2228
- this.options = options;
2229
- this.orchestrator = new Orchestrator(config);
2230
- this.worker = new Worker(
2231
- config,
2232
- {
2233
- sandbox: options.sandbox ?? true,
2234
- yes: options.yes ?? false,
2235
- workingDir: options.workingDir,
2236
- allowedDomains: options.allowedDomains
2237
- },
2238
- options.mute
2239
- );
2240
- this.git = new GitManager();
2241
- this.setupGracefulShutdown();
2242
- }
2243
- // ── Graceful Shutdown ────────────────────────────────────
2244
- setupGracefulShutdown() {
2245
- const handler = async () => {
2246
- if (this.shuttingDown) return;
2247
- this.shuttingDown = true;
2248
- if (this.activeSpinner?.isSpinning) {
2249
- this.activeSpinner.stop();
2250
- }
2251
- console.log(chalk4.yellow("\n\n Interrupted \u2014 saving progress..."));
2252
- try {
2253
- if (this.plan) {
2254
- await stateManager.savePlan(this.plan);
2255
- await this.git.commitAll("forge: save progress (interrupted)");
2256
- console.log(chalk4.dim(" Progress saved. Resume with: forge resume\n"));
2257
- }
2258
- } catch {
2259
- console.log(chalk4.dim(" Could not save progress.\n"));
2260
- }
2261
- this.stopChatListener();
2262
- process.exit(130);
2263
- };
2264
- process.on("SIGINT", handler);
2265
- if (process.platform !== "win32") {
2266
- process.on("SIGTERM", handler);
2267
- }
2268
- }
2269
- // ── Resume an interrupted sprint ──────────────────────────
2270
- async resume(existingPlan) {
2271
- const errors = [];
2272
- this.startTime = Date.now();
2273
- this.plan = existingPlan;
2274
- console.log(chalk4.bold("\n forge") + chalk4.dim(" resume"));
2275
- console.log(chalk4.dim(` Continuing from last checkpoint
2276
- `));
2277
- this.startChatListener();
2278
- await this.git.ensureRepo();
2279
- await this.git.ensureMainBranch();
2280
- const allStories = this.getAllStories(this.plan);
2281
- const needsDesign = allStories.some(
2282
- (s) => s.status === "planned" && (s.type === "ui" || s.type === "fullstack")
2283
- );
2284
- const needsBuild = allStories.some(
2285
- (s) => s.status === "planned" || s.status === "design-approved"
2286
- );
2287
- const needsReview = allStories.some((s) => s.status === "reviewing");
2288
- if (!needsDesign && !needsBuild && !needsReview) {
2289
- console.log(chalk4.green(" Nothing to resume \u2014 all stories are complete or blocked.\n"));
2290
- this.stopChatListener();
2291
- return { success: true, plan: this.plan, errors };
2292
- }
2293
- if (needsDesign && !this.options.skipDesign) {
2294
- try {
2295
- await this.runDesignPhase(this.plan);
2296
- } catch (err) {
2297
- await this.saveProgressOnError("design");
2298
- errors.push(`Design: ${err instanceof Error ? err.message : err}`);
2299
- }
2300
- }
2301
- if (needsBuild) {
2302
- try {
2303
- await this.runBuildPhase(this.plan);
2304
- } catch (err) {
2305
- await this.saveProgressOnError("build");
2306
- errors.push(`Build: ${err instanceof Error ? err.message : err}`);
2307
- }
2308
- }
2309
- if (needsReview || needsBuild) {
2310
- const gateAction = await this.reviewGate();
2311
- if (gateAction === "abort") {
2312
- this.stopChatListener();
2313
- this.printSummary(errors);
2314
- return { success: false, plan: this.plan, errors: [...errors, "Aborted"] };
2315
- }
2316
- if (gateAction !== "skip") {
2317
- try {
2318
- await this.runReviewPhase(this.plan);
2319
- } catch (err) {
2320
- await this.saveProgressOnError("review");
2321
- errors.push(`Review: ${err instanceof Error ? err.message : err}`);
2322
- }
2323
- }
2324
- }
2325
- await this.autoPush();
2326
- this.stopChatListener();
2327
- this.printSummary(errors);
2328
- await stateManager.updatePhase("done");
2329
- const failedCount = allStories.filter((s) => s.status === "blocked").length;
2330
- return { success: errors.length === 0 && failedCount === 0, plan: this.plan, errors };
2331
- }
2332
- // ── Full Autonomous Sprint ─────────────────────────────────
2333
- async run(description) {
2334
- const errors = [];
2335
- this.startTime = Date.now();
2336
- console.log(chalk4.bold("\n forge") + chalk4.dim(" auto"));
2337
- const flags = [
2338
- `sandbox ${this.options.sandbox !== false ? "on" : "off"}`,
2339
- ...this.options.yes ? ["auto-approve on"] : []
2340
- ].join(" \xB7 ");
2341
- console.log(chalk4.dim(` ${flags} \xB7 type a message anytime to queue feedback
2342
- `));
2343
- this.startChatListener();
2344
- await this.git.ensureRepo();
2345
- await this.git.ensureMainBranch();
2346
- const planSpinner = ora2({ text: `${this.elapsed()} Planning...`, indent: 2 }).start();
2347
- this.activeSpinner = planSpinner;
2348
- try {
2349
- const attachmentContext = buildAttachmentPrompt(this.options.attachments || []);
2350
- this.plan = await this.orchestrator.generatePlan(description + attachmentContext);
2351
- planSpinner.succeed(`${this.elapsed()} Plan ready`);
2352
- this.activeSpinner = null;
2353
- this.displayPlan(this.plan);
2354
- await stateManager.savePlan(this.plan);
2355
- await stateManager.updatePhase("plan");
2356
- await this.git.commitState("Sprint plan");
2357
- await this.git.tag("forge/v0.0-plan");
2358
- await this.syncToGitHub(this.plan);
2359
- } catch (err) {
2360
- planSpinner.fail(`${this.elapsed()} Planning failed`);
2361
- this.activeSpinner = null;
2362
- const msg = err instanceof Error ? err.message : String(err);
2363
- console.log(chalk4.red(`
2364
- ${msg}
2365
- `));
2366
- if (!msg.includes("Possible fixes")) {
2367
- console.log(chalk4.dim(" Possible fixes:"));
2368
- console.log(chalk4.dim(" 1. Run: claude login"));
2369
- console.log(chalk4.dim(" 2. Check your internet connection"));
2370
- console.log(chalk4.dim(" 3. Run: forge doctor\n"));
2371
- }
2372
- this.stopChatListener();
2373
- return { success: false, plan: null, errors: [`Plan: ${msg}`] };
2374
- }
2375
- this.showInputHint();
2376
- if (this.options.skipDesign) {
2377
- for (const story of this.getAllStories(this.plan)) {
2378
- if (story.status === "planned") {
2379
- story.designApproved = true;
2380
- }
2381
- }
2382
- console.log(chalk4.dim(`
2383
- Design skipped (--skip-design)
2384
- `));
2385
- } else {
2386
- try {
2387
- await this.runDesignPhase(this.plan);
2388
- } catch (err) {
2389
- await this.saveProgressOnError("design");
2390
- errors.push(`Design: ${err instanceof Error ? err.message : err}`);
2391
- }
2392
- }
2393
- try {
2394
- await this.runBuildPhase(this.plan);
2395
- } catch (err) {
2396
- await this.saveProgressOnError("build");
2397
- errors.push(`Build: ${err instanceof Error ? err.message : err}`);
2398
- }
2399
- try {
2400
- await this.generateReadme(this.plan);
2401
- } catch (err) {
2402
- errors.push(`README: ${err instanceof Error ? err.message : err}`);
2403
- }
2404
- const gateAction = await this.reviewGate();
2405
- if (gateAction === "abort") {
2406
- this.stopChatListener();
2407
- this.printSummary(errors);
2408
- await stateManager.updatePhase("done");
2409
- return { success: false, plan: this.plan, errors: [...errors, "Aborted by user"] };
2410
- }
2411
- if (gateAction !== "skip") {
2412
- try {
2413
- await this.runReviewPhase(this.plan);
2414
- } catch (err) {
2415
- await this.saveProgressOnError("review");
2416
- errors.push(`Review: ${err instanceof Error ? err.message : err}`);
2417
- }
2418
- }
2419
- if (this.options.deploy) {
2420
- try {
2421
- await this.runDeployPhase(this.plan);
2422
- } catch (err) {
2423
- errors.push(`Deploy: ${err instanceof Error ? err.message : err}`);
2424
- }
2425
- }
2426
- await this.autoPush();
2427
- this.stopChatListener();
2428
- this.printSummary(errors);
2429
- await stateManager.updatePhase("done");
2430
- const allStories = this.getAllStories(this.plan);
2431
- const failedCount = allStories.filter((s) => s.status === "blocked").length;
2432
- return { success: errors.length === 0 && failedCount === 0, plan: this.plan, errors };
2433
- }
2434
- /** Save current progress when a phase fails — prevents data loss */
2435
- async saveProgressOnError(phase) {
2436
- try {
2437
- if (this.plan) {
2438
- await stateManager.savePlan(this.plan);
2439
- await stateManager.updatePhase(phase);
2440
- await this.git.commitAll(`forge: save progress (${phase} interrupted)`);
2441
- console.log(chalk4.dim(` Progress saved. Resume with: forge resume
2442
- `));
2443
- }
2444
- } catch {
2445
- }
2446
- }
2447
- // ── Elapsed Timer ─────────────────────────────────────────
2448
- elapsed() {
2449
- const sec = Math.floor((Date.now() - this.startTime) / 1e3);
2450
- const minutes = Math.floor(sec / 60);
2451
- const seconds = sec % 60;
2452
- return chalk4.dim(`[${minutes}:${seconds.toString().padStart(2, "0")}]`);
2453
- }
2454
- // ── Progress Helper with Timer ────────────────────────────
2455
- makeProgress(spinner, storyTitle) {
2456
- if (this.options.quiet) {
2457
- return { onProgress: () => {
2458
- }, stop: () => {
2459
- } };
2460
- }
2461
- let lastDetail = "";
2462
- const update = () => {
2463
- if (!spinner.isSpinning) return;
2464
- const prefix = this.elapsed();
2465
- const detail = lastDetail ? " " + chalk4.dim(lastDetail) : "";
2466
- spinner.text = `${prefix} ${storyTitle}${detail}`;
2467
- };
2468
- const interval = setInterval(update, 5e3);
2469
- const onProgress = (event) => {
2470
- switch (event.type) {
2471
- case "tool_use":
2472
- lastDetail = event.content;
2473
- break;
2474
- case "tool_running":
2475
- if (event.elapsed && event.elapsed > 3) {
2476
- lastDetail = `${event.tool} (${Math.round(event.elapsed)}s)`;
2477
- }
2478
- break;
2479
- case "tool_done":
2480
- lastDetail = event.content.slice(0, 80);
2481
- break;
2482
- }
2483
- update();
2484
- };
2485
- return {
2486
- onProgress,
2487
- stop: () => clearInterval(interval)
2488
- };
2489
- }
2490
- // ── Input Hint ────────────────────────────────────────────
2491
- showInputHint() {
2492
- if (process.stdout.isTTY && !this.options.quiet) {
2493
- const wasSpinning = this.activeSpinner?.isSpinning ?? false;
2494
- const spinnerText = this.activeSpinner?.text ?? "";
2495
- if (wasSpinning) this.activeSpinner.stop();
2496
- process.stdout.write(chalk4.dim("\n \u2500 type a message and press enter \u2500\n > "));
2497
- if (wasSpinning && this.activeSpinner) {
2498
- this.activeSpinner.start(spinnerText);
2499
- }
2500
- }
2501
- }
2502
- // ── Dependency Grouping ───────────────────────────────────
2503
- groupByDependency(stories) {
2504
- const batches = [];
2505
- const completed = /* @__PURE__ */ new Set();
2506
- const remaining = new Set(stories.map((s) => s.id));
2507
- const storyMap = new Map(stories.map((s) => [s.id, s]));
2508
- if (this.plan) {
2509
- for (const s of this.getAllStories(this.plan)) {
2510
- if (s.status === "done" || s.status === "reviewing") {
2511
- completed.add(s.id);
2512
- }
2513
- }
2514
- }
2515
- let iterations = 0;
2516
- const maxIterations = stories.length + 1;
2517
- while (remaining.size > 0 && iterations < maxIterations) {
2518
- iterations++;
2519
- const batch = [];
2520
- for (const id of remaining) {
2521
- const story = storyMap.get(id);
2522
- const depsReady = story.dependencies.every(
2523
- (dep) => completed.has(dep) || !remaining.has(dep)
2524
- // dep done or not in buildable set
2525
- );
2526
- if (depsReady) {
2527
- batch.push(story);
2528
- }
2529
- }
2530
- if (batch.length === 0) {
2531
- for (const id of remaining) {
2532
- batches.push([storyMap.get(id)]);
2533
- }
2534
- break;
2535
- }
2536
- batches.push(batch);
2537
- for (const s of batch) {
2538
- completed.add(s.id);
2539
- remaining.delete(s.id);
2540
- }
2541
- }
2542
- return batches;
2543
- }
2544
- // ── Design Phase (parallel) ───────────────────────────────
2545
- async runDesignPhase(plan) {
2546
- const uiStories = this.getStoriesByType(plan, ["ui", "fullstack"]).filter(
2547
- (s) => s.status === "planned"
2548
- );
2549
- if (uiStories.length === 0) return;
2550
- console.log(chalk4.bold(`
2551
- Design`) + chalk4.dim(` \xB7 ${uiStories.length} stories
2552
- `));
2553
- if (uiStories.length > 1) {
2554
- await Promise.all(
2555
- uiStories.map(
2556
- (story, i) => this.designSingleStory(story, plan, i + 1, uiStories.length)
2557
- )
2558
- );
2559
- } else {
2560
- await this.designSingleStory(uiStories[0], plan, 1, 1);
2561
- }
2562
- await stateManager.savePlan(plan);
2563
- await stateManager.updatePhase("design");
2564
- await this.git.commitState("Designs complete");
2565
- await this.git.tag("forge/v0.1-designs");
2566
- await this.processChatQueue();
2567
- this.showInputHint();
2568
- }
2569
- async designSingleStory(story, plan, index, total) {
2570
- const label = `[${index}/${total}] ${story.title}`;
2571
- const spinner = ora2({ text: `${this.elapsed()} ${label}`, indent: 2 }).start();
2572
- this.activeSpinner = spinner;
2573
- const progress = this.makeProgress(spinner, label);
2574
- try {
2575
- story.status = "designing";
2576
- const prompt = this.orchestrator.craftWorkerPrompt(story, "design", { plan });
2577
- const result = await this.worker.run("design", prompt, {
2578
- onProgress: progress.onProgress
2579
- });
2580
- this.addUsage(result.usage);
2581
- const tokens = chalk4.dim(
2582
- `${this.formatTokens(result.usage.inputTokens)} in / ${this.formatTokens(result.usage.outputTokens)} out`
2583
- );
2584
- if (result.success) {
2585
- spinner.succeed(`${this.elapsed()} ${label} ${tokens}`);
2586
- story.designApproved = true;
2587
- story.status = "design-approved";
2588
- } else {
2589
- spinner.warn(`${this.elapsed()} ${label}` + chalk4.dim(" skipped") + ` ${tokens}`);
2590
- story.designApproved = false;
2591
- story.status = "design-approved";
2592
- }
2593
- } finally {
2594
- progress.stop();
2595
- this.activeSpinner = null;
2596
- }
2597
- }
2598
- // ── Build Phase (parallel by dependency) ──────────────────
2599
- async runBuildPhase(plan) {
2600
- const buildable = this.getAllStories(plan).filter(
2601
- (s) => s.status === "design-approved" || s.status === "planned"
2602
- );
2603
- if (buildable.length === 0) return;
2604
- console.log(chalk4.bold(`
2605
- Build`) + chalk4.dim(` \xB7 ${buildable.length} stories
2606
- `));
2607
- const batches = this.groupByDependency(buildable);
2608
- let storyIndex = 0;
2609
- for (const batch of batches) {
2610
- if (batch.length === 1) {
2611
- storyIndex++;
2612
- await this.buildSingleStory(batch[0], plan, storyIndex, buildable.length, false);
2613
- } else {
2614
- console.log(chalk4.dim(` ${this.elapsed()} parallel: ${batch.map((s) => s.title).join(", ")}`));
2615
- const indexedBatch = batch.map((story) => {
2616
- storyIndex++;
2617
- return { story, index: storyIndex };
2618
- });
2619
- await Promise.all(
2620
- indexedBatch.map(
2621
- ({ story, index }) => this.buildSingleStory(story, plan, index, buildable.length, true)
2622
- )
2623
- );
2624
- const builtTitles = batch.filter((s) => s.status === "reviewing").map((s) => s.title);
2625
- if (builtTitles.length > 0) {
2626
- await this.git.commitAll(`feat: ${builtTitles.join(", ")}`);
2627
- }
2628
- }
2629
- await stateManager.savePlan(plan);
2630
- await this.processChatQueue();
2631
- this.showInputHint();
2632
- }
2633
- await stateManager.updatePhase("build");
2634
- }
2635
- async buildSingleStory(story, plan, index, total, parallel) {
2636
- const label = `[${index}/${total}] ${story.title}`;
2637
- const spinner = ora2({ text: `${this.elapsed()} ${label}`, indent: 2 }).start();
2638
- this.activeSpinner = spinner;
2639
- const progress = this.makeProgress(spinner, label);
2640
- try {
2641
- const headBefore = await this.git.getHead();
2642
- await stateManager.saveSnapshot({
2643
- action: "build",
2644
- storyId: story.id,
2645
- branch: "main",
2646
- commitBefore: headBefore
2647
- });
2648
- story.status = "building";
2649
- const prompt = this.orchestrator.craftWorkerPrompt(story, "build", {
2650
- plan,
2651
- designMeta: story.designApproved ? { storyId: story.id } : void 0
2652
- });
2653
- const result = await this.worker.run("build", prompt, {
2654
- onProgress: progress.onProgress
2655
- });
2656
- this.addUsage(result.usage);
2657
- const tokens = chalk4.dim(
2658
- `${this.formatTokens(result.usage.inputTokens)} in / ${this.formatTokens(result.usage.outputTokens)} out`
2659
- );
2660
- if (result.success) {
2661
- if (!parallel) {
2662
- await this.git.commitAll(`feat: ${story.title}`);
2663
- }
2664
- story.status = "reviewing";
2665
- spinner.succeed(
2666
- `${this.elapsed()} ${label}` + chalk4.dim(` \xB7 ${result.filesCreated.length} files`) + ` ${tokens}`
2667
- );
2668
- } else {
2669
- story.status = "blocked";
2670
- spinner.fail(`${this.elapsed()} ${label} ${tokens}`);
2671
- for (const error of result.errors) {
2672
- console.log(chalk4.red(` ${error}`));
2673
- }
2674
- if (result.errors.some((e) => e.includes("safety cap"))) {
2675
- console.log(chalk4.yellow(` Tip: Story may be too large. Try splitting it into smaller stories.`));
2676
- }
2677
- }
2678
- } finally {
2679
- progress.stop();
2680
- this.activeSpinner = null;
2681
- }
2682
- }
2683
- // ── README Generation ─────────────────────────────────────
2684
- async generateReadme(plan) {
2685
- const builtStories = this.getAllStories(plan).filter(
2686
- (s) => s.status === "reviewing" || s.status === "done"
2687
- );
2688
- if (builtStories.length === 0) return;
2689
- const label = "README.md";
2690
- const spinner = ora2({ text: `${this.elapsed()} Generating ${label}`, indent: 2 }).start();
2691
- this.activeSpinner = spinner;
2692
- const progress = this.makeProgress(spinner, label);
2693
- try {
2694
- const storyList = builtStories.map((s) => `- ${s.title}: ${s.description}`).join("\n");
2695
- const prompt = `
2696
- Generate a README.md for this project.
2697
-
2698
- Project: ${plan.project}
2699
- Description: ${plan.description}
2700
- Framework: ${plan.framework}
2701
-
2702
- Features built:
2703
- ${storyList}
2704
-
2705
- Read the actual codebase to understand the project structure.
2706
-
2707
- Include these sections:
2708
- - Project name and description
2709
- - Key features (from the stories above)
2710
- - Tech stack
2711
- - Getting started (install, dev server, build)
2712
- - Project structure (based on actual files you can see)
2713
-
2714
- Write the README.md file in the project root.
2715
- Keep it concise and professional.
2716
- `;
2717
- const result = await this.worker.run("build", prompt, {
2718
- onProgress: progress.onProgress
2719
- });
2720
- this.addUsage(result.usage);
2721
- if (result.success) {
2722
- await this.git.commitAll("docs: generate README.md");
2723
- spinner.succeed(`${this.elapsed()} ${label}`);
2724
- } else {
2725
- spinner.warn(`${this.elapsed()} ${label}` + chalk4.dim(" skipped"));
2726
- }
2727
- } finally {
2728
- progress.stop();
2729
- this.activeSpinner = null;
2730
- }
2731
- }
2732
- // ── Review Gate ───────────────────────────────────────────
2733
- async reviewGate() {
2734
- if (this.options.yes) return "continue";
2735
- if (!process.stdout.isTTY) return "continue";
2736
- if (!this.options.mute) {
2737
- playSound();
2738
- }
2739
- if (this.activeSpinner?.isSpinning) {
2740
- this.activeSpinner.stop();
2741
- this.activeSpinner = null;
2742
- }
2743
- this.stopChatListener();
2744
- console.log("");
2745
- const { action } = await inquirer2.prompt([
2746
- {
2747
- type: "list",
2748
- name: "action",
2749
- message: `${this.elapsed()} Build complete. What next?`,
2750
- choices: [
2751
- { name: "Continue to review", value: "continue" },
2752
- { name: "Skip review", value: "skip" },
2753
- { name: "Abort", value: "abort" }
2754
- ]
2755
- }
2756
- ]);
2757
- this.startChatListener();
2758
- return action;
2759
- }
2760
- // ── Review Phase ──────────────────────────────────────────
2761
- async runReviewPhase(plan) {
2762
- const reviewable = this.getAllStories(plan).filter((s) => s.status === "reviewing");
2763
- if (reviewable.length === 0) return;
2764
- console.log(chalk4.bold(`
2765
- Review`) + chalk4.dim(` \xB7 ${reviewable.length} stories
2766
- `));
2767
- for (let i = 0; i < reviewable.length; i++) {
2768
- const story = reviewable[i];
2769
- const label = `[${i + 1}/${reviewable.length}] ${story.title}`;
2770
- const spinner = ora2({ text: `${this.elapsed()} ${label}`, indent: 2 }).start();
2771
- this.activeSpinner = spinner;
2772
- const progress = this.makeProgress(spinner, label);
2773
- try {
2774
- const prompt = this.orchestrator.craftWorkerPrompt(story, "review", {
2775
- plan,
2776
- designMeta: story.designApproved ? { storyId: story.id } : void 0
2777
- });
2778
- const result = await this.worker.run("review", prompt, {
2779
- onProgress: progress.onProgress
2780
- });
2781
- this.addUsage(result.usage);
2782
- const tokens = chalk4.dim(
2783
- `${this.formatTokens(result.usage.inputTokens)} in / ${this.formatTokens(result.usage.outputTokens)} out`
2784
- );
2785
- if (result.success) {
2786
- const tagName = `forge/v0.${this.tagCounter++}-${story.id}`;
2787
- await this.git.tag(tagName);
2788
- story.tags.push(tagName);
2789
- story.status = "done";
2790
- spinner.succeed(`${this.elapsed()} ${label}` + chalk4.dim(` [${tagName}]`) + ` ${tokens}`);
2791
- } else {
2792
- spinner.text = `${this.elapsed()} ${label}` + chalk4.dim(" fixing issues...");
2793
- const fixed = await this.autoFix(story, plan, result.summary, spinner, label);
2794
- if (fixed) {
2795
- const tagName = `forge/v0.${this.tagCounter++}-${story.id}`;
2796
- await this.git.tag(tagName);
2797
- story.tags.push(tagName);
2798
- story.status = "done";
2799
- spinner.succeed(`${this.elapsed()} ${label}` + chalk4.dim(` [${tagName}] fixed`));
2800
- } else {
2801
- story.status = "blocked";
2802
- spinner.fail(`${this.elapsed()} ${label}` + chalk4.dim(" blocked"));
2803
- }
2804
- }
2805
- } finally {
2806
- progress.stop();
2807
- this.activeSpinner = null;
2808
- }
2809
- await stateManager.savePlan(plan);
2810
- await this.processChatQueue();
2811
- this.showInputHint();
2812
- }
2813
- await stateManager.updatePhase("review");
2814
- }
2815
- // ── Auto-Fix ──────────────────────────────────────────────
2816
- async autoFix(story, _plan, reviewSummary, spinner, label) {
2817
- const fixPrompt = `
2818
- The review found these issues:
2819
- ${reviewSummary}
2820
-
2821
- Fix them. Make minimal changes. Then re-run build/lint/typecheck to verify.
2822
- `;
2823
- const fixProgress = this.makeProgress(spinner, label + chalk4.dim(" fix"));
2824
- try {
2825
- const result = await this.worker.run("fix", fixPrompt, {
2826
- onProgress: fixProgress.onProgress
2827
- });
2828
- if (result.success) {
2829
- await this.git.commitAll(`fix: review issues in ${story.title}`);
2830
- return true;
2831
- }
2832
- return false;
2833
- } finally {
2834
- fixProgress.stop();
2835
- }
2836
- }
2837
- // ── Deploy Phase (optional) ───────────────────────────────
2838
- async runDeployPhase(plan) {
2839
- console.log(chalk4.bold(`
2840
- Deploy`) + chalk4.dim(` \xB7 GitHub Pages
2841
- `));
2842
- const label = "GitHub Pages";
2843
- const spinner = ora2({ text: `${this.elapsed()} Configuring ${label}`, indent: 2 }).start();
2844
- this.activeSpinner = spinner;
2845
- const progress = this.makeProgress(spinner, label);
2846
- try {
2847
- const framework = plan.framework || "Next.js";
2848
- const prompt = `
2849
- Set up GitHub Pages deployment for this ${framework} project.
2850
-
2851
- 1. Update next.config.js or next.config.ts to add: output: "export"
2852
- (merge with existing config, do not overwrite other settings)
2853
- 2. Create .github/workflows/deploy.yml for GitHub Pages deployment
2854
- 3. The workflow should:
2855
- - Trigger on push to main
2856
- - Install dependencies with npm ci
2857
- - Build with npm run build
2858
- - Use actions/configure-pages, actions/upload-pages-artifact, actions/deploy-pages
2859
-
2860
- Do NOT run any git commands. Just create/update the configuration files.
2861
- `;
2862
- const result = await this.worker.run("build", prompt, {
2863
- onProgress: progress.onProgress
2864
- });
2865
- this.addUsage(result.usage);
2866
- if (result.success) {
2867
- await this.git.commitAll("ci: add GitHub Pages deployment");
2868
- spinner.succeed(`${this.elapsed()} ${label} configured`);
2869
- } else {
2870
- spinner.warn(`${this.elapsed()} ${label}` + chalk4.dim(" skipped"));
2871
- }
2872
- } finally {
2873
- progress.stop();
2874
- this.activeSpinner = null;
2875
- }
2876
- }
2877
- // ── Chat Queue ────────────────────────────────────────────
2878
- startChatListener() {
2879
- if (!process.stdin.isTTY) return;
2880
- if (this.rl) return;
2881
- this.rl = readline.createInterface({
2882
- input: process.stdin,
2883
- terminal: false
2884
- });
2885
- this.rl.on("line", (line) => {
2886
- const msg = line.trim();
2887
- if (!msg) return;
2888
- this.chatQueue.push({
2889
- type: "content-change",
2890
- message: msg,
2891
- queuedAt: (/* @__PURE__ */ new Date()).toISOString()
2892
- });
2893
- const count = this.chatQueue.length;
2894
- const wasSpinning = this.activeSpinner?.isSpinning ?? false;
2895
- const spinnerText = this.activeSpinner?.text ?? "";
2896
- if (wasSpinning) this.activeSpinner.stop();
2897
- if (process.stdout.isTTY) {
2898
- process.stdout.write(`\r\x1B[K`);
2899
- }
2900
- console.log(
2901
- chalk4.green(` [queued${count > 1 ? ` #${count}` : ""}]`) + chalk4.dim(` ${msg}`)
2902
- );
2903
- console.log(chalk4.dim(" > "));
2904
- if (wasSpinning && this.activeSpinner) {
2905
- this.activeSpinner.start(spinnerText);
2906
- }
2907
- });
2908
- }
2909
- stopChatListener() {
2910
- if (this.rl) {
2911
- this.rl.close();
2912
- this.rl = null;
2913
- }
2914
- }
2915
- async processChatQueue() {
2916
- if (this.chatQueue.length === 0) return;
2917
- if (this.activeSpinner?.isSpinning) {
2918
- this.activeSpinner.stop();
2919
- }
2920
- console.log(chalk4.dim(`
2921
- Processing ${this.chatQueue.length} queued message${this.chatQueue.length > 1 ? "s" : ""}...
2922
- `));
2923
- const messages = [...this.chatQueue];
2924
- this.chatQueue = [];
2925
- for (const change of messages) {
2926
- try {
2927
- const state = await stateManager.getState();
2928
- const decision = await this.orchestrator.routeUserInput(change.message, state);
2929
- if (decision.action === "answer" && decision.response) {
2930
- console.log(chalk4.white(` > ${change.message}`));
2931
- console.log(chalk4.dim(` ${decision.response}
2932
- `));
2933
- } else if (decision.action === "route-to-worker" && decision.prompt) {
2934
- const mode = decision.workerMode || "fix";
2935
- const spinner = ora2({ text: change.message, indent: 2 }).start();
2936
- const result = await this.worker.run(mode, decision.prompt);
2937
- if (result.success) {
2938
- await this.git.commitAll(`fix: ${change.message}`);
2939
- spinner.succeed(change.message);
2940
- } else {
2941
- spinner.fail(change.message);
2942
- }
2943
- } else if (decision.action === "add-story" && this.plan) {
2944
- const newStory = {
2945
- id: `story-${Date.now()}`,
2946
- title: decision.story?.title || change.message,
2947
- description: decision.story?.description || change.message,
2948
- type: decision.story?.type || "fullstack",
2949
- status: "planned",
2950
- branch: null,
2951
- designApproved: false,
2952
- tags: [],
2953
- priority: 99,
2954
- dependencies: []
2955
- };
2956
- if (this.plan.epics.length > 0) {
2957
- this.plan.epics[this.plan.epics.length - 1].stories.push(newStory);
2958
- console.log(chalk4.dim(` + Story added: ${newStory.title}`));
2959
- }
2960
- }
2961
- } catch (err) {
2962
- console.log(chalk4.dim(` Could not process: ${change.message}`));
2963
- }
2964
- }
2965
- }
2966
- // ── Summary ───────────────────────────────────────────────
2967
- printSummary(errors) {
2968
- const elapsedSec = (Date.now() - this.startTime) / 1e3;
2969
- const elapsed = elapsedSec >= 60 ? `${Math.floor(elapsedSec / 60)}m ${Math.round(elapsedSec % 60)}s` : `${Math.round(elapsedSec)}s`;
2970
- const allStories = this.getAllStories(this.plan);
2971
- const done = allStories.filter((s) => s.status === "done").length;
2972
- const blocked = allStories.filter((s) => s.status === "blocked").length;
2973
- const total = allStories.length;
2974
- if (!this.options.mute) {
2975
- playSound();
2976
- }
2977
- console.log(chalk4.dim("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2978
- console.log(chalk4.bold(" Done") + chalk4.dim(` in ${elapsed} \xB7 ${done}/${total} stories`));
2979
- const u = this.totalUsage;
2980
- if (u.inputTokens > 0 || u.outputTokens > 0) {
2981
- console.log(chalk4.dim(
2982
- ` ${this.formatTokens(u.inputTokens)} in / ${this.formatTokens(u.outputTokens)} out` + (u.costUsd > 0 ? ` \xB7 ${this.formatCost(u.costUsd)}` : "")
2983
- ));
2984
- }
2985
- if (blocked > 0) {
2986
- console.log(chalk4.yellow(` ${blocked} blocked`) + chalk4.dim(" \u2014 " + chalk4.white("forge status")));
2987
- }
2988
- for (const e of errors) {
2989
- console.log(chalk4.dim(` ! ${e}`));
2990
- }
2991
- console.log("");
2992
- }
2993
- // ── Usage Tracking ────────────────────────────────────────
2994
- addUsage(usage) {
2995
- this.totalUsage.inputTokens += usage.inputTokens;
2996
- this.totalUsage.outputTokens += usage.outputTokens;
2997
- this.totalUsage.costUsd += usage.costUsd;
2998
- this.totalUsage.durationMs += usage.durationMs;
2999
- }
3000
- formatTokens(n) {
3001
- if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
3002
- if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
3003
- return String(n);
3004
- }
3005
- formatCost(usd) {
3006
- return "$" + usd.toFixed(4);
3007
- }
3008
- // ── GitHub Sync ────────────────────────────────────────────
3009
- async syncToGitHub(plan) {
3010
- if (!this.config.githubSync || !this.config.githubRepo) return;
3011
- if (!GitHubSync.isAvailable()) return;
3012
- try {
3013
- const gh = new GitHubSync(this.config.githubRepo);
3014
- await gh.ensureLabels();
3015
- const { created, updated } = await gh.syncPlan(plan);
3016
- if (created > 0 || updated > 0) {
3017
- console.log(chalk4.dim(` GitHub: ${created} issues created, ${updated} updated`));
3018
- }
3019
- } catch {
3020
- }
3021
- }
3022
- // ── Auto-Push ───────────────────────────────────────────
3023
- async autoPush() {
3024
- const hasRemote = await this.git.hasRemote();
3025
- if (!hasRemote) return;
3026
- const spinner = ora2({ text: `${this.elapsed()} Pushing to GitHub...`, indent: 2 }).start();
3027
- this.activeSpinner = spinner;
3028
- try {
3029
- await this.git.push();
3030
- await this.git.pushTags();
3031
- spinner.succeed(`${this.elapsed()} Pushed to GitHub`);
3032
- } catch {
3033
- spinner.warn(`${this.elapsed()} Push failed` + chalk4.dim(" \u2014 run forge push to retry"));
3034
- } finally {
3035
- this.activeSpinner = null;
3036
- }
3037
- }
3038
- // ── Helpers ───────────────────────────────────────────────
3039
- displayPlan(plan) {
3040
- console.log(chalk4.bold(`
3041
- ${plan.project}`) + chalk4.dim(` \xB7 ${plan.framework}`));
3042
- const total = this.getAllStories(plan).length;
3043
- console.log(chalk4.dim(` ${total} stories across ${plan.epics.length} epics
3044
- `));
3045
- for (const epic of plan.epics) {
3046
- console.log(chalk4.dim(` ${epic.title}`));
3047
- for (const story of epic.stories) {
3048
- const tag = story.type === "ui" ? "ui" : story.type === "backend" ? "api" : "full";
3049
- console.log(` ${chalk4.dim(`[${tag}]`)} ${story.title}`);
3050
- }
3051
- }
3052
- console.log("");
3053
- }
3054
- getAllStories(plan) {
3055
- return plan.epics.flatMap((e) => e.stories);
3056
- }
3057
- getStoriesByType(plan, types) {
3058
- return this.getAllStories(plan).filter((s) => types.includes(s.type));
3059
- }
3060
- };
3061
-
3062
- // src/core/utils/config.ts
3063
- import chalk5 from "chalk";
3064
- function validateConfig(config) {
3065
- const errors = [];
3066
- const warnings = [];
3067
- if (!config || typeof config !== "object") {
3068
- return { valid: false, errors: ["Config is empty or not an object"], warnings: [] };
3069
- }
3070
- const frameworks = listAdapters().map((a) => a.id);
3071
- if (!config.framework) {
3072
- errors.push("Missing 'framework' field");
3073
- } else if (!frameworks.includes(config.framework)) {
3074
- errors.push(
3075
- `Unknown framework "${config.framework}". Supported: ${frameworks.join(", ")}`
3076
- );
3077
- }
3078
- const validModels = ["sonnet", "opus", "haiku"];
3079
- if (!config.model) {
3080
- errors.push("Missing 'model' field");
3081
- } else if (!validModels.includes(config.model)) {
3082
- warnings.push(
3083
- `Unknown model "${config.model}". Expected: ${validModels.join(", ")}`
3084
- );
3085
- }
3086
- if (config.githubSync && !config.githubRepo) {
3087
- warnings.push("GitHub sync enabled but no repo configured");
3088
- }
3089
- return {
3090
- valid: errors.length === 0,
3091
- errors,
3092
- warnings
3093
- };
3094
- }
3095
- function loadAndValidateConfig(raw) {
3096
- const result = validateConfig(raw);
3097
- if (result.warnings.length > 0) {
3098
- for (const w of result.warnings) {
3099
- console.log(chalk5.yellow(` warning: ${w}`));
3100
- }
3101
- }
3102
- if (!result.valid) {
3103
- for (const e of result.errors) {
3104
- console.log(chalk5.red(` error: ${e}`));
3105
- }
3106
- console.log(chalk5.dim("\n Fix forge.config.json or run: forge init\n"));
3107
- return null;
3108
- }
3109
- return raw;
3110
- }
3111
-
3112
- export {
3113
- getAdapter,
3114
- listAdapters,
3115
- playSound,
3116
- Orchestrator,
3117
- Worker,
3118
- GitManager,
3119
- stateManager,
3120
- Pipeline,
3121
- parseAttachments,
3122
- stageAttachments,
3123
- formatAttachmentList,
3124
- buildAttachmentPrompt,
3125
- AutoPipeline,
3126
- validateConfig,
3127
- loadAndValidateConfig
3128
- };
3129
- //# sourceMappingURL=chunk-NYOV5D5J.js.map