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