forgecraft 1.0.0

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